diff --git a/.claude/skills/rustmotion b/.claude/skills/rustmotion
new file mode 120000
index 0000000..d1b1299
--- /dev/null
+++ b/.claude/skills/rustmotion
@@ -0,0 +1 @@
+../../crates/rustmotion/skills
\ No newline at end of file
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 5a2a02e..8a70df1 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -62,6 +62,6 @@ jobs:
# pouvait donc jamais reussir, et la release 0.6.0 s'est arretee la — apres
# avoir publie rustmotion-core, definitivement.
#
- # studio et cli portent `publish = false` : cargo les saute tout seul.
+ # rustmotion-studio porte `publish = false` : cargo la saute tout seul.
- name: Publish all crates
run: cargo publish --workspace --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index fd93be2..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,230 +0,0 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code when working with this repository.
-See .claude/skills/ for detailed instructions on generating rustmotion scenarios.
-
-## Règle obligatoire
-
-Tout JSON de scénario généré doit être validé avec `rustmotion validate` avant d'être présenté à l'utilisateur. Le validateur fait deux passes : **schema** et **geometry** (détection de débordement viewport). Les deux doivent passer.
-
-## Sécurité géométrique (viewport)
-
-Aucun contenu textuel ne doit dépasser du device. Quatre propriétés contrôlent ce comportement :
-
-- `style.white-space` (default `normal`, donc wrap actif) sur `text` : le texte wrap sur la largeur du parent par défaut. `white-space: "nowrap"` (ou `"pre"`) est légitime uniquement si un `max-width` fini + `font-size` raisonnable garantissent que la ligne tient. Le validateur émet `unwrappable_text_overflow` sinon. Il n'existe pas de champ `style.wrap` — c'est un vocabulaire hérité de l'ancien modèle de style, supprimé de `CssStyle`. Voir [rules/geometry-safety.md](.claude/skills/rustmotion/rules/geometry-safety.md).
-- `auto_scroll` (default `true`) sur `codeblock` et `terminal` : quand le contenu dépasse la hauteur du `size`, le moteur scrolle (clip + translate) sans réduire la `font-size`. `auto_scroll: false` → `auto_scroll_disabled_overflow`.
-- `style.text-autofit` (default absent) sur `text` et `gradient_text` : réduit la `font-size` jusqu'à ce que le contenu tienne dans sa boîte. À réserver au texte piloté par des données, dont on ne peut pas connaître la longueur à l'avance — pas pour compenser une mise en page qu'on peut simplement dimensionner. Le rétrécissement s'arrête à un plancher de lisibilité calibré ; si ça ne suffit pas, **la violation est toujours signalée**. Seuls ces deux composants l'implémentent : le déclarer ailleurs est inerte.
-- `style.overflow` (default `visible`) sur les conteneurs : sémantique CSS. `hidden` clippe au bord du parent. Le validateur ne se plaint que si le contenu sort du **viewport**, pas d'un parent `visible`.
-
-`marquee` et `cursor` sont exemptés (leur rôle est de bleed).
-
-CLI :
-- `rustmotion validate -f file.json` — schema + geometry
-- `--fix` — auto-fix sûr : `auto_scroll: true` sur `auto_scroll_disabled_overflow`, retrait de `style.white-space` sur `unwrappable_text_overflow` (retour au wrapping), et `text-autofit: true` sur `content_overflows_box` pour `text`/`gradient_text`. Les débordements de viewport restent non corrigés : ils demandent un arbitrage de mise en page. `--fix` **refuse** d'écrire sur un scénario templaté, utilisant `include`, ou utilisant `for-each`/`use` — les index de chemin ne correspondraient plus à la source.
-- `--report r.json` — rapport JSON
-- `--strict-anim` — vérification frame par frame ; ajoute la détection `animated_text_overflow` (transform animé qui sort du viewport à un instant échantillonné). L'échantillonnage s'arrête à `scene.freeze_at`, puisque rien n'est rendu au-delà.
-- `--strict-attrs` — promeut en erreurs les attributs inconnus (détection schéma + did-you-mean, activée par défaut en warnings)
-- `--lenient` — warnings au lieu d'errors
-
-## Encodage
-
-- ffmpeg est auto-détecté et utilisé par défaut (10-bit H.264, meilleure qualité sur les gradients sombres)
-- `--hardware-acceleration` sonde `ffmpeg -encoders` et bascule sur VideoToolbox/NVENC/QSV/AMF si la machine en offre un. Indisponible → message explicite et repli logiciel, jamais de bascule silencieuse. Le CRF n'a pas de sens sur la plupart des encodeurs matériels : le passer avec l'accélération produit un avertissement.
-- `--frames a-b` rend une plage de frames en segment autonome, avec **sa** tranche d'audio (les pistes ne repartent pas de zéro). `rustmotion concat seg1.mp4 seg2.mp4 -o out.mp4` les recolle via le concat demuxer de ffmpeg. C'est la brique d'un rendu distribué.
-- Sans ffmpeg, le fallback openh264 intégré encode en 8-bit
-- Pour les vidéos avec des gradients sombres, recommander `--codec prores` pour une qualité maximale
-
-## Factorisation : `components`, `for-each`, `use`
-
-Ne duplique pas un sous-arbre. Si dix cartes ne diffèrent que par leurs données, écris-en une et itère — c'est le mode d'échec le plus fréquent de la génération, chaque copie étant une occasion de diverger.
-
-```json
-"components": { "stat_card": { "params": { "label": { "type": "string" } }, "template": { … } } },
-"children": [{
- "for-each": [ { "label": "Revenue" }, { "label": "Users" } ],
- "template": { "use": "stat_card", "props": { "label": "$label" } }
-}]
-```
-
-Chaque élément du `for-each` lie ses champs directement (`$label`), plus `$index` et `$item`. `params` a la forme de `config` ; omettre `default` rend le paramètre requis. La clé d'overrides est **`props`**, pas `config` — ce nom-là est réservé et serait sauté par la substitution.
-
-`components` est local au fichier qui le déclare. On peut itérer sur un tableau venu d'une variable ; on ne peut pas instancier un composant défini dans un fichier inclus. Toute erreur — cycle, tableau manquant, composant inconnu, paramètre absent — est nommée et située. Voir [rules/templates-and-iteration.md](.claude/skills/rustmotion/rules/templates-and-iteration.md).
-
-> `--fix` refuse de réécrire un scénario qui utilise ces directives : les index de chemin ne correspondent plus à la source. `validate` fonctionne normalement, sur l'arbre expansé.
-
-## Composition : `scenes` vs `composition` (vues `slide` / `world`)
-
-Un scénario est soit une liste plate `scenes` (racine) — implicitement enveloppée dans une seule vue `slide` — soit un `composition: [...]` explicite, un tableau de **vues** typées `"slide"` ou `"world"`. Les deux sont mutuellement exclusifs (`CompositionAndScenesConflict` si les deux sont présents).
-
-Dans une vue `slide`, les `transition` entre scènes sont des **composites pixel de deux frame-buffers déjà rendus** (fade, wipe, zoom, flip, iris, slide, chromatic_wipe…) : aucun élément ne survit à la coupe, seuls les pixels sont mélangés.
-
-> `chromatic_wipe` is a fast slide whose reveal edge splits into red/cyan at its peak, then recomposes. `direction` (`left`/`right`/`up`/`down`) orients it, `aberration` sets how far the channels split (`0` = a plain slide, `2` = fully doubled). The flash is zero at both ends: a leftover fringe on the last frame would bleed into the next scene.
-
-La vue **`world`** est le seul mécanisme qui produit une continuité réelle entre beats : une caméra virtuelle se déplace en continu à travers un espace 2D où chaque scène occupe une position (`world-position`), avec un fondu de recouvrement pendant le pan au lieu d'une coupe. C'est la brique à utiliser pour une vidéo qui doit se lire comme un plan continu, sans limite de scène perceptible. Voir [rules/world-view.md](.claude/skills/rustmotion/rules/world-view.md) pour le modèle de coordonnées (le piège `world-position` = waypoint caméra, pas origine de scène), la recette du halo ambiant en `view.background`, et un exemple multi-beat validé.
-
-**Piège de casing à connaître :** `world-position` (scène) est en kebab-case, alors que son voisin `freeze_at` (même struct `Scene`) est en snake_case. Vraie inconsistance du schéma, pas une faute de frappe — copier la casse telle quelle.
-
-## Animated backgrounds
-
-`scene["animated-background"]` (kebab-case, like `world-position`) accepts a `preset` from `gradient_shift`, `grid_dots`, `grid_lines`, `concentric_circles`, `halo`, `pixel_grid`, `heropattern`, with its config under a key of the same name, plus the common `x`/`y`/`speed`/`direction` that drift the texture.
-
-> `grid_dots` marks the intersections and reads as a texture; `grid_lines` is a grid of ruled **lines** and reads as a structure — the one to put behind a chart or a code panel. Config: `cell`, `weight`, `color`, plus `major_every`/`major_weight` for the graph-paper effect.
-
-## Composants disponibles (60)
-
-### Basiques
-`text`, `shape`, `image`, `icon`, `svg`, `video`, `gif`, `caption`, `rich_text`, `gradient_text`
-
-### Conteneurs
-`card`, `flex`, `grid`, `div` (alias de `container`), `container`, `positioned`
-
-> `div` = layout pur sans décoration visuelle (HTML `
`). `card` = même chose mais avec fond/border-radius/ombre attendus.
-
-### Data Visualization
-- `chart` — 12 types: bar, line, pie, donut, horizontal_bar, area, stacked_bar, radar, scatter, radial_bar, funnel, waterfall. Supporte axes/grilles/labels.
-- `gauge` — jauge semi-circulaire pour KPIs
-- `sparkline` — mini-chart inline sans axes
-- `stat` — carte KPI composite (valeur + label + tendance + sparkline)
-- `heatmap` — grille colorée type GitHub contributions
-- `treemap` — rectangles proportionnels (slice-and-dice)
-- `dot_map` — carte mondiale en dot-pattern avec points de données, pulse, lat/lng
-- `progress` — barre linéaire ou circulaire
-- `counter` — compteur animé (standalone uniquement, pas dans les cards)
-- `number_wheel` — digits that scroll like a mechanical odometer and land on the figure. Not to be confused with `counter`, which interpolates a value and rewrites the number (its glyphs jump). See [rules/number-wheel.md](.claude/skills/rustmotion/rules/number-wheel.md).
-- `table` — tableau avec column_widths, column_align, cell_padding, show_borders
-
-### UI Components
-- `badge` — pill avec icon, dot indicator, pulse animation, count badge
-- `avatar` / `avatar_group` — avatar circulaire / groupe empilé avec "+N"
-- `switch` — toggle animé on/off avec toggle_at
-- `slider` — curseur horizontal animé avec animate_to/animate_at
-- `rating` — étoiles avec remplissage partiel animé
-- `kbd` — touche clavier visuelle (effet 3D)
-- `tooltip` — label flottant avec flèche directionnelle
-- `notification` — toast fade-in/out avec stack push (info/success/warning/error)
-- `pill_nav` — tabs avec pill indicator animé entre onglets
-- `list` — liste bullet/numbered/checklist avec icônes
-- `stepper` — étapes numérotées connectées avec progression animée
-- `comparison` — vue avant/après avec divider animé
-- `countdown` — timer digital flip-clock style
-- `marquee` — texte défilant continu
-- `skeleton` — placeholder de chargement avec shimmer (rectangle/circle/text)
-- `tag_cloud` — nuage de mots avec tailles pondérées
-- `callout` — bulle avec flèche
-- `divider` — séparateur visuel
-- `success_check` — a checkmark that draws itself inside a halo, with a pop and a settling rotation
-- `pointer` — a simulated **mouse** cursor (arrow + click ring) following waypoints. `cursor` is a text caret, not this. See [rules/pointer-walkthrough.md](.claude/skills/rustmotion/rules/pointer-walkthrough.md).
-
-### Code & Terminal
-- `codeblock` — code syntax-highlighted avec reveal, diff mode (`diff: true`), state transitions
-- `terminal` — terminal avec chrome macOS, reveal typewriter + curseur clignotant
-
-### Diagrammes
-`arrow`, `connector`, `timeline`, `line`
-
-> Pour faire suivre une trajectoire à un composant, utilise l'effet d'animation `motion_path` (données de chemin SVG, orientation optionnelle selon la tangente) plutôt que d'empiler des `translate`. Voir [rules/motion-path.md](.claude/skills/rustmotion/rules/motion-path.md).
-
-### Média
-`mockup`, `lottie`, `cursor`, `particle`, `qr_code`
-
-### Audio
-- `waveform` — visualisation d'onde audio réactive au volume de la piste
-- `audio_spectrum` — barres de spectre audio réactives (FFT)
-
-> Voir [rules/audio-reactive.md](.claude/skills/rustmotion/rules/audio-reactive.md) pour lier un composant à une piste `audio` via `style.audio-reactive`.
-
-## Text finishes
-
-Four mechanisms that each used to demand a hand-assembled sub-tree. Details and pitfalls in [rules/text-polish.md](.claude/skills/rustmotion/rules/text-polish.md).
-
-- **`shimmer`** — animation effect: a band of light sweeps over the element. Composited `SrcATop` inside the node's layer, so it only lights up **pixels that are actually painted** (the glyphs, not the box). Combined with `char_blur_in`, this is "text stagger".
-- **`text.states` + `text.swap`** — a label that becomes another one: the outgoing one rises while blurring, the incoming one rises from below while unblurring. The box is measured on the **longest** label, not the first one.
-- **`text.caret`** — a caret (`line` or `block`) pinned to a `typewriter`'s reveal head. A `cursor` composited alongside would stay where it was placed while the text grows underneath it.
-- **`pop_in`** — a two-beat entry preset: a back-out scale that *places* the element, then a short elastic pulse that draws the eye back to it.
-
-The seven `char_*` presets are tuned via `direction` (up/down/left/right), `distance`, `scale_from`, `jitter`+`seed` (deterministic stagger irregularity) and `ink_from` (each unit's starting colour). See [rules/char-animation-tuning.md](.claude/skills/rustmotion/rules/char-animation-tuning.md) and, for tokens arriving, [rules/streaming-text.md](.claude/skills/rustmotion/rules/streaming-text.md).
-
-> If you're asked for an effect named in the Hyperframes vocabulary ("streaming text", "number wheel", "badge pop", "top-down letters"…), check the mapping table first: [rules/hyperframes-mapping.md](.claude/skills/rustmotion/rules/hyperframes-mapping.md).
-
-## Architecture
-
-### Render Pipeline (CSS engine)
-
-Le moteur utilise un pipeline **box_tree → layout_pass → paint_pass** inspiré des navigateurs web :
-
-1. **box_tree** (`box_builder.rs`) — construit un arbre de `BoxNode { css: CssStyle, children, intrinsic }` depuis les composants JSON résolus
-2. **layout_pass** (`engine/layout_pass.rs`) — orchestre taffy pour calculer les `BoxLayout { x, y, width, height }` de chaque nœud. Les feuilles avec un `IntrinsicMeasure` (texte, image, codeblock) sont mesurées via une `measure_fn`.
-3. **paint_pass** (`engine/paint_pass.rs`) — descend l'arbre, applique transform/opacity, peint les décorations (background, border, shadow), délègue au `Painter` du composant pour le contenu.
-
-Chaque composant implémente le trait `Painter` :
-
-```rust
-pub trait Painter {
- fn paint_content(&self, canvas: &Canvas, layout: &BoxLayout, props: &AnimatedProperties, ctx: &PaintCtx);
- fn intrinsic_size(&self, available: AvailableSize, ctx: &MeasureCtx) -> Option<(f32, f32)> { None }
-}
-```
-
-`PaintCtx` contient : `time`, `scene_duration`, `fps`, `frame_index`, `video_width`, `video_height`, `stagger_offset`.
-
-### Structure des crates
-
-```
-crates/
-├── rustmotion-core/src/
-│ ├── css/ # Modèle CSS
-│ │ ├── style.rs # CssStyle (propriétés CSS kebab-case)
-│ │ ├── units.rs # Length, LengthPercentage (px, %, em, rem, vw, vh)
-│ │ ├── cascade.rs # Héritage color/font-* parent → enfant
-│ │ ├── taffy_bridge.rs # CssStyle → taffy::Style
-│ │ └── animation.rs # Résolution des animations → override CssStyle
-│ ├── engine/
-│ │ ├── box_tree.rs # BoxNode, BoxKind, IntrinsicMeasure
-│ │ ├── layout_pass.rs # Orchestration taffy, BoxLayout résultant
-│ │ ├── paint_pass.rs # Walk top-down, décorations, dispatch Painter
-│ │ ├── animator.rs # Résolution animations, easing, spring solver
-│ │ ├── transition.rs # Transitions entre scènes
-│ │ ├── renderer/ # Primitives Skia (colors, fonts, shapes, text)
-│ │ └── text/cosmic.rs # Bridge cosmic-text — PAS branché sur le rendu réel
-│ ├── schema/ # Modèles de données JSON
-│ │ ├── scenario.rs # Scenario, ResolvedScenario, View, Scene, VideoConfig
-│ │ ├── style.rs # Specialized types (CardBorder, CardShadow, Fill, etc.)
-│ │ ├── background.rs # AnimatedBackground, BackgroundPreset
-│ │ ├── animation.rs # EasingType, AnimationPreset, PresetConfig
-│ │ ├── codeblock_types.rs # CodeblockChrome, CodeblockState
-│ │ └── video.rs # AnimationEffect, Size, ShapeType, Stroke
-│ └── traits/
-│ ├── painter.rs # Painter trait + PaintCtx + AvailableSize + MeasureCtx
-│ ├── animatable.rs # Animatable trait
-│ ├── timed.rs # Timed trait + TimingConfig
-│ └── styled.rs # Styled trait
-│
-├── rustmotion-components/src/
-│ ├── lib.rs # Enum Component + dispatch (as_painter, as_animatable, etc.)
-│ ├── box_builder.rs # build_scene() → BuiltScene (components + stagger_delays)
-│ ├── intrinsic.rs # TextIntrinsic, BadgeIntrinsic, CounterIntrinsic, etc.
-│ ├── legacy_dispatch.rs # LegacyPaintDispatcher (bridge NodeId → Painter)
-│ ├── chart/ # 10 fichiers (mod + bar/line/pie/radar/scatter/radial/funnel/waterfall/axes)
-│ └── *.rs # Un fichier par composant (impl Painter)
-│
-└── rustmotion-cli/src/
- └── commands/ # validate, render, schema, info
-```
-
-### Ajouter un nouveau composant
-
-1. Créer `crates/rustmotion-components/src/mon_composant.rs` avec struct serde + `impl Painter` (`paint_content`)
-2. Ajouter `rustmotion_core::impl_traits!(MonComposant { Animatable => animation, Timed => timing, Styled => style });`
-3. Ajouter le variant dans l'enum `Component` dans `lib.rs`
-4. Ajouter les match arms dans les méthodes de dispatch (`as_painter`, `as_animatable`, `as_timed`, `as_styled`)
-5. Ajouter `pub mod mon_composant;` et `pub use mon_composant::MonComposant;` dans `lib.rs`
-6. Si le composant a une taille fixe: la déclarer via apply_intrinsic_overrides dans box_builder.rs
-7. Si le composant mesure son propre contenu : ajouter `XxxIntrinsic` dans `intrinsic.rs`
-
-### Tests
-
-```bash
-cargo test --workspace # ~200 tests (layout + serde round-trip + pixel regressions + smoke)
-cargo check # Vérification compilation
-rustmotion validate file.json # Validation scénario
-```
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 0000000..7ddc0a6
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+crates/rustmotion/CLAUDE.md
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 061d4b0..6f47668 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5053,11 +5053,16 @@ dependencies = [
name = "rustmotion"
version = "0.6.1"
dependencies = [
+ "clap",
+ "clap_complete",
+ "crossterm",
+ "dirs",
"gif 0.13.3",
"image",
"minimp4",
"notify",
"openh264",
+ "ratatui",
"rayon",
"resvg",
"rubato",
@@ -5077,25 +5082,6 @@ dependencies = [
"usvg",
]
-[[package]]
-name = "rustmotion-cli"
-version = "0.6.1"
-dependencies = [
- "clap",
- "clap_complete",
- "crossterm",
- "dirs",
- "image",
- "notify",
- "ratatui",
- "rayon",
- "rustmotion",
- "rustmotion-studio",
- "schemars",
- "serde",
- "serde_json",
-]
-
[[package]]
name = "rustmotion-components"
version = "0.6.1"
diff --git a/Cargo.toml b/Cargo.toml
index 498dbb3..1ee1fd3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,12 +4,11 @@ members = [
"crates/rustmotion-core",
"crates/rustmotion-components",
"crates/rustmotion",
- "crates/rustmotion-cli",
"crates/rustmotion-studio",
"crates/rustmotion-html",
]
-# Les six crates avancent ensemble. La version se change ici, et nulle part
+# Les cinq crates avancent ensemble. La version se change ici, et nulle part
# ailleurs : elle était auparavant répétée dix fois — une par manifeste, plus une
# par dépendance interne — et une seule oubliée fait échouer la publication après
# que les précédentes soient parties, ce qui ne se rattrape pas.
diff --git a/crates/rustmotion-cli/Cargo.toml b/crates/rustmotion-cli/Cargo.toml
deleted file mode 100644
index df660b7..0000000
--- a/crates/rustmotion-cli/Cargo.toml
+++ /dev/null
@@ -1,27 +0,0 @@
-[package]
-name = "rustmotion-cli"
-version.workspace = true
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/lib.rs"
-
-[[bin]]
-name = "rustmotion"
-path = "src/bin/main.rs"
-
-[dependencies]
-rustmotion = { path = "../rustmotion" }
-rustmotion-studio = { path = "../rustmotion-studio" }
-clap = { version = "4", features = ["derive"] }
-clap_complete = "4.6.0"
-ratatui = "0.29"
-crossterm = "0.28"
-notify = "7"
-schemars = "0.8"
-serde = { version = "1", features = ["derive"] }
-serde_json = "1"
-dirs = "6.0.0"
-rayon = "1"
-image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
diff --git a/crates/rustmotion-components/src/intrinsic.rs b/crates/rustmotion-components/src/intrinsic.rs
index 3375e19..eb016cf 100644
--- a/crates/rustmotion-components/src/intrinsic.rs
+++ b/crates/rustmotion-components/src/intrinsic.rs
@@ -116,7 +116,7 @@ pub struct TextIntrinsic {
impl TextIntrinsic {
/// M1: `white-space: nowrap|pre` disables wrapping — the geometry
/// validator's `unwrappable_text_overflow`/`ContentOverflowsBox` checks
- /// (crates/rustmotion-cli/src/commands/geometry.rs) already branch on
+ /// (crates/rustmotion/src/cli/commands/geometry.rs) already branch on
/// exactly this pair of variants and re-measure via this same
/// `TextIntrinsic`, so the wrap decision here must match theirs exactly
/// or the validator's assumption about what the renderer produces is
@@ -164,7 +164,7 @@ impl TextIntrinsic {
// No *real* `LengthContext` (real viewport, real parent width) is
// reachable here without changing this constructor's signature —
// its only callers are `box_builder.rs` and
- // `rustmotion-cli/src/commands/geometry.rs`, both outside this
+ // `rustmotion/src/cli/commands/geometry.rs`, both outside this
// workstream's scope (box_builder.rs is a sibling's live file this
// wave; the geometry validator re-measures via this exact type and
// must keep agreeing with it byte-for-byte, so changing what it
diff --git a/crates/rustmotion-components/src/lib.rs b/crates/rustmotion-components/src/lib.rs
index f889ac5..2ac0556 100644
--- a/crates/rustmotion-components/src/lib.rs
+++ b/crates/rustmotion-components/src/lib.rs
@@ -319,7 +319,7 @@ pub struct ChildComponent {
/// `style` property — `CssStyle` is `deny_unknown_fields` and belongs to
/// no one this wave. Defaults to `false`: no existing scenario changes
/// behaviour. Exempts only `viewport_overflow` and `animated_text_overflow`
- /// (see `crates/rustmotion-cli/src/commands/geometry.rs`); it does NOT
+ /// (see `crates/rustmotion/src/cli/commands/geometry.rs`); it does NOT
/// exempt `content_overflows_box` — content larger than its own box stays
/// a reported defect regardless of `bleed`. Applies to this component
/// only: a bled container does not suppress checks on its children, since
diff --git a/crates/rustmotion-core/src/css/style.rs b/crates/rustmotion-core/src/css/style.rs
index 01545ff..6be471f 100644
--- a/crates/rustmotion-core/src/css/style.rs
+++ b/crates/rustmotion-core/src/css/style.rs
@@ -13,10 +13,10 @@ use super::units::{Length, LengthContext, LengthPercentage, ParsedLength};
// shape either way — a css-local mirror would only duplicate the struct.
use crate::schema::{deserialize_animation_effects, AnimationEffect, GradientBorder, InnerShadow};
-// ─── Legibility floor (relocated from `rustmotion-cli/src/commands/
+// ─── Legibility floor (relocated from `rustmotion/src/cli/commands/
// geometry.rs`'s `check_legibility`, issue #110/#102 — moved here, not
// duplicated, so `text-autofit` below can shrink down to the exact same
-// calibrated threshold instead of inventing a second one; `rustmotion-cli`
+// calibrated threshold instead of inventing a second one; `rustmotion`
// depends on `rustmotion-core`, never the other way around, so the shared
// value has to live on this side of that boundary) ─────────────────────────
//
@@ -124,7 +124,7 @@ pub struct CssStyle {
/// instead of overflowing it. This is what lets an author declare "this
/// text must fit here" and closes `ContentOverflowsBox` as a possible
/// validator failure for that node — see `apply_fixes`
- /// (`rustmotion-cli/src/commands/validate.rs`)'s comment on why it
+ /// (`rustmotion/src/cli/commands/validate.rs`)'s comment on why it
/// deliberately refuses to auto-fix that violation today: growing the
/// box, shrinking the font, and shortening the copy are all legitimate
/// fixes, and picking one was never this tool's call to make silently.
@@ -163,7 +163,7 @@ pub struct CssStyle {
///
/// **The floor.** Never shrinks below [`TEXT_AUTOFIT_MIN_FONT_PX`] — the
/// same calibrated legibility ratio `check_legibility`
- /// (`rustmotion-cli/src/commands/geometry.rs`) already enforces, not a
+ /// (`rustmotion/src/cli/commands/geometry.rs`) already enforces, not a
/// new threshold. If the content still doesn't fit at the floor, the
/// floor size is used anyway (illegible-but-smallest beats an even
/// larger overflow) and the geometry validator's `ContentOverflowsBox`
diff --git a/crates/rustmotion-core/src/engine/animator.rs b/crates/rustmotion-core/src/engine/animator.rs
index 89e9331..152c796 100644
--- a/crates/rustmotion-core/src/engine/animator.rs
+++ b/crates/rustmotion-core/src/engine/animator.rs
@@ -596,7 +596,7 @@ fn spring_settle_time(damping: f64, stiffness: f64, mass: f64, threshold: f64, m
/// set, else `DEFAULT_SPRING_REST_THRESHOLD`, floored so `spring_settle_time`
/// always has a well-defined (nonzero) target — the same belt-and-suspenders
/// pattern `spring_value` already applies to `damping`/`stiffness`/`mass`.
-/// `rustmotion-cli`'s `check_spring_config` rejects non-positive or absurd
+/// the CLI's `check_spring_config` rejects non-positive or absurd
/// (`>= 1.0`) values at the author-facing layer; this floor is the
/// solver-side backstop.
fn spring_rest_threshold(config: &SpringConfig) -> f64 {
@@ -1369,7 +1369,7 @@ fn motion_path_sample(cfg: &MotionPathConfig, time: f64) -> MotionPathSample {
/// apply_animated_props` already bridges into `css.transform`'s
/// `translate`/`rotate` functions. That bridge — not a new one — is what
/// makes a `motion_path` excursion past the viewport visible to
-/// `--strict-anim` (`rustmotion-cli::commands::geometry::
+/// `--strict-anim` (`rustmotion::cli::commands::geometry::
/// apply_static_node_transform`, which folds `css.transform` to detect
/// overflow): this function must never write position/orientation anywhere
/// else, or that detection silently stops seeing it.
diff --git a/crates/rustmotion-core/src/engine/renderer/assets.rs b/crates/rustmotion-core/src/engine/renderer/assets.rs
index 070e947..fdd65d9 100644
--- a/crates/rustmotion-core/src/engine/renderer/assets.rs
+++ b/crates/rustmotion-core/src/engine/renderer/assets.rs
@@ -252,7 +252,7 @@ pub fn extract_video_frame(src: &str, time: f64, width: u32, height: u32) -> Res
// ─── Media metadata probing ────────────────────────────────────────────────
//
// "How long is this audio file? What are the dimensions of this image?" —
-// `rustmotion info` (`crates/rustmotion-cli/src/commands/info.rs`) answers
+// `rustmotion info` (`crates/rustmotion/src/cli/commands/info.rs`) answers
// these by calling the functions below, one per asset kind. Two rules tie
// them together:
//
diff --git a/crates/rustmotion-core/tests/spring_duration.rs b/crates/rustmotion-core/tests/spring_duration.rs
index 90e80e9..631cb4d 100644
--- a/crates/rustmotion-core/tests/spring_duration.rs
+++ b/crates/rustmotion-core/tests/spring_duration.rs
@@ -1,6 +1,6 @@
//! Issue #167 lot E: `SpringConfig::duration`/`rest_threshold` and
//! `engine::animator::spring_rest_time` are meant to be used from outside
-//! `rustmotion-core` (by `rustmotion-cli`'s `info` command, in particular).
+//! `rustmotion-core` (by `rustmotion`'s `info` command, in particular).
//! This is a black-box check that the public surface actually works end to
//! end — the white-box coverage (settle-time correctness against a
//! brute-force reference, over/under-damped edge cases, shape preservation)
diff --git a/crates/rustmotion/CLAUDE.md b/crates/rustmotion/CLAUDE.md
new file mode 100644
index 0000000..670d7a6
--- /dev/null
+++ b/crates/rustmotion/CLAUDE.md
@@ -0,0 +1,238 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code when working with this repository.
+See .claude/skills/ for detailed instructions on generating rustmotion scenarios.
+
+## Règle obligatoire
+
+Tout JSON de scénario généré doit être validé avec `rustmotion validate` avant d'être présenté à l'utilisateur. Le validateur fait deux passes : **schema** et **geometry** (détection de débordement viewport). Les deux doivent passer.
+
+## Sécurité géométrique (viewport)
+
+Aucun contenu textuel ne doit dépasser du device. Quatre propriétés contrôlent ce comportement :
+
+- `style.white-space` (default `normal`, donc wrap actif) sur `text` : le texte wrap sur la largeur du parent par défaut. `white-space: "nowrap"` (ou `"pre"`) est légitime uniquement si un `max-width` fini + `font-size` raisonnable garantissent que la ligne tient. Le validateur émet `unwrappable_text_overflow` sinon. Il n'existe pas de champ `style.wrap` — c'est un vocabulaire hérité de l'ancien modèle de style, supprimé de `CssStyle`. Voir [rules/geometry-safety.md](.claude/skills/rustmotion/rules/geometry-safety.md).
+- `auto_scroll` (default `true`) sur `codeblock` et `terminal` : quand le contenu dépasse la hauteur du `size`, le moteur scrolle (clip + translate) sans réduire la `font-size`. `auto_scroll: false` → `auto_scroll_disabled_overflow`.
+- `style.text-autofit` (default absent) sur `text` et `gradient_text` : réduit la `font-size` jusqu'à ce que le contenu tienne dans sa boîte. À réserver au texte piloté par des données, dont on ne peut pas connaître la longueur à l'avance — pas pour compenser une mise en page qu'on peut simplement dimensionner. Le rétrécissement s'arrête à un plancher de lisibilité calibré ; si ça ne suffit pas, **la violation est toujours signalée**. Seuls ces deux composants l'implémentent : le déclarer ailleurs est inerte.
+- `style.overflow` (default `visible`) sur les conteneurs : sémantique CSS. `hidden` clippe au bord du parent. Le validateur ne se plaint que si le contenu sort du **viewport**, pas d'un parent `visible`.
+
+`marquee` et `cursor` sont exemptés (leur rôle est de bleed).
+
+CLI :
+- `rustmotion validate -f file.json` — schema + geometry
+- `--fix` — auto-fix sûr : `auto_scroll: true` sur `auto_scroll_disabled_overflow`, retrait de `style.white-space` sur `unwrappable_text_overflow` (retour au wrapping), et `text-autofit: true` sur `content_overflows_box` pour `text`/`gradient_text`. Les débordements de viewport restent non corrigés : ils demandent un arbitrage de mise en page. `--fix` **refuse** d'écrire sur un scénario templaté, utilisant `include`, ou utilisant `for-each`/`use` — les index de chemin ne correspondraient plus à la source.
+- `--report r.json` — rapport JSON
+- `--strict-anim` — vérification frame par frame ; ajoute la détection `animated_text_overflow` (transform animé qui sort du viewport à un instant échantillonné). L'échantillonnage s'arrête à `scene.freeze_at`, puisque rien n'est rendu au-delà.
+- `--strict-attrs` — promeut en erreurs les attributs inconnus (détection schéma + did-you-mean, activée par défaut en warnings)
+- `--lenient` — warnings au lieu d'errors
+
+## Encodage
+
+- ffmpeg est auto-détecté et utilisé par défaut (10-bit H.264, meilleure qualité sur les gradients sombres)
+- `--hardware-acceleration` sonde `ffmpeg -encoders` et bascule sur VideoToolbox/NVENC/QSV/AMF si la machine en offre un. Indisponible → message explicite et repli logiciel, jamais de bascule silencieuse. Le CRF n'a pas de sens sur la plupart des encodeurs matériels : le passer avec l'accélération produit un avertissement.
+- `--frames a-b` rend une plage de frames en segment autonome, avec **sa** tranche d'audio (les pistes ne repartent pas de zéro). `rustmotion concat seg1.mp4 seg2.mp4 -o out.mp4` les recolle via le concat demuxer de ffmpeg. C'est la brique d'un rendu distribué.
+- Sans ffmpeg, le fallback openh264 intégré encode en 8-bit
+- Pour les vidéos avec des gradients sombres, recommander `--codec prores` pour une qualité maximale
+
+## Factorisation : `components`, `for-each`, `use`
+
+Ne duplique pas un sous-arbre. Si dix cartes ne diffèrent que par leurs données, écris-en une et itère — c'est le mode d'échec le plus fréquent de la génération, chaque copie étant une occasion de diverger.
+
+```json
+"components": { "stat_card": { "params": { "label": { "type": "string" } }, "template": { … } } },
+"children": [{
+ "for-each": [ { "label": "Revenue" }, { "label": "Users" } ],
+ "template": { "use": "stat_card", "props": { "label": "$label" } }
+}]
+```
+
+Chaque élément du `for-each` lie ses champs directement (`$label`), plus `$index` et `$item`. `params` a la forme de `config` ; omettre `default` rend le paramètre requis. La clé d'overrides est **`props`**, pas `config` — ce nom-là est réservé et serait sauté par la substitution.
+
+`components` est local au fichier qui le déclare. On peut itérer sur un tableau venu d'une variable ; on ne peut pas instancier un composant défini dans un fichier inclus. Toute erreur — cycle, tableau manquant, composant inconnu, paramètre absent — est nommée et située. Voir [rules/templates-and-iteration.md](.claude/skills/rustmotion/rules/templates-and-iteration.md).
+
+> `--fix` refuse de réécrire un scénario qui utilise ces directives : les index de chemin ne correspondent plus à la source. `validate` fonctionne normalement, sur l'arbre expansé.
+
+## Composition : `scenes` vs `composition` (vues `slide` / `world`)
+
+Un scénario est soit une liste plate `scenes` (racine) — implicitement enveloppée dans une seule vue `slide` — soit un `composition: [...]` explicite, un tableau de **vues** typées `"slide"` ou `"world"`. Les deux sont mutuellement exclusifs (`CompositionAndScenesConflict` si les deux sont présents).
+
+Dans une vue `slide`, les `transition` entre scènes sont des **composites pixel de deux frame-buffers déjà rendus** (fade, wipe, zoom, flip, iris, slide, chromatic_wipe…) : aucun élément ne survit à la coupe, seuls les pixels sont mélangés.
+
+> `chromatic_wipe` is a fast slide whose reveal edge splits into red/cyan at its peak, then recomposes. `direction` (`left`/`right`/`up`/`down`) orients it, `aberration` sets how far the channels split (`0` = a plain slide, `2` = fully doubled). The flash is zero at both ends: a leftover fringe on the last frame would bleed into the next scene.
+
+La vue **`world`** est le seul mécanisme qui produit une continuité réelle entre beats : une caméra virtuelle se déplace en continu à travers un espace 2D où chaque scène occupe une position (`world-position`), avec un fondu de recouvrement pendant le pan au lieu d'une coupe. C'est la brique à utiliser pour une vidéo qui doit se lire comme un plan continu, sans limite de scène perceptible. Voir [rules/world-view.md](.claude/skills/rustmotion/rules/world-view.md) pour le modèle de coordonnées (le piège `world-position` = waypoint caméra, pas origine de scène), la recette du halo ambiant en `view.background`, et un exemple multi-beat validé.
+
+**Piège de casing à connaître :** `world-position` (scène) est en kebab-case, alors que son voisin `freeze_at` (même struct `Scene`) est en snake_case. Vraie inconsistance du schéma, pas une faute de frappe — copier la casse telle quelle.
+
+## Animated backgrounds
+
+`scene["animated-background"]` (kebab-case, like `world-position`) accepts a `preset` from `gradient_shift`, `grid_dots`, `grid_lines`, `concentric_circles`, `halo`, `pixel_grid`, `heropattern`, with its config under a key of the same name, plus the common `x`/`y`/`speed`/`direction` that drift the texture.
+
+> `grid_dots` marks the intersections and reads as a texture; `grid_lines` is a grid of ruled **lines** and reads as a structure — the one to put behind a chart or a code panel. Config: `cell`, `weight`, `color`, plus `major_every`/`major_weight` for the graph-paper effect.
+
+## Composants disponibles (60)
+
+### Basiques
+`text`, `shape`, `image`, `icon`, `svg`, `video`, `gif`, `caption`, `rich_text`, `gradient_text`
+
+### Conteneurs
+`card`, `flex`, `grid`, `div` (alias de `container`), `container`, `positioned`
+
+> `div` = layout pur sans décoration visuelle (HTML `
`). `card` = même chose mais avec fond/border-radius/ombre attendus.
+
+### Data Visualization
+- `chart` — 12 types: bar, line, pie, donut, horizontal_bar, area, stacked_bar, radar, scatter, radial_bar, funnel, waterfall. Supporte axes/grilles/labels.
+- `gauge` — jauge semi-circulaire pour KPIs
+- `sparkline` — mini-chart inline sans axes
+- `stat` — carte KPI composite (valeur + label + tendance + sparkline)
+- `heatmap` — grille colorée type GitHub contributions
+- `treemap` — rectangles proportionnels (slice-and-dice)
+- `dot_map` — carte mondiale en dot-pattern avec points de données, pulse, lat/lng
+- `progress` — barre linéaire ou circulaire
+- `counter` — compteur animé (standalone uniquement, pas dans les cards)
+- `number_wheel` — digits that scroll like a mechanical odometer and land on the figure. Not to be confused with `counter`, which interpolates a value and rewrites the number (its glyphs jump). See [rules/number-wheel.md](.claude/skills/rustmotion/rules/number-wheel.md).
+- `table` — tableau avec column_widths, column_align, cell_padding, show_borders
+
+### UI Components
+- `badge` — pill avec icon, dot indicator, pulse animation, count badge
+- `avatar` / `avatar_group` — avatar circulaire / groupe empilé avec "+N"
+- `switch` — toggle animé on/off avec toggle_at
+- `slider` — curseur horizontal animé avec animate_to/animate_at
+- `rating` — étoiles avec remplissage partiel animé
+- `kbd` — touche clavier visuelle (effet 3D)
+- `tooltip` — label flottant avec flèche directionnelle
+- `notification` — toast fade-in/out avec stack push (info/success/warning/error)
+- `pill_nav` — tabs avec pill indicator animé entre onglets
+- `list` — liste bullet/numbered/checklist avec icônes
+- `stepper` — étapes numérotées connectées avec progression animée
+- `comparison` — vue avant/après avec divider animé
+- `countdown` — timer digital flip-clock style
+- `marquee` — texte défilant continu
+- `skeleton` — placeholder de chargement avec shimmer (rectangle/circle/text)
+- `tag_cloud` — nuage de mots avec tailles pondérées
+- `callout` — bulle avec flèche
+- `divider` — séparateur visuel
+- `success_check` — a checkmark that draws itself inside a halo, with a pop and a settling rotation
+- `pointer` — a simulated **mouse** cursor (arrow + click ring) following waypoints. `cursor` is a text caret, not this. See [rules/pointer-walkthrough.md](.claude/skills/rustmotion/rules/pointer-walkthrough.md).
+
+### Code & Terminal
+- `codeblock` — code syntax-highlighted avec reveal, diff mode (`diff: true`), state transitions
+- `terminal` — terminal avec chrome macOS, reveal typewriter + curseur clignotant
+
+### Diagrammes
+`arrow`, `connector`, `timeline`, `line`
+
+> Pour faire suivre une trajectoire à un composant, utilise l'effet d'animation `motion_path` (données de chemin SVG, orientation optionnelle selon la tangente) plutôt que d'empiler des `translate`. Voir [rules/motion-path.md](.claude/skills/rustmotion/rules/motion-path.md).
+
+### Média
+`mockup`, `lottie`, `cursor`, `particle`, `qr_code`
+
+### Audio
+- `waveform` — visualisation d'onde audio réactive au volume de la piste
+- `audio_spectrum` — barres de spectre audio réactives (FFT)
+
+> Voir [rules/audio-reactive.md](.claude/skills/rustmotion/rules/audio-reactive.md) pour lier un composant à une piste `audio` via `style.audio-reactive`.
+
+## Text finishes
+
+Four mechanisms that each used to demand a hand-assembled sub-tree. Details and pitfalls in [rules/text-polish.md](.claude/skills/rustmotion/rules/text-polish.md).
+
+- **`shimmer`** — animation effect: a band of light sweeps over the element. Composited `SrcATop` inside the node's layer, so it only lights up **pixels that are actually painted** (the glyphs, not the box). Combined with `char_blur_in`, this is "text stagger".
+- **`text.states` + `text.swap`** — a label that becomes another one: the outgoing one rises while blurring, the incoming one rises from below while unblurring. The box is measured on the **longest** label, not the first one.
+- **`text.caret`** — a caret (`line` or `block`) pinned to a `typewriter`'s reveal head. A `cursor` composited alongside would stay where it was placed while the text grows underneath it.
+- **`pop_in`** — a two-beat entry preset: a back-out scale that *places* the element, then a short elastic pulse that draws the eye back to it.
+
+The seven `char_*` presets are tuned via `direction` (up/down/left/right), `distance`, `scale_from`, `jitter`+`seed` (deterministic stagger irregularity) and `ink_from` (each unit's starting colour). See [rules/char-animation-tuning.md](.claude/skills/rustmotion/rules/char-animation-tuning.md) and, for tokens arriving, [rules/streaming-text.md](.claude/skills/rustmotion/rules/streaming-text.md).
+
+> If you're asked for an effect named in the Hyperframes vocabulary ("streaming text", "number wheel", "badge pop", "top-down letters"…), check the mapping table first: [rules/hyperframes-mapping.md](.claude/skills/rustmotion/rules/hyperframes-mapping.md).
+
+## Architecture
+
+### Render Pipeline (CSS engine)
+
+Le moteur utilise un pipeline **box_tree → layout_pass → paint_pass** inspiré des navigateurs web :
+
+1. **box_tree** (`box_builder.rs`) — construit un arbre de `BoxNode { css: CssStyle, children, intrinsic }` depuis les composants JSON résolus
+2. **layout_pass** (`engine/layout_pass.rs`) — orchestre taffy pour calculer les `BoxLayout { x, y, width, height }` de chaque nœud. Les feuilles avec un `IntrinsicMeasure` (texte, image, codeblock) sont mesurées via une `measure_fn`.
+3. **paint_pass** (`engine/paint_pass.rs`) — descend l'arbre, applique transform/opacity, peint les décorations (background, border, shadow), délègue au `Painter` du composant pour le contenu.
+
+Chaque composant implémente le trait `Painter` :
+
+```rust
+pub trait Painter {
+ fn paint_content(&self, canvas: &Canvas, layout: &BoxLayout, props: &AnimatedProperties, ctx: &PaintCtx);
+ fn intrinsic_size(&self, available: AvailableSize, ctx: &MeasureCtx) -> Option<(f32, f32)> { None }
+}
+```
+
+`PaintCtx` contient : `time`, `scene_duration`, `fps`, `frame_index`, `video_width`, `video_height`, `stagger_offset`.
+
+### Structure des crates
+
+```
+crates/
+├── rustmotion-core/src/
+│ ├── css/ # Modèle CSS
+│ │ ├── style.rs # CssStyle (propriétés CSS kebab-case)
+│ │ ├── units.rs # Length, LengthPercentage (px, %, em, rem, vw, vh)
+│ │ ├── cascade.rs # Héritage color/font-* parent → enfant
+│ │ ├── taffy_bridge.rs # CssStyle → taffy::Style
+│ │ └── animation.rs # Résolution des animations → override CssStyle
+│ ├── engine/
+│ │ ├── box_tree.rs # BoxNode, BoxKind, IntrinsicMeasure
+│ │ ├── layout_pass.rs # Orchestration taffy, BoxLayout résultant
+│ │ ├── paint_pass.rs # Walk top-down, décorations, dispatch Painter
+│ │ ├── animator.rs # Résolution animations, easing, spring solver
+│ │ ├── transition.rs # Transitions entre scènes
+│ │ ├── renderer/ # Primitives Skia (colors, fonts, shapes, text)
+│ │ └── text/cosmic.rs # Bridge cosmic-text — PAS branché sur le rendu réel
+│ ├── schema/ # Modèles de données JSON
+│ │ ├── scenario.rs # Scenario, ResolvedScenario, View, Scene, VideoConfig
+│ │ ├── style.rs # Specialized types (CardBorder, CardShadow, Fill, etc.)
+│ │ ├── background.rs # AnimatedBackground, BackgroundPreset
+│ │ ├── animation.rs # EasingType, AnimationPreset, PresetConfig
+│ │ ├── codeblock_types.rs # CodeblockChrome, CodeblockState
+│ │ └── video.rs # AnimationEffect, Size, ShapeType, Stroke
+│ └── traits/
+│ ├── painter.rs # Painter trait + PaintCtx + AvailableSize + MeasureCtx
+│ ├── animatable.rs # Animatable trait
+│ ├── timed.rs # Timed trait + TimingConfig
+│ └── styled.rs # Styled trait
+│
+├── rustmotion-components/src/
+│ ├── lib.rs # Enum Component + dispatch (as_painter, as_animatable, etc.)
+│ ├── box_builder.rs # build_scene() → BuiltScene (components + stagger_delays)
+│ ├── intrinsic.rs # TextIntrinsic, BadgeIntrinsic, CounterIntrinsic, etc.
+│ ├── legacy_dispatch.rs # LegacyPaintDispatcher (bridge NodeId → Painter)
+│ ├── chart/ # 10 fichiers (mod + bar/line/pie/radar/scatter/radial/funnel/waterfall/axes)
+│ └── *.rs # Un fichier par composant (impl Painter)
+│
+└── rustmotion/src/
+ ├── cli/ # Le binaire `rustmotion` (clap + sous-commandes)
+ │ └── commands/ # validate, render, schema, info
+ ├── encode/ # Encodeurs vidéo/audio, mux
+ └── loader.rs # Chargement JSON/HTML → ResolvedScenario
+```
+
+> Le binaire vit dans la crate publiée `rustmotion` : une crate qui n'a qu'une
+> `[lib]` n'installe rien d'exécutable via `cargo install`. Il n'y a pas de
+> sous-commande `studio` — `rustmotion-studio` dépend de `rustmotion`, donc la
+> dépendance inverse serait un cycle. Le studio s'ouvre par son propre binaire.
+
+### Ajouter un nouveau composant
+
+1. Créer `crates/rustmotion-components/src/mon_composant.rs` avec struct serde + `impl Painter` (`paint_content`)
+2. Ajouter `rustmotion_core::impl_traits!(MonComposant { Animatable => animation, Timed => timing, Styled => style });`
+3. Ajouter le variant dans l'enum `Component` dans `lib.rs`
+4. Ajouter les match arms dans les méthodes de dispatch (`as_painter`, `as_animatable`, `as_timed`, `as_styled`)
+5. Ajouter `pub mod mon_composant;` et `pub use mon_composant::MonComposant;` dans `lib.rs`
+6. Si le composant a une taille fixe: la déclarer via apply_intrinsic_overrides dans box_builder.rs
+7. Si le composant mesure son propre contenu : ajouter `XxxIntrinsic` dans `intrinsic.rs`
+
+### Tests
+
+```bash
+cargo test --workspace # ~200 tests (layout + serde round-trip + pixel regressions + smoke)
+cargo check # Vérification compilation
+rustmotion validate file.json # Validation scénario
+```
diff --git a/crates/rustmotion/Cargo.toml b/crates/rustmotion/Cargo.toml
index fad36df..eb212f5 100644
--- a/crates/rustmotion/Cargo.toml
+++ b/crates/rustmotion/Cargo.toml
@@ -9,6 +9,16 @@ repository = "https://github.com/LeadcodeDev/rustmotion"
[lib]
path = "src/lib.rs"
+# Le binaire vit ici, et pas dans une crate à part, parce que c'est la seule
+# façon que `cargo install rustmotion` livre une commande : une crate qui n'a
+# qu'une `[lib]` installe une bibliothèque et rien d'exécutable. La
+# sous-commande `studio` ne peut pas y être : `rustmotion-studio` dépend de
+# `rustmotion`, donc la dépendance inverse — même optionnelle — est un cycle
+# que cargo refuse. Le studio garde son propre binaire `rustmotion-studio`.
+[[bin]]
+name = "rustmotion"
+path = "src/bin/main.rs"
+
[dependencies]
rustmotion-core.workspace = true
rustmotion-components = { workspace = true, features = ["lottie-native"] }
@@ -16,6 +26,15 @@ rustmotion-html.workspace = true
skia-safe = "0.82"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+clap = { version = "4", features = ["derive"] }
+clap_complete = "4.6.0"
+ratatui = "0.29"
+crossterm = "0.28"
+dirs = "6.0.0"
+# Le CLI produit le JSON Schema (`rustmotion schema`) et s'en sert pour la
+# détection d'attributs inconnus : schemars passe donc de dev-dependency à
+# dépendance normale.
+schemars = "0.8"
openh264 = "0.6"
minimp4 = { version = "0.1", features = ["aac"] }
gif = "0.13"
@@ -38,6 +57,3 @@ ffmpeg_integration = []
## Re-expose native Lottie decoding (default-on). Activating this feature here
## propagates to rustmotion-components so consumers only need to touch rustmotion.
lottie-native = ["rustmotion-components/lottie-native"]
-
-[dev-dependencies]
-schemars = "0.8"
diff --git a/crates/rustmotion-cli/build.rs b/crates/rustmotion/build.rs
similarity index 81%
rename from crates/rustmotion-cli/build.rs
rename to crates/rustmotion/build.rs
index 2525a63..a815fd5 100644
--- a/crates/rustmotion-cli/build.rs
+++ b/crates/rustmotion/build.rs
@@ -1,5 +1,5 @@
//! Generates the `SKILL_FILES` table embedded into the `rustmotion` binary by
-//! `src/skills.rs`.
+//! `src/cli/skills.rs`.
//!
//! `rustmotion skills install` is the only channel through which an LLM agent
//! receives the generation rules under `.claude/skills/rustmotion/`. Before
@@ -42,19 +42,13 @@ fn collect_md_files(dir: &Path, out: &mut Vec
) {
fn main() {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo"));
- // crates/rustmotion-cli -> crates ->
- let workspace_root = manifest_dir
- .parent()
- .and_then(Path::parent)
- .unwrap_or_else(|| {
- panic!(
- "expected {} to live at /crates/rustmotion-cli",
- manifest_dir.display()
- )
- })
- .to_path_buf();
- let skills_root = workspace_root.join(".claude/skills/rustmotion");
+ // L'arbre des skills vit DANS la crate, et `.claude/skills/rustmotion` à la
+ // racine du dépôt n'en est qu'un lien symbolique. C'est ce qui permet à
+ // `cargo package` de l'embarquer : il n'archive que ce qui est sous le
+ // répertoire du paquet, et une lecture hors de ce périmètre produit un
+ // tarball qui ne compile chez personne.
+ let skills_root = manifest_dir.join("skills");
// Rerun whenever any file under the skills tree is added, removed, or
// edited — Cargo scans directories given to rerun-if-changed recursively.
@@ -84,10 +78,14 @@ fn main() {
let mut generated = String::from("&[\n");
for path in &all_files {
- let rel_path = path
- .strip_prefix(&workspace_root)
- .unwrap_or(path)
+ // Le chemin embarqué est celui que `skills install` écrira chez
+ // l'utilisateur, pas celui d'où le fichier vient : la source a beau
+ // vivre dans la crate, la destination reste l'emplacement que Claude
+ // Code sait découvrir.
+ let rel_path = Path::new(".claude/skills/rustmotion")
+ .join(path.strip_prefix(&skills_root).unwrap_or(path))
.to_str()
+ .map(str::to_owned)
.unwrap_or_else(|| panic!("non-UTF-8 skill file path: {}", path.display()))
.replace('\\', "/"); // normalize separators if built on Windows
let abs_path = path
diff --git a/.claude/skills/rustmotion/SKILL.md b/crates/rustmotion/skills/SKILL.md
similarity index 100%
rename from .claude/skills/rustmotion/SKILL.md
rename to crates/rustmotion/skills/SKILL.md
diff --git a/.claude/skills/rustmotion/rules/1600-brutalist-style.md b/crates/rustmotion/skills/rules/1600-brutalist-style.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/1600-brutalist-style.md
rename to crates/rustmotion/skills/rules/1600-brutalist-style.md
diff --git a/.claude/skills/rustmotion/rules/3d-perspective.md b/crates/rustmotion/skills/rules/3d-perspective.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/3d-perspective.md
rename to crates/rustmotion/skills/rules/3d-perspective.md
diff --git a/.claude/skills/rustmotion/rules/animation-completion-budget.md b/crates/rustmotion/skills/rules/animation-completion-budget.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/animation-completion-budget.md
rename to crates/rustmotion/skills/rules/animation-completion-budget.md
diff --git a/.claude/skills/rustmotion/rules/audio-reactive.md b/crates/rustmotion/skills/rules/audio-reactive.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/audio-reactive.md
rename to crates/rustmotion/skills/rules/audio-reactive.md
diff --git a/.claude/skills/rustmotion/rules/badge-video-sizing.md b/crates/rustmotion/skills/rules/badge-video-sizing.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/badge-video-sizing.md
rename to crates/rustmotion/skills/rules/badge-video-sizing.md
diff --git a/.claude/skills/rustmotion/rules/captions-workflow.md b/crates/rustmotion/skills/rules/captions-workflow.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/captions-workflow.md
rename to crates/rustmotion/skills/rules/captions-workflow.md
diff --git a/.claude/skills/rustmotion/rules/card-flex-layout.md b/crates/rustmotion/skills/rules/card-flex-layout.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/card-flex-layout.md
rename to crates/rustmotion/skills/rules/card-flex-layout.md
diff --git a/.claude/skills/rustmotion/rules/card-parallax.md b/crates/rustmotion/skills/rules/card-parallax.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/card-parallax.md
rename to crates/rustmotion/skills/rules/card-parallax.md
diff --git a/.claude/skills/rustmotion/rules/card-resize.md b/crates/rustmotion/skills/rules/card-resize.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/card-resize.md
rename to crates/rustmotion/skills/rules/card-resize.md
diff --git a/.claude/skills/rustmotion/rules/char-animation-tuning.md b/crates/rustmotion/skills/rules/char-animation-tuning.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/char-animation-tuning.md
rename to crates/rustmotion/skills/rules/char-animation-tuning.md
diff --git a/.claude/skills/rustmotion/rules/chart-types.md b/crates/rustmotion/skills/rules/chart-types.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/chart-types.md
rename to crates/rustmotion/skills/rules/chart-types.md
diff --git a/.claude/skills/rustmotion/rules/color-palettes.md b/crates/rustmotion/skills/rules/color-palettes.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/color-palettes.md
rename to crates/rustmotion/skills/rules/color-palettes.md
diff --git a/.claude/skills/rustmotion/rules/component-field-placement.md b/crates/rustmotion/skills/rules/component-field-placement.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/component-field-placement.md
rename to crates/rustmotion/skills/rules/component-field-placement.md
diff --git a/.claude/skills/rustmotion/rules/continuous-presets.md b/crates/rustmotion/skills/rules/continuous-presets.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/continuous-presets.md
rename to crates/rustmotion/skills/rules/continuous-presets.md
diff --git a/.claude/skills/rustmotion/rules/counter-standalone.md b/crates/rustmotion/skills/rules/counter-standalone.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/counter-standalone.md
rename to crates/rustmotion/skills/rules/counter-standalone.md
diff --git a/.claude/skills/rustmotion/rules/data-viz-components.md b/crates/rustmotion/skills/rules/data-viz-components.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/data-viz-components.md
rename to crates/rustmotion/skills/rules/data-viz-components.md
diff --git a/.claude/skills/rustmotion/rules/depth-layering.md b/crates/rustmotion/skills/rules/depth-layering.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/depth-layering.md
rename to crates/rustmotion/skills/rules/depth-layering.md
diff --git a/.claude/skills/rustmotion/rules/dot-map-coordinates.md b/crates/rustmotion/skills/rules/dot-map-coordinates.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/dot-map-coordinates.md
rename to crates/rustmotion/skills/rules/dot-map-coordinates.md
diff --git a/.claude/skills/rustmotion/rules/dynamic-depth.md b/crates/rustmotion/skills/rules/dynamic-depth.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/dynamic-depth.md
rename to crates/rustmotion/skills/rules/dynamic-depth.md
diff --git a/.claude/skills/rustmotion/rules/easing-guidelines.md b/crates/rustmotion/skills/rules/easing-guidelines.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/easing-guidelines.md
rename to crates/rustmotion/skills/rules/easing-guidelines.md
diff --git a/.claude/skills/rustmotion/rules/even-dimensions.md b/crates/rustmotion/skills/rules/even-dimensions.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/even-dimensions.md
rename to crates/rustmotion/skills/rules/even-dimensions.md
diff --git a/.claude/skills/rustmotion/rules/geometry-safety.md b/crates/rustmotion/skills/rules/geometry-safety.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/geometry-safety.md
rename to crates/rustmotion/skills/rules/geometry-safety.md
diff --git a/.claude/skills/rustmotion/rules/glassmorphism.md b/crates/rustmotion/skills/rules/glassmorphism.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/glassmorphism.md
rename to crates/rustmotion/skills/rules/glassmorphism.md
diff --git a/.claude/skills/rustmotion/rules/gradient-quality.md b/crates/rustmotion/skills/rules/gradient-quality.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/gradient-quality.md
rename to crates/rustmotion/skills/rules/gradient-quality.md
diff --git a/.claude/skills/rustmotion/rules/grid-card-height.md b/crates/rustmotion/skills/rules/grid-card-height.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/grid-card-height.md
rename to crates/rustmotion/skills/rules/grid-card-height.md
diff --git a/.claude/skills/rustmotion/rules/hex-colors.md b/crates/rustmotion/skills/rules/hex-colors.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/hex-colors.md
rename to crates/rustmotion/skills/rules/hex-colors.md
diff --git a/.claude/skills/rustmotion/rules/html-css-mental-model.md b/crates/rustmotion/skills/rules/html-css-mental-model.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/html-css-mental-model.md
rename to crates/rustmotion/skills/rules/html-css-mental-model.md
diff --git a/.claude/skills/rustmotion/rules/hyperframes-mapping.md b/crates/rustmotion/skills/rules/hyperframes-mapping.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/hyperframes-mapping.md
rename to crates/rustmotion/skills/rules/hyperframes-mapping.md
diff --git a/.claude/skills/rustmotion/rules/icon-format.md b/crates/rustmotion/skills/rules/icon-format.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/icon-format.md
rename to crates/rustmotion/skills/rules/icon-format.md
diff --git a/.claude/skills/rustmotion/rules/icon-sizing-hierarchy.md b/crates/rustmotion/skills/rules/icon-sizing-hierarchy.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/icon-sizing-hierarchy.md
rename to crates/rustmotion/skills/rules/icon-sizing-hierarchy.md
diff --git a/.claude/skills/rustmotion/rules/layer-order.md b/crates/rustmotion/skills/rules/layer-order.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/layer-order.md
rename to crates/rustmotion/skills/rules/layer-order.md
diff --git a/.claude/skills/rustmotion/rules/module-structure.md b/crates/rustmotion/skills/rules/module-structure.md
similarity index 98%
rename from .claude/skills/rustmotion/rules/module-structure.md
rename to crates/rustmotion/skills/rules/module-structure.md
index 6d1ffc5..471d410 100644
--- a/.claude/skills/rustmotion/rules/module-structure.md
+++ b/crates/rustmotion/skills/rules/module-structure.md
@@ -8,7 +8,7 @@ The workspace is split into three crates:
|---|---|
| `rustmotion-core` | Engine, CSS model, schema types, Painter trait |
| `rustmotion-components` | 57 component structs + Painter impls + box builder |
-| `rustmotion-cli` | CLI commands (render, validate, schema, info) |
+| `rustmotion` | Encode/loader pipeline + the `rustmotion` binary (`src/cli/`) |
---
diff --git a/.claude/skills/rustmotion/rules/motion-path.md b/crates/rustmotion/skills/rules/motion-path.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/motion-path.md
rename to crates/rustmotion/skills/rules/motion-path.md
diff --git a/.claude/skills/rustmotion/rules/notification-stacking.md b/crates/rustmotion/skills/rules/notification-stacking.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/notification-stacking.md
rename to crates/rustmotion/skills/rules/notification-stacking.md
diff --git a/.claude/skills/rustmotion/rules/number-wheel.md b/crates/rustmotion/skills/rules/number-wheel.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/number-wheel.md
rename to crates/rustmotion/skills/rules/number-wheel.md
diff --git a/.claude/skills/rustmotion/rules/paint-context.md b/crates/rustmotion/skills/rules/paint-context.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/paint-context.md
rename to crates/rustmotion/skills/rules/paint-context.md
diff --git a/.claude/skills/rustmotion/rules/pixel-product-register.md b/crates/rustmotion/skills/rules/pixel-product-register.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/pixel-product-register.md
rename to crates/rustmotion/skills/rules/pixel-product-register.md
diff --git a/.claude/skills/rustmotion/rules/pointer-walkthrough.md b/crates/rustmotion/skills/rules/pointer-walkthrough.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/pointer-walkthrough.md
rename to crates/rustmotion/skills/rules/pointer-walkthrough.md
diff --git a/.claude/skills/rustmotion/rules/prefer-presets.md b/crates/rustmotion/skills/rules/prefer-presets.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/prefer-presets.md
rename to crates/rustmotion/skills/rules/prefer-presets.md
diff --git a/.claude/skills/rustmotion/rules/responsive-device-sizing.md b/crates/rustmotion/skills/rules/responsive-device-sizing.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/responsive-device-sizing.md
rename to crates/rustmotion/skills/rules/responsive-device-sizing.md
diff --git a/.claude/skills/rustmotion/rules/scene-pacing.md b/crates/rustmotion/skills/rules/scene-pacing.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/scene-pacing.md
rename to crates/rustmotion/skills/rules/scene-pacing.md
diff --git a/.claude/skills/rustmotion/rules/stagger-animations.md b/crates/rustmotion/skills/rules/stagger-animations.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/stagger-animations.md
rename to crates/rustmotion/skills/rules/stagger-animations.md
diff --git a/.claude/skills/rustmotion/rules/stat-cards.md b/crates/rustmotion/skills/rules/stat-cards.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/stat-cards.md
rename to crates/rustmotion/skills/rules/stat-cards.md
diff --git a/.claude/skills/rustmotion/rules/streaming-text.md b/crates/rustmotion/skills/rules/streaming-text.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/streaming-text.md
rename to crates/rustmotion/skills/rules/streaming-text.md
diff --git a/.claude/skills/rustmotion/rules/templates-and-iteration.md b/crates/rustmotion/skills/rules/templates-and-iteration.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/templates-and-iteration.md
rename to crates/rustmotion/skills/rules/templates-and-iteration.md
diff --git a/.claude/skills/rustmotion/rules/text-background.md b/crates/rustmotion/skills/rules/text-background.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/text-background.md
rename to crates/rustmotion/skills/rules/text-background.md
diff --git a/.claude/skills/rustmotion/rules/text-polish.md b/crates/rustmotion/skills/rules/text-polish.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/text-polish.md
rename to crates/rustmotion/skills/rules/text-polish.md
diff --git a/.claude/skills/rustmotion/rules/time-remapping.md b/crates/rustmotion/skills/rules/time-remapping.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/time-remapping.md
rename to crates/rustmotion/skills/rules/time-remapping.md
diff --git a/.claude/skills/rustmotion/rules/timeline-sequencing.md b/crates/rustmotion/skills/rules/timeline-sequencing.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/timeline-sequencing.md
rename to crates/rustmotion/skills/rules/timeline-sequencing.md
diff --git a/.claude/skills/rustmotion/rules/timing-constraints.md b/crates/rustmotion/skills/rules/timing-constraints.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/timing-constraints.md
rename to crates/rustmotion/skills/rules/timing-constraints.md
diff --git a/.claude/skills/rustmotion/rules/typography-readability.md b/crates/rustmotion/skills/rules/typography-readability.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/typography-readability.md
rename to crates/rustmotion/skills/rules/typography-readability.md
diff --git a/.claude/skills/rustmotion/rules/ui-controls.md b/crates/rustmotion/skills/rules/ui-controls.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/ui-controls.md
rename to crates/rustmotion/skills/rules/ui-controls.md
diff --git a/.claude/skills/rustmotion/rules/validate-json.md b/crates/rustmotion/skills/rules/validate-json.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/validate-json.md
rename to crates/rustmotion/skills/rules/validate-json.md
diff --git a/.claude/skills/rustmotion/rules/vertical-align.md b/crates/rustmotion/skills/rules/vertical-align.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/vertical-align.md
rename to crates/rustmotion/skills/rules/vertical-align.md
diff --git a/.claude/skills/rustmotion/rules/video-wizard.md b/crates/rustmotion/skills/rules/video-wizard.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/video-wizard.md
rename to crates/rustmotion/skills/rules/video-wizard.md
diff --git a/.claude/skills/rustmotion/rules/wiggle-additive.md b/crates/rustmotion/skills/rules/wiggle-additive.md
similarity index 100%
rename from .claude/skills/rustmotion/rules/wiggle-additive.md
rename to crates/rustmotion/skills/rules/wiggle-additive.md
diff --git a/.claude/skills/rustmotion/rules/world-view.md b/crates/rustmotion/skills/rules/world-view.md
similarity index 99%
rename from .claude/skills/rustmotion/rules/world-view.md
rename to crates/rustmotion/skills/rules/world-view.md
index cd70893..5ccb02c 100644
--- a/.claude/skills/rustmotion/rules/world-view.md
+++ b/crates/rustmotion/skills/rules/world-view.md
@@ -70,7 +70,7 @@ Only set `world-position` explicitly when the path isn't a straight horizontal f
A scene's `children` are laid out exactly like a `slide`-view scene: `0..video_width` × `0..video_height`, independent of that scene's `world-position`. The camera translation is applied to the whole scene as one block, after layout — a child never needs to know where its scene sits in world space.
-This was verified against the geometry validator (`crates/rustmotion-cli/src/commands/geometry.rs`, `validate_geometry`): it checks every scene's children against `(scenario.video.width, scenario.video.height)` regardless of that scene's `world-position`. Placing a child at what looks like the world-space X (e.g. `2880` for the second beat) is rejected as viewport overflow:
+This was verified against the geometry validator (`crates/rustmotion/src/cli/commands/geometry.rs`, `validate_geometry`): it checks every scene's children against `(scenario.video.width, scenario.video.height)` regardless of that scene's `world-position`. Placing a child at what looks like the world-space X (e.g. `2880` for the second beat) is rejected as viewport overflow:
```json
// Scene 2 has "world-position": {"x": 2880, ...}. A child positioned at
diff --git a/crates/rustmotion-cli/src/bin/main.rs b/crates/rustmotion/src/bin/main.rs
similarity index 65%
rename from crates/rustmotion-cli/src/bin/main.rs
rename to crates/rustmotion/src/bin/main.rs
index d29d970..e00d27e 100644
--- a/crates/rustmotion-cli/src/bin/main.rs
+++ b/crates/rustmotion/src/bin/main.rs
@@ -1,5 +1,5 @@
fn main() {
- if let Err(e) = rustmotion_cli::run() {
+ if let Err(e) = rustmotion::cli::run() {
eprintln!("Error: {}", e);
std::process::exit(1);
}
diff --git a/crates/rustmotion-cli/src/claude_md.rs b/crates/rustmotion/src/cli/claude_md.rs
similarity index 100%
rename from crates/rustmotion-cli/src/claude_md.rs
rename to crates/rustmotion/src/cli/claude_md.rs
diff --git a/crates/rustmotion-cli/src/commands/batch.rs b/crates/rustmotion/src/cli/commands/batch.rs
similarity index 99%
rename from crates/rustmotion-cli/src/commands/batch.rs
rename to crates/rustmotion/src/cli/commands/batch.rs
index 1220601..b8ec364 100644
--- a/crates/rustmotion-cli/src/commands/batch.rs
+++ b/crates/rustmotion/src/cli/commands/batch.rs
@@ -22,8 +22,8 @@ use std::collections::HashMap;
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex};
-use crate::commands::render::cmd_render;
-use crate::commands::validation::{self, ValidationSource};
+use crate::cli::commands::render::cmd_render;
+use crate::cli::commands::validation::{self, ValidationSource};
/// One row parsed from the JSONL data file.
struct BatchRow {
@@ -872,7 +872,7 @@ mod batch_integration_tests {
/// because `std::env::set_var`/`remove_var` are `unsafe`: the standard
/// library only guarantees soundness when nothing else in the process
/// reads or writes the environment concurrently. This is the only test
- /// file in `rustmotion-cli` whose tests spawn a real (non `png-seq` /
+ /// file in the CLI whose tests spawn a real (non `png-seq` /
/// `gif` / `raw`) video encode — every other test in this crate's test
/// binary never reads `PATH` — so this lock only needs to protect
/// against concurrent runs of tests within this file.
diff --git a/crates/rustmotion-cli/src/commands/captions.rs b/crates/rustmotion/src/cli/commands/captions.rs
similarity index 100%
rename from crates/rustmotion-cli/src/commands/captions.rs
rename to crates/rustmotion/src/cli/commands/captions.rs
diff --git a/crates/rustmotion-cli/src/commands/geometry.rs b/crates/rustmotion/src/cli/commands/geometry.rs
similarity index 99%
rename from crates/rustmotion-cli/src/commands/geometry.rs
rename to crates/rustmotion/src/cli/commands/geometry.rs
index 707f741..929f592 100644
--- a/crates/rustmotion-cli/src/commands/geometry.rs
+++ b/crates/rustmotion/src/cli/commands/geometry.rs
@@ -4122,7 +4122,7 @@ mod legibility_tests {
#[test]
fn legibility_never_blocks_validation() {
- use crate::commands::validate_schema::validate_scenario;
+ use crate::cli::commands::validate_schema::validate_scenario;
let json = r##"{
"video": { "width": 1920, "height": 1080 },
"scenes": [{
diff --git a/crates/rustmotion-cli/src/commands/info.rs b/crates/rustmotion/src/cli/commands/info.rs
similarity index 100%
rename from crates/rustmotion-cli/src/commands/info.rs
rename to crates/rustmotion/src/cli/commands/info.rs
diff --git a/crates/rustmotion-cli/src/commands/mod.rs b/crates/rustmotion/src/cli/commands/mod.rs
similarity index 100%
rename from crates/rustmotion-cli/src/commands/mod.rs
rename to crates/rustmotion/src/cli/commands/mod.rs
diff --git a/crates/rustmotion-cli/src/commands/render.rs b/crates/rustmotion/src/cli/commands/render.rs
similarity index 99%
rename from crates/rustmotion-cli/src/commands/render.rs
rename to crates/rustmotion/src/cli/commands/render.rs
index 3172bbb..2c6a11d 100644
--- a/crates/rustmotion-cli/src/commands/render.rs
+++ b/crates/rustmotion/src/cli/commands/render.rs
@@ -1,12 +1,12 @@
-use crate::tui;
-use crate::OutputFormat;
+use crate::cli::tui;
+use crate::cli::OutputFormat;
use rustmotion::encode;
use rustmotion::engine;
use rustmotion::error::{Result, RustmotionError};
use rustmotion::schema::ResolvedScenario;
use std::path::{Path, PathBuf};
-use crate::commands::validation::{self, ValidationSource};
+use crate::cli::commands::validation::{self, ValidationSource};
/// Load + validate a scenario for watch mode. On validation failure prints the
/// report and returns the typed error so the caller can decide how to handle it.
diff --git a/crates/rustmotion-cli/src/commands/schema.rs b/crates/rustmotion/src/cli/commands/schema.rs
similarity index 100%
rename from crates/rustmotion-cli/src/commands/schema.rs
rename to crates/rustmotion/src/cli/commands/schema.rs
diff --git a/crates/rustmotion-cli/src/commands/still.rs b/crates/rustmotion/src/cli/commands/still.rs
similarity index 100%
rename from crates/rustmotion-cli/src/commands/still.rs
rename to crates/rustmotion/src/cli/commands/still.rs
diff --git a/crates/rustmotion-cli/src/commands/validate.rs b/crates/rustmotion/src/cli/commands/validate.rs
similarity index 100%
rename from crates/rustmotion-cli/src/commands/validate.rs
rename to crates/rustmotion/src/cli/commands/validate.rs
diff --git a/crates/rustmotion-cli/src/commands/validate_attrs.rs b/crates/rustmotion/src/cli/commands/validate_attrs.rs
similarity index 99%
rename from crates/rustmotion-cli/src/commands/validate_attrs.rs
rename to crates/rustmotion/src/cli/commands/validate_attrs.rs
index ce85c47..cd8c30d 100644
--- a/crates/rustmotion-cli/src/commands/validate_attrs.rs
+++ b/crates/rustmotion/src/cli/commands/validate_attrs.rs
@@ -185,7 +185,7 @@ fn truncate(msg: &str, max: usize) -> String {
#[cfg(test)]
mod tests {
use super::*;
- use crate::commands::validation::{load, run_checks, ValidationSource};
+ use crate::cli::commands::validation::{load, run_checks, ValidationSource};
fn resolved(children: serde_json::Value) -> ResolvedScenario {
let json = serde_json::json!({
diff --git a/crates/rustmotion-cli/src/commands/validate_schema.rs b/crates/rustmotion/src/cli/commands/validate_schema.rs
similarity index 99%
rename from crates/rustmotion-cli/src/commands/validate_schema.rs
rename to crates/rustmotion/src/cli/commands/validate_schema.rs
index 518c4b2..a42a28c 100644
--- a/crates/rustmotion-cli/src/commands/validate_schema.rs
+++ b/crates/rustmotion/src/cli/commands/validate_schema.rs
@@ -983,7 +983,7 @@ mod style_warning_tests {
fn negative_spring_damping_is_an_error() {
// Constat #6: damping < 0 makes the spring solver diverge instead of
// settle (a `SpringConfig` accepts any f64 — nothing in
- // `rustmotion-cli` checked `damping`/`stiffness` before this).
+ // `rustmotion`'s CLI checked `damping`/`stiffness` before this).
let child: ChildComponent = serde_json::from_value(serde_json::json!({
"type": "text",
"content": "hi",
diff --git a/crates/rustmotion-cli/src/commands/validation.rs b/crates/rustmotion/src/cli/commands/validation.rs
similarity index 100%
rename from crates/rustmotion-cli/src/commands/validation.rs
rename to crates/rustmotion/src/cli/commands/validation.rs
diff --git a/crates/rustmotion-cli/src/lib.rs b/crates/rustmotion/src/cli/mod.rs
similarity index 97%
rename from crates/rustmotion-cli/src/lib.rs
rename to crates/rustmotion/src/cli/mod.rs
index ea95cf4..0ca9926 100644
--- a/crates/rustmotion-cli/src/lib.rs
+++ b/crates/rustmotion/src/cli/mod.rs
@@ -1,11 +1,19 @@
+//! Le binaire `rustmotion` : analyse des arguments et aiguillage vers
+//! `commands`.
+//!
+//! Il n'y a pas de sous-commande `studio` ici. Le studio est une app Dioxus
+//! qui dépend de cette crate ; l'appeler depuis ce module ferait dépendre
+//! `rustmotion` de `rustmotion-studio`, donc d'elle-même, et cargo refuse le
+//! cycle. Le studio s'ouvre par son propre binaire, `rustmotion-studio -f
+//! scenario.json`.
+
mod claude_md;
mod commands;
mod skills;
-pub mod tui;
+mod tui;
use clap::{CommandFactory, Parser, Subcommand};
use rustmotion::error::{Result, RustmotionError};
-use rustmotion::loader::load_input;
use rustmotion::schema::ResolvedScenario;
use std::collections::HashMap;
use std::io::Write;
@@ -32,13 +40,6 @@ struct Cli {
#[derive(Subcommand)]
enum Commands {
- /// Open a live preview window with hot-reload
- Studio {
- /// Path to the JSON scenario file
- #[arg(short, long)]
- file: PathBuf,
- },
-
/// Render a JSON scenario to video or a single frame
Render {
/// Path to the JSON scenario file
@@ -651,10 +652,6 @@ pub fn run() -> Result<()> {
}
match cli.command {
- Commands::Studio { file } => match load_input(&file) {
- Ok(scenario) => rustmotion_studio::run_preview(scenario, Some(file), true),
- Err(e) => rustmotion_studio::run_preview_with_error(format!("{}", e), Some(file), true),
- },
Commands::Render {
file,
json,
@@ -884,12 +881,6 @@ pub fn run() -> Result<()> {
"rustmotion",
&mut std::io::stdout(),
);
- clap_complete::generate(
- shell,
- &mut rustmotion_studio::command(),
- "rustmotion-studio",
- &mut std::io::stdout(),
- );
Ok(())
}
CompletionsAction::Install => install_completions(),
@@ -920,12 +911,6 @@ fn install_completions() -> Result<()> {
let mut buf = Vec::new();
clap_complete::generate(shell, &mut Cli::command(), "rustmotion", &mut buf);
- clap_complete::generate(
- shell,
- &mut rustmotion_studio::command(),
- "rustmotion-studio",
- &mut buf,
- );
let completions = String::from_utf8(buf).unwrap();
let home = dirs::home_dir()
diff --git a/crates/rustmotion-cli/src/skills.rs b/crates/rustmotion/src/cli/skills.rs
similarity index 97%
rename from crates/rustmotion-cli/src/skills.rs
rename to crates/rustmotion/src/cli/skills.rs
index 7fbced6..34f3c89 100644
--- a/crates/rustmotion-cli/src/skills.rs
+++ b/crates/rustmotion/src/cli/skills.rs
@@ -10,7 +10,7 @@ struct SkillFile {
}
/// CLAUDE.md project instructions (written at project root).
-const CLAUDE_MD: &str = include_str!("../../../CLAUDE.md");
+const CLAUDE_MD: &str = include_str!("../../CLAUDE.md");
/// All skill files embedded at compile time.
///
@@ -79,7 +79,7 @@ pub fn install(global: bool) -> Result<()> {
if !global {
let claude_path = root.join("CLAUDE.md");
let existing = std::fs::read_to_string(&claude_path).ok();
- let merged = crate::claude_md::merge(existing.as_deref(), CLAUDE_MD);
+ let merged = crate::cli::claude_md::merge(existing.as_deref(), CLAUDE_MD);
if write_if_changed(&claude_path, &merged)? {
written += 1;
} else {
@@ -194,7 +194,7 @@ pub fn uninstall(global: bool) -> Result<()> {
if !global {
let claude_path = root.join("CLAUDE.md");
if let Ok(existing) = std::fs::read_to_string(&claude_path) {
- match crate::claude_md::strip(&existing) {
+ match crate::cli::claude_md::strip(&existing) {
Some(remaining) => {
if remaining != existing {
std::fs::write(&claude_path, remaining)?;
diff --git a/crates/rustmotion-cli/src/tui.rs b/crates/rustmotion/src/cli/tui.rs
similarity index 100%
rename from crates/rustmotion-cli/src/tui.rs
rename to crates/rustmotion/src/cli/tui.rs
diff --git a/crates/rustmotion/src/encode/audio.rs b/crates/rustmotion/src/encode/audio.rs
index 4752fc9..85f0979 100644
--- a/crates/rustmotion/src/encode/audio.rs
+++ b/crates/rustmotion/src/encode/audio.rs
@@ -808,7 +808,7 @@ mod tests {
// ── media-io: probe_audio_metadata ──────────────────────────────────────
//
- // `rustmotion info` (crates/rustmotion-cli/src/commands/info.rs) needs
+ // `rustmotion info` (crates/rustmotion/src/cli/commands/info.rs) needs
// "how long is this audio file, at what rate/channel count" for every
// `audio[].src` a scenario declares. The brief for that fix is explicit:
// reuse `decode_audio_file` — the exact decode `mix_audio_tracks_segment`
diff --git a/crates/rustmotion/src/lib.rs b/crates/rustmotion/src/lib.rs
index d7aba3c..70a89d6 100644
--- a/crates/rustmotion/src/lib.rs
+++ b/crates/rustmotion/src/lib.rs
@@ -1,3 +1,10 @@
+// `cli` était une crate séparée qui consommait celle-ci sous le nom
+// `rustmotion::`. Cet alias garde ces chemins valides depuis l'intérieur, ce
+// qui laisse le module CLI écrire `rustmotion::schema::…` exactement comme
+// n'importe quel consommateur externe — sans quoi il faudrait réécrire des
+// centaines de chemins en `crate::` pour un déplacement de fichiers.
+extern crate self as rustmotion;
+
// Re-export core and components for downstream users
pub use rustmotion_core as core;
pub use rustmotion_core::error;
@@ -19,6 +26,7 @@ pub mod engine {
// Local modules
pub mod assets;
+pub mod cli;
pub mod encode;
pub mod include;
pub mod loader;
diff --git a/crates/rustmotion-cli/tests/motion_path_strict_anim.rs b/crates/rustmotion/tests/motion_path_strict_anim.rs
similarity index 97%
rename from crates/rustmotion-cli/tests/motion_path_strict_anim.rs
rename to crates/rustmotion/tests/motion_path_strict_anim.rs
index 99df072..315b992 100644
--- a/crates/rustmotion-cli/tests/motion_path_strict_anim.rs
+++ b/crates/rustmotion/tests/motion_path_strict_anim.rs
@@ -13,8 +13,8 @@
//! viewport while following its curve would pass validation silently. This
//! test is run against the actual compiled `rustmotion` binary (not an
//! internal call into `validate_geometry_animated`) because
-//! `rustmotion-cli::commands` is a private module (`mod commands;` in
-//! `src/lib.rs`) — the CLI subprocess is the only externally-observable
+//! `rustmotion::cli::commands` is a private module (`mod commands;` in
+//! `src/cli/mod.rs`) — the CLI subprocess is the only externally-observable
//! contract for "`--strict-anim` sees this".
//!
//! Three scenarios, each isolating one variable:
@@ -36,7 +36,7 @@ struct ScratchFile(PathBuf);
impl ScratchFile {
fn new(label: &str) -> Self {
let unique = format!(
- "rustmotion-cli-motion-path-test-{label}-{}-{}",
+ "rustmotion-motion-path-test-{label}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
diff --git a/crates/rustmotion-cli/tests/skill_files_match_disk.rs b/crates/rustmotion/tests/skill_files_match_disk.rs
similarity index 96%
rename from crates/rustmotion-cli/tests/skill_files_match_disk.rs
rename to crates/rustmotion/tests/skill_files_match_disk.rs
index 98f4bf5..9efc3be 100644
--- a/crates/rustmotion-cli/tests/skill_files_match_disk.rs
+++ b/crates/rustmotion/tests/skill_files_match_disk.rs
@@ -1,6 +1,6 @@
//! Guards `rustmotion skills install` against silently dropping rule files.
//!
-//! `SKILL_FILES` (embedded by `build.rs`, see `src/skills.rs`) is meant to
+//! `SKILL_FILES` (embedded by `build.rs`, see `src/cli/skills.rs`) is meant to
//! mirror every `.md` file under `.claude/skills/rustmotion/` exactly.
//! Before `build.rs` existed, that table was a hand-maintained literal list
//! that fell 17 files behind disk without ever failing a build or a test
@@ -18,12 +18,12 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
-/// `crates/rustmotion-cli` -> `crates` -> ``.
+/// `crates/rustmotion` -> `crates` -> ``.
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
- .expect("rustmotion-cli is expected at /crates/rustmotion-cli")
+ .expect("rustmotion is expected at /crates/rustmotion")
.to_path_buf()
}
@@ -60,7 +60,7 @@ struct ScratchDir(PathBuf);
impl ScratchDir {
fn new(label: &str) -> Self {
let unique = format!(
- "rustmotion-cli-test-{label}-{}-{}",
+ "rustmotion-skills-test-{label}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)