canonrs-core 0.1.0

CanonRS core types, traits and primitives
1
[{"number":1,"slug":"types","title":"Component Types","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["components","ssr","state","effects"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Incorrect classification of components in Leptos leads to SSR issues, unsafe browser API usage, and inconsistent state handling. Mixing state, effects, and DOM APIs without clear boundaries breaks hydration and component predictability.","problem":"components mix state, effects, and browser APIs without clear classification","solution":"classify components into pure, stateful, or interactive with strict rules","signals":["hydration errors","unsafe effects","browser api panic"],"search_intent":"how to classify leptos components","keywords":["leptos component types","ssr component classification","leptos state vs effects","interactive component leptos"],"body":"# Classification\n\n| Type                     | State      | Effects | APIs Browser | Exemple         |\n| ------------------------ | ----------- | ------- | ------------ | --------------- |\n| **Type 1 - Pure**        | ❌          | ❌      | ❌           | Label, Badge    |\n| **Type 2 - Stateful**    | ✅ RwSignal | ❌      | ❌           | Toggle, Tabs    |\n| **Type 3 - Interactive** | ✅          | ✅      | ✅           | Popover, Dialog |\n\n## Type 1 - Pure Components\n```rust\n#[component]\npub fn Label(children: Children) -> impl IntoView {\n    view! { <label>{children()}</label> }\n}\n```\n\n### Characteristics:\n- No internal state\n- No side effects\n- SSR-safe by default\n\n## Type 2 - Stateful Components\n```rust\n#[component]\npub fn Toggle(checked: RwSignal<bool>) -> impl IntoView {\n    view! {\n        <button on:click=move |_| checked.update(|v| *v = !*v)>\n            {move || if checked.get() { \"On\" } else { \"Off\" }}\n        </button>\n    }\n}\n```\n\n### Characteristics:\n- Uses `RwSignal` for state\n- No browser APIs\n- No Effects\n\n## Type 3 - Interactive Components\n```rust\n#[component]\npub fn Dialog(open: RwSignal<bool>) -> impl IntoView {\n    #[cfg(target_arch = \"wasm32\")]\n    Effect::new(move |_| {\n        if open.get() {\n            // Focus trap, scroll lock, etc.\n        }\n    });\n    \n    view! { ... }\n}\n```\n\n### Characteristics:\n- Uses Effects with `#[cfg]` guards\n- May use browser APIs (document, window)\n- SSR placeholder when needed\n\n###  Decision Tree\n```\nHas internal state? \n  ├─ No  → Type 1\n  └─ Yes → Uses Effects or Browser APIs?\n            ├─ No  → Type 2\n            └─ Yes → Type 3\n```"},{"number":2,"slug":"ownership","title":"Ownership Rules","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["ownership","signals","storedvalue","closures"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Incorrect ownership handling in Leptos causes move errors, broken reactivity, and invalid closures. Passing non-reactive data or using improper children types leads to rendering inconsistencies and runtime failures.","problem":"improper ownership and reactive patterns break closures and rendering","solution":"use StoredValue, ChildrenFn, and move closures for correct ownership and reactivity","signals":["move error","fnonce error","stale data"],"search_intent":"how to fix leptos ownership errors","keywords":["leptos ownership rules","storedvalue leptos usage","childrenfn leptos reactivity","leptos move closure pattern"],"body":"## Rule #1: StoredValue is DEFAULT\n\n### Wrong\n```rust\nlet label = \"Users\".to_string();\nview! { <span>{label}</span> } // Move error\n```\n\n### Correct\n```rust\nlet label = StoredValue::new(\"Users\".to_string());\nview! { <span>{label.get_value()}</span> }\n```\n\n**When to use StoredValue:**\n- Any non-reactive data passed to closures\n- Props that don't need reactivity\n- Callbacks stored for later use\n\n## Rule #2: ChildrenFn for Re-render\n\n### Wrong\n```rust\n#[component]\npub fn Show(children: Children) -> impl IntoView {\n    // FnOnce - can only render once\n}\n```\n\n### Correct\n```rust\n#[component]\npub fn Show(children: ChildrenFn) -> impl IntoView {\n    // Can re-render when reactive dependencies change\n}\n```\n\n**Use ChildrenFn in:**\n- `Show`, `Suspense`, `Transition`\n- Any component that conditionally renders children\n- Components with reactive visibility\n\n## Rule #3: move || for Reactive Access\n\n### Wrong\n```rust\nview! { <div hidden=is_open.get() /> }\n```\n\n### Correct\n```rust\nview! { <div hidden=move || is_open.get() /> }\n```\n\n**Pattern:**\n- Always wrap signal access in `move ||`\n- Leptos tracks reactive dependencies automatically\n\n## Common Pitfalls\n\n### Callback Cloning\n```rust\n// ❌ WRONG\nlet cb = callback.clone();\nview! { <button on:click=move |_| cb.run() /> }\n\n// ✅ CORRECT\nlet cb = StoredValue::new(callback);\nview! { <button on:click=move |_| cb.get_value().run() /> }\n```\n\n### Arc Sharing\n```rust\n// ✅ CORRECT for shared immutable data\nlet columns = Arc::new(columns);\nlet columns_clone = Arc::clone(&columns);\n```"},{"number":3,"slug":"lists","title":"Lists and Iteration","status":"ENFORCED","severity":"MEDIUM","category":"state-reactivity","tags":["lists","for","storedvalue","iteration"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Improper list rendering in Leptos causes FnOnce errors and hydration mismatches. Using iterators with closures breaks ownership and reactivity, leading to unstable UI behavior.","problem":"using map and collect_view breaks ownership and hydration in lists","solution":"use component isolation with For and place StoredValue inside components","signals":["fnonce error","hydration mismatch","moved callback"],"search_intent":"how to fix leptos list rendering","keywords":["leptos list iteration","leptos for component pattern","storedvalue list leptos","leptos map collect_view error"],"body":"## The FnOnce Hell Problem\n\n### Never Do This\n```rust\nitems.iter().map(|item| {\n    view! { <Item on_click=callback /> } // FnOnce error\n}).collect_view()\n```\n\n### Also Wrong\n```rust\nitems.iter().map(|item| {\n    let val = StoredValue::new(item.value); // Hydration mismatch\n    view! { <Item /> }\n}).collect_view()\n```\n\n## Canonical Solution\n\n### Step 1: Isolated Component\n```rust\n#[component]\nfn ItemComponent(\n    value: String,\n    on_select: Callback<String>,\n) -> impl IntoView {\n    let val = StoredValue::new(value); // ✅ Inside component\n    \n    view! {\n        <div on:click=move |_| on_select.run(val.get_value())>\n            {val.get_value()}\n        </div>\n    }\n}\n```\n\n### Step 2: Use <For>\n```rust\n<For\n    each=move || items.get_value()\n    key=|item| item.id.clone()\n    children=move |item| {\n        view! {\n            <ItemComponent\n                value=item.value\n                on_select=callback\n            />\n        }\n    }\n/>\n```\n\n## Pattern Rules\n\n1. **Component Isolation:** Every list item = separate component\n2. **StoredValue Placement:** Inside the component (not in `.map()`)\n3. **<For> Only:** Never use `.iter().map().collect_view()`\n4. **Key Function:** Must be unique and stable\n\n## Advanced: Nested Lists\n```rust\n#[component]\nfn CategoryRow(category: Category, on_select: Callback<String>) -> impl IntoView {\n    let category = StoredValue::new(category);\n    \n    view! {\n        <div>\n            <h3>{category.get_value().name}</h3>\n            <For\n                each=move || category.get_value().items\n                key=|item| item.id\n                children=move |item| {\n                    view! {\n                        <ItemComponent value=item.value on_select=on_select />\n                    }\n                }\n            />\n        </div>\n    }\n}\n```\n\n## Common Mistakes\n\n### Callback in closure\n```rust\n// ❌ WRONG\n<For\n    children=move |item| {\n        view! { <div on:click=callback /> } // Moved\n    }\n/>\n\n// ✅ CORRECT - Component receives callback as prop\n<For\n    children=move |item| {\n        view! { <ItemComponent on_click=callback /> }\n    }\n/>\n```"},{"number":4,"slug":"hydration","title":"Anti-Hydration Rules","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","ssr","for","suspense"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Hydration mismatches occur when SSR output differs from client rendering in Leptos. Conditional rendering, async state, and improper list usage create DOM divergence and runtime errors.","problem":"ssr and client render different html structures causing hydration failure","solution":"ensure identical ssr and client output using suspense, isolated components, and stable props","signals":["hydration panic","text node error","dom mismatch"],"search_intent":"how to fix leptos hydration mismatch","keywords":["leptos hydration rules","ssr client mismatch leptos","leptos suspense usage","leptos for hydration issue"],"body":"## Golden Rule\n**SSR HTML MUST BE IDENTICAL to first WASM render**\n\n## Common Violations\n\n### Violation 1: Conditional with Async State\n```rust\n{if data.is_empty() { \n    view! { \"Loading\" } \n} else { \n    view! { <Table /> } \n}}\n```\n\n**Problem:** SSR renders one thing, client renders another.\n\n### Solution: Suspense\n```rust\n<Suspense fallback=|| \"Loading\">\n    <Resource ... />\n</Suspense>\n```\n\n### Violation 2: For in Dropdown\n```rust\nview! { \n    <DropdownMenuContent>\n        <For each=items ... />\n    </DropdownMenuContent> \n}\n```\n\n**Problem:** <For> renders differently SSR vs client.\n\n### Solution: Component Isolation\n```rust\n#[component]\nfn DropdownItem(value: String) -> impl IntoView {\n    let value = StoredValue::new(value);\n    view! { <div>{value.get_value()}</div> }\n}\n\n<For\n    each=items\n    children=|item| view! { <DropdownItem value=item /> }\n/>\n```\n\n### Violation 3: RwSignal Props\n```rust\n#[component]\nfn DataTable(data: RwSignal<Vec<T>>) -> impl IntoView {\n    // SSR gets empty Vec, client gets populated Vec\n}\n```\n\n### Solution: Vec Props\n```rust\n#[component]\nfn DataTable(data: Vec<T>) -> impl IntoView {\n    // SSR and client get same snapshot\n}\n```\n\n## Checklist\n\n- [ ] `<For>` only with isolated components\n- [ ] `StoredValue` inside component (not in `.map()`)\n- [ ] Use `<Suspense>` for async data\n- [ ] Props: `Vec<T>` not `RwSignal<Vec<T>>`\n- [ ] No `if/else` with async state\n- [ ] No conditional `#[cfg]` inside `view!`\n\n## Debug Hydration Mismatch\n```\nError: failed_to_cast_text_node\nat tachys::hydration\n```\n\n**Steps:**\n1. Find the text node in error (e.g., \"Columns\")\n2. Check if SSR and client render same HTML\n3. Look for:\n   - `<For>` without component\n   - `StoredValue` in `.map()`\n   - Conditional rendering\n   - Effects without `#[cfg]` guard\n\n## SSR Placeholders (Last Resort)\n```rust\nif leptos::is_server() {\n    return view! { <div>\"Server placeholder\"</div> }.into_any();\n}\n\n// Client-only code\nview! { <ComplexInteractive /> }.into_any()\n```\n\n**Use only when:**\n- Component fundamentally incompatible with SSR\n- Performance critical (avoid server work)\n- Third-party JS library integration"},{"number":5,"slug":"ssr-effects","title":"SSR Effects and Browser API Safety","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["ssr","effects","browser api","wasm"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using browser APIs or async runtime features during SSR leads to runtime panics and undefined behavior. Leptos components must isolate client-only logic to prevent crashes and security issues.","problem":"browser apis and effects execute on server causing runtime failures","solution":"guard all browser and async logic with wasm cfg conditions","signals":["window undefined","spawn_local panic","context not found"],"search_intent":"how to prevent leptos ssr panic","keywords":["leptos ssr effects","browser api ssr panic","leptos cfg wasm guard","spawn_local ssr error"],"body":"## Critical Rule\n**BROWSER APIs MUST NEVER RUN ON SERVER**\n\nViolation = Runtime panic / Silent data corruption / Security breach\n\n---\n\n## API Blacklist Server Fatal\n\n### Category A: DOM APIs\n```rust\n// ❌ FORBIDDEN without guard\nwindow()\ndocument()\nweb_sys::Element\nweb_sys::HtmlElement\nlocalStorage\nsessionStorage\nnavigator\nlocation\nhistory\n```\n\n### Category B: Leptos Router (Client-Contextual)\n**Rule:** Router hooks are client-contextual, not SSR-safe APIs.  \nThey require active router context which only exists during client navigation.\n\n```rust\n// ❌ FORBIDDEN in component body\nlet navigate = use_navigate(); // Panic on SSR\nlet params = use_params(); // Context not available\n\n// ✅ CORRECT: Inside event handler\nview! {\n    <button on:click=move |_| {\n        let navigate = use_navigate(); // Safe in client event\n        navigate(\"/home\");\n    }>\n}\n```\n\n### Category C: Async Runtime\n```rust\n// ❌ FORBIDDEN without guard\nleptos::task::spawn_local(async { ... }); // Panic on SSR\nwasm_bindgen_futures::spawn_local(async { ... });\n\n// ✅ CORRECT\n#[cfg(target_arch = \"wasm32\")]\nleptos::task::spawn_local(async { ... });\n```\n\n### Category D: JS Interop\n```rust\n// ❌ FORBIDDEN\n#[wasm_bindgen]\nextern \"C\" {\n    fn myJsFunction(); // Undefined on server\n}\n\n// ✅ CORRECT\n#[cfg(target_arch = \"wasm32\")]\n{\n    myJsFunction();\n}\n```\n\n---\n\n## Guard Patterns Mandatory\n\n### Pattern #1: Effect Guard (Most Common)\n```rust\n// ❌ WRONG - Runs on server!\nEffect::new(move |_| {\n    let elem = document().get_element_by_id(\"root\");\n});\n\n// ✅ CORRECT\n#[cfg(target_arch = \"wasm32\")]\nEffect::new(move |_| {\n    let elem = document().get_element_by_id(\"root\");\n});\n```\n\n### Pattern #2: Conditional Block\n```rust\n#[cfg(target_arch = \"wasm32\")]\n{\n    // Multiple browser API calls\n    let window = web_sys::window().unwrap();\n    let doc = window.document().unwrap();\n    let body = doc.body().unwrap();\n    // ...\n}\n```\n\n### Pattern #3: Function-Level Guard\n```rust\n#[cfg(target_arch = \"wasm32\")]\nfn setup_scroll_lock() {\n    document().body().unwrap()\n        .style()\n        .set_property(\"overflow\", \"hidden\")\n        .unwrap();\n}\n\n// Call safely anywhere\n#[cfg(target_arch = \"wasm32\")]\nsetup_scroll_lock();\n```\n\n### Pattern #4: SSR Placeholder (Last Resort)\n```rust\n#[component]\npub fn BrowserOnlyWidget() -> impl IntoView {\n    if leptos::is_server() {\n        return view! { <div>\"Loading...\"</div> }.into_any();\n    }\n    \n    // Client-only code\n    view! { <ComplexBrowserStuff /> }.into_any()\n}\n```\n\n---\n\n## Decision Matrix\n\n| API Type          | SSR Safe? | Guard Required | Pattern              |\n|-------------------|-----------|----------------|----------------------|\n| `RwSignal::new()` | ✅ Yes    | ❌ No          | Use freely           |\n| `Effect::new()`   | ❌ No     | ✅ Yes         | `#[cfg(...)]`        |\n| `use_context()`   | ✅ Yes    | ❌ No          | SSR-safe             |\n| `use_navigate()`  | ❌ No     | ✅ Yes         | Event handler only   |\n| `window()`        | ❌ No     | ✅ Yes         | `#[cfg(...)]`        |\n| `spawn_local()`   | ❌ No     | ✅ Yes         | `#[cfg(...)]`        |\n| `<Suspense>`      | ✅ Yes    | ❌ No          | Use freely           |\n\n---\n\n## Real Production Errors\n\n### Error #1: spawn_local Panic\n```\nthread 'main' panicked at 'cannot call spawn_local on non-wasm target'\n```\n\n**Cause:**\n```rust\nEffect::new(move |_| {\n    spawn_local(async { ... }); // Runs on server!\n});\n```\n\n**Fix:**\n```rust\n#[cfg(target_arch = \"wasm32\")]\nEffect::new(move |_| {\n    spawn_local(async { ... });\n});\n```\n\n---\n\n### Error #2: use_navigate in Body\n```\nthread 'main' panicked at 'navigate context not found'\n```\n\n**Cause:**\n```rust\n#[component]\npub fn MyComponent() -> impl IntoView {\n    let navigate = use_navigate(); // SSR has no router context\n    view! { ... }\n}\n```\n\n**Fix:**\n```rust\n#[component]\npub fn MyComponent() -> impl IntoView {\n    view! {\n        <button on:click=move |_| {\n            let navigate = use_navigate(); // Safe in event\n            navigate(\"/\");\n        }>\n    }\n}\n```\n\n---\n\n### Error #3: document() in Effect\n```\nthread 'main' panicked at 'window is undefined'\n```\n\n**Cause:**\n```rust\nEffect::new(move |_| {\n    document().body(); // No DOM on server\n});\n```\n\n**Fix:**\n```rust\n#[cfg(target_arch = \"wasm32\")]\nEffect::new(move |_| {\n    document().body();\n});\n```\n\n---\n\n### Error #4: localStorage Access\n```\nReferenceError: localStorage is not defined\n```\n\n**Cause:**\n```rust\nlet value = window()\n    .local_storage()\n    .unwrap()\n    .get_item(\"key\"); // Runs on server\n```\n\n**Fix:**\n```rust\n#[cfg(target_arch = \"wasm32\")]\n{\n    let value = window()\n        .local_storage()\n        .unwrap()\n        .get_item(\"key\");\n}\n```\n\n---\n\n## Common Patterns\n\n### Focus Management\n```rust\n#[cfg(target_arch = \"wasm32\")]\nEffect::new(move |_| {\n    if is_open.get() {\n        if let Some(elem) = document()\n            .get_element_by_id(\"dialog\")\n        {\n            let _ = elem.dyn_ref::<HtmlElement>()\n                .map(|e| e.focus());\n        }\n    }\n});\n```\n\n### Scroll Lock\n```rust\n#[cfg(target_arch = \"wasm32\")]\nEffect::new(move |_| {\n    if is_open.get() {\n        document().body()\n            .unwrap()\n            .style()\n            .set_property(\"overflow\", \"hidden\")\n            .ok();\n    } else {\n        document().body()\n            .unwrap()\n            .style()\n            .set_property(\"overflow\", \"\")\n            .ok();\n    }\n});\n```\n\n### Click Outside\n```rust\n#[cfg(target_arch = \"wasm32\")]\nEffect::new(move |_| {\n    let closure = Closure::wrap(Box::new(move |e: web_sys::Event| {\n        // Handle click outside\n    }) as Box<dyn FnMut(_)>);\n    \n    document()\n        .add_event_listener_with_callback(\n            \"click\",\n            closure.as_ref().unchecked_ref()\n        )\n        .ok();\n    \n    closure.forget();\n});\n```\n\n### Keyboard Trap\n```rust\n#[cfg(target_arch = \"wasm32\")]\nEffect::new(move |_| {\n    if is_open.get() {\n        let closure = Closure::wrap(Box::new(move |e: web_sys::KeyboardEvent| {\n            if e.key() == \"Escape\" {\n                is_open.set(false);\n            }\n        }) as Box<dyn FnMut(_)>);\n        \n        document()\n            .add_event_listener_with_callback(\n                \"keydown\",\n                closure.as_ref().unchecked_ref()\n            )\n            .ok();\n        \n        closure.forget();\n    }\n});\n```\n\n---\n\n## Testing SSR Safety\n\n### Test #1: Server Build\n```bash\ncargo build --features ssr\n# Should compile without wasm APIs\n```\n\n### Test #2: Check for Violations\n```bash\ngrep -r \"window()\" src/ --include=\"*.rs\" | grep -v \"#\\[cfg\"\ngrep -r \"document()\" src/ --include=\"*.rs\" | grep -v \"#\\[cfg\"\ngrep -r \"use_navigate()\" src/ --include=\"*.rs\"\n```\n\n### Test #3: Runtime Check\n```rust\n// Add to component\n#[cfg(debug_assertions)]\n{\n    if leptos::is_server() {\n        leptos::logging::log!(\"⚠️ Running on SERVER\");\n    }\n}\n```\n\n---\n\n## References\n\n- [Leptos SSR Guide](https://leptos-rs.github.io/leptos/ssr/index.html)\n- [web-sys Documentation](https://rustwasm.github.io/wasm-bindgen/api/web_sys/)\n- Canon Rule #3: Lists & Iteration\n- Canon Rule #4: Anti-Hydration\n\n---\n\n## Quick Reference Card\n```rust\n// ✅ ALWAYS SAFE (SSR + Client)\nRwSignal::new()\nStoredValue::new()\nCallback::new()\nuse_context()\n<Suspense>\n<For>\n\n// ⚠️ GUARD REQUIRED\n#[cfg(target_arch = \"wasm32\")]\nEffect::new()\nspawn_local()\nwindow()\ndocument()\n\n// 🚫 NEVER (Context-Only)\nuse_navigate() // Only in event handlers\nuse_params() // Only in event handlers\nuse_location() // Only in event handlers\n```\n\n---\n\n**Last Updated:** 2025-12-28 | Canon v1.2"},{"number":6,"slug":"visual-state","title":"Visual State Declaration","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["tokens","css","state","theming"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Coupling component state with visual styling breaks theming, dark mode, and design system evolution. Inline styles and hardcoded colors prevent scalable UI architecture.","problem":"components mix state and styling causing theme and css inconsistencies","solution":"expose state via data attributes and delegate visuals to css tokens","signals":["theme break","dark mode fail","style drift"],"search_intent":"how to separate state and style","keywords":["leptos visual state pattern","css data attributes state","design system tokens usage","tailwind semantic tokens"],"body":"## Critical Rule\n**UI components expose semantic state via data-attributes. CSS + tokens decide visual representation.**\n\nViolation = Theme breaks / Dark mode fails / Design debt / IA generates brittle code\n\n---\n\n## Philosophy\n\nState ≠ Style\n\nComponents declare **what they are** (selected, disabled, active).\nCSS decides **how they look** (colors, spacing, effects).\n\nThis separation enables:\n- Theme switching without component changes\n- Dark mode via CSS variables only\n- Design system evolution without refactoring\n- AI-friendly predictable patterns\n\n---\n\n## Wrong Patterns\n\n### Anti-Pattern 1: Hardcoded Colors\n```rust\n// ❌ FORBIDDEN - Concrete colors in UI\nview! {\n    <tr\n        class:bg-slate-100=move || selected.get()\n        class:hover:bg-slate-200=move || selected.get()\n    >\n}\n```\n\n**Why wrong:**\n- Breaks themes (can't override slate)\n- Dark mode requires component rewrite\n- No semantic meaning\n- Design system can't evolve\n\n### Anti-Pattern 2: Inline Style State\n```rust\n// ❌ FORBIDDEN - JS-controlled styling\nview! {\n    <tr\n        style=move || if selected.get() {\n            \"background: #f0f0f0\"\n        } else {\n            \"\"\n        }\n    >\n}\n```\n\n**Why wrong:**\n- CSS variables ignored\n- Theme system bypassed\n- Imperative, not declarative\n- SSR mismatch risk\n\n### Anti-Pattern 3: Dynamic Class Strings\n```rust\n// ❌ FORBIDDEN - String concatenation\nview! {\n    <tr\n        class=move || format!(\n            \"bg-{} hover:bg-{}\",\n            if selected.get() { \"blue-100\" } else { \"white\" },\n            if selected.get() { \"blue-200\" } else { \"gray-50\" }\n        )\n    >\n}\n```\n\n**Why wrong:**\n- Tailwind may not generate classes\n- Untrackable in build\n- String-based logic is fragile\n- No type safety\n\n---\n\n## Correct Pattern\n\n### Canonical Implementation\n```rust\nuse leptos::prelude::*;\n\n#[component]\npub fn TableRowSelectable(\n    #[prop(into)] selected: Signal<bool>,\n    children: Children,\n) -> impl IntoView {\n    view! {\n        <tr\n            data-selected=move || selected.get()\n            class=\"\n                border-b\n                transition-colors duration-150\n                bg-[hsl(var(--color-muted)/0.4)]\n                hover:bg-[hsl(var(--color-muted)/0.6)]\n                data-[selected=true]:bg-[hsl(var(--color-muted)/0.8)]\n                data-[selected=true]:hover:bg-[hsl(var(--color-muted)/1)]\n            \"\n        >\n            {children()}\n        </tr>\n    }\n}\n```\n\n### Why Correct:\n✅ `data-selected` = semantic state (not visual)\n✅ `var(--color-muted)` = theme-controlled token\n✅ CSS decides all visuals\n✅ Works with any theme/density/mode\n✅ Declarative, SSR-safe\n✅ AI can generate this pattern\n\n---\n\n## State → Attribute Mapping\n\n| Component State | data-attribute | CSS Selector |\n|----------------|----------------|--------------|\n| `selected: bool` | `data-selected=\"true\"` | `data-[selected=true]:` |\n| `disabled: bool` | `data-disabled=\"true\"` | `data-[disabled=true]:` |\n| `active: bool` | `data-active=\"true\"` | `data-[active=true]:` |\n| `variant: enum` | `data-variant=\"primary\"` | `data-[variant=primary]:` |\n| `size: enum` | `data-size=\"md\"` | `data-[size=md]:` |\n\n---\n\n## Token Usage Patterns\n\n### ✅ Semantic Tokens (ALWAYS)\n```rust\n// State variations via opacity\nbg-[hsl(var(--color-muted)/0.4)]          // Base\nhover:bg-[hsl(var(--color-muted)/0.6)]    // Hover\ndata-[selected=true]:bg-[hsl(var(--color-muted)/0.8)]  // Selected\ndata-[selected=true]:hover:bg-[hsl(var(--color-muted)/1)]  // Selected + Hover\n\n// Border states\nborder-[hsl(var(--color-border))]\ndata-[error=true]:border-[hsl(var(--color-danger-border))]\n\n// Text states\ntext-[hsl(var(--color-fg-default))]\ndata-[disabled=true]:text-[hsl(var(--color-fg-muted))]\ndata-[disabled=true]:opacity-[var(--state-disabled-opacity)]\n```\n\n### Concrete Colors\n```rust\n// ❌ FORBIDDEN\nbg-slate-100\ntext-blue-600\nborder-red-500\nhover:bg-gray-200\n```\n\n---\n\n## Common Use Cases\n\n### Interactive Lists\n```rust\n#[component]\npub fn ListItem(\n    #[prop(into)] selected: Signal<bool>,\n    #[prop(into)] disabled: Signal<bool>,\n) -> impl IntoView {\n    view! {\n        <li\n            data-selected=move || selected.get()\n            data-disabled=move || disabled.get()\n            class=\"\n                p-[var(--space-md)]\n                transition-colors\n                hover:bg-[hsl(var(--color-muted)/0.5)]\n                data-[selected=true]:bg-[hsl(var(--color-primary-bg)/0.1)]\n                data-[disabled=true]:opacity-[var(--state-disabled-opacity)]\n                data-[disabled=true]:cursor-not-allowed\n            \"\n        >\n}\n```\n\n### Tab Navigation\n```rust\n#[component]\npub fn TabButton(\n    #[prop(into)] active: Signal<bool>,\n) -> impl IntoView {\n    view! {\n        <button\n            data-active=move || active.get()\n            class=\"\n                px-[var(--space-md)]\n                py-[var(--space-sm)]\n                border-b-2\n                border-transparent\n                transition-colors\n                hover:border-[hsl(var(--color-border))]\n                data-[active=true]:border-[hsl(var(--color-primary))]\n                data-[active=true]:text-[hsl(var(--color-primary))]\n            \"\n        >\n}\n```\n\n### Form Controls\n```rust\n#[component]\npub fn InputField(\n    #[prop(into)] error: Signal<bool>,\n    #[prop(into)] disabled: Signal<bool>,\n) -> impl IntoView {\n    view! {\n        <input\n            data-error=move || error.get()\n            data-disabled=move || disabled.get()\n            disabled=move || disabled.get()\n            class=\"\n                h-[var(--size-control-md)]\n                px-[var(--space-control-x)]\n                border\n                border-[hsl(var(--color-border))]\n                rounded-[var(--radius-md)]\n                focus:ring-1\n                focus:ring-[hsl(var(--state-focus-ring))]\n                data-[error=true]:border-[hsl(var(--color-danger-border))]\n                data-[disabled=true]:opacity-[var(--state-disabled-opacity)]\n            \"\n        />\n}\n```\n\n---\n\n## Decision Matrix\n\n| Pattern | Allowed? | Use Case | Canonical Layer |\n|---------|----------|----------|-----------------|\n| `data-selected=signal` | ✅ YES | State exposure | UI Component |\n| `var(--color-*)` | ✅ YES | Visual styling | UI Component |\n| `bg-slate-100` | ❌ NO | Never | None |\n| `style=\"background: ...\"` | ❌ NO | Never | None |\n| `class:bg-*=signal` | ⚠️ AVOID | Couples state+style | Use data-attr instead |\n| Tailwind arbitrary values `[hsl(...)]` | ✅ YES | Token consumption | UI Component |\n\n---\n\n## Real Production Benefits\n\n### Before Canon (Brittle)\n```rust\n// Component locked to light theme\nview! {\n    <tr class:bg-blue-50=selected class:hover:bg-blue-100=selected>\n}\n```\n\n**Problems:**\n- Dark mode: rewrite component\n- Rebrand: find/replace all colors\n- Density change: touch every component\n\n### After Canon (Flexible)\n```rust\n// Component theme-agnostic\nview! {\n    <tr\n        data-selected=selected\n        class=\"bg-[hsl(var(--color-muted)/0.4)]\n               data-[selected=true]:bg-[hsl(var(--color-muted)/0.8)]\"\n    >\n}\n```\n\n**Benefits:**\n- Dark mode: update `tokens.css` only\n- Rebrand: change tokens, zero component edits\n- Density: tokens adjust, components unaware\n\n---\n\n## Testing Canon Compliance\n\n### Audit Command\n```bash\n# Find violations in codebase\ngrep -r \"class:bg-\" src/ --include=\"*.rs\"\ngrep -r \"bg-slate\\|bg-blue\\|bg-red\" src/ --include=\"*.rs\"\ngrep -r 'style=\"' src/ --include=\"*.rs\"\n```\n\n### Manual Check\n```rust\n// ❌ If you see this pattern, it's wrong\nclass:bg-{color}=signal\nstyle=move || format!(...)\nbg-{concrete-color}\n\n// ✅ If you see this pattern, it's correct\ndata-{state}=signal\nbg-[hsl(var(--color-{token}))]\n```\n\n---\n\n## Related Canon Rules\n\n- **Rule #1 (Types):** State belongs in Type 2 components\n- **Rule #2 (Ownership):** `Signal<bool>` for reactive state\n- **Rule #5 (SSR):** data-attributes are SSR-safe\n- **ARCHITECTURE:** UI layer consumes tokens, never defines them\n\n---\n\n## Canonical Checklist\n\nBefore merging any UI component:\n\n- [ ] No hardcoded colors (`slate`, `blue`, `red`, etc.)\n- [ ] State exposed via `data-*` attributes\n- [ ] Visual styling uses `var(--color-*)` tokens only\n- [ ] No `style=` with inline values\n- [ ] No string concatenation for classes\n- [ ] All hover/focus states via CSS selectors\n- [ ] Works in light + dark mode (test both)\n\n---\n\n## Quick Reference Card\n```rust\n// ✅ CANONICAL PATTERN (copy-paste ready)\n\nuse leptos::prelude::*;\n\n#[component]\npub fn InteractiveElement(\n    #[prop(into)] selected: Signal<bool>,\n    #[prop(into)] disabled: Signal<bool>,\n    children: Children,\n) -> impl IntoView {\n    view! {\n        <div\n            data-selected=move || selected.get()\n            data-disabled=move || disabled.get()\n            class=\"\n                transition-colors\n                bg-[hsl(var(--color-background))]\n                hover:bg-[hsl(var(--color-muted)/0.5)]\n                data-[selected=true]:bg-[hsl(var(--color-muted)/0.8)]\n                data-[disabled=true]:opacity-[var(--state-disabled-opacity)]\n            \"\n        >\n            {children()}\n        </div>\n    }\n}\n\n// ❌ ANTI-PATTERNS (never do this)\n\n// Hardcoded colors\nclass:bg-slate-100=selected  // ❌\n\n// Inline styles\nstyle=\"background: #f0f0f0\"  // ❌\n\n// String concat\nclass=format!(\"bg-{}\", color)  // ❌\n\n// Concrete Tailwind\nbg-blue-500 text-red-600  // ❌\n```\n\n---\n\n**Last Updated:** 2025-12-28 | Canon v1.3\n**Dependencies:** tokens.css, ARCHITECTURE.md, Rule #2 (Ownership)\n\n---\n\n## Summary\n\n**One Rule to Remember:**\n> Components expose **what** (state).\n> CSS + tokens decide **how** (appearance).\n\nNever mix them."},{"number":7,"slug":"token-governance","title":"Theme and Token Governance","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","theming","tailwind","monorepo"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Direct token imports and hardcoded styles create coupling, build instability, and design drift in monorepos. Lack of token governance prevents scalable theming and consistent UI across apps.","problem":"apps directly depend on design system tokens causing coupling and inconsistency","solution":"enforce token distribution via dedicated bridge and semantic token usage","signals":["build break","design drift","theme inconsistency"],"search_intent":"how to manage design tokens","keywords":["design system token governance","tailwind token pipeline","monorepo token architecture","css variable theming"],"body":"## Critical Principle\n**Tokens are not owned by Tailwind. Theme is not a UI concern. Apps never import design system internals.**\n\nViolation = Coupling / Build breaks / Design drift / Maintenance hell\n\n---\n\n## The Common Monorepo Mistake\n\n### What Developers Try (Breaks At Scale)\n```css\n/* App imports design system directly */\n@import \"../../packages-rust/rs-design/style/tokens.css\";\n```\n\n**Why this fails:**\n- Path fragility (breaks on refactor)\n- Couples app to internal structure\n- Multiple apps = multiple import points\n- Build cache invalidation issues\n- Can't version tokens independently\n\n---\n\n## Canon Architecture\n\n### Three-Layer Token Pipeline\n```\n┌─────────────────────────────────────┐\n│   rs-design (Source of Truth)      │\n│   ├── style/tokens.css              │  ← AUTHORITATIVE\n│   └── src/ui/*.rs                   │  ← Consumes tokens\n└─────────────────────────────────────┘\n              ↓ SYNC (build-time)\n┌─────────────────────────────────────┐\n│   rs-tailwind (Distribution Bridge) │\n│   └── tokens/theme.css              │  ← COPY of tokens\n└─────────────────────────────────────┘\n              ↓ IMPORT (npm package)\n┌─────────────────────────────────────┐\n│   Apps (Consumers)                  │\n│   @import \"@monorepo/rs-tailwind\"   │  ← Clean dependency\n└─────────────────────────────────────┘\n```\n\n**Golden Rule:**\n> Apps import from `rs-tailwind`, never from `rs-design`.\n\n---\n\n## Responsibility Matrix\n\n| Layer | Decides Design? | Applies Styling? | Distributes Tokens? |\n|-------|----------------|------------------|---------------------|\n| **rs-design** | ✅ YES | ❌ NO | ❌ NO |\n| **rs-tailwind** | ❌ NO | ❌ NO | ✅ YES |\n| **App** | ❌ NO | ✅ YES | ❌ NO |\n\n### rs-design (Design System)\n**Purpose:** Define design tokens, export UI components\n```css\n/* style/tokens.css - SOURCE OF TRUTH */\n@theme inline {\n  --color-background: 0 0% 100%;\n  --color-primary: 38 92% 50%;\n}\n\n.dark {\n  --color-background: 0 0% 9%;\n}\n```\n\n**Rules:**\n- ✅ Defines all semantic tokens\n- ✅ Exports UI components\n- ❌ Does NOT distribute tokens to apps\n- ❌ Does NOT know about app builds\n\n### rs-tailwind (Distribution Bridge)\n**Purpose:** Package tokens for Tailwind consumption\n```\nrs-tailwind/\n├── package.json          ← npm package\n├── tokens/\n│   └── theme.css         ← SYNCED from rs-design\n└── preset/\n    └── index.js          ← Tailwind config\n```\n\n**Rules:**\n- ✅ Copies tokens from rs-design (build step)\n- ✅ Exposes as npm package\n- ❌ Does NOT create design decisions\n- ❌ Does NOT modify token values\n\n### Apps (Consumers)\n**Purpose:** Consume tokens via Tailwind\n```css\n/* app/style/tailwind.css */\n@import \"tailwindcss\";\n@import \"@monorepo/rs-tailwind/tokens\";\n\n@layer base {\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n```\n\n**Rules:**\n- ✅ Imports from `@monorepo/rs-tailwind`\n- ✅ Uses utility classes (`bg-background`)\n- ❌ Does NOT import from `rs-design`\n- ❌ Does NOT hardcode colors (`bg-slate-100`)\n\n---\n\n## Theme System (Dark/Light)\n\n### Architecture\n\n**State Management:**\n```rust\n// providers/theme_provider.rs\n#[component]\npub fn ThemeProvider(children: Children) -> impl IntoView {\n    let theme = RwSignal::new(Theme::Light);\n    \n    // Apply class to <html>\n    #[cfg(target_arch = \"wasm32\")]\n    Effect::new(move |_| {\n        if let Some(html) = document().document_element() {\n            match theme.get() {\n                Theme::Light => { html.class_list().remove_1(\"dark\"); }\n                Theme::Dark => { html.class_list().add_1(\"dark\"); }\n            }\n        }\n    });\n    \n    provide_context(theme);\n    view! { {children()} }\n}\n```\n\n**CSS Resolution:**\n```css\n/* tokens.css */\n:root {\n  --color-background: 0 0% 100%;  /* Light */\n}\n\n.dark {\n  --color-background: 0 0% 9%;    /* Dark */\n}\n```\n\n### Separation of Concerns\n\n| Concern | Owner | Mechanism |\n|---------|-------|-----------|\n| **State** (light/dark) | ThemeProvider | `RwSignal<Theme>` + context |\n| **DOM** (`.dark` class) | ThemeProvider | Effect + `html.classList` |\n| **Appearance** | CSS | `.dark { --color-*: ... }` |\n| **Consumption** | Components | `bg-[hsl(var(--color-background))]` |\n\n**Critical Rule:**\n> ThemeProvider manipulates ONLY `html.classList`. Never touches CSS/tokens.\n\n---\n\n## Tailwind's Correct Role\n\n### ✅ Tailwind SHOULD:\n- Consume semantic tokens (`bg-background`)\n- Provide utility ergonomics\n- Generate responsive variants\n- Optimize CSS output\n\n### ❌ Tailwind SHOULD NOT:\n- Define semantic tokens\n- Own color palette\n- Decide dark mode values\n- Be source of truth\n\n### Example (Correct)\n```css\n/* App uses semantic tokens */\n<div class=\"bg-background text-foreground\" />\n```\n```css\n/* Tailwind resolves to */\n.bg-background { background-color: hsl(var(--color-background)); }\n```\n\n**Why correct:**\n- Token value lives in CSS variables\n- Tailwind is just utility generator\n- Theme change = CSS update, zero build\n- Components unaware of theme\n\n---\n\n## Anti-Patterns (Real Production Issues)\n\n### Anti-Pattern 1: Direct rs-design Import\n```css\n/* ❌ WRONG */\n@import \"../../packages-rust/rs-design/style/tokens.css\";\n```\n\n**Problems:**\n- Build fails on path changes\n- Couples to internal structure\n- Can't version independently\n\n**Fix:**\n```css\n/* ✅ CORRECT */\n@import \"@monorepo/rs-tailwind/tokens\";\n```\n\n### Anti-Pattern 2: Hardcoded Colors\n```rust\n// ❌ WRONG\nclass=\"bg-slate-100 text-blue-600\"\n```\n\n**Problems:**\n- Theme ignored\n- Dark mode broken\n- Design drift\n\n**Fix:**\n```rust\n// ✅ CORRECT\nclass=\"bg-background text-primary\"\n```\n\n### Anti-Pattern 3: ThemeProvider with CSS\n```rust\n// ❌ WRONG\n#[component]\npub fn ThemeProvider() -> impl IntoView {\n    Effect::new(|| {\n        document().body().style().set_property(\n            \"background\", \n            if theme == Dark { \"#000\" } else { \"#fff\" }\n        );\n    });\n}\n```\n\n**Problems:**\n- Bypasses CSS variables\n- JS-controlled styling\n- SSR mismatch\n\n**Fix:**\n```rust\n// ✅ CORRECT\nEffect::new(|| {\n    html.class_list().toggle_class(\"dark\", theme == Dark);\n});\n```\n\n---\n\n## Token Sync Workflow\n\n### Manual (Current)\n```bash\n# When tokens change in rs-design:\ncp packages-rust/rs-design/style/tokens.css \\\n   packages-rust/rs-tailwind/tokens/theme.css\n```\n\n### Automated (Recommended)\n```bash\n# Add to package.json scripts\n{\n  \"scripts\": {\n    \"sync-tokens\": \"cp ../rs-design/style/tokens.css ./tokens/theme.css\"\n  }\n}\n```\n\n### Build Integration (Production)\n```bash\n# In CI/CD before build\nnpm run sync-tokens\ncargo leptos build --release\n```\n\n---\n\n## Compliance Checklist\n\nBefore merging theme/token changes:\n\n- [ ] Tokens defined in `rs-design/style/tokens.css`\n- [ ] Synced to `rs-tailwind/tokens/theme.css`\n- [ ] App imports `@monorepo/rs-tailwind/tokens`\n- [ ] No hardcoded colors (`bg-slate-*`, `text-blue-*`)\n- [ ] ThemeProvider only touches `html.classList`\n- [ ] Dark mode works without component changes\n- [ ] No relative imports from rs-design\n- [ ] Tailwind uses semantic tokens only\n\n---\n\n## Testing Theme Governance\n\n### Audit Commands\n```bash\n# Find violations: hardcoded colors\nrg \"bg-(slate|blue|red|green)\" apps/ --type rust\n\n# Find violations: direct rs-design imports\nrg \"packages-rust/rs-design\" apps/ --type css\n\n# Find violations: inline styles\nrg 'style=\"' packages-rust/rs-design/src --type rust\n```\n\n### Runtime Check\n```javascript\n// Console test after theme toggle\ndocument.documentElement.classList.contains('dark') // true\ngetComputedStyle(document.body).getPropertyValue('--color-background') // \"0 0% 9%\"\n```\n\n---\n\n## Migration Guide\n\n### From Anti-Pattern to Canon\n\n**Before (broken):**\n```css\n/* app/style/tailwind.css */\n@import \"../../packages-rust/rs-design/style/tokens.css\";\n```\n```rust\n// app/src/component.rs\nclass=\"bg-slate-100 hover:bg-slate-200\"\n```\n\n**After (Canon):**\n```css\n/* app/style/tailwind.css */\n@import \"@monorepo/rs-tailwind/tokens\";\n```\n```rust\n// app/src/component.rs\nclass=\"bg-background hover:bg-muted\"\n```\n\n**Steps:**\n1. Sync tokens to rs-tailwind\n2. Change app import\n3. Replace hardcoded colors with semantic tokens\n4. Verify dark mode works\n5. Remove old imports\n\n---\n\n## Why This Matters (Business Case)\n\n### Without Governance\n- 3 apps = 3 token copies\n- Theme update = 3 manual syncs\n- Drift between products\n- Designer can't enforce consistency\n- Dark mode = per-app implementation\n\n### With Governance\n- 1 source of truth\n- Theme update = 1 file change\n- Guaranteed consistency\n- Designer controls all products\n- Dark mode = CSS toggle\n\n**ROI:**\n- Design iteration: 10x faster\n- Maintenance cost: 5x lower\n- Brand consistency: enforced\n- Developer onboarding: 2x faster\n\n---\n\n## Related Canon Rules\n\n- **Rule #6 (Visual State):** Components expose state, CSS decides appearance\n- **Rule #5 (SSR Effects):** ThemeProvider uses `#[cfg(target_arch = \"wasm32\")]`\n- **ARCHITECTURE:** UI layer consumes tokens, never defines\n\n---\n\n## Quick Reference\n```rust\n// ✅ CANONICAL PATTERN\n\n// 1. ThemeProvider (providers layer)\n#[component]\npub fn ThemeProvider(children: Children) -> impl IntoView {\n    let theme = RwSignal::new(Theme::Light);\n    \n    #[cfg(target_arch = \"wasm32\")]\n    Effect::new(move |_| {\n        html.class_list().toggle_class(\"dark\", theme.get() == Theme::Dark);\n    });\n    \n    provide_context(theme);\n    view! { {children()} }\n}\n\n// 2. ThemeToggle (UI layer)\n#[component]\npub fn ThemeToggle() -> impl IntoView {\n    let theme = expect_context::<RwSignal<Theme>>();\n    view! {\n        <button on:click=move |_| theme.update(|t| t.toggle())>\n            {move || if theme.get() == Theme::Dark { \"🌙\" } else { \"☀️\" }}\n        </button>\n    }\n}\n\n// 3. Component (uses semantic tokens)\n#[component]\npub fn Card() -> impl IntoView {\n    view! {\n        <div class=\"bg-card text-card-foreground\">\n            // Never knows about dark/light\n        </div>\n    }\n}\n```\n\n---\n\n**Last Updated:** 2025-12-28 | Canon v1.3\n**Dependencies:** ARCHITECTURE.md, Rule #6 (Visual State)\n\n---\n\n## Summary\n\n**Three Laws of Token Governance:**\n\n1. **Single Source:** Tokens live ONLY in `rs-design/style/tokens.css`\n2. **Clean Distribution:** Apps import from `rs-tailwind`, never `rs-design`\n3. **CSS Decides:** Theme is CSS classes, not JS logic\n\nViolate these = technical debt.\nFollow these = enterprise scale.\n\n---\n\n## Normative Status\n\n**This is a normative Canon document.**\n\n- Violations MAY block PRs\n- Design decisions MUST reference this document\n- Token changes MUST follow sync workflow\n- Apps MUST NOT bypass rs-tailwind bridge\n\nExceptions require explicit approval from design system maintainers.\n\n---\n\n**Canon Rule #7** | Enterprise Foundation | Normative Document"},{"number":8,"slug":"overlay-islands","title":"Overlay Islands (Client-Only Architecture)","status":"ENFORCED","severity":"HIGH","category":"core-runtime","tags":["hydration","ssr","overlay","island"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Dynamic overlays with reactive lists cause hydration mismatch in Leptos SSR. The client DOM diverges from server-rendered HTML when reactive lists are nested inside overlays. Use the island pattern to isolate client-only rendering.","problem":"SSR and client render produce different DOM structures when dynamic lists are inside overlays","solution":"use runtime browser detection to isolate client-only overlay rendering","signals":["hydration panic","dom mismatch warning","dropdown breaks after load"],"search_intent":"how to fix leptos overlay hydration","keywords":["leptos overlay hydration mismatch","ssr dynamic overlay issue","leptos island pattern","client only overlay leptos"],"body":"---\n\n## The Problem\n\n**Dynamic overlays** (with reactive lists) + **SSR** = **Hydration Hell**\n\n### Why?\n\n1. SSR generates static HTML  \n2. Client tries to hydrate  \n3. Dynamic list changes DOM  \n4. Leptos detects mismatch  \n5. **Panic**\n\n### Attempted Solutions (All Failed)\n\n| Approach | Why It Failed |\n|----------|--------------|\n| `cfg!(feature = \"ssr\")` | Compile-time, not runtime |\n| `#[cfg(feature = \"hydrate\")]` | Code does not exist in SSR, breaks composition |\n| `is_server()` | Edge case: may return `true` on client in some builds |\n| `.map().collect_view()` | `Vec<View>` is not `Sync` |\n| `StoredValue<Vec<View>>` | Same problem, does not compile |\n| `<For>` inside overlay | Direct hydration mismatch |\n\n---\n\n## The Correct Solution: Browser Detection\n\n### Pattern\n```rust\nuse leptos::prelude::*;\n\n#[component]\npub fn DynamicOverlay(items: Vec<String>) -> impl IntoView {\n    let items = StoredValue::new(items);\n    let is_browser = RwSignal::new(false);\n    \n    // Runtime browser detection\n    #[cfg(target_arch = \"wasm32\")]\n    Effect::new(move |_| {\n        if web_sys::window().is_some() {\n            is_browser.set(true);\n        }\n    });\n\n    view! {\n        <Show\n            when=move || is_browser.get()\n            fallback=|| view! {\n                // SSR placeholder (same size as real component)\n                <div class=\"w-20 h-8\"></div>\n            }\n        >\n            <DropdownMenu>\n                <DropdownMenuContent>\n                    {move || {\n                        items.get_value()\n                            .iter()\n                            .map(|item| view! { <MenuItem>{item}</MenuItem> })\n                            .collect_view()\n                    }}\n                </DropdownMenuContent>\n            </DropdownMenu>\n        </Show>\n    }\n}\n```\n\n### Why This Works\n\n1. **SSR:** `window` does not exist → `is_browser = false` → renders placeholder  \n2. **Client:** `window` exists → Effect runs → `is_browser = true` → renders overlay  \n3. **Zero hydration mismatch:** Placeholder in SSR, overlay on client  \n4. **Pure runtime:** Does not depend on feature flags  \n5. **Robust:** Works in all build modes (dev/watch/release/SSR)  \n\n---\n\n## Affected Components\n\n### Must Use Island Pattern\n\n- ✅ `DropdownMenu` (with dynamic list)  \n- ✅ `Popover` (with dynamic content)  \n- ✅ `ContextMenu` (with dynamic items)  \n- ✅ `CommandPalette` (always dynamic)  \n- ✅ `Select` (with dynamic options)  \n- ✅ `DataTableViewOptions` (column toggle)  \n\n### Can Use SSR Normally\n\n- ✅ `Table` (static data)  \n- ✅ `Card` (fixed layout)  \n- ✅ `Button` (no overlay)  \n- ✅ `Input` (form)  \n- ✅ `Checkbox` (simple control)  \n\n---\n\n## Implementation Checklist\n\n- [ ] Component uses overlay (Dropdown/Popover/Dialog)?  \n- [ ] Overlay contains dynamic list (`<For>` or `.map()`)?  \n- [ ] If YES to both: apply Island Pattern  \n- [ ] SSR placeholder has **same size** as real component  \n- [ ] Tested in SSR + hydration + client  \n\n---\n\n## Edge Cases\n\n### \"is_server() returns true on client\"\n\n**Symptom:** `is_server()` remains `true` even after hydration  \n\n**Cause:** Known Leptos bug in some build setups  \n\n**Solution:** Use `web_sys::window()` instead of `is_server()`  \n\n### \"Flash of placeholder\"\n\n**Symptom:** User sees placeholder before overlay appears  \n\n**Cause:** Normal — Effect needs to run first  \n\n**Solution:**  \n- Placeholder must have similar size/style  \n- Add `opacity-0` to placeholder if needed  \n- Or accept as trade-off for correct SSR  \n\n---\n\n## Performance Implications\n\n### SSR\n\n- ✅ Smaller payload (no overlay in HTML)  \n- ✅ Faster First Paint  \n- ❌ Component not visible in SSR  \n\n### Client\n\n- ✅ Zero hydration errors  \n- ✅ Overlay fully functional  \n- ⚠️ Slight delay until Effect runs (imperceptible)  \n\n---\n\n## Comparison: CanonRS vs shadcn\n\n| Aspect | shadcn (React) | CanonRS (Leptos) |\n|--------|----------------|------------------|\n| **Strategy** | Implicit client-only | Explicit island |\n| **SSR** | Not real (Next.js client components) | True SSR |\n| **Governance** | Not documented | Canonical rule |\n| **Robustness** | \"Just works\" | Conscious engineering |\n\n**Verdict:** CanonRS is **more rigorous**, not inferior.\n\n---\n\n## Examples\n\n### ✅ Correct: DataTableViewOptions\n```rust\n// SSR: placeholder\n// Client: dropdown with dynamic checkboxes\n<Show when=move || is_browser.get() ...>\n    <DropdownMenu>\n        {move || columns.iter().map(...).collect_view()}\n    </DropdownMenu>\n</Show>\n```\n\n### ❌ Wrong: Dynamic list without Island\n```rust\n// BREAKS hydration\n<DropdownMenu>\n    <For each=|| items .../>\n</DropdownMenu>\n```\n\n### ❌ Wrong: cfg!() for runtime\n```rust\n// DOES NOT WORK\n<Show when=|| !cfg!(feature = \"ssr\") ...>\n```\n\n---\n\n## Normative Status\n\n- Violations **MUST** block PRs  \n- Exceptions require explicit approval  \n- All overlay components **MUST** document if they need Islands  \n- Design system **MUST** provide Island utilities  \n\n---\n\n**Author:** Canon Working Group  \n**Replaces:** None (first formal definition)"},{"number":9,"slug":"clipboard-apis","title":"Clipboard and Browser APIs","status":"ENFORCED","severity":"MEDIUM","category":"behavior","tags":["clipboard","browser api","wasm","callbacks"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using modern clipboard APIs in Leptos callbacks fails due to focus requirements and async constraints. This causes runtime errors and unreliable clipboard operations.","problem":"clipboard api fails in callbacks due to focus and async constraints","solution":"use document execcommand with wasm guard for reliable clipboard operations","signals":["notallowederror","document not focused","clipboard fail"],"search_intent":"how to fix clipboard leptos","keywords":["leptos clipboard api","execcommand clipboard rust","clipboard callback error leptos","wasm clipboard copy pattern"],"body":"---\n\n## The Problem\n\n`navigator.clipboard.writeText()` fails in Leptos callbacks because:\n\n1. **Requires document focus** - async callbacks lose focus\n2. **HTTPS only** - dev environments may not have it\n3. **User gesture in stack** - callbacks break the chain\n4. **Promise handling** - needs `spawn_local` (SSR unsafe)\n\n### Attempted Solutions (All Failed)\n\n| Approach | Why It Failed |\n|----------|--------------|\n| `navigator.clipboard` | Document not focused in callbacks |\n| `spawn_local` + Promise | Panic on SSR |\n| `wasm_bindgen_futures` | Extra dependency, still needs focus |\n| Focus management | Unreliable, race conditions |\n\n**Error:**\n```\nNotAllowedError: Failed to execute 'writeText' on 'Clipboard': \nDocument is not focused.\n```\n\n---\n\n## The Correct Solution: document.execCommand\n\n### Why This Works\n\n- **No focus required** - works in background tabs\n- **Synchronous** - no Promise/async needed\n- **SSR-safe** - with `#[cfg]` guard\n- **No extra deps** - just `wasm_bindgen`\n\n### Canonical Implementation\n```rust\nuse wasm_bindgen::prelude::*;\n\n#[wasm_bindgen]\nextern \"C\" {\n    #[wasm_bindgen(js_namespace = document, js_name = execCommand)]\n    fn exec_command(command: &str) -> bool;\n}\n\npub fn copy_to_clipboard(text: &str) {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        use wasm_bindgen::JsCast;\n        \n        if let Some(window) = web_sys::window() {\n            if let Some(document) = window.document() {\n                if let Ok(textarea) = document.create_element(\"textarea\") {\n                    if let Ok(textarea) = textarea.dyn_into::<web_sys::HtmlTextAreaElement>() {\n                        textarea.set_value(text);\n                        \n                        // Hide off-screen\n                        let style = textarea.style();\n                        let _ = style.set_property(\"position\", \"absolute\");\n                        let _ = style.set_property(\"left\", \"-9999px\");\n                        \n                        if let Some(body) = document.body() {\n                            let _ = body.append_child(&textarea);\n                            textarea.select();\n                            \n                            // Copy!\n                            let _ = exec_command(\"copy\");\n                            \n                            // Cleanup\n                            let _ = body.remove_child(&textarea);\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\n---\n\n## Required Setup\n\n### Cargo.toml\n```toml\n[dependencies]\nwasm-bindgen = \"0.2\"\n\n[dependencies.web-sys]\nversion = \"0.3\"\nfeatures = [\n    \"Window\",\n    \"Document\",\n    \"Element\",\n    \"HtmlElement\",\n    \"HtmlTextAreaElement\",\n    \"CssStyleDeclaration\",\n]\n```\n\n### Project Structure\n```\nsrc/\n  utils/\n    clipboard.rs  ← Implementation here\n    mod.rs        ← pub mod clipboard; pub use clipboard::*;\n  lib.rs          ← mod utils;\n```\n\n---\n\n## Usage in Actions\n```rust\nuse crate::utils::copy_to_clipboard;\n\nDataTableColumn::actions(move |row| {\n    let value = row.value.clone();\n    \n    vec![\n        RowAction {\n            label: \"Copy value\".to_string(),\n            on_click: Callback::new(move |_| {\n                copy_to_clipboard(&value);\n            }),\n            variant: RowActionVariant::Default,\n        },\n    ]\n})\n```\n\n---\n\n## Decision Matrix\n\n| Scenario | Use This Pattern | Why |\n|----------|------------------|-----|\n| Copy text in callback | ✅ YES | Works without focus |\n| Copy in async context | ✅ YES | Synchronous, no spawn_local |\n| Copy in dropdown action | ✅ YES | Dropdown closes, document unfocused |\n| Copy on button click | ✅ YES | Always works |\n| Modern clipboard API | ❌ NO | Fails in callbacks |\n\n---\n\n## Alternative: Modern API (Not Recommended)\n```rust\n// ❌ FAILS in callbacks\n#[cfg(target_arch = \"wasm32\")]\n{\n    let clipboard = window.navigator().clipboard();\n    let promise = clipboard.write_text(text);\n    \n    // Requires spawn_local (SSR panic risk)\n    wasm_bindgen_futures::spawn_local(async move {\n        let _ = JsFuture::from(promise).await;\n    });\n}\n```\n\n**Problems:**\n- Needs `wasm_bindgen_futures` dependency\n- Requires `spawn_local` (needs `#[cfg]` guard)\n- Still fails if document not focused\n- More complex error handling\n\n---\n\n## Browser Compatibility\n\n| Browser | execCommand | clipboard API |\n|---------|-------------|---------------|\n| Chrome | ✅ Always | ⚠️ Focus needed |\n| Firefox | ✅ Always | ⚠️ Focus needed |\n| Safari | ✅ Always | ⚠️ Focus needed |\n| Edge | ✅ Always | ⚠️ Focus needed |\n\n**Verdict:** `execCommand` is deprecated but MORE reliable for our use case.\n\n---\n\n## Normative Status\n\n- Violations **MUST** block PRs\n- `navigator.clipboard` in callbacks **FORBIDDEN**\n- All clipboard operations **MUST** use this pattern\n- Alternative implementations require explicit approval\n\n---\n\n**Author:** Canon Working Group"},{"number":10,"slug":"modal-state","title":"Modal Reactive State Management","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["modal","signals","state","reactivity"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Passing static props to modals prevents reactive updates and leads to stale UI data. Modal components must maintain synchronization with parent state to ensure correct behavior.","problem":"modal props are static and do not react to parent state changes","solution":"use signal props with local state and guarded effects for synchronization","signals":["stale data","no update","reactivity lost"],"search_intent":"how to fix leptos modal state","keywords":["leptos modal signal props","reactive modal state leptos","signal derive leptos modal","leptos stale props issue"],"body":"---\n\n## The Problem\n\nPassing `String` props to modals = **values never update**:\n```rust\n// ❌ WRONG\n#[component]\nfn EditDialog(\n    open: RwSignal<bool>,\n    name: String,  // ← Static! Won't react\n) -> impl IntoView {\n    view! {\n        <Input value=name />  // ← Never updates when parent changes\n    }\n}\n```\n\n**Why it fails:**\n- Props are **captured at creation time**\n- Parent signal changes don't propagate\n- Modal always shows stale data\n\n---\n\n## The Correct Solution: Signal Props\n\n### Pattern\n```rust\n#[component]\npub fn EditDialog(\n    #[prop(into)] open: RwSignal<bool>,\n    #[prop(into)] name: Signal<String>,      // ← Signal, not String\n    #[prop(into)] value: Signal<String>,\n    // ... other fields\n) -> impl IntoView {\n    // Local editable state\n    let local_name = RwSignal::new(String::new());\n    let local_value = RwSignal::new(String::new());\n    \n    // Sync when modal opens\n    #[cfg(target_arch = \"wasm32\")]\n    Effect::new(move |_| {\n        if open.get() {\n            local_name.set(name.get());\n            local_value.set(value.get());\n        }\n    });\n    \n    let on_save = move |_| {\n        leptos::logging::log!(\"Saving: {}, {}\", local_name.get(), local_value.get());\n        open.set(false);\n    };\n    \n    view! {\n        <Dialog open=open>\n            <DialogContent>\n                <Input value=local_name />\n                <Input value=local_value />\n                <Button on:click=on_save>\"Save\"</Button>\n            </DialogContent>\n        </Dialog>\n    }\n}\n```\n\n### Parent Component\n```rust\n#[component]\npub fn ParentComponent() -> impl IntoView {\n    let edit_open = RwSignal::new(false);\n    let edit_name = RwSignal::new(String::new());\n    let edit_value = RwSignal::new(String::new());\n    \n    view! {\n        <Button on:click=move |_| {\n            edit_name.set(\"New Name\".to_string());\n            edit_value.set(\"New Value\".to_string());\n            edit_open.set(true);\n        }>\n            \"Edit\"\n        </Button>\n        \n        <EditDialog\n            open=edit_open\n            name=Signal::derive(move || edit_name.get())     // ← Signal::derive\n            value=Signal::derive(move || edit_value.get())\n        />\n    }\n}\n```\n\n---\n\n## Why This Works\n\n1. **`Signal<T>` props** = reactive by design\n2. **`Signal::derive`** = creates reactive binding\n3. **Effect with guard** = syncs safely (SSR-safe)\n4. **Local state** = user edits don't affect parent until save\n\n---\n\n## Critical Requirements\n\n### ✅ DO\n```rust\n// Props\n#[prop(into)] data: Signal<String>\n\n// Parent call\n<Modal data=Signal::derive(move || state.get()) />\n\n// Effect guard\n#[cfg(target_arch = \"wasm32\")]\nEffect::new(move |_| { ... });\n```\n\n### ❌ DON'T\n```rust\n// Props\ndata: String  // ← Static\n\n// Parent call\n<Modal data=state.get() />  // ← No reactivity\n\n// Effect without guard\nEffect::new(move |_| { ... });  // ← SSR panic\n```\n\n---\n\n## Common Patterns\n\n### Multiple Fields\n```rust\n#[component]\npub fn EditTokenDialog(\n    #[prop(into)] open: RwSignal<bool>,\n    #[prop(into)] token_id: Signal<String>,\n    #[prop(into)] token_name: Signal<String>,\n    #[prop(into)] token_value: Signal<String>,\n    #[prop(into)] token_scope: Signal<String>,\n    #[prop(into)] token_category: Signal<String>,\n    #[prop(into)] token_status: Signal<String>,\n) -> impl IntoView {\n    let name = RwSignal::new(String::new());\n    let value = RwSignal::new(String::new());\n    let scope = RwSignal::new(String::new());\n    let category = RwSignal::new(String::new());\n    let status = RwSignal::new(String::new());\n\n    #[cfg(target_arch = \"wasm32\")]\n    Effect::new(move |_| {\n        if open.get() {\n            name.set(token_name.get());\n            value.set(token_value.get());\n            scope.set(token_scope.get());\n            category.set(token_category.get());\n            status.set(token_status.get());\n        }\n    });\n    \n    // ... rest\n}\n```\n\n### Optional Fields\n```rust\n#[component]\npub fn EditDialog(\n    #[prop(into)] open: RwSignal<bool>,\n    #[prop(into, optional)] description: Option<Signal<String>>,\n) -> impl IntoView {\n    let local_desc = RwSignal::new(String::new());\n    \n    #[cfg(target_arch = \"wasm32\")]\n    Effect::new(move |_| {\n        if open.get() {\n            if let Some(desc) = description {\n                local_desc.set(desc.get());\n            }\n        }\n    });\n    \n    // ...\n}\n```\n\n---\n\n## Decision Matrix\n\n| Scenario | Prop Type | Pattern |\n|----------|-----------|---------|\n| Modal shows data | `Signal<T>` | ✅ This rule |\n| Modal edits data | `Signal<T>` + local state | ✅ This rule |\n| Static label | `String` | ✅ OK (never changes) |\n| Computed value | `Signal::derive` | ✅ This rule |\n| Optional field | `Option<Signal<T>>` | ✅ This rule |\n\n---\n\n## Real Production Example\n```rust\n// Parent: TokensTable\nDataTableColumn::actions(move |t: &TokenRow| {\n    let token_id = t.id.clone();\n    let token_name = t.full_name.clone();\n    let token_value = t.value.clone();\n    // ...\n    \n    vec![\n        RowAction {\n            label: \"Edit line\".to_string(),\n            on_click: Callback::new(move |_| {\n                edit_token_id.set(token_id.clone());\n                edit_token_name.set(token_name.clone());\n                edit_token_value.set(token_value.clone());\n                // ...\n                edit_dialog_open.set(true);\n            }),\n            variant: RowActionVariant::Default,\n        },\n    ]\n})\n\n// Later in view!\n<EditTokenDialog\n    open=edit_dialog_open\n    token_id=Signal::derive(move || edit_token_id.get())\n    token_name=Signal::derive(move || edit_token_name.get())\n    token_value=Signal::derive(move || edit_token_value.get())\n    // ...\n/>\n```\n\n---\n\n## Normative Status\n\n- Violations **MUST** block PRs\n- Modal props **MUST** use `Signal<T>` for reactive data\n- Effect sync **MUST** have `#[cfg(target_arch = \"wasm32\")]` guard\n- Exceptions require explicit justification\n\n---\n\n**Author:** Canon Working Group"},{"number":11,"slug":"multi-callback-ownership","title":"Multi-Callback Ownership","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["callbacks","ownership","closures","rust"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using the same value across multiple callbacks causes move errors due to ownership rules in Rust. This leads to compilation failures and incorrect closure behavior.","problem":"values moved into one callback cannot be reused in others","solution":"clone values before each callback to ensure independent ownership","signals":["use of moved value","e0382 error","closure failure"],"search_intent":"how to fix rust moved value","keywords":["rust closure ownership error","leptos callback clone pattern","use of moved value fix","multi callback ownership rust"],"body":"---\n\n## The Problem\n\n**Multiple callbacks = moved value errors:**\n```rust\n// ❌ WRONG\nDataTableColumn::actions(|t: &TokenRow| {\n    let name = t.name.clone();\n    \n    vec![\n        RowAction {\n            on_click: Callback::new(move |_| {\n                copy_to_clipboard(&name);  // ← name moved here\n            }),\n        },\n        RowAction {\n            on_click: Callback::new(move |_| {\n                edit_signal.set(name.clone());  // ← ERROR: name already moved\n            }),\n        },\n    ]\n})\n```\n\n**Error:**\n```\nerror[E0382]: use of moved value: `name`\nhelp: consider cloning the value before moving it\n```\n\n---\n\n## The Correct Solution: Clone Before Each Callback\n\n### Pattern\n```rust\nDataTableColumn::actions(move |t: &TokenRow| {  // ← move here\n    // Extract all values\n    let name = t.name.clone();\n    let value = t.value.clone();\n    let id = t.id.clone();\n    \n    // Clone for EACH callback\n    let name_copy = name.clone();\n    let value_copy = value.clone();\n    let name_edit = name.clone();\n    let value_edit = value.clone();\n    let id_edit = id.clone();\n    \n    vec![\n        RowAction {\n            label: \"Copy name\".to_string(),\n            on_click: Callback::new(move |_| {\n                copy_to_clipboard(&name_copy);  // ← uses clone\n            }),\n        },\n        RowAction {\n            label: \"Copy value\".to_string(),\n            on_click: Callback::new(move |_| {\n                copy_to_clipboard(&value_copy);  // ← uses clone\n            }),\n        },\n        RowAction {\n            label: \"Edit\".to_string(),\n            on_click: Callback::new(move |_| {\n                edit_name.set(name_edit.clone());    // ← uses clone\n                edit_value.set(value_edit.clone());\n                edit_id.set(id_edit.clone());\n                edit_open.set(true);\n            }),\n        },\n    ]\n})\n```\n\n---\n\n## Why This Works\n\n1. **`move` on outer closure** = captures parent signals\n2. **Clone per callback** = each closure owns its data\n3. **No shared ownership** = no borrow checker issues\n4. **Clear intent** = `name_copy`, `name_edit` naming\n\n---\n\n## Common Patterns\n\n### Two Callbacks (Same Data)\n```rust\nlet value = t.value.clone();\n\nlet value_copy = value.clone();\nlet value_edit = value.clone();\n\nvec![\n    RowAction {\n        on_click: Callback::new(move |_| {\n            copy(&value_copy);\n        }),\n    },\n    RowAction {\n        on_click: Callback::new(move |_| {\n            edit(&value_edit);\n        }),\n    },\n]\n```\n\n### Three+ Callbacks\n```rust\nlet name = t.name.clone();\n\nlet name_1 = name.clone();\nlet name_2 = name.clone();\nlet name_3 = name.clone();\n\nvec![\n    RowAction { on_click: Callback::new(move |_| use_1(&name_1)) },\n    RowAction { on_click: Callback::new(move |_| use_2(&name_2)) },\n    RowAction { on_click: Callback::new(move |_| use_3(&name_3)) },\n]\n```\n\n### Capturing External Signals\n```rust\nDataTableColumn::actions(move |t: &TokenRow| {  // ← MUST use move\n    // Now can capture external signals\n    let value = t.value.clone();\n    let value_copy = value.clone();\n    \n    vec![\n        RowAction {\n            on_click: Callback::new(move |_| {\n                external_signal.set(value_copy.clone());  // ← captures signal\n            }),\n        },\n    ]\n})\n```\n\n---\n\n## Critical Requirements\n\n### ✅ DO\n```rust\n// Outer move\nDataTableColumn::actions(move |t| { ... })\n\n// Clone per callback\nlet value_copy = value.clone();\nlet value_edit = value.clone();\n\n// Use in closures\non_click: Callback::new(move |_| {\n    use_it(&value_copy);\n})\n```\n\n### ❌ DON'T\n```rust\n// No outer move (if capturing external)\nDataTableColumn::actions(|t| { ... })  // ← Missing move\n\n// Reuse same value\non_click: Callback::new(move |_| {\n    use_it(&value);  // ← Moved\n})\n// ...\non_click: Callback::new(move |_| {\n    use_it(&value);  // ← ERROR\n})\n```\n\n---\n\n## Real Production Example\n```rust\n#[component]\npub fn TokensTable() -> impl IntoView {\n    // External signals\n    let edit_open = RwSignal::new(false);\n    let edit_name = RwSignal::new(String::new());\n    let edit_value = RwSignal::new(String::new());\n    \n    // ...\n    \n    DataTableColumn::actions(move |t: &TokenRow| {  // ← move\n        // Extract\n        let token_name = t.full_name.clone();\n        let token_value = t.value.clone();\n        let token_id = t.id.clone();\n        let token_scope = t.scope.clone();\n        let token_category = t.category.clone();\n        let token_status = t.status.clone();\n        \n        // Clone for each callback\n        let token_name_copy = token_name.clone();\n        let token_value_copy = token_value.clone();\n        let token_name_edit = token_name.clone();\n        let token_value_edit = token_value.clone();\n        let token_id_edit = token_id.clone();\n        let token_scope_edit = token_scope.clone();\n        let token_category_edit = token_category.clone();\n        let token_status_edit = token_status.clone();\n        \n        vec![\n            RowAction {\n                label: \"Copy token name\".to_string(),\n                on_click: Callback::new(move |_| {\n                    crate::utils::copy_to_clipboard(&token_name_copy);\n                }),\n                variant: RowActionVariant::Default,\n            },\n            RowAction {\n                label: \"Copy token value\".to_string(),\n                on_click: Callback::new(move |_| {\n                    crate::utils::copy_to_clipboard(&token_value_copy);\n                }),\n                variant: RowActionVariant::Default,\n            },\n            RowAction {\n                label: \"Edit line\".to_string(),\n                on_click: Callback::new(move |_| {\n                    edit_name.set(token_name_edit.clone());\n                    edit_value.set(token_value_edit.clone());\n                    edit_id.set(token_id_edit.clone());\n                    edit_scope.set(token_scope_edit.clone());\n                    edit_category.set(token_category_edit.clone());\n                    edit_status.set(token_status_edit.clone());\n                    edit_open.set(true);\n                }),\n                variant: RowActionVariant::Default,\n            },\n        ]\n    })\n}\n```\n\n---\n\n## Decision Matrix\n\n| Scenario | Pattern | Why |\n|----------|---------|-----|\n| 1 callback | Clone once | Simple |\n| 2+ callbacks (same data) | Clone per callback | Ownership |\n| Callbacks + external signals | `move` + clones | Captures |\n| Callbacks + no external | No `move` needed | Local only |\n\n---\n\n## Compiler Hints\n\nWhen you see:\n```\nerror[E0382]: use of moved value: `name`\nhelp: consider cloning the value before moving it\n```\n\n**Fix:**\n1. Add clone BEFORE first callback: `let name_copy = name.clone();`\n2. Use `name_copy` in first callback\n3. Keep `name` for other clones\n\n---\n\n## Normative Status\n\n- Violations **MUST** block PRs\n- Multiple callbacks **MUST** use dedicated clones\n- Outer closure **MUST** use `move` if capturing external signals\n- Exceptions require explicit justification\n\n---\n\n**Author:** Canon Working Group"},{"number":12,"slug":"select-vs-combobox","title":"canon-rule-12-select-vs-combobox","status":"ENFORCED","severity":"MEDIUM","category":"component-architecture","tags":["components","ui","ssr","forms"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Confusing Select and Combobox leads to incorrect architectural decisions affecting SSR, performance, and accessibility. These components have distinct responsibilities and cannot be interchanged.","problem":"select and combobox are used interchangeably causing architectural issues","solution":"choose explicitly between select and combobox based on requirements","signals":["wrong component choice","performance issue","ux mismatch"],"search_intent":"when to use select vs combobox","keywords":["select vs combobox ui","component choice forms","leptos select combobox","ui component selection rule"],"body":"**Short statement (easy to remember):**  \nSelect and Combobox are semantically different components and **MUST NOT be used as substitutes for each other**.\n\n---\n\n## Formal Definition\n```\nSelect   = Native HTML control (Type 1)\nCombobox = Interactive component with overlay and search (Type 3)\n```\n\n---\n\n## 🔒 WHAT THIS RULE PROHIBITS (binding)\n\n### ❌ FORBIDDEN\n\n- Using Combobox where Select is sufficient  \n- Adding search, overlay or JS to Select  \n- Creating \"smart Select\", \"advanced Select\", \"SelectSearch\"  \n- Creating hybrid component `SelectOrCombobox`  \n- Automatic choice based on heuristic (`if items > x`)  \n\n👉 **The choice MUST be EXPLICIT and JUSTIFIED.**\n\n---\n\n## ✅ WHAT THE RULE REQUIRES\n\nEvery decision between Select and Combobox **MUST** consider:\n\n| Criteria              | Required             |\n|-----------------------|----------------------|\n| SSR critical?         | **Select**           |\n| Small list (<50)?     | **Select**           |\n| Mobile-first?         | **Select**           |\n| Search required?      | **Combobox**         |\n| Overlay required?     | **Combobox**         |\n| Rich UX > performance?| **Combobox**         |\n\n---\n\n## 🧠 WHY THIS IS A RULE (not just a guideline)\n\nBecause it directly affects:\n\n| Area          | Impact                      |\n|---------------|------------------------------|\n| Architecture  | Type 1 vs Type 3             |\n| SSR           | Native vs Client-only        |\n| Performance   | O(1) vs O(n)                 |\n| Accessibility | Native HTML vs manual ARIA   |\n| Mobile UX     | OS picker vs overlay         |\n| Bundle size   | ~2KB vs ~8KB                 |\n| Governance    | Avoids emotional decisions   |\n\n**This is not aesthetic preference. It is structural decision.**\n\n---\n\n## 🏷️ RULE CLASSIFICATION\n\n| Field       | Value                          |\n|-------------|--------------------------------|\n| Rule ID     | Canon Rule #12                 |\n| Category    | Component Choice               |\n| Type        | Architectural Rule             |\n| Severity    | **High**                       |\n| Scope       | UI / Forms / UX / SSR          |\n| Violation   | **Review Blocker**             |\n\n---\n\n## 🧪 HOW THIS RULE IS APPLIED IN PRACTICE\n\n### In Code Review\n\n**Mandatory checklist:**\n\n- [ ] PR uses Combobox with <20 options?  \n- [ ] PR uses Combobox in SSR-critical form?  \n- [ ] PR documents the choice between Select vs Combobox?  \n\n**If it fails → PR NOT APPROVED**\n\n### In Auditing\n\nYou can run queries like:\n```sql\nSELECT file, component\nFROM component_usage\nWHERE component = 'Combobox'\n  AND list_size < 10;\n```\n\n👉 This is **platform-level**, not app-level.\n\n---\n\n## 🧱 DOCUMENTATION STRUCTURE\n```\ndocs/canon/\n├── rules/\n│   └── canon-rule-12-select-vs-combobox.md\n└── records/\n    └── canon-record-12-architectural-decision.md\n```\n\n- **Rule:** permanent standard  \n- **Record:** decision history  \n\n---\n\n## 🏁 FINAL VERDICT\n\n- ✅ It is **Canon Rule #12**  \n- ✅ It is **architectural**, not stylistic  \n- ✅ It **blocks wrong PRs**  \n- ✅ It protects **SSR, DX, UX and performance**  \n- ✅ It prevents future **component creep**  \n\n---\n\n## References\n\n- Canon Record #12: `/docs/canon/records/canon-record-12-architectural-decision.md`\n- Implementation: `/packages-rust/rs-design/src/ui/combobox/README.md`\n- Original discussion: `/docs/canon/12-select-vs-combobox.md`"},{"number":13,"slug":"specialization-vs-substitution","title":"canon-rule-13-specialization-vs-substitution","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["components","specialization","api-design","composition"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using specialized components as replacements for base components leads to feature creep and loss of semantic clarity. Base components become overloaded and difficult to maintain.","problem":"specialized components are used to replace base components instead of extending them","solution":"keep base components generic and create explicit specialized components that extend semantics","signals":["god component","magic flags","component overload"],"search_intent":"when to use specialized vs base components","keywords":["component specialization vs substitution","ui component design patterns","avoid god component frontend","explicit component architecture"],"body":"**Short statement (easy to remember):**  \nA specialized component never replaces its base component. It extends semantics, not rewrites it.\n\n---\n\n## Formal Definition\n```\nBase Component        = Generic, foundational component\nSpecialized Component = Semantic extension of the base, specific case\n```\n\n**Specialization** is about **extending semantics**.  \n**Substitution** is about **rewriting behavior**.\n\n**Rule #13 forbids substitution disguised as specialization.**\n\n---\n\n## 🔒 WHAT THIS RULE PROHIBITS (binding)\n\n### ❌ FORBIDDEN\n\n#### 1. Using specialized as default\n```rust\n// ❌ WRONG - MaskedInput as default\nfn UserForm() {\n    view! {\n        <MaskedInput value=name />\n    }\n}\n\n// ✅ CORRECT\nfn UserForm() {\n    view! {\n        <Input value=name />\n        <MaskedInput value=cpf mask_type=MaskType::CPF />\n    }\n}\n```\n\n#### 2. Magic flags turning base into specialized\n```rust\n// ❌ FORBIDDEN\n<Input mask=\"cpf\" />\n<Select searchable=true />\n<Button loading=true spinner />\n\n// ✅ CORRECT\n<MaskedInput mask_type=MaskType::CPF />\n<Combobox />\n<LoadingButton />\n```\n\n#### 3. \"Smart\" components deciding automatically\n```rust\n// ❌ FORBIDDEN\n<SelectOrCombo options=list />\n\n// ✅ CORRECT\nif list.len() < 50 {\n    <Select options=list />\n} else {\n    <Combobox options=list />\n}\n```\n\n#### 4. Silent substitution\n```rust\n// ❌ FORBIDDEN\ntype Select = Combobox;\n\n// ✅ CORRECT\n<Select />\n<Combobox />\n```\n\n---\n\n## ✅ WHAT THE RULE REQUIRES\n\n### 1. Base Component ALWAYS exists\n\nEvery specialized component **must have a generic base component**:\n\n| Base         | Exists as? | Purpose                    |\n|--------------|------------|----------------------------|\n| `Input`      | ✅ Type 1  | Generic field              |\n| `Select`     | ✅ Type 1  | Native selection           |\n| `Button`     | ✅ Type 1  | Generic action             |\n| `DataTable`  | ✅ Type 3  | Human-scale semantic table |\n\n### 2. Specialization is EXPLICIT and SEPARATE\n\n| Base         | Specialized      | Relation                         |\n|--------------|------------------|----------------------------------|\n| `Input`      | `MaskedInput`    | Extension (required format)      |\n| `Select`     | `Combobox`       | Extension (search + overlay)     |\n| `Button`     | `IconButton`     | Extension (visual variant)       |\n| `DataTable`  | `VirtualTable`   | **Change of nature** (Rule #17)  |\n\n### 3. Specialized DEPENDS semantically on Base\n\n**MaskedInput:**\n- ✅ Reuses Input tokens  \n- ✅ Reuses Field wrapper  \n- ✅ Reuses validation patterns  \n- ✅ Adds: deterministic mask  \n\n**Combobox:**\n- ✅ Reuses \"selection\" concept  \n- ✅ Reuses option rendering  \n- ✅ Adds: search, overlay, virtualization  \n\n---\n\n## 🧠 WHY THIS IS A RULE\n\nThis rule solves **component creep**:\n\n### Without Rule #13:\n```rust\n<Input \n    mask=\"cpf\"\n    autocomplete=true\n    debounce=300\n    validation=\"email\"\n    prefix_icon=\"user\"\n    clearable=true\n/>\n```\n\n**Problem:** Input becomes a God Component.\n\n### With Rule #13:\n```rust\n<Input />\n<MaskedInput />\n<AutocompleteInput />\n<DebouncedInput />\n<EmailInput />\n<IconInput />\n```\n\n**Solution:** Small, focused, composable components.\n\n---\n\n## 🏷️ RULE CLASSIFICATION\n\n| Field       | Value                              |\n|-------------|------------------------------------|\n| Rule ID     | Canon Rule #13                     |\n| Category    | Component Design / Specialization  |\n| Type        | Architectural Rule                 |\n| Severity    | **High**                           |\n| Scope       | All Components / DX / Maintenance  |\n| Violation   | **Review Blocker**                 |\n\n---\n\n## 🧬 CANONICAL MATRIX: Base → Specialized\n\n| Base Component | Specialized Component | Extension Type        | Related Rule |\n|----------------|----------------------|------------------------|--------------|\n| `Input`        | `MaskedInput`        | Required format        | #13          |\n| `Input`        | `EmailInput`         | Specific validation    | #13          |\n| `Select`       | `Combobox`           | Search + overlay       | #12, #13     |\n| `Button`       | `IconButton`         | Visual variant         | #13          |\n| `Button`       | `LoadingButton`      | Specialized state      | #13          |\n| `DataTable`    | `VirtualTable`       | **Change of nature**   | #14, #17     |\n| `Grid`         | -                    | Base (no specialization)| -           |\n\n**Note:** DataTable → VirtualTable is NOT specialization, it is **scale change** (Rule #17).\n\n---\n\n## 🎯 HOW RULE #13 FITS IN THE CANON\n```\nRule #12 (Select vs Combobox)\n    ↓\nRule #13 (Specialization vs Substitution)\n    ↓\nRule #17 (Human vs Machine Scale)\n```\n\n---\n\n## Specialization vs Substitution\n\n| Aspect              | Specialization (✅)      | Substitution (❌)       |\n|----------------------|--------------------------|-------------------------|\n| Relation to base     | Extends                  | Replaces                |\n| Tokens               | Reuses                   | Redefines               |\n| API                  | Adds props               | Changes base behavior   |\n| Semantics            | Preserves + adds         | Rewrites                |\n| Usage                | Specific case            | \"Improved version\"      |\n| Example              | MaskedInput extends Input| Input with mask flag    |\n\n---\n\n## 🧪 HOW THIS RULE IS APPLIED IN PRACTICE\n\n### Code Review Checklist\n\n- [ ] PR adds flag to base component that changes behavior?  \n- [ ] PR uses specialized where base would suffice?  \n- [ ] PR creates auto-deciding component?  \n- [ ] Feature should be separate component?  \n\n**If yes → PR NOT APPROVED**\n\n---\n\n## Anti-Patterns\n\n### ❌ God Component\n```rust\n<Input\n    type=\"text\"\n    mask=\"cpf\"\n    autocomplete=true\n    debounce=300\n    currency=true\n    validation=\"email\"\n/>\n```\n\n### ❌ Magic Flags\n```rust\n<Select searchable=true />\n```\n\n### ❌ Silent Substitution\n```rust\npub type Input = MaskedInput;\n```\n\n---\n\n## ✅ CORRECT PATTERNS\n\n### Pattern 1: Clear Extension\n```rust\n#[component]\npub fn Input(\n    value: Signal<String>,\n    on_change: Callback<String>,\n) -> impl IntoView { /* ... */ }\n\n#[component]\npub fn MaskedInput(\n    value: Signal<String>,\n    on_change: Callback<String>,\n    mask_type: MaskType,\n) -> impl IntoView {\n}\n```\n\n### Pattern 2: Composition\n```rust\n#[component]\npub fn EmailInput(\n    value: Signal<String>,\n    on_change: Callback<String>,\n) -> impl IntoView {\n    view! {\n        <Field label=\"Email\">\n            <Input\n                value=value\n                on_change=on_change\n                type=\"email\"\n            />\n        </Field>\n    }\n}\n```\n\n### Pattern 3: Explicit Decision\n```rust\n#[component]\nfn DocumentForm() -> impl IntoView {\n    view! {\n        <Input value=name />\n        <MaskedInput value=cpf mask_type=MaskType::CPF />\n        <EmailInput value=email />\n    }\n}\n```\n\n---\n\n## Design Principles\n\n### 1. Specialization Adds Semantics\n```\nInput       → generic field\nMaskedInput → field + required format\nEmailInput  → field + email validation\n```\n\n### 2. Base is Always Available\n\n### 3. No Magic Flags\n\n---\n\n## 🏁 FINAL VERDICT\n\n- ✅ Canon Rule #13  \n- ✅ Specialization principle  \n- ✅ Prevents God Components  \n- ✅ Forces explicit types  \n- ✅ Blocks bad PRs  \n\n---\n\n## References\n\n- Canon Rule #12  \n- Canon Rule #17  \n\n---\n\n**Mantra:** *Specialization extends semantics. Substitution rewrites it. Never confuse them.*"},{"number":14,"slug":"datatable-vs-virtualtable","title":"canon-rule-14-datatable-vs-virtualtable","status":"ENFORCED","severity":"MEDIUM","category":"component-architecture","tags":["datatable","virtualization","performance","ssr"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Choosing between DataTable and VirtualTable affects SSR, performance, and accessibility. Mixing both concerns leads to incorrect architecture and degraded user experience.","problem":"datatable and virtualtable are used interchangeably without considering architectural constraints","solution":"choose explicitly based on scale semantics ssr and performance requirements","signals":["performance issue","ssr break","wrong component"],"search_intent":"when to use datatable vs virtualtable","keywords":["datatable vs virtualtable","frontend table performance pattern","ssr vs virtualization table","ui component choice tables"],"body":"**Short statement (easy to remember):**  \nSemantics does not scale. Performance does not provide semantics.\n\n---\n\n## Formal Definition\n```\nDataTable    = Semantic table + rich UX + accessibility (Type 3)\nVirtualTable = Virtual rendering engine + performance (Type 4)\n```\n\n**DataTable** is a UI component for human-scale data.  \n**VirtualTable** is a rendering system for machine-scale data.\n\n---\n\n## 🔒 WHAT THIS RULE PROHIBITS (binding)\n\n### ❌ FORBIDDEN\n\n- Using VirtualTable for small tables (<1k rows)  \n- Adding virtualization to DataTable  \n- Creating \"DataTable with `virtual=true` flag\"  \n- Using VirtualTable in SSR-critical contexts  \n- Adding complex inline actions to VirtualTable  \n- Mixing HTML semantics (`<table>`) with virtualization  \n\n👉 **The choice must be EXPLICIT and JUSTIFIED.**\n\n---\n\n## ✅ WHAT THE RULE REQUIRES\n\nEvery decision between DataTable and VirtualTable **MUST** consider:\n\n| Criteria                    | Required            |\n|-----------------------------|----------------------|\n| < 1,000 rows?               | **DataTable**        |\n| SSR / SEO critical?         | **DataTable**        |\n| Rich accessibility?         | **DataTable**        |\n| Inline actions (edit/delete)? | **DataTable**      |\n| 10k+ rows?                  | **VirtualTable**     |\n| Logs / metrics / traces?    | **VirtualTable**     |\n| Streaming / real-time?      | **VirtualTable**     |\n| Performance > UX?           | **VirtualTable**     |\n\n---\n\n## 🧠 WHY THIS IS A RULE (not just a guideline)\n\nBecause it directly affects:\n\n| Area            | Impact                          |\n|-----------------|----------------------------------|\n| Architecture    | Type 3 vs Type 4                 |\n| SSR             | Full vs Client-only              |\n| Performance     | O(n) vs O(1) DOM nodes           |\n| Accessibility   | HTML semantics vs limited ARIA   |\n| SEO             | Indexable vs Non-indexable       |\n| Bundle size     | Medium vs High                   |\n| Complexity      | Medium vs High                   |\n\n**This is not a UX trade-off. It is an architectural decision.**\n\n---\n\n## 🏷️ RULE CLASSIFICATION\n\n| Field       | Value                          |\n|-------------|--------------------------------|\n| Rule ID     | Canon Rule #14                 |\n| Category    | Component Choice / Performance |\n| Type        | Architectural Rule             |\n| Severity    | **High**                       |\n| Scope       | UI / Data / Performance / SSR  |\n| Violation   | **Review Blocker**             |\n\n---\n\n## 📊 DataTable vs VirtualTable: Comparison\n\n| Aspect         | DataTable           | VirtualTable        |\n|-----------------|---------------------|---------------------|\n| **Render**      | All rows            | Only viewport       |\n| **DOM nodes**   | O(n)                | O(1)                |\n| **SSR**         | ✅ Full             | ❌ Client-only      |\n| **A11y**        | ✅ High             | ⚠️ Limited         |\n| **Scroll**      | Normal              | Windowed            |\n| **Bundle**      | ~5KB                | ~8KB                |\n| **Complexity**  | Medium              | High                |\n| **Max rows**    | ~1k                 | 1M+                 |\n| **Tokens**      | Canonical + C       | Canonical + D       |\n\n---\n\n## 🧪 HOW THIS RULE IS APPLIED IN PRACTICE\n\n### Code Review\n\n**Checklist:**\n\n- [ ] PR uses VirtualTable with <1k rows?  \n- [ ] PR uses DataTable with 10k+ rows?  \n- [ ] PR uses VirtualTable in SSR-critical?  \n- [ ] PR documents the decision?  \n\n**If fails → PR NOT APPROVED**\n\n---\n\n## 🧱 ARCHITECTURAL DIFFERENCE\n\n### DataTable (Type 3)\n```rust\n<DataTable<User>\n    data=users.get()\n    columns=vec![/* ... */]\n    get_id=|u| u.id\n    actions=|u| view! { <EditButton /> }\n />\n```\n\n### VirtualTable (Type 4)\n```rust\n<VirtualTable\n    rows=logs.into()\n    columns=vec![/* ... */]\n    row_height=36.0\n    viewport_height=600.0\n/>\n```\n\n---\n\n## 🎯 CANONICAL USE CASES\n\n### Use DataTable\n\n- Admin panels  \n- Backoffice CRUD  \n- Forms  \n- SEO-critical tables  \n\n### Use VirtualTable\n\n- Logs  \n- Metrics  \n- Traces  \n- Big datasets  \n\n---\n\n## Anti-Patterns\n\n### ❌ Virtualized DataTable\n```rust\n<DataTable virtual=true />\n```\n\n### ❌ VirtualTable with actions\n```rust\n<VirtualTable>\n  actions=|row| view! {\n    <EditDialog />\n  }\n</VirtualTable>\n```\n\n---\n\n## 🏁 FINAL VERDICT\n\n- ✅ Canon Rule #14  \n- ✅ Architectural decision  \n- ✅ Blocks wrong PRs  \n- ✅ Protects SSR, Performance, A11y  \n\n---\n\n**Mantra:** *Semantics does not scale. Performance does not provide semantics.*"},{"number":15,"slug":"pagination-vs-virtualization","title":"canon-rule-15-pagination-vs-virtualization","status":"ENFORCED","severity":"MEDIUM","category":"component-architecture","tags":["pagination","virtualization","performance","ux"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Choosing between pagination and virtualization impacts backend design, SEO, and performance. Mixing both leads to architectural conflicts and inconsistent UX.","problem":"pagination and virtualization are combined or misused causing architectural conflicts","solution":"choose one strategy explicitly based on ux navigation or performance needs","signals":["mixed strategies","performance issue","seo conflict"],"search_intent":"when to use pagination vs virtualization","keywords":["pagination vs virtualization","frontend navigation strategy","virtualization performance pattern","ui data rendering choice"],"body":"**Short statement (easy to remember):**  \nPagination is UX. Virtualization is an engine. Never mix them.\n\n---\n\n## Formal Definition\n```\nPagination     = UX navigation in chunks (human-driven)\nVirtualization = Rendering performance engine (machine-driven)\n```\n\n---\n\n## 🔒 WHAT THIS RULE PROHIBITS\n\n- Pagination inside VirtualTable  \n- Virtualization inside paginated components  \n- Hybrid flags (`paginated=true` + `virtual=true`)  \n- Mixing both strategies  \n\n---\n\n## ✅ WHAT THE RULE REQUIRES\n\n| Criteria                      | Solution          |\n|-------------------------------|-------------------|\n| Navigate to page X?           | Pagination        |\n| Deep links needed?            | Pagination        |\n| SEO important?                | Pagination        |\n| Dataset in client?            | Virtualization    |\n| Infinite scroll?              | Virtualization    |\n| Performance critical?         | Virtualization    |\n\n---\n\n## 📊 Comparison\n\n| Aspect        | Pagination | Virtualization |\n|---------------|------------|----------------|\n| UX            | Pages      | Scroll         |\n| Backend       | Chunked    | Full dataset   |\n| SEO           | ✅         | ❌             |\n| Performance   | Chunked    | O(1) window    |\n\n---\n\n## Anti-Patterns\n\n```rust\n<VirtualTable pagination=true />\n```\n\n```rust\n<DataTable virtual=true />\n```\n\n---\n\n## 🏁 FINAL VERDICT\n\n- ✅ Canon Rule #15  \n- ✅ Architectural  \n- ✅ Blocks wrong PRs  \n\n---\n\n**Mantra:** *Pagination is UX. Virtualization is engine.*"},{"number":16,"slug":"client-vs-server-filtering","title":"canon-rule-16-client-vs-server-filtering","status":"ENFORCED","severity":"MEDIUM","category":"state-reactivity","tags":["filtering","performance","data","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Choosing between client-side and server-side filtering impacts performance, security, and scalability. Mixing both incorrectly leads to inefficient data handling and poor UX.","problem":"filtering strategy is chosen incorrectly or mixed causing performance and data issues","solution":"choose filtering strategy explicitly based on data location and scale","signals":["slow filtering","overfetching","api overload"],"search_intent":"when to use client vs server filtering","keywords":["client vs server filtering","frontend data filtering strategy","backend filtering performance","reactive filtering pattern"],"body":"**Short statement (easy to remember):**  \nFiltering is about where the data is, not where the user sees it.\n\n---\n\n## Formal Definition\n```\nClient-side = Local filtering (O(n))\nServer-side = Backend query filtering\n```\n\n---\n\n## 🔒 WHAT THIS RULE PROHIBITS\n\n- Client filtering with large datasets  \n- Server filtering without debounce  \n- Automatic flags (`client_filter=true`)  \n- Mixing both without strategy  \n\n---\n\n## ✅ WHAT THE RULE REQUIRES\n\n| Criteria                    | Solution        |\n|----------------------------|-----------------|\n| Dataset in client?         | Client-side     |\n| <500 rows?                 | Client-side     |\n| 10k+ rows?                 | Server-side     |\n| Sensitive data?            | Server-side     |\n| SEO needed?                | Server-side     |\n\n---\n\n## 📊 Comparison\n\n| Aspect        | Client-side | Server-side |\n|---------------|-------------|-------------|\n| Performance   | O(n)        | O(log n)    |\n| Latency       | 0ms         | Network     |\n| Scale         | Limited     | Unlimited   |\n| Security      | Low         | High        |\n\n---\n\n## Anti-Patterns\n\n```rust\nlet filtered = all_data.filter(...)\n```\n\n```rust\nResource::new(search, fetch)\n```\n\n---\n\n## 🏁 FINAL VERDICT\n\n- ✅ Canon Rule #16  \n- ✅ Architectural  \n- ✅ Protects performance and security  \n\n---\n\n**Mantra:** *Filtering depends on where the data lives.*"},{"number":17,"slug":"human-vs-machine-scale","title":"canon-rule-17-human-vs-machine-scale","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["scale","components","performance","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Choosing components without considering scale leads to performance issues or unnecessary complexity. Human-scale and machine-scale components have fundamentally different constraints.","problem":"components are used outside their intended scale causing performance or complexity issues","solution":"choose components explicitly based on data scale and cognitive constraints","signals":["slow rendering","over engineering","wrong component"],"search_intent":"when to use human scale vs","keywords":["human vs machine scale components","frontend scale architecture","ui performance scaling pattern","component selection based on scale"],"body":"**Short statement (easy to remember):**  \nBuild for humans or build for machines. Never pretend one is the other.\n\n---\n\n## 🧠 THE META-RULE\n\nThis is the **conceptual generalization** that underpins all previous Canon Rules:\n\n| Canon Rule | Human-scale | Machine-scale |\n|------------|-------------|---------------|\n| **#12** | Select | Combobox |\n| **#14** | DataTable | VirtualTable |\n| **#15** | Pagination | Virtualization |\n| **#16** | Client Filtering | Server Filtering |\n| **#18** | Client Sorting | Server Sorting |\n| **#19** | Snapshot | Streaming |\n| **#20** | Eventual Consistency | Real-time |\n\n**Universal pattern:**  \nWhen you cross from human-scale → machine-scale, **the component fundamentally changes**.\n\n---\n\n## Formal Definition\n```\nHuman-scale  = Components for quantities humans process (~1-1000)\nMachine-scale = Components for quantities machines process (10k-1M+)\n```\n\n**Human-scale** is about **human cognition, rich UX, semantics**.  \n**Machine-scale** is about **technical performance, density, scalability**.\n\n---\n\n## 🔒 WHAT THIS RULE PROHIBITS\n\n### ❌ FORBIDDEN\n\n- Using human-scale components for machine-scale data  \n- Adding flags `virtual=true`, `big_data=true` to human-scale components  \n- Creating \"smart components\" that auto-select scale  \n- Using machine-scale components for human-scale data (over-engineering)  \n- Scaling human components beyond cognitive limits  \n- Downscaling machine components losing performance  \n\n👉 **Scale is an architectural decision, not configuration.**\n\n---\n\n## ✅ WHAT THE RULE REQUIRES\n\nEvery component decision **MUST** consider data scale:\n\n| Aspect                    | Human-scale      | Machine-scale    |\n|----------------------------|------------------|------------------|\n| **Quantity**               | 1-1000           | 10k-1M+          |\n| **Cognition**              | Human processes  | Machine processes |\n| **UX**                     | Rich, semantic   | Performance-driven |\n| **HTML semantics**         | Full             | Limited/Div-based |\n| **Accessibility**          | Full             | Partial          |\n| **SSR**                    | Preferred        | Optional         |\n| **Performance target**     | UX > performance | Performance > UX |\n\n---\n\n## The Threshold Rule\n\n**When crossing the threshold, the component changes nature.**\n\n### Canonical Thresholds\n```\nHuman-scale:    1 -----------------> ~1000\nMachine-scale:  10k ----------------> 1M+\n```\n\n**Gray zone (1k-10k):**  \nConscious decision area based on:\n- UX vs performance  \n- SSR needs  \n- Accessibility requirements  \n\n---\n\n## 📊 Fundamental Divide\n\n| Aspect              | Human-scale                | Machine-scale              |\n|----------------------|----------------------------|----------------------------|\n| Target               | Human cognition            | Technical performance      |\n| Quantity             | 1-1000                     | 10k-1M+                    |\n| UX                   | Rich                       | Efficient                  |\n| Semantics            | Native HTML                | Optimized divs             |\n| Accessibility        | Full                       | Limited                    |\n| SSR                  | ✅ Preferred               | ⚠️ Optional/impossible     |\n| Performance          | O(n) acceptable            | O(1) required              |\n| Navigation           | Discrete                   | Continuous                 |\n| Type                 | Type 1-3                   | Type 4                     |\n\n---\n\n## 🚫 ANTI-PATTERNS\n\n### ❌ Scaling human component\n```rust\n<DataTable data=fifty_thousand_rows />\n```\n\n### ❌ Using machine component for small data\n```rust\n<VirtualTable rows=ten_items />\n```\n\n### ❌ Universal component\n```rust\n<Table \n    data=rows\n    virtual={rows.len() > 1000}\n    paginated={rows.len() > 100}\n/>\n```\n\n---\n\n## ✅ CORRECT PATTERNS\n```rust\n<DataTable<User>\n    data=users\n/>\n\n<VirtualTable\n    rows=logs\n/>\n```\n\n---\n\n## 🏷️ RULE CLASSIFICATION\n\n| Field       | Value                              |\n|-------------|------------------------------------|\n| Rule ID     | Canon Rule #17                     |\n| Category    | Meta-Rule / Design Philosophy      |\n| Type        | Foundational Architectural Rule    |\n| Severity    | **Critical**                       |\n| Scope       | System-wide                        |\n| Violation   | **Review Blocker**                 |\n\n---\n\n## 🧬 DNA OF SCALE\n\n**Human-scale:**\n```\nSemantics → Cognition → UX → SSR → Accessibility\n```\n\n**Machine-scale:**\n```\nPerformance → Efficiency → Scaling → Client-only → Trade-offs\n```\n\n---\n\n## 🏁 FINAL VERDICT\n\n- ✅ Canon Rule #17  \n- ✅ Meta-rule  \n- ✅ Explains all others  \n- ✅ Blocks bad architecture  \n\n---\n\n**Mantra:** *Build for humans or build for machines.*"},{"number":18,"slug":"client-vs-server-sorting","title":"Client vs Server","status":"ENFORCED","severity":"MEDIUM","category":"state-reactivity","tags":["sorting","data","performance","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Sorting data in the wrong layer leads to performance issues or unnecessary backend complexity. The strategy must align with dataset size and location.","problem":"sorting is performed in wrong layer causing performance or scalability issues","solution":"sort data where it resides based on dataset size and architecture","signals":["slow sorting","overload","inefficient queries"],"search_intent":"when to use client vs server sorting","keywords":["client vs server sorting","data sorting architecture","frontend backend sorting strategy","sorting performance pattern"],"body":"**Short statement (easy to remember):**  \nSort where the data is. Not where the user clicks.\n\n---\n\n## Formal Definition\n```\nClient-side Sorting = Local sorting (O(n log n))\nServer-side Sorting = Backend ORDER BY\n```\n\n---\n\n## 🔒 WHAT THIS RULE PROHIBITS\n\n- Client sorting with large datasets  \n- Server sorting without optimization  \n- Mixing both on same dataset  \n\n---\n\n## Decision Matrix\n\n| Criteria            | Solution        |\n|--------------------|-----------------|\n| <500 rows          | Client-side     |\n| Instant feedback   | Client-side     |\n| 10k+ rows          | Server-side     |\n| Indexed DB         | Server-side     |\n\n---\n\n## 🎯 WHEN TO USE\n\n### Client-side\n```rust\ndata.sort_by(...)\n```\n\n### Server-side\n```rust\nfetch_sorted(...)\n```\n\n---\n\n**Classification:** High severity, Review Blocker  \n**Related:** Rule #16, Rule #17"},{"number":19,"slug":"streaming-vs-snapshot","title":"canon-rule-19-streaming-vs-snapshot","status":"ENFORCED","severity":"MEDIUM","category":"state-reactivity","tags":["data","streaming","snapshot","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using the wrong data delivery model leads to inefficiency and complexity. Streaming and snapshot approaches must align with data behavior and requirements.","problem":"data delivery model is misused causing inefficiency and complexity","solution":"choose streaming or snapshot based on data update frequency and requirements","signals":["polling misuse","latency","over complexity"],"search_intent":"when to use streaming vs snapshot data","keywords":["streaming vs snapshot data","real time vs fetch architecture","frontend data strategy pattern","websocket vs rest decision"],"body":"**Short statement (easy to remember):**  \nStreaming is continuous flow. Snapshot is fixed state.\n\n---\n\n## Formal Definition\n```\nStreaming = Continuous data (WebSocket, SSE)\nSnapshot  = Point-in-time data (REST)\n```\n\n---\n\n## 🔒 WHAT THIS RULE PROHIBITS\n\n- Polling as fake streaming  \n- Streaming for static data  \n- Mixing both without strategy  \n\n---\n\n## Decision Matrix\n\n| Criteria        | Solution     |\n|----------------|--------------|\n| Real-time      | Streaming    |\n| Static data    | Snapshot     |\n| SEO            | Snapshot     |\n\n---\n\n## 🎯 WHEN TO USE\n\n### Streaming\n```rust\nuse_websocket(...)\n```\n\n### Snapshot\n```rust\nfetch_data(...)\n```\n\n---\n\n**Classification:** High severity, Review Blocker  \n**Related:** Rule #17, Rule #20"},{"number":20,"slug":"realtime-vs-eventual","title":"canon-rule-20-realtime-vs-eventual","status":"ENFORCED","severity":"MEDIUM","category":"state-reactivity","tags":["realtime","consistency","state","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Confusing real-time guarantees with eventual consistency leads to incorrect system design and poor UX expectations. Each model has distinct constraints.","problem":"real time and eventual consistency are misused causing incorrect ux and system design","solution":"choose real time or eventual consistency explicitly based on guarantees required","signals":["polling misuse","latency issue","over engineering"],"search_intent":"when to use realtime vs eventual consistency","keywords":["realtime vs eventual consistency","frontend data sync strategy","websocket vs polling decision","state consistency architecture"],"body":"**Short statement (easy to remember):**  \nReal-time is guarantee. Eventual is acceptance.\n\n---\n\n## Formal Definition\n```\nReal-time UI          = Immediate update, guaranteed sync (<100ms)\nEventual Consistency  = Asynchronous update, accepts lag (seconds/minutes)\n```\n\n---\n\n## 🔒 WHAT THIS RULE PROHIBITS\n\n### ❌ FORBIDDEN\n\n- Promising real-time without infrastructure (WebSocket/SSE)  \n- Using eventual consistency for critical UX (financial transactions)  \n- Aggressive polling (anti-pattern, not real-time)  \n- Unnecessary real-time (over-engineering)  \n\n---\n\n## Decision Matrix\n\n| Criteria                      | Solution                  |\n|-------------------------------|--------------------------|\n| Financial/critical operations | **Real-time**            |\n| Collaboration (docs, chat)    | **Real-time**            |\n| Live dashboards/monitoring    | **Real-time**            |\n| Social media feeds            | **Eventual** (acceptable)|\n| Analytics/reports             | **Eventual** (acceptable)|\n| Non-critical notifications    | **Eventual** (acceptable)|\n\n---\n\n## 🎯 PATTERNS\n\n### Real-time (WebSocket)\n```rust\n// Collaborative editing\nlet doc_state = use_websocket(\"/ws/doc/{id}\");\n\n// Immediate sync\nEffect::new(move || {\n    doc_state.get(); // always updated\n});\n```\n\n**Guarantees:**\n- <100ms latency  \n- Conflict resolution  \n- Guaranteed delivery  \n\n### Eventual Consistency (Polling/Background sync)\n```rust\n// Social feed\nlet feed = Resource::new(\n    || interval(60_000), // 1min refresh\n    |_| fetch_feed()\n);\n\n// User accepts lag\n```\n\n**Accepts:**\n- Seconds/minutes delay  \n- Temporary inconsistency  \n- Background reconciliation  \n\n---\n\n## Anti-Pattern: Aggressive Polling\n```rust\n// FORBIDDEN - not real-time, wasteful\nsetInterval(() => fetch(), 100);\n```\n\n**Why it's wrong:**\n- Does not guarantee <100ms (network latency)  \n- Overloads backend/client  \n- Not true real-time  \n\n**Correct:** Use WebSocket OR accept eventual consistency.\n\n---\n\n## Trade-Offs\n\n| Aspect       | Real-time        | Eventual         |\n|---------------|------------------|------------------|\n| **Latency**   | <100ms           | Seconds/minutes  |\n| **Infra**     | WebSocket/SSE    | REST/polling     |\n| **Cost**      | High             | Low              |\n| **Complexity**| High             | Low              |\n| **UX**        | Full sync        | Accepts lag      |\n\n---\n\n**Classification:** Critical severity, Review Blocker  \n**Related:** Canon Rule #19 (Streaming), Rule #17 (Scale)"},{"number":21,"slug":"canonical-color-tokens","title":"Canonical Color Tokens vs Semantic Intents","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["tokens","colors","design-system","ui"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using non canonical color tokens creates incompatibility and design drift. Components must rely on a stable token contract.","problem":"non canonical color tokens are used causing inconsistency and incompatibility","solution":"use only canonical token set and map semantic intents in application layer","signals":["token drift","variant misuse","design inconsistency"],"search_intent":"how to enforce canonical color tokens design system","keywords":["canonical color tokens","design system token standard","semantic intent mapping","frontend token contract"],"body":"## Principle\nComponents consume a **stable contract of theme tokens**. Semantic intents (success, warning, info) are **application-layer mappings**, not theme-layer requirements.\n\n## Canonical Tokens\n\n### Brand / Action\n- `primary` + `primary-foreground`\n\n### Secondary / Neutral\n- `secondary` + `secondary-foreground`\n\n### Accent (emphasis without destruction)\n- `accent` + `accent-foreground`\n\n### Danger / Error\n- `destructive` + `destructive-foreground`\n\n### Base UI\n- `background` + `foreground`\n- `muted` + `muted-foreground`\n- `card` + `card-foreground`\n- `popover` + `popover-foreground`\n\n### Structure\n- `border`\n- `input`\n- `ring`\n\n### Charts (optional but recommended)\n- `chart-1` through `chart-5`\n\n### Sidebar (optional)\n- `sidebar-background` + `sidebar-foreground`\n- `sidebar-primary` + `sidebar-primary-foreground`\n- `sidebar-accent` + `sidebar-accent-foreground`\n- `sidebar-border` + `sidebar-ring`\n\n## Prohibited Tokens\n❌ `success` / `success-foreground`  \n❌ `warning` / `warning-foreground`  \n❌ `info` / `info-foreground`  \n❌ `danger` (use `destructive`)\n\n**Rationale:**\n1. Breaks shadcn/ui compatibility  \n2. Makes themes less portable  \n3. Creates design drift  \n\n## Component Variant Rules\n\n### Allowed\n```rust\npub enum ButtonVariant {\n    Solid,\n    Outline,\n    Ghost,\n    Destructive,\n    Secondary,\n    Muted,\n    Accent,\n}\n```\n\n### Forbidden\n```rust\npub enum ButtonVariant {\n    Success,\n    Warning,\n    Info,\n    Danger,\n}\n```\n\n## Semantic Intent Layer\n```rust\npub enum AlertIntent {\n    Success,\n    Warning,\n    Info,\n    Error,\n}\n```\n\n## Migration Strategy\n\n### Phase 1: Audit\n```bash\ngrep -r \"Success\\|Warning\\|Danger\" src/ui/*/variants.rs\n```\n\n### Phase 2: Refactor\n1. Remove non-canonical variants  \n2. Replace with canonical equivalents  \n\n### Phase 3: Semantic Layer\n1. Create mapping layer  \n\n---\n\n## Compliance Checklist\n- [ ] Only canonical tokens used  \n- [ ] No success/warning/info in variants  \n\n---\n\n## References\n- shadcn/ui  \n- Radix Colors  \n- Design Tokens W3C"},{"number":22,"slug":"tailwind-v4-rust-integration","title":"Tailwind v4 + Rust Integration","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["tailwind","css","build","rust"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Tailwind jit cannot parse rust syntax for arbitrary values, causing missing utilities and styling failures. Pre compilation is required.","problem":"tailwind jit fails to parse rust syntax causing missing styles","solution":"predefine all utilities in css and avoid arbitrary values in rust code","signals":["missing utility","css not generated","jit fail"],"search_intent":"how to use tailwind with rust leptos","keywords":["tailwind rust integration","jit rust parsing issue","css utility precompile","frontend tailwind architecture"],"body":"## Principle\nTailwind v4 JIT **cannot reliably parse Rust syntax**. All utilities must be **precompiled**.\n\n---\n\n## ❌ DOES NOT WORK\n```rust\nclass=\"bg-[hsl(var(--color-primary))]\"\nclass=\"h-[var(--size-control-md)]\"\n```\n\n---\n\n## ✅ WORKS\n```rust\nclass=\"bg-primary\"\nclass=\"h-control-md\"\n```\n\n---\n\n## Architecture\n\n### Layer 1: Tokens\n```css\n--color-primary: 38 92% 50%;\n```\n\n### Layer 2: Utilities\n```css\n.bg-primary { background-color: hsl(var(--color-primary)); }\n```\n\n### Layer 3: Rust\n```rust\n\"bg-primary text-primary-foreground\"\n```\n\n---\n\n## Critical Rules\n\n### No Arbitrary Values\n```rust\n// ❌\nbg-[...]\n\n// ✅\nbg-primary\n```\n\n### Predefined Utilities Only\n\n### CSS Order Matters\n\n---\n\n## Build Pipeline\n\n```bash\ncargo leptos watch\nnpm run watch:css\n```\n\n---\n\n## Pitfalls\n\n- Cache issues  \n- Missing utilities  \n- Inline styles don't help  \n\n---\n\n## Validation\n- [ ] Utilities defined  \n- [ ] No arbitrary values  \n\n---\n\n## Lessons Learned\n1. JIT ≠ Rust parsing  \n2. Precompile > runtime  \n3. PostCSS > CLI"},{"number":23,"slug":"state-tokens","title":"State Tokens (Hover, Focus, Disabled, Pressed)","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["tokens","state","css","interaction"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using color variants for interaction states breaks theme portability and consistency. State behavior must be standardized via tokens.","problem":"state styles rely on color variants causing inconsistency across themes","solution":"use opacity based state tokens and consistent variables for all states","signals":["state inconsistency","hover mismatch","theme break"],"search_intent":"how to define state tokens design system","keywords":["state tokens design system","css hover focus tokens","opacity state pattern","frontend interaction tokens"],"body":"## Principle\nInteractive states must use **consistent, theme-independent opacity values** rather than color variants. This ensures state feedback works across all themes without requiring theme authors to define state-specific colors.\n\n## Canonical State Tokens\n\n### Interaction States\n```css\n:root {\n  /* Opacity-based states (theme-independent) */\n  --state-hover-opacity: 0.9;\n  --state-active-opacity: 0.8;\n  --state-disabled-opacity: 0.5;\n  \n  /* Focus ring */\n  --state-focus-ring: hsl(var(--color-ring));\n  --border-width-focus: 2px;\n}\n```\n\n### State Application Pattern\n\n#### Correct\n```rust\nconst BASE_CLASSES: &str = \"\\\n    hover:opacity-[var(--state-hover-opacity)] \\\n    active:opacity-[var(--state-active-opacity)] \\\n    disabled:opacity-[var(--state-disabled-opacity)] \\\n    focus-visible:ring-[color:var(--state-focus-ring)]\";\n```\n\n#### Incorrect\n```rust\n// Don't create separate hover colors\nconst HOVER_CLASSES: &str = \"\\\n    hover:bg-primary-hover \\      // ❌ Requires theme definition\n    hover:border-primary-hover\";  // ❌ Not portable\n```\n\n## State Categories\n\n### Hover State\n**Token:** `--state-hover-opacity: 0.9`\n\n**Usage:**\n```rust\n\"hover:opacity-[var(--state-hover-opacity)]\"\n```\n\n**Rationale:** Reduces brightness by 10%, works on any color\n\n### Active State\n**Token:** `--state-active-opacity: 0.8`\n\n**Usage:**\n```rust\n\"active:opacity-[var(--state-active-opacity)]\"\n```\n\n**Rationale:** Deeper press feedback than hover\n\n### Disabled State\n**Token:** `--state-disabled-opacity: 0.5`\n\n**Usage:**\n```rust\n\"disabled:opacity-[var(--state-disabled-opacity)] \\\n disabled:cursor-not-allowed \\\n disabled:pointer-events-none\"\n```\n\n**Rationale:** 50% opacity universally signals \"not available\"\n\n### Focus State\n**Token:** `--state-focus-ring: hsl(var(--color-ring))`\n\n**Usage:**\n```rust\n\"focus-visible:outline-none \\\n focus-visible:ring-2 \\\n focus-visible:ring-[color:var(--state-focus-ring)] \\\n focus-visible:ring-offset-2\"\n```\n\n**Rationale:** Uses the theme ring color for consistency\n\n### Loading State\n**Token:** `--state-loading-opacity: 0.7`\n\n**Usage:**\n```rust\n\"data-[loading=true]:opacity-[var(--state-loading-opacity)] \\\n data-[loading=true]:cursor-wait\"\n```\n\n## State Combinations\n\n### Layered States\n```rust\n\"hover:opacity-[var(--state-hover-opacity)] \\\n focus-visible:ring-2 \\\n disabled:opacity-[var(--state-disabled-opacity)]\"\n```\n\n### Priority Order\n1. `disabled`  \n2. `active` / `pressed`  \n3. `focus-visible`  \n4. `hover`  \n\n---\n\n## Prohibited Patterns\n\n### ❌ State Color Variants\n```css\n--color-primary-hover: ...;\n```\n\n### ❌ Hardcoded Opacity\n```rust\n\"hover:opacity-90\"\n```\n\n### ❌ Mixed State Changes\n```rust\n\"hover:opacity-90 hover:bg-blue-600\"\n```\n\n---\n\n## Accessibility Requirements\n\n### Focus Indicators\n```rust\n\"focus-visible:outline-none \\\n focus-visible:ring-2 \\\n focus-visible:ring-[color:var(--state-focus-ring)] \\\n focus-visible:ring-offset-2\"\n```\n\n### Disabled State\n```rust\n\"disabled:pointer-events-none \\\n disabled:cursor-not-allowed \\\n disabled:opacity-[var(--state-disabled-opacity)]\"\n```\n\n---\n\n## Theme Contract\n\n### MUST Provide\n- `--color-ring`\n\n### SHOULD NOT Provide\n- State color variants  \n\n---\n\n## Validation Checklist\n- [ ] Uses opacity tokens  \n- [ ] No hardcoded opacity  \n- [ ] Accessible focus states  \n\n---\n\n## References\n- WCAG 2.1  \n- Material Design"},{"number":24,"slug":"density-size-scaling","title":"Density & Size Scaling","status":"ENFORCED","severity":"MEDIUM","category":"design-system","tags":["tokens","density","scaling","ui"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Inconsistent sizing and density break visual harmony and usability. A unified scaling system ensures predictable layouts.","problem":"component sizes and density are inconsistent causing layout issues","solution":"use modular scale and density driven tokens for all sizing","signals":["layout inconsistency","size mismatch","ui drift"],"search_intent":"how to implement density scaling design system","keywords":["density scaling design system","modular scale ui tokens","responsive size tokens css","frontend spacing architecture"],"body":"## Principle\nUI density and component sizes must be **data-attribute driven** and **mathematically consistent** across the design system. Size scales follow a **modular scale** (1.25x ratio).\n\n---\n\n## Density Levels\n\n```rust\npub enum Density {\n    Compact,\n    Comfortable,\n    Spacious,\n}\n```\n\n```css\n:root {\n  --density-multiplier: 1.0;\n}\n\n[data-density=\"compact\"] {\n  --density-multiplier: 0.75;\n}\n\n[data-density=\"spacious\"] {\n  --density-multiplier: 1.25;\n}\n```\n\n---\n\n## Size Scale\n\n```css\n--size-control-md: 2.5rem;\n--space-md: 1rem;\n```\n\n---\n\n## Button Sizes\n\n```rust\npub enum ButtonSize {\n    Xs,\n    Sm,\n    Md,\n    Lg,\n    Xl,\n    Icon,\n}\n```\n\n---\n\n## Typography\n\n```css\n--font-size-md: 1rem;\n--line-height-normal: 1.5;\n```\n\n---\n\n## Rules\n\n- Use CSS variables  \n- Maintain modular scale  \n- Respect density  \n\n---\n\n## Prohibited\n\n```rust\n\"h-10 px-4\"\n```\n\n---\n\n## Validation\n- [ ] Uses tokens  \n- [ ] No fixed pixels  \n\n---\n\n## References\n- Modular Scale  \n- WCAG"},{"number":25,"slug":"theme-presets-contract","title":"Theme Presets Contract","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["tokens","theming","css","contracts"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Themes defining non-color properties cause fragmentation and break system consistency. Theme scope must be strictly limited to color palettes.","problem":"themes override non color system properties causing inconsistency and fragmentation","solution":"restrict themes to defining color palettes only and enforce system wide constants for all other properties","signals":["theme drift","inconsistent spacing","design fragmentation"],"search_intent":"what should themes define design system","keywords":["theme presets contract design system","color only theme architecture","design system theme constraints","css theming token rules"],"body":"## Principle\nTheme presets define **color palettes only**. All other design decisions (spacing, typography, radius, shadows) are **system-wide constants** that themes cannot override. This ensures visual consistency and prevents theme fragmentation.\n\n## What Themes Can Define\n\n### Color Palette\n```typescript\nexport const myTheme: ThemeDefinition = {\n  modes: {\n    light: {\n      colors: {\n        // Brand\n        primary: { h: 221, s: 83, l: 53 },\n        primaryForeground: { h: 0, s: 0, l: 100 },\n        \n        // Neutrals\n        secondary: { h: 220, s: 13, l: 91 },\n        secondaryForeground: { h: 217, s: 19, l: 27 },\n        \n        // Accent\n        accent: { h: 48, s: 100, l: 96 },\n        accentForeground: { h: 23, s: 83, l: 31 },\n        \n        // Semantic\n        destructive: { h: 0, s: 84, l: 60 },\n        destructiveForeground: { h: 0, s: 0, l: 100 },\n        \n        // Base\n        background: { h: 0, s: 0, l: 100 },\n        foreground: { h: 0, s: 0, l: 15 },\n        muted: { h: 210, s: 20, l: 98 },\n        mutedForeground: { h: 220, s: 9, l: 46 },\n        \n        // Containers\n        card: { h: 0, s: 0, l: 100 },\n        cardForeground: { h: 0, s: 0, l: 15 },\n        popover: { h: 0, s: 0, l: 100 },\n        popoverForeground: { h: 0, s: 0, l: 15 },\n        \n        // Structure\n        border: { h: 220, s: 13, l: 91 },\n        input: { h: 220, s: 13, l: 91 },\n        ring: { h: 221, s: 83, l: 53 },\n        \n        // Charts (optional)\n        chart1: { h: 221, s: 83, l: 53 },\n        chart2: { h: 212, s: 95, l: 68 },\n        chart3: { h: 216, s: 92, l: 60 },\n        chart4: { h: 210, s: 98, l: 78 },\n        chart5: { h: 212, s: 95, l: 68 },\n      },\n    },\n    dark: {\n      colors: { /* ... */ },\n    },\n  },\n};\n```\n\n### Color Format Rules\n- **Format:** `{ h: number, s: number, l: number }`\n- **Ranges:** H: 0-360, S: 0-100, L: 0-100\n- **Output:** `hsl(H S% L%)` without `deg` or commas\n\n## What Themes Cannot Define\n\n### Typography\n```typescript\n// FORBIDDEN in themes\nfonts: {\n  sans: 'Comic Sans',      // System-wide constant\n  mono: 'Courier New',\n}\n```\n\n**Why:** Font stacks are infrastructure, not aesthetics\n\n### Spacing\n```typescript\n// FORBIDDEN in themes\nspacing: {\n  md: '2rem',  // System-wide modular scale\n  lg: '3rem',\n}\n```\n\n**Why:** Spacing ratios ensure mathematical harmony\n\n### Border Radius\n```typescript\n// FORBIDDEN in themes\nradius: {\n  sm: '0.125rem',  // System-wide constant\n  md: '0.375rem',\n  lg: '0.5rem',\n}\n```\n\n**Why:** Radius defines brand personality system-wide\n\n### Shadows\n```typescript\n// FORBIDDEN in themes\nshadows: {\n  sm: '0 1px 2px ...',  // System-wide elevation\n  md: '0 4px 6px ...',\n}\n```\n\n**Why:** Shadow hierarchy is structural, not thematic\n\n### Motion\n```typescript\n// FORBIDDEN in themes\nmotion: {\n  fast: '150ms',   // System-wide timing\n  normal: '300ms',\n}\n```\n\n**Why:** Animation timing affects UX consistency\n\n## Theme Metadata\n\n### Required Fields\n```typescript\n{\n  version: 1,              // Schema version\n  id: 'my-theme',          // Unique kebab-case ID\n  name: 'My Theme',        // Display name\n  author: 'Your Name',     // Creator attribution\n  description: '...',      // Brief description\n  modes: { light, dark },  // Both modes required\n}\n```\n\n### Optional Fields\n```typescript\n{\n  homepage: 'https://...',    // Theme homepage\n  repository: 'https://...',  // Source code\n  license: 'MIT',             // License identifier\n  tags: ['minimal', 'blue'],  // Discovery tags\n}\n```\n\n## Theme Validation\n\n### Schema Compliance\n```bash\n# Validate theme structure\nnpm run validate:themes\n\n# Check for missing required colors\nnpm run check:theme-contract\n```\n\n### Required Colors Checklist\n- [ ] primary + primaryForeground\n- [ ] secondary + secondaryForeground\n- [ ] accent + accentForeground\n- [ ] destructive + destructiveForeground\n- [ ] background + foreground\n- [ ] muted + mutedForeground\n- [ ] card + cardForeground\n- [ ] popover + popoverForeground\n- [ ] border + input + ring\n\n### Optional Colors\n- [ ] chart1 through chart5\n- [ ] sidebar* (8 tokens)\n\n## Theme Portability\n\n### What Makes a Theme Portable\n✅ Uses only canonical color tokens  \n✅ Provides both light and dark modes  \n✅ Follows HSL format specification  \n✅ Passes accessibility contrast checks  \n✅ No custom tokens outside contract\n\n### What Breaks Portability\n❌ Requires custom spacing/typography  \n❌ Only provides light mode  \n❌ Uses RGB/HEX instead of HSL  \n❌ Defines non-canonical tokens  \n❌ Overrides system constants\n\n## Accessibility Requirements\n\n### Contrast Ratios\n- **Normal text:** ≥4.5:1\n- **Large text:** ≥3:1\n- **UI components:** ≥3:1\n\n### Validation\n```bash\n# Check contrast ratios\nnpm run check:contrast\n```\n\n### Common Failures\n```typescript\n// ❌ Insufficient contrast\nforeground: { h: 0, s: 0, l: 65 },  // On white bg\nbackground: { h: 0, s: 0, l: 100 },\n\n// ✅ Sufficient contrast\nforeground: { h: 0, s: 0, l: 15 },  // Dark enough\nbackground: { h: 0, s: 0, l: 100 },\n```\n\n## Theme Registration\n\n### File Structure\n```\nsrc/tokens/themes/presets/\n├── my-theme.ts           # Theme definition\n└── README.md             # Documentation\n```\n\n### Export Pattern\n```typescript\n// my-theme.ts\nimport { ThemeDefinition } from '../../engine/schema';\n\nexport const myTheme: ThemeDefinition = { /* ... */ };\n```\n\n### Registry Addition\n```typescript\n// src/tokens/themes/registry.ts\nimport { myTheme } from './presets/my-theme';\n\nexport const THEME_REGISTRY = [\n  amberMinimal,\n  cleanSlate,\n  myTheme,  // Add here\n];\n```\n\n## Build Process\n\n### TypeScript To CSS Generation\n```bash\n# Generate CSS from TypeScript themes\nnpm run build:themes\n\n# Output: src/themes/generated.css\n```\n\n### Generated Output\n```css\n[data-theme=\"my-theme\"] {\n  --color-primary: 221 83% 53% !important;\n  --color-primary-foreground: 0 0% 100% !important;\n  /* ... */\n}\n\n[data-theme=\"my-theme\"] .dark {\n  --color-primary: 228 94% 67% !important;\n  /* ... */\n}\n```\n\n## Versioning Strategy\n\n### Schema Versions\n- **v1:** Initial canonical tokens\n- **v2:** (future) Additional semantic tokens\n\n### Breaking Changes\nTheme schema updates require:\n1. Major version bump\n2. Migration guide\n3. Backward compatibility layer\n\n## Prohibited Customizations\n\n### Component Specific Overrides\n```css\n/* FORBIDDEN in theme.css */\n[data-theme=\"my-theme\"] .button {\n  border-radius: 0; /* Use system radius */\n}\n```\n\n### Layout Modifications\n```css\n/* FORBIDDEN in theme.css */\n[data-theme=\"my-theme\"] .container {\n  max-width: 1400px; /* Use system grid */\n}\n```\n\n### Breakpoint Changes\n```css\n/* FORBIDDEN in theme.css */\n[data-theme=\"my-theme\"] {\n  --breakpoint-md: 900px; /* System constant */\n}\n```\n\n## Validation Checklist\n- [ ] All required color tokens defined\n- [ ] Both light and dark modes provided\n- [ ] HSL format used consistently\n- [ ] Contrast ratios pass WCAG AA\n- [ ] No typography/spacing overrides\n- [ ] Schema version specified\n- [ ] Theme builds without errors\n\n## References\n- [shadcn/ui Themes](https://ui.shadcn.com/themes)\n- [WCAG 2.1 Contrast](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)\n- [Canon Rule #21: Canonical Color Tokens](./canon-rule-21-canonical-color-tokens.md)\n- [HSL Color Space](https://en.wikipedia.org/wiki/HSL_and_HSV)"},{"number":26,"slug":"elevation-shadow-system","title":"Elevation & Shadow System","status":"ENFORCED","severity":"MEDIUM","category":"design-system","tags":["tokens","shadows","elevation","css"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Inconsistent shadow usage breaks visual hierarchy and theme predictability. Elevation must be standardized through system tokens.","problem":"shadow definitions vary across components causing inconsistent elevation hierarchy","solution":"use canonical shadow tokens and fixed elevation scale across all components","signals":["shadow inconsistency","z index conflict","visual hierarchy break"],"search_intent":"how to implement elevation system design tokens","keywords":["shadow tokens design system","elevation hierarchy css","z index layering tokens","frontend shadow system"],"body":"## Principle\nElevation creates visual hierarchy through **consistent shadow layers**. Shadows are **system-wide constants** that themes cannot override, ensuring predictable z-axis relationships across all themes.\n\n## Elevation Levels\n\n### Canonical Shadow Scale\n```css\n:root {\n  /* Shadow parameters */\n  --shadow-color: hsl(0 0% 0%);\n  --shadow-x: 0px;\n  --shadow-y: 4px;\n  --shadow-blur: 8px;\n  --shadow-spread: -1px;\n  --shadow-opacity: 0.1;\n  \n  /* Level 0: Flat (no shadow) */\n  --shadow-none: none;\n  \n  /* Level 1: Subtle lift */\n  --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);\n  --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.1),\n               0px 1px 2px -1px hsl(0 0% 0% / 0.1);\n  \n  /* Level 2: Standard elevation */\n  --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10),\n               0px 1px 2px -2px hsl(0 0% 0% / 0.10);\n  --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10),\n            0px 2px 4px -2px hsl(0 0% 0% / 0.10);\n  \n  /* Level 3: Raised elements */\n  --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10),\n               0px 2px 4px -2px hsl(0 0% 0% / 0.10);\n  --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10),\n               0px 4px 6px -2px hsl(0 0% 0% / 0.10);\n  \n  /* Level 4: Floating UI */\n  --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10),\n               0px 8px 10px -2px hsl(0 0% 0% / 0.10);\n  \n  /* Level 5: Modal/Dialog */\n  --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);\n}\n```\n\n## Elevation Hierarchy\n\n### Layer Mapping\n```\nLevel 0 (z-0):  Base surface (background)\n  └─ shadow-none\n  \nLevel 1 (z-10): Cards, panels\n  └─ shadow-sm\n  \nLevel 2 (z-20): Raised buttons, inputs\n  └─ shadow-md\n  \nLevel 3 (z-30): Dropdowns, tooltips\n  └─ shadow-lg\n  \nLevel 4 (z-40): Popovers, menus\n  └─ shadow-xl\n  \nLevel 5 (z-50): Modals, dialogs\n  └─ shadow-2xl\n```\n\n## Component Shadow Assignment\n\n### Cards And Containers\n```rust\n// Card component\nconst CARD_CLASSES: &str = \"shadow-sm\";  // Level 1\n\n// Elevated card\nconst CARD_ELEVATED: &str = \"shadow-md\"; // Level 2\n```\n\n### Interactive Elements\n```rust\n// Button (raised)\nconst BUTTON_SHADOW: &str = \"shadow-sm hover:shadow-md\";\n\n// Input fields\nconst INPUT_SHADOW: &str = \"shadow-sm focus-visible:shadow-md\";\n```\n\n### Floating UI\n```rust\n// Dropdown menu\nconst DROPDOWN_SHADOW: &str = \"shadow-lg\"; // Level 3\n\n// Popover\nconst POPOVER_SHADOW: &str = \"shadow-xl\"; // Level 4\n\n// Dialog\nconst DIALOG_SHADOW: &str = \"shadow-2xl\"; // Level 5\n```\n\n## Dark Mode Adjustments\n\n### Shadow Intensity\n```css\n.dark {\n  /* Increase shadow opacity for visibility on dark backgrounds */\n  --shadow-opacity: 0.5;\n  \n  /* Redefine shadows with higher opacity */\n  --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.50),\n               0px 1px 2px -2px hsl(0 0% 0% / 0.50);\n  \n  --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.75);\n}\n```\n\n## Animation And Transitions\n\n### Shadow Transitions\n```rust\n// Smooth shadow changes on interaction\nconst SHADOW_TRANSITION: &str = \"\\\n    transition-shadow \\\n    duration-[var(--motion-duration-normal)] \\\n    ease-[var(--motion-ease-standard)]\";\n\n// Usage\nview! {\n    <div class=\"shadow-sm hover:shadow-lg transition-shadow\">\n}\n```\n\n## Z Index Scale\n\n### Canonical Z Layers\n```css\n:root {\n  --z-base: 0;\n  --z-dropdown: 1000;\n  --z-sticky: 1100;\n  --z-fixed: 1200;\n  --z-modal-backdrop: 1300;\n  --z-modal: 1400;\n  --z-popover: 1500;\n  --z-tooltip: 1600;\n  --z-toast: 1700;\n}\n```\n\n### Component Z Index Assignment\n```rust\n// Dropdown\n\"z-[var(--z-dropdown)]\"\n\n// Modal backdrop\n\"z-[var(--z-modal-backdrop)]\"\n\n// Modal content\n\"z-[var(--z-modal)]\"\n\n// Toast notifications (highest)\n\"z-[var(--z-toast)]\"\n```\n\n## Prohibited Patterns\n\n### Theme Specific Shadows\n```css\n/* FORBIDDEN: Themes cannot override shadows */\n[data-theme=\"my-theme\"] {\n  --shadow-sm: 0px 10px 20px ...;  /* System constant */\n}\n```\n\n### Hardcoded Shadow Values\n```rust\n// FORBIDDEN\n\"shadow-[0_4px_6px_rgba(0,0,0,0.1)]\"\n\n// CORRECT\n\"shadow-md\"\n```\n\n### Arbitrary Z Index\n```rust\n// FORBIDDEN\n\"z-[999]\"\n\n// CORRECT\n\"z-[var(--z-modal)]\"\n```\n\n## Accessibility Considerations\n\n### High Contrast Mode\n```css\n@media (prefers-contrast: high) {\n  :root {\n    /* Increase shadow opacity for better definition */\n    --shadow-opacity: 0.3;\n  }\n}\n```\n\n### Reduced Motion\n```css\n@media (prefers-reduced-motion: reduce) {\n  * {\n    /* Disable shadow transitions */\n    transition-property: none !important;\n  }\n}\n```\n\n## Performance Optimization\n\n### Layer Promotion\n```css\n/* Promote frequently animated elements to GPU layer */\n.shadow-animated {\n  will-change: box-shadow;\n  transform: translateZ(0); /* Force GPU acceleration */\n}\n```\n\n### Shadow Compositing\n```css\n/* Use transform for elevation instead of multiple shadows when possible */\n.elevated {\n  transform: translateY(-2px);\n  box-shadow: var(--shadow-md);\n}\n```\n\n## Validation Checklist\n- [ ] All shadows use CSS variable tokens\n- [ ] Z-index values from canonical scale\n- [ ] No theme-specific shadow overrides\n- [ ] Dark mode shadows have increased opacity\n- [ ] Shadow transitions respect `prefers-reduced-motion`\n- [ ] Floating UI elements use correct elevation level\n\n## References\n- [Material Design: Elevation](https://m3.material.io/styles/elevation/overview)\n- [Box Shadow Generator](https://shadows.brumm.af/)\n- [Canon Rule #23: State Tokens](./canon-rule-23-state-tokens.md)"},{"number":27,"slug":"motion-timing-tokens","title":"Motion & Timing Tokens","status":"ENFORCED","severity":"MEDIUM","category":"design-system","tags":["tokens","motion","animation","css"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Hardcoded animation values create inconsistency and accessibility issues. Motion must be tokenized.","problem":"animations use hardcoded timing and easing causing inconsistency and accessibility issues","solution":"use canonical motion tokens for duration and easing across all components","signals":["inconsistent animation","motion mismatch","accessibility issue"],"search_intent":"how to define motion tokens design system","keywords":["motion timing tokens css","animation duration tokens","frontend easing functions design system","accessibility motion preferences"],"body":"## Principle\nMotion creates **purposeful transitions** that guide user attention and provide feedback. All animations use **system-wide timing tokens** that respect user preferences and maintain consistency across components.\n\n## Duration Tokens\n\n### Canonical Timing Scale\n```css\n:root {\n  /* Micro-interactions (instant feedback) */\n  --motion-duration-instant: 0ms;\n  --motion-duration-fast: 150ms;\n  \n  /* Standard transitions */\n  --motion-duration-normal: 300ms;\n  --motion-duration-comfortable: 400ms;\n  \n  /* Deliberate animations */\n  --motion-duration-slow: 500ms;\n  --motion-duration-slower: 700ms;\n  \n  /* Page transitions */\n  --motion-duration-page: 1000ms;\n}\n```\n\n### Usage Guidelines\n\n**Instant (0ms):** Immediate state changes\n```rust\n// Color changes, visibility toggles\n\"transition-colors duration-[var(--motion-duration-instant)]\"\n```\n\n**Fast (150ms):** Hover states, focus rings\n```rust\n// Interactive feedback\n\"transition-all duration-[var(--motion-duration-fast)]\"\n```\n\n**Normal (300ms):** Default transitions\n```rust\n// Component state changes\n\"transition-all duration-[var(--motion-duration-normal)]\"\n```\n\n**Slow (500ms+):** Deliberate animations\n```rust\n// Slide-ins, expansions\n\"transition-transform duration-[var(--motion-duration-slow)]\"\n```\n\n## Easing Functions\n\n### Canonical Easing Tokens\n```css\n:root {\n  /* Standard easings */\n  --motion-ease-linear: linear;\n  --motion-ease-in: cubic-bezier(0.4, 0, 1, 1);\n  --motion-ease-out: cubic-bezier(0, 0, 0.2, 1);\n  --motion-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n  \n  /* Material Design easings */\n  --motion-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);      /* Balanced */\n  --motion-ease-emphasized: cubic-bezier(0.2, 0, 0, 1);      /* Snappy exit */\n  --motion-ease-decelerate: cubic-bezier(0, 0, 0.2, 1);      /* Enter screen */\n  --motion-ease-accelerate: cubic-bezier(0.4, 0, 1, 1);      /* Exit screen */\n  \n  /* Playful easings */\n  --motion-ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);\n  --motion-ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);\n}\n```\n\n### Easing Application\n\n**Entering Elements:** Use decelerate\n```rust\n\"ease-[var(--motion-ease-decelerate)]\"  // Slides in smoothly\n```\n\n**Exiting Elements:** Use accelerate\n```rust\n\"ease-[var(--motion-ease-accelerate)]\"  // Snaps away\n```\n\n**State Changes:** Use standard\n```rust\n\"ease-[var(--motion-ease-standard)]\"    // Balanced feel\n```\n\n**Playful UI:** Use bounce/spring sparingly\n```rust\n\"ease-[var(--motion-ease-spring)]\"      // Attention-grabbing\n```\n\n## Component Motion Patterns\n\n### Button Interactions\n```rust\nconst BUTTON_MOTION: &str = \"\\\n    transition-all \\\n    duration-[var(--motion-duration-fast)] \\\n    ease-[var(--motion-ease-standard)]\";\n```\n\n### Modal Dialog Animations\n```rust\n// Backdrop fade\nconst BACKDROP_MOTION: &str = \"\\\n    transition-opacity \\\n    duration-[var(--motion-duration-normal)]\";\n\n// Content scale + fade\nconst DIALOG_MOTION: &str = \"\\\n    transition-all \\\n    duration-[var(--motion-duration-comfortable)] \\\n    ease-[var(--motion-ease-emphasized)] \\\n    data-[state=open]:animate-in \\\n    data-[state=closed]:animate-out \\\n    data-[state=open]:fade-in-0 \\\n    data-[state=closed]:fade-out-0 \\\n    data-[state=open]:zoom-in-95 \\\n    data-[state=closed]:zoom-out-95\";\n```\n\n### Dropdown Menus\n```rust\nconst DROPDOWN_MOTION: &str = \"\\\n    data-[state=open]:animate-in \\\n    data-[state=closed]:animate-out \\\n    data-[state=closed]:fade-out-0 \\\n    data-[state=open]:fade-in-0 \\\n    data-[state=closed]:zoom-out-95 \\\n    data-[state=open]:zoom-in-95 \\\n    duration-[var(--motion-duration-fast)]\";\n```\n\n### Slide Transitions\n```rust\n// Sidebar slide-in\nconst SIDEBAR_MOTION: &str = \"\\\n    transition-transform \\\n    duration-[var(--motion-duration-comfortable)] \\\n    ease-[var(--motion-ease-emphasized)] \\\n    data-[state=open]:translate-x-0 \\\n    data-[state=closed]:-translate-x-full\";\n```\n\n## Reduced Motion Support\n\n### System Preference Detection\n```css\n@media (prefers-reduced-motion: reduce) {\n  :root {\n    /* Override all durations to instant */\n    --motion-duration-fast: 0ms;\n    --motion-duration-normal: 0ms;\n    --motion-duration-comfortable: 0ms;\n    --motion-duration-slow: 0ms;\n    --motion-duration-slower: 0ms;\n    --motion-duration-page: 0ms;\n  }\n  \n  * {\n    /* Disable all animations */\n    animation-duration: 0ms !important;\n    animation-iteration-count: 1 !important;\n    transition-duration: 0ms !important;\n  }\n}\n```\n\n### Rust Implementation\n```rust\n#[component]\npub fn AnimatedComponent() -> impl IntoView {\n    let prefers_reduced_motion = RwSignal::new(false);\n    \n    #[cfg(target_arch = \"wasm32\")]\n    Effect::new(move |_| {\n        if let Some(window) = web_sys::window() {\n            if let Ok(media_query) = window.match_media(\"(prefers-reduced-motion: reduce)\") {\n                if let Some(query) = media_query {\n                    prefers_reduced_motion.set(query.matches());\n                }\n            }\n        }\n    });\n    \n    let motion_class = move || {\n        if prefers_reduced_motion.get() {\n            \"\"  // No animation\n        } else {\n            \"transition-all duration-[var(--motion-duration-normal)]\"\n        }\n    };\n    \n    view! {\n        <div class=motion_class>\n    }\n}\n```\n\n## Animation Keyframes\n\n### Fade Animations\n```css\n@keyframes fade-in {\n  from { opacity: 0; }\n  to { opacity: 1; }\n}\n\n@keyframes fade-out {\n  from { opacity: 1; }\n  to { opacity: 0; }\n}\n```\n\n### Scale Animations\n```css\n@keyframes zoom-in {\n  from { transform: scale(0.95); opacity: 0; }\n  to { transform: scale(1); opacity: 1; }\n}\n\n@keyframes zoom-out {\n  from { transform: scale(1); opacity: 1; }\n  to { transform: scale(0.95); opacity: 0; }\n}\n```\n\n### Slide Animations\n```css\n@keyframes slide-in-from-right {\n  from { transform: translateX(100%); }\n  to { transform: translateX(0); }\n}\n\n@keyframes slide-out-to-right {\n  from { transform: translateX(0); }\n  to { transform: translateX(100%); }\n}\n```\n\n## Performance Optimization\n\n### GPU Accelerated Properties\n```rust\n// Only animate transform and opacity for 60fps\nconst PERFORMANT_MOTION: &str = \"\\\n    transition-transform \\\n    transition-opacity \\\n    will-change-transform\"; // Hint to browser\n```\n\n### Avoid Animating\n```rust\n// ❌ Triggers layout recalculation\n\"transition-all\"  // Animates width, height, etc.\n\n// ✅ GPU-accelerated only\n\"transition-transform transition-opacity\"\n```\n\n## Choreography\n\n### Stagger Delays\n```css\n/* Stagger list item animations */\n.list-item:nth-child(1) { animation-delay: 0ms; }\n.list-item:nth-child(2) { animation-delay: 50ms; }\n.list-item:nth-child(3) { animation-delay: 100ms; }\n.list-item:nth-child(4) { animation-delay: 150ms; }\n```\n\n### Sequential Animations\n```rust\n// Fade in → Slide in → Scale\n\"animate-[fade-in_300ms_ease-out,slide-in_300ms_200ms_ease-out,scale-in_200ms_400ms_ease-out]\"\n```\n\n## Prohibited Patterns\n\n### Hardcoded Timing\n```rust\n// FORBIDDEN\n\"duration-300\"  // Use var(--motion-duration-normal)\n\"ease-in-out\"   // Use var(--motion-ease-standard)\n```\n\n### Transition All\n```rust\n// FORBIDDEN (performance)\n\"transition-all duration-300\"\n\n// CORRECT (specific properties)\n\"transition-transform transition-opacity duration-[var(--motion-duration-normal)]\"\n```\n\n### Excessive Animation\n```rust\n// FORBIDDEN (motion sickness risk)\n\"animate-spin animate-bounce animate-pulse\"  // Too much!\n```\n\n## Validation Checklist\n- [ ] All durations use CSS variable tokens\n- [ ] Easings match motion purpose (enter/exit/state)\n- [ ] Reduced motion preference supported\n- [ ] Only transform/opacity animated for performance\n- [ ] No animations exceed 1000ms\n- [ ] Stagger delays used for list animations\n\n## References\n- [WCAG 2.1: Animation from Interactions](https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions.html)\n- [Material Motion System](https://m3.material.io/styles/motion/overview)\n- [Framer Motion Easings](https://www.framer.com/motion/transition/)\n- [Canon Rule #23: State Tokens](./canon-rule-23-state-tokens.md)"},{"number":28,"slug":"responsive-grid-contract","title":"Responsive Grid Contract","status":"ENFORCED","severity":"MEDIUM","category":"component-architecture","tags":["responsive","grid","css","layout"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Inconsistent breakpoints and grid systems create unpredictable layouts across devices. A unified mobile-first grid contract ensures consistency.","problem":"custom breakpoints and inconsistent grids break responsive layout predictability","solution":"use canonical mobile-first breakpoints and 12-column grid system","signals":["layout break","overflow","inconsistent spacing"],"search_intent":"how to implement responsive grid system mobile first","keywords":["responsive grid 12 column system","mobile first breakpoints css","layout consistency design system","tailwind grid responsive patterns"],"body":"## Principle\nResponsive layout uses **mobile-first breakpoints** and a **12-column grid system** with consistent gutters. Breakpoints and grid settings are **system-wide constants** that ensure predictable layout behavior across all viewports.\n\n## Breakpoint System\n\n### Canonical Breakpoints\n```css\n:root {\n  --breakpoint-sm: 640px;   /* Mobile landscape / Small tablet */\n  --breakpoint-md: 768px;   /* Tablet portrait */\n  --breakpoint-lg: 1024px;  /* Tablet landscape / Small desktop */\n  --breakpoint-xl: 1280px;  /* Desktop */\n  --breakpoint-2xl: 1536px; /* Large desktop */\n}\n```\n\n### Mobile First Media Queries\n```css\n/* Base: Mobile (< 640px) */\n.container { width: 100%; }\n\n/* Small devices (≥ 640px) */\n@media (min-width: 640px) {\n  .container { max-width: 640px; }\n}\n\n/* Medium devices (≥ 768px) */\n@media (min-width: 768px) {\n  .container { max-width: 768px; }\n}\n\n/* Large devices (≥ 1024px) */\n@media (min-width: 1024px) {\n  .container { max-width: 1024px; }\n}\n\n/* Extra large (≥ 1280px) */\n@media (min-width: 1280px) {\n  .container { max-width: 1280px; }\n}\n\n/* 2XL (≥ 1536px) */\n@media (min-width: 1536px) {\n  .container { max-width: 1536px; }\n}\n```\n\n## Grid System\n\n### 12 Column Grid\n```css\n:root {\n  --grid-columns: 12;\n  --grid-gutter-mobile: 1rem;    /* 16px */\n  --grid-gutter-tablet: 1.5rem;  /* 24px */\n  --grid-gutter-desktop: 2rem;   /* 32px */\n}\n\n.grid {\n  display: grid;\n  grid-template-columns: repeat(var(--grid-columns), 1fr);\n  gap: var(--grid-gutter-mobile);\n}\n\n@media (min-width: 768px) {\n  .grid { gap: var(--grid-gutter-tablet); }\n}\n\n@media (min-width: 1024px) {\n  .grid { gap: var(--grid-gutter-desktop); }\n}\n```\n\n### Column Spans\n```rust\n// Tailwind utilities for 12-column grid\n\"col-span-1\"   // 1/12 width\n\"col-span-2\"   // 2/12 width\n\"col-span-3\"   // 3/12 (quarter)\n\"col-span-4\"   // 4/12 (third)\n\"col-span-6\"   // 6/12 (half)\n\"col-span-8\"   // 8/12 (two-thirds)\n\"col-span-12\"  // Full width\n```\n\n### Responsive Column Spans\n```rust\n// Mobile: full width, Tablet: half, Desktop: third\n\"col-span-12 md:col-span-6 lg:col-span-4\"\n\n// Mobile: full, Desktop: two-thirds\n\"col-span-12 lg:col-span-8\"\n```\n\n## Container System\n\n### Container Sizes\n```css\n.container {\n  width: 100%;\n  margin-left: auto;\n  margin-right: auto;\n  padding-left: var(--grid-gutter-mobile);\n  padding-right: var(--grid-gutter-mobile);\n}\n\n@media (min-width: 640px) {\n  .container {\n    max-width: 640px;\n    padding-left: var(--grid-gutter-tablet);\n    padding-right: var(--grid-gutter-tablet);\n  }\n}\n\n@media (min-width: 1536px) {\n  .container {\n    max-width: 1280px; /* Cap at 1280px for readability */\n  }\n}\n```\n\n### Fluid Container\n```rust\n// Full-width container with responsive padding\n\"w-full px-4 md:px-6 lg:px-8\"\n```\n\n### Fixed Container\n```rust\n// Centered container with max-width\n\"container mx-auto\"\n```\n\n## Responsive Utilities\n\n### Display\n```rust\n// Hide on mobile, show on desktop\n\"hidden lg:block\"\n\n// Show on mobile, hide on desktop\n\"block lg:hidden\"\n```\n\n### Spacing\n```rust\n// Responsive padding\n\"p-4 md:p-6 lg:p-8\"\n\n// Responsive margins\n\"mt-4 md:mt-8 lg:mt-12\"\n```\n\n### Typography\n```rust\n// Responsive font sizes\n\"text-sm md:text-base lg:text-lg\"\n\n// Responsive headings\n\"text-2xl md:text-3xl lg:text-4xl\"\n```\n\n## Layout Patterns\n\n### Sidebar Layout\n```rust\nview! {\n    <div class=\"grid grid-cols-1 lg:grid-cols-12 gap-6\">\n        // Sidebar: full width on mobile, 3 columns on desktop\n        <aside class=\"lg:col-span-3\">\n        \n        // Main: full width on mobile, 9 columns on desktop\n        <main class=\"lg:col-span-9\">\n    </div>\n}\n```\n\n### Card Grid\n```rust\nview! {\n    <div class=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4\">\n        // 1 column mobile, 2 tablet, 3 desktop, 4 large desktop\n        <Card />\n        <Card />\n        <Card />\n    </div>\n}\n```\n\n### Dashboard Layout\n```rust\nview! {\n    <div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-6\">\n        // Stats: 2 columns on tablet, 4 columns on desktop\n        <div class=\"md:col-span-1 lg:col-span-3\"><Stat /></div>\n        <div class=\"md:col-span-1 lg:col-span-3\"><Stat /></div>\n        <div class=\"md:col-span-1 lg:col-span-3\"><Stat /></div>\n        <div class=\"md:col-span-1 lg:col-span-3\"><Stat /></div>\n        \n        // Chart: full width on mobile, 8 columns on desktop\n        <div class=\"md:col-span-2 lg:col-span-8\"><Chart /></div>\n        \n        // Sidebar: full width on mobile, 4 columns on desktop\n        <div class=\"md:col-span-2 lg:col-span-4\"><Activity /></div>\n    </div>\n}\n```\n\n## Flexbox Patterns\n\n### Responsive Flex Direction\n```rust\n// Stack on mobile, row on desktop\n\"flex flex-col lg:flex-row\"\n\n// Reverse order on desktop\n\"flex flex-col-reverse lg:flex-row\"\n```\n\n### Responsive Alignment\n```rust\n// Center on mobile, left on desktop\n\"items-center lg:items-start\"\n\n// Space between on desktop only\n\"justify-start lg:justify-between\"\n```\n\n## Touch vs Desktop\n\n### Touch Targets\n```css\n@media (pointer: coarse) {\n  /* Increase tap targets for touch devices */\n  :root {\n    --size-control-min: 44px; /* WCAG minimum */\n  }\n}\n```\n\n### Hover States\n```css\n@media (hover: hover) {\n  /* Only apply hover effects on devices that support hover */\n  .button:hover {\n    opacity: var(--state-hover-opacity);\n  }\n}\n```\n\n## Orientation Detection\n\n### Landscape vs Portrait\n```css\n@media (orientation: landscape) {\n  /* Optimize for landscape */\n  .sidebar {\n    display: block;\n  }\n}\n\n@media (orientation: portrait) {\n  /* Optimize for portrait */\n  .sidebar {\n    display: none;\n  }\n}\n```\n\n## Prohibited Patterns\n\n### ❌ Custom Breakpoints\n```css\n/* FORBIDDEN: Use canonical breakpoints */\n@media (min-width: 900px) { /* Not a standard breakpoint */ }\n```\n\n### ❌ Desktop-First Approach\n```css\n/* FORBIDDEN */\n@media (max-width: 1024px) { /* Use min-width */ }\n```\n\n### ❌ Fixed Pixel Widths\n```rust\n// FORBIDDEN\n\"w-[800px]\"  // Not responsive\n\n// CORRECT\n\"w-full max-w-4xl\"\n```\n\n### ❌ Breakpoint-Specific Classes in Components\n```rust\n// FORBIDDEN (component shouldn't know about layout)\nconst BUTTON_CLASSES: &str = \"w-full lg:w-auto\";\n\n// CORRECT (layout decides)\nview! {\n    <div class=\"w-full lg:w-auto\">\n        <Button />\n    </div>\n}\n```\n\n## Testing Checklist\n- [ ] Layout tested at all breakpoints (sm, md, lg, xl, 2xl)\n- [ ] Touch targets ≥44px on mobile\n- [ ] Text remains readable at all sizes\n- [ ] Horizontal scroll never occurs\n- [ ] Grid maintains alignment at all breakpoints\n- [ ] Orientation changes handled gracefully\n\n## Validation Tools\n```bash\n# Test responsive breakpoints\nnpm run test:responsive\n\n# Lighthouse mobile score\nnpm run lighthouse:mobile\n```\n\n## References\n- [Tailwind Breakpoints](https://tailwindcss.com/docs/responsive-design)\n- [WCAG 2.5.5 Target Size](https://www.w3.org/WAI/WCAG21/Understanding/target-size.html)\n- [CSS Grid Layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout)\n- [Canon Rule #24: Density & Size Scaling](./canon-rule-24-density-size-scaling.md)"},{"number":29,"slug":"typography-contract","title":"Typography Contract","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["typography","tokens","accessibility","css"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Typography inconsistency reduces readability and breaks design system coherence. A strict typographic scale ensures hierarchy and accessibility.","problem":"inconsistent typography usage breaks readability and system consistency","solution":"enforce canonical font stacks, sizes, weights, and line heights","signals":["mixed font sizes","poor readability","contrast issues"],"search_intent":"how to standardize typography design system","keywords":["typography scale design system","font tokens css system","wcag readable text guidelines","responsive typography patterns"],"body":"## Principle\nTypography creates **information hierarchy** and **readability**. Font stacks, sizes, weights, and line heights are **system-wide constants** that ensure visual consistency and performance across all themes.\n\n## Font Stacks\n\n### Canonical Font Families\n```css\n:root {\n  /* Sans-serif (default UI) */\n  --font-sans: ui-sans-serif, system-ui, sans-serif, \n               'Apple Color Emoji', 'Segoe UI Emoji', \n               'Segoe UI Symbol', 'Noto Color Emoji';\n  \n  /* Monospace (code, data) */\n  --font-mono: ui-monospace, 'SFMono-Regular', 'Menlo', \n               'Monaco', 'Consolas', 'Liberation Mono', \n               'Courier New', monospace;\n  \n  /* Serif (editorial content) */\n  --font-serif: ui-serif, 'Georgia', 'Cambria', \n                'Times New Roman', 'Times', serif;\n}\n```\n\n### Why System Fonts?\n✅ Zero network requests (instant load)  \n✅ Native OS rendering (crisp on all screens)  \n✅ Reduced bundle size  \n✅ Better privacy (no font CDN tracking)  \n✅ Cross-platform consistency\n\n### Prohibited\n❌ Web fonts (Google Fonts, Adobe Fonts)  \n❌ Custom font files (@font-face)  \n❌ Icon fonts (use SVG icons instead)\n\n**Exception:** Brand-specific applications may override fonts at the **application layer**, not theme layer.\n\n## Type Scale\n\n### Canonical Font Sizes\n```css\n:root {\n  /* Base: 16px = 1rem */\n  --font-size-xs: 0.75rem;    /* 12px */\n  --font-size-sm: 0.875rem;   /* 14px */\n  --font-size-md: 1rem;       /* 16px (base) */\n  --font-size-lg: 1.125rem;   /* 18px */\n  --font-size-xl: 1.25rem;    /* 20px */\n  --font-size-2xl: 1.5rem;    /* 24px */\n  --font-size-3xl: 1.875rem;  /* 30px */\n  --font-size-4xl: 2.25rem;   /* 36px */\n  --font-size-5xl: 3rem;      /* 48px */\n}\n```\n\n### Usage Guidelines\n\n**XS (12px):** Captions, labels, metadata\n```rust\n\"text-xs\"  // Small helper text\n```\n\n**SM (14px):** Body text (compact UI)\n```rust\n\"text-sm\"  // Table cells, list items\n```\n\n**MD (16px):** Default body text\n```rust\n\"text-base\"  // Paragraphs, descriptions\n```\n\n**LG (18px):** Emphasized body\n```rust\n\"text-lg\"  // Lead paragraphs\n```\n\n**XL-5XL:** Headings\n```rust\n\"text-xl\"   // H5\n\"text-2xl\"  // H4\n\"text-3xl\"  // H3\n\"text-4xl\"  // H2\n\"text-5xl\"  // H1\n```\n\n## Font Weights\n\n### Canonical Weights\n```css\n:root {\n  --font-weight-regular: 400;   /* Body text */\n  --font-weight-medium: 500;    /* Emphasis */\n  --font-weight-semibold: 600;  /* Subheadings */\n  --font-weight-bold: 700;      /* Headings */\n}\n```\n\n### Weight Pairing Rules\n```rust\n// Body text\n\"font-normal\"  // 400 - default\n\n// UI labels, buttons\n\"font-medium\"  // 500 - subtle emphasis\n\n// Section headers\n\"font-semibold\"  // 600 - clear hierarchy\n\n// Page titles\n\"font-bold\"  // 700 - maximum impact\n```\n\n### Prohibited\n❌ Thin (100-300) — insufficient contrast  \n❌ Extra Bold (800-900) — visually harsh\n\n## Line Heights\n\n### Canonical Line Heights\n```css\n:root {\n  --line-height-tight: 1.25;    /* Headings */\n  --line-height-normal: 1.5;    /* Body text */\n  --line-height-relaxed: 1.75;  /* Long-form reading */\n}\n```\n\n### Application Rules\n\n**Tight (1.25):** Headings, titles\n```rust\n\"leading-tight\"  // H1-H6\n```\n\n**Normal (1.5):** UI text, labels\n```rust\n\"leading-normal\"  // Default for all UI\n```\n\n**Relaxed (1.75):** Article body, documentation\n```rust\n\"leading-relaxed\"  // Long-form content\n```\n\n## Letter Spacing\n\n### Canonical Tracking\n```css\n:root {\n  --tracking-tighter: -0.05em;  /* Large headings */\n  --tracking-tight: -0.025em;   /* Headings */\n  --tracking-normal: 0em;       /* Body text */\n  --tracking-wide: 0.025em;     /* Uppercase labels */\n  --tracking-wider: 0.05em;     /* All-caps headings */\n}\n```\n\n### Usage\n```rust\n// Large display text (48px+)\n\"tracking-tighter\"  // Tighten for optical balance\n\n// Uppercase labels\n\"tracking-wide uppercase\"  // Improve readability\n\n// Default\n\"tracking-normal\"  // Most text\n```\n\n## Responsive Typography\n\n### Mobile-First Scaling\n```css\n/* Mobile base (16px) */\n:root {\n  --font-size-base: 1rem;\n}\n\n/* Tablet (18px base for comfort) */\n@media (min-width: 768px) {\n  :root {\n    --font-size-base: 1.125rem;\n  }\n}\n\n/* Desktop (back to 16px for density) */\n@media (min-width: 1024px) {\n  :root {\n    --font-size-base: 1rem;\n  }\n}\n```\n\n### Responsive Headings\n```rust\n// H1: Mobile 30px → Desktop 48px\n\"text-3xl lg:text-5xl\"\n\n// H2: Mobile 24px → Desktop 36px\n\"text-2xl lg:text-4xl\"\n\n// Body: Always 16px (don't scale)\n\"text-base\"\n```\n\n## Accessibility Requirements\n\n### Minimum Contrast Ratios (WCAG AA)\n- **Normal text (< 18px):** 4.5:1\n- **Large text (≥ 18px):** 3:1\n- **UI components:** 3:1\n\n### Readable Line Lengths\n```css\n/* Optimal: 45-75 characters per line */\n.prose {\n  max-width: 65ch;  /* ~65 characters */\n}\n```\n\n### Scalable Text\n```css\n/* NEVER use fixed pixel sizes */\n❌ font-size: 14px;\n\n/* ALWAYS use rem for scalability */\n✅ font-size: 0.875rem;  /* Scales with user preference */\n```\n\n### Zoom Support\nText must remain readable at 200% zoom:\n```css\n/* Use relative units */\n✅ padding: 1rem;\n✅ margin: 0.5em;\n\n/* Avoid fixed containers */\n❌ width: 300px;\n✅ max-width: 20rem;\n```\n\n## Component Typography Patterns\n\n### Button Text\n```rust\nconst BUTTON_TEXT: &str = \"\\\n    font-medium \\\n    text-sm \\\n    leading-normal\";\n```\n\n### Input Labels\n```rust\nconst LABEL_TEXT: &str = \"\\\n    font-medium \\\n    text-sm \\\n    leading-normal\";\n```\n\n### Helper Text\n```rust\nconst HELPER_TEXT: &str = \"\\\n    font-normal \\\n    text-xs \\\n    leading-normal \\\n    text-muted-foreground\";\n```\n\n### Card Titles\n```rust\nconst CARD_TITLE: &str = \"\\\n    font-semibold \\\n    text-lg \\\n    leading-tight\";\n```\n\n### Table Headers\n```rust\nconst TABLE_HEADER: &str = \"\\\n    font-medium \\\n    text-sm \\\n    leading-tight \\\n    uppercase \\\n    tracking-wide\";\n```\n\n## Prose Styling\n\n### Long-Form Content\n```rust\nconst PROSE_CLASSES: &str = \"\\\n    font-serif \\\n    text-base \\\n    leading-relaxed \\\n    max-w-prose \\\n    text-foreground\";\n```\n\n### Article Headings\n```css\n.prose h1 { @apply text-4xl font-bold leading-tight mt-8 mb-4; }\n.prose h2 { @apply text-3xl font-semibold leading-tight mt-6 mb-3; }\n.prose h3 { @apply text-2xl font-semibold leading-tight mt-4 mb-2; }\n.prose p  { @apply text-base leading-relaxed mb-4; }\n```\n\n## Code And Monospace\n\n### Inline Code\n```rust\nconst INLINE_CODE: &str = \"\\\n    font-mono \\\n    text-sm \\\n    bg-muted \\\n    px-1 \\\n    py-0.5 \\\n    rounded\";\n```\n\n### Code Blocks\n```rust\nconst CODE_BLOCK: &str = \"\\\n    font-mono \\\n    text-sm \\\n    leading-relaxed \\\n    bg-muted \\\n    p-4 \\\n    rounded-lg \\\n    overflow-x-auto\";\n```\n\n## Dark Mode Typography\n\n### Adjustments for Readability\n```css\n.dark {\n  /* Slightly reduce font weight for dark backgrounds */\n  --font-weight-regular: 350;  /* Instead of 400 */\n  --font-weight-medium: 450;   /* Instead of 500 */\n  \n  /* Increase line height slightly */\n  --line-height-normal: 1.6;   /* Instead of 1.5 */\n}\n```\n\n## Prohibited Patterns\n\n### ❌ Hardcoded Font Families\n```rust\n// FORBIDDEN\n\"font-['Arial']\"\n\"font-['Helvetica']\"\n\n// CORRECT\n\"font-sans\"\n```\n\n### ❌ Pixel-Based Font Sizes\n```rust\n// FORBIDDEN\n\"text-[14px]\"\n\n// CORRECT\n\"text-sm\"  // Uses var(--font-size-sm)\n```\n\n### ❌ Non-Standard Weights\n```rust\n// FORBIDDEN\n\"font-[350]\"\n\"font-[450]\"\n\n// CORRECT\n\"font-normal\"\n\"font-medium\"\n```\n\n### ❌ Custom Line Heights\n```rust\n// FORBIDDEN\n\"leading-[1.3]\"\n\n// CORRECT\n\"leading-tight\"\n\"leading-normal\"\n```\n\n## Performance Optimization\n\n### Font Loading Strategy\n```html\n<!-- Preload system fonts (no action needed) -->\n<!-- System fonts load instantly -->\n```\n\n### Font Display\n```css\n/* Not applicable - system fonts don't need font-display */\n```\n\n### Subsetting\n```css\n/* Not applicable - system fonts are pre-installed */\n```\n\n## Validation Checklist\n- [ ] All font families use system font stacks\n- [ ] Font sizes use canonical scale (xs → 5xl)\n- [ ] Font weights limited to 400, 500, 600, 700\n- [ ] Line heights use tight/normal/relaxed\n- [ ] Text remains readable at 200% zoom\n- [ ] Contrast ratios pass WCAG AA\n- [ ] No hardcoded pixel sizes\n- [ ] No web fonts or @font-face\n\n## References\n- [WCAG 2.1: Text Spacing](https://www.w3.org/WAI/WCAG21/Understanding/text-spacing.html)\n- [WCAG 2.1: Contrast Minimum](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)\n- [Modular Scale](https://www.modularscale.com/)\n- [System Font Stack](https://systemfontstack.com/)\n- [Canon Rule #24: Density & Size Scaling](./canon-rule-24-density-size-scaling.md)"},{"number":30,"slug":"iconography-system","title":"Iconography System","status":"ENFORCED","severity":"MEDIUM","category":"design-system","tags":["icons","svg","accessibility","ui"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using inconsistent icon systems or raster assets degrades performance and accessibility. A unified SVG-based system ensures consistency.","problem":"multiple icon systems or raster icons break consistency and accessibility","solution":"use single svg icon library with standardized sizing and aria patterns","signals":["icon inconsistency","raster usage","missing aria labels"],"search_intent":"how to design icon system svg","keywords":["svg icon system design","lucide icons usage frontend","accessible icons aria patterns","icon size scale ui system"],"body":"## Principle\nIcons are **SVG-based, inline, and accessible**. The system uses a **single canonical icon library** (Lucide) with consistent sizing, coloring, and ARIA patterns. Icon fonts are prohibited.\n\n## Icon Library\n\n### Canonical Library: Lucide React\n```toml\n[dependencies]\n# Rust binding to Lucide icons\nlucide-leptos = \"0.1\"  # or equivalent\n```\n\n**Why Lucide?**\n✅ MIT licensed (no attribution required)  \n✅ 1000+ consistent icons  \n✅ Designed for 24×24 grid  \n✅ Tree-shakeable (only import used icons)  \n✅ Active maintenance  \n✅ shadcn/ui compatible\n\n### Prohibited\n❌ Font Awesome (icon font)  \n❌ Material Icons (inconsistent licensing)  \n❌ Custom icon fonts  \n❌ Raster images (PNG, JPG) for UI icons  \n❌ Multiple icon libraries in one project\n\n## Icon Sizes\n\n### Canonical Size Scale\n```css\n:root {\n  --size-icon-xs: 0.875rem;  /* 14px */\n  --size-icon-sm: 1rem;      /* 16px */\n  --size-icon-md: 1.25rem;   /* 20px */\n  --size-icon-lg: 1.5rem;    /* 24px (native Lucide size) */\n  --size-icon-xl: 2rem;      /* 32px */\n}\n```\n\n### Size Application\n```rust\nuse leptos_lucide::*;\n\n// XS (14px): Inline with small text\nview! {\n    <CheckIcon size=14 />\n    <span class=\"text-xs\">\"Verified\"</span>\n}\n\n// SM (16px): Inline with body text\nview! {\n    <InfoIcon size=16 />\n    <span class=\"text-sm\">\"Learn more\"</span>\n}\n\n// MD (20px): Buttons, inputs\nview! {\n    <SearchIcon size=20 />\n}\n\n// LG (24px): Standalone icons, toolbar\nview! {\n    <MenuIcon size=24 />\n}\n\n// XL (32px): Large touch targets, hero icons\nview! {\n    <AlertCircleIcon size=32 />\n}\n```\n\n## Icon Colors\n\n### Color Inheritance\n```rust\n// Icons inherit currentColor by default\nview! {\n    <button class=\"text-primary\">\n        <CheckIcon size=16 />  // Automatically primary color\n        \"Confirm\"\n    </button>\n}\n```\n\n### Semantic Coloring\n```rust\n// Success state\nview! {\n    <CheckCircleIcon size=16 class=\"text-success\" />\n}\n\n// Error state\nview! {\n    <XCircleIcon size=16 class=\"text-destructive\" />\n}\n\n// Muted/inactive\nview! {\n    <InfoIcon size=16 class=\"text-muted-foreground\" />\n}\n```\n\n### Prohibited\n❌ Hardcoded fill colors  \n❌ Multi-color icons (use monochrome)  \n❌ Gradient fills\n\n## Icon Positioning\n\n### Inline with Text\n```rust\n// Icon + text alignment\nconst ICON_TEXT: &str = \"inline-flex items-center gap-2\";\n\nview! {\n    <div class=ICON_TEXT>\n        <CheckIcon size=16 />\n        <span>\"Completed\"</span>\n    </div>\n}\n```\n\n### Button Icons\n```rust\n// Icon-only button\nview! {\n    <button class=\"p-2\">\n        <MenuIcon size=20 />\n        <span class=\"sr-only\">\"Open menu\"</span>\n    </button>\n}\n\n// Icon + text button\nview! {\n    <button class=\"inline-flex items-center gap-2 px-4 py-2\">\n        <PlusIcon size=16 />\n        <span>\"Add Item\"</span>\n    </button>\n}\n```\n\n### Leading/Trailing Icons\n```rust\n// Input with leading icon\nview! {\n    <div class=\"relative\">\n        <SearchIcon size=16 class=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\" />\n        <input class=\"pl-10\" placeholder=\"Search...\" />\n    </div>\n}\n\n// Input with trailing icon\nview! {\n    <div class=\"relative\">\n        <input placeholder=\"Enter email...\" />\n        <CheckIcon size=16 class=\"absolute right-3 top-1/2 -translate-y-1/2 text-success\" />\n    </div>\n}\n```\n\n## Accessibility\n\n### ARIA Labels\n```rust\n// Icon-only buttons MUST have labels\nview! {\n    <button aria-label=\"Close dialog\">\n        <XIcon size=20 />\n    </button>\n}\n\n// OR use sr-only text\nview! {\n    <button>\n        <XIcon size=20 />\n        <span class=\"sr-only\">\"Close dialog\"</span>\n    </button>\n}\n```\n\n### Decorative Icons\n```rust\n// Icons that don't convey meaning\nview! {\n    <div>\n        <StarIcon size=16 aria-hidden=\"true\" />\n        <span>\"Featured\"</span>  // Text provides meaning\n    </div>\n}\n```\n\n### Semantic Icons\n```rust\n// Icons that convey state/meaning\nview! {\n    <div role=\"alert\">\n        <AlertTriangleIcon size=20 aria-label=\"Warning\" />\n        <span>\"This action cannot be undone\"</span>\n    </div>\n}\n```\n\n## Icon States\n\n### Interactive States\n```rust\n// Hover effect\nview! {\n    <button class=\"group\">\n        <HeartIcon \n            size=20 \n            class=\"group-hover:text-destructive transition-colors\"\n        />\n    </button>\n}\n\n// Active/Selected state\nview! {\n    <button data-state=is_active class=\"data-[state=true]:text-primary\">\n        <CheckIcon size=16 />\n    </button>\n}\n```\n\n### Loading State\n```rust\n// Spinning loader\nview! {\n    <Loader2Icon size=20 class=\"animate-spin\" />\n}\n```\n\n### Disabled State\n```rust\nview! {\n    <button disabled class=\"disabled:opacity-50\">\n        <SaveIcon size=16 />\n        \"Save\"\n    </button>\n}\n```\n\n## Icon Variants\n\n### Outlined vs Filled\n```rust\n// Lucide icons are outlined by default (preferred)\nview! {\n    <HeartIcon size=20 />  // Outlined\n}\n\n// Use filled sparingly (active states)\nview! {\n    <HeartIcon size=20 class=\"fill-current\" />  // Filled\n}\n```\n\n### Stroke Width\n```rust\n// Default stroke width: 2px\nview! {\n    <Icon size=24 stroke-width=\"2\" />\n}\n\n// Thin icons (delicate UI)\nview! {\n    <Icon size=24 stroke-width=\"1\" />\n}\n\n// Bold icons (emphasis)\nview! {\n    <Icon size=24 stroke-width=\"3\" />\n}\n```\n\n## Performance\n\n### Tree Shaking\n```rust\n// ✅ CORRECT: Import only used icons\nuse leptos_lucide::{CheckIcon, XIcon, MenuIcon};\n\n// ❌ FORBIDDEN: Import all icons\nuse leptos_lucide::*;  // Only in examples\n```\n\n### SVG Optimization\n```rust\n// Icons should be optimized with SVGO\n// Lucide icons are pre-optimized\n```\n\n### Lazy Loading\n```rust\n// Icons used below fold can be lazy loaded\n#[component]\nfn LazyIcon() -> impl IntoView {\n    let IconComponent = || async {\n        // Dynamic import for code splitting\n        leptos::spawn_local(async {\n            // Load icon component\n        });\n    };\n}\n```\n\n## Custom Icons\n\n### When Allowed\n✅ Brand logos (as SVG)  \n✅ Product-specific icons not in Lucide  \n✅ Data visualizations\n\n### Requirements\n```rust\n// Custom icons MUST:\n// 1. Be SVG format\n// 2. Use currentColor for fills/strokes\n// 3. Have viewBox=\"0 0 24 24\" (24×24 grid)\n// 4. Be optimized with SVGO\n// 5. Include ARIA labels\n\nview! {\n    <svg \n        width=\"24\" \n        height=\"24\" \n        viewBox=\"0 0 24 24\" \n        fill=\"none\" \n        stroke=\"currentColor\"\n        stroke-width=\"2\"\n        stroke-linecap=\"round\"\n        stroke-linejoin=\"round\"\n        aria-label=\"Custom brand icon\"\n    >\n        <path d=\"...\" />\n    </svg>\n}\n```\n\n## Icon Grid System\n\n### 24×24 Grid (Lucide Standard)\n```\nAll icons fit within a 24×24 pixel grid\nOptical alignment takes priority over mathematical centering\n2px default stroke width\nRounded line caps and joins\n```\n\n### Alignment Rules\n```rust\n// Vertically center with text\n\"inline-flex items-center\"\n\n// Baseline align with text\n\"inline-flex items-baseline\"\n\n// Pixel-perfect alignment\n\"inline-block align-middle\"\n```\n\n## Dark Mode\n\n### Icon Visibility\n```css\n/* Icons inherit theme colors automatically */\n.dark .icon {\n  /* No special handling needed */\n  color: inherit;\n}\n\n/* Reduce stroke width in dark mode for optical balance */\n.dark {\n  --icon-stroke-width: 1.5px;  /* Instead of 2px */\n}\n```\n\n## Prohibited Patterns\n\n### ❌ Icon Fonts\n```html\n<!-- FORBIDDEN -->\n<i class=\"fa fa-check\"></i>\n<span class=\"material-icons\">check</span>\n```\n\n### ❌ Raster Icons\n```html\n<!-- FORBIDDEN -->\n<img src=\"icon.png\" alt=\"Check\" />\n```\n\n### ❌ Inline SVG Strings\n```rust\n// FORBIDDEN\nconst ICON_SVG: &str = \"<svg>...</svg>\";\n\n// CORRECT\nuse leptos_lucide::CheckIcon;\n```\n\n### ❌ Hardcoded Sizes\n```rust\n// FORBIDDEN\n<Icon size=18 />  // Not in scale\n\n// CORRECT\n<Icon size=16 />  // Uses canonical size\n```\n\n## Validation Checklist\n- [ ] All icons from Lucide library\n- [ ] Icon sizes use canonical scale (14, 16, 20, 24, 32)\n- [ ] Icons use currentColor for theming\n- [ ] Icon-only buttons have ARIA labels\n- [ ] Decorative icons have aria-hidden=\"true\"\n- [ ] No icon fonts used\n- [ ] No raster image icons\n- [ ] Custom icons follow 24×24 grid\n\n## References\n- [Lucide Icons](https://lucide.dev/)\n- [WCAG 2.1: Non-text Content](https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html)\n- [SVG Accessibility](https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA24.html)\n- [Canon Rule #23: State Tokens](./canon-rule-23-state-tokens.md)"},{"number":31,"slug":"accessibility-contract","title":"Accessibility Contract (ARIA + Roles)","status":"ENFORCED","severity":"CRITICAL","category":"accessibility","tags":["aria","wcag","ui","a11y"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Lack of accessibility standards leads to unusable interfaces for assistive technologies. Strict WCAG compliance is required.","problem":"components lack accessibility compliance breaking usability for assistive tech","solution":"enforce aria roles, keyboard navigation, and wcag 2.1 aa standards","signals":["missing aria","keyboard trap","poor contrast"],"search_intent":"how to implement wcag accessibility frontend","keywords":["wcag accessibility frontend","aria roles implementation","keyboard navigation ui","accessible components design"],"body":"## Principle\nAccessibility is **non-negotiable**. All components must be **keyboard navigable**, **screen reader friendly**, and **WCAG 2.1 AA compliant**. ARIA attributes and semantic HTML are mandatory, not optional.\n\n## WCAG 2 1 AA Compliance\n\n### Mandatory Requirements\n✅ **Perceivable:** Content visible to all users\n✅ **Operable:** Keyboard navigation works\n✅ **Understandable:** Clear labels and instructions\n✅ **Robust:** Works with assistive technologies\n\n### Success Criteria Checklist\n- [ ] 1.4.3 Contrast (Minimum) — 4.5:1 for text\n- [ ] 2.1.1 Keyboard — All functionality via keyboard\n- [ ] 2.1.2 No Keyboard Trap — Focus can move away\n- [ ] 2.4.3 Focus Order — Logical tab sequence\n- [ ] 2.4.7 Focus Visible — Clear focus indicators\n- [ ] 2.5.5 Target Size — 44×44px minimum touch targets\n- [ ] 3.2.1 On Focus — No context changes on focus\n- [ ] 4.1.2 Name, Role, Value — Proper ARIA attributes\n\n## Semantic HTML\n\n### Use Correct Elements\n```rust\n// ✅ CORRECT\nview! {\n    <button onClick=handler>\"Click me\"</button>\n    <a href=\"/page\">\"Navigate\"</a>\n    <input type=\"text\" />\n}\n\n// ❌ FORBIDDEN\nview! {\n    <div onClick=handler>\"Click me\"</div>  // Use <button>\n    <span onClick=navigate>\"Navigate\"</span>  // Use <a>\n}\n```\n\n### Heading Hierarchy\n```rust\n// ✅ CORRECT: Logical order\nview! {\n    <h1>\"Page Title\"</h1>\n    <h2>\"Section\"</h2>\n    <h3>\"Subsection\"</h3>\n}\n\n// ❌ FORBIDDEN: Skipping levels\nview! {\n    <h1>\"Page Title\"</h1>\n    <h3>\"Section\"</h3>  // Skipped H2\n}\n```\n\n## ARIA Roles\n\n### Standard Roles\n```rust\n// Navigation\nview! {\n    <nav role=\"navigation\" aria-label=\"Main navigation\">\n}\n\n// Main content\nview! {\n    <main role=\"main\">\n}\n\n// Complementary content\nview! {\n    <aside role=\"complementary\">\n}\n\n// Search\nview! {\n    <div role=\"search\">\n        <input type=\"search\" aria-label=\"Search\" />\n    </div>\n}\n```\n\n### Interactive Roles\n```rust\n// Button (when not using <button>)\nview! {\n    <div role=\"button\" tabindex=\"0\" onClick=handler aria-label=\"Close\">\n}\n\n// Checkbox (when custom)\nview! {\n    <div\n        role=\"checkbox\"\n        aria-checked=is_checked\n        tabindex=\"0\"\n        onClick=toggle\n    >\n}\n\n// Tab interface\nview! {\n    <div role=\"tablist\">\n        <button role=\"tab\" aria-selected=\"true\">\n    </div>\n    <div role=\"tabpanel\">\n}\n```\n\n### Widget Roles\n```rust\n// Dialog\nview! {\n    <div role=\"dialog\" aria-labelledby=\"dialog-title\" aria-modal=\"true\">\n        <h2 id=\"dialog-title\">\"Confirm Action\"</h2>\n    </div>\n}\n\n// Alert\nview! {\n    <div role=\"alert\" aria-live=\"assertive\">\n        \"Error: Invalid input\"\n    </div>\n}\n\n// Menu\nview! {\n    <div role=\"menu\">\n        <button role=\"menuitem\">\n    </div>\n}\n```\n\n## ARIA Attributes\n\n### Labels\n```rust\n// aria-label: Invisible label\nview! {\n    <button aria-label=\"Close dialog\">\n        <XIcon />\n    </button>\n}\n\n// aria-labelledby: Reference visible label\nview! {\n    <div role=\"dialog\" aria-labelledby=\"title\">\n        <h2 id=\"title\">\"Settings\"</h2>\n    </div>\n}\n\n// aria-describedby: Additional description\nview! {\n    <input\n        id=\"email\"\n        aria-describedby=\"email-hint\"\n    />\n    <span id=\"email-hint\">\"We'll never share your email\"</span>\n}\n```\n\n### States\n```rust\n// aria-checked\nview! {\n    <div role=\"checkbox\" aria-checked=is_checked>\n}\n\n// aria-pressed (toggle button)\nview! {\n    <button aria-pressed=is_active>\n}\n\n// aria-selected (tabs, lists)\nview! {\n    <button role=\"tab\" aria-selected=is_active>\n}\n\n// aria-expanded (collapsible)\nview! {\n    <button aria-expanded=is_open aria-controls=\"panel\">\n    <div id=\"panel\">\n}\n\n// aria-disabled\nview! {\n    <button aria-disabled=\"true\" disabled>\n}\n```\n\n### Properties\n```rust\n// aria-required\nview! {\n    <input aria-required=\"true\" required />\n}\n\n// aria-invalid\nview! {\n    <input aria-invalid=has_error />\n}\n\n// aria-live\nview! {\n    <div aria-live=\"polite\">  // Announcements\n}\n\n// aria-hidden\nview! {\n    <div aria-hidden=\"true\">  // Decorative content\n}\n```\n\n### Relationships\n```rust\n// aria-controls\nview! {\n    <button aria-controls=\"menu\" aria-expanded=is_open>\n    <div id=\"menu\">\n}\n\n// aria-owns\nview! {\n    <div aria-owns=\"child-list\">\n    <ul id=\"child-list\">\n}\n\n// aria-activedescendant (combobox)\nview! {\n    <input\n        role=\"combobox\"\n        aria-activedescendant=active_option_id\n        aria-controls=\"listbox\"\n    />\n    <ul id=\"listbox\" role=\"listbox\">\n}\n```\n\n## Keyboard Navigation\n\n### Focus Management\n```rust\n// Focusable elements\nconst FOCUSABLE: &str = \"focus:outline-none focus:ring-2 focus:ring-primary\";\n\n// Tab order\nview! {\n    <button tabindex=\"0\">\"First\"</button>\n    <button tabindex=\"0\">\"Second\"</button>\n    <button tabindex=\"-1\">\"Skip in tab order\"</button>\n}\n```\n\n### Keyboard Event Handlers\n```rust\n#[component]\npub fn KeyboardComponent() -> impl IntoView {\n    let handle_key = move |ev: KeyboardEvent| {\n        match ev.key().as_str() {\n            \"Enter\" | \" \" => {\n                // Activate\n                ev.prevent_default();\n            }\n            \"Escape\" => {\n                // Close/cancel\n                ev.prevent_default();\n            }\n            \"ArrowUp\" | \"ArrowDown\" => {\n                // Navigate\n                ev.prevent_default();\n            }\n            _ => {}\n        }\n    };\n\n    view! {\n        <div onKeyDown=handle_key>\n    }\n}\n```\n\n### Focus Trapping (Modals)\n```rust\n// Keep focus inside dialog\n#[component]\npub fn Dialog() -> impl IntoView {\n    let dialog_ref = NodeRef::<html::Div>::new();\n\n    Effect::new(move |_| {\n        if let Some(dialog) = dialog_ref.get() {\n            // Focus first focusable element\n            if let Some(first) = dialog\n                .query_selector(\"button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])\")\n                .ok()\n                .flatten()\n            {\n                let _ = first.dyn_ref::<web_sys::HtmlElement>()\n                    .map(|el| el.focus());\n            }\n        }\n    });\n\n    view! {\n        <div node_ref=dialog_ref role=\"dialog\" aria-modal=\"true\">\n    }\n}\n```\n\n## Screen Reader Support\n\n### Announcements\n```rust\n// Live regions\nview! {\n    <div role=\"status\" aria-live=\"polite\">\n        \"Item added to cart\"\n    </div>\n}\n\n// Assertive announcements (errors)\nview! {\n    <div role=\"alert\" aria-live=\"assertive\">\n        \"Form submission failed\"\n    </div>\n}\n```\n\n### Hidden Content\n```rust\n// Visually hidden but screen reader accessible\nconst SR_ONLY: &str = \"sr-only\";\n\nview! {\n    <button>\n        <Icon />\n        <span class=SR_ONLY>\"Close\"</span>\n    </button>\n}\n```\n\n### Skip Links\n```rust\n// Allow keyboard users to skip navigation\nview! {\n    <a href=\"#main-content\" class=\"sr-only focus:not-sr-only\">\n        \"Skip to main content\"\n    </a>\n    <nav>...</nav>\n    <main id=\"main-content\">\n}\n```\n\n## Form Accessibility\n\n### Labels\n```rust\n// Always associate labels\nview! {\n    <label for=\"email\">\"Email\"</label>\n    <input id=\"email\" type=\"email\" />\n}\n\n// OR wrap input\nview! {\n    <label>\n        \"Email\"\n        <input type=\"email\" />\n    </label>\n}\n```\n\n### Error Messages\n```rust\nview! {\n    <div>\n        <label for=\"password\">\"Password\"</label>\n        <input\n            id=\"password\"\n            aria-invalid=has_error\n            aria-describedby=move || if has_error.get() { Some(\"password-error\") } else { None }\n        />\n        <Show when=has_error>\n            <span id=\"password-error\" role=\"alert\" class=\"text-destructive\">\n                \"Password must be at least 8 characters\"\n            </span>\n        </Show>\n    </div>\n}\n```\n\n### Required Fields\n```rust\nview! {\n    <label for=\"name\">\n        \"Name\"\n        <span aria-label=\"required\">\"*\"</span>\n    </label>\n    <input id=\"name\" required aria-required=\"true\" />\n}\n```\n\n## Touch Targets\n\n### Minimum Size (WCAG 2.5.5)\n```css\n/* Ensure 44×44px minimum touch targets */\n@media (pointer: coarse) {\n  button, a, input[type=\"checkbox\"], input[type=\"radio\"] {\n    min-width: 44px;\n    min-height: 44px;\n  }\n}\n```\n\n### Spacing\n```rust\n// Adequate spacing between interactive elements\nview! {\n    <div class=\"flex gap-2\">  // Minimum 8px gap\n        <button>\"Button 1\"</button>\n        <button>\"Button 2\"</button>\n    </div>\n}\n```\n\n## Testing\n\n### Automated Testing\n```bash\n# Axe DevTools\nnpm install --save-dev @axe-core/cli\n\n# Run accessibility audit\naxe http://localhost:3000\n```\n\n### Manual Testing\n✅ **Keyboard only:** Unplug mouse, navigate with Tab/Enter/Esc\n✅ **Screen reader:** Test with NVDA (Windows) or VoiceOver (Mac)\n✅ **Zoom:** Test at 200% zoom\n✅ **Color blindness:** Use Chrome DevTools emulation\n✅ **Touch:** Test on mobile device\n\n### Checklist\n- [ ] All interactive elements keyboard accessible\n- [ ] Focus indicators visible\n- [ ] Screen reader announces all content\n- [ ] No keyboard traps\n- [ ] Forms have labels and error messages\n- [ ] Touch targets ≥44×44px on mobile\n- [ ] Headings in logical order\n- [ ] Color contrast passes WCAG AA\n\n## Prohibited Patterns\n\n### ❌ Inaccessible Clickables\n```rust\n// FORBIDDEN\nview! {\n    <div onClick=handler>\"Click me\"</div>\n}\n\n// CORRECT\nview! {\n    <button onClick=handler>\"Click me\"</button>\n}\n```\n\n### ❌ Missing Labels\n```rust\n// FORBIDDEN\nview! {\n    <input type=\"text\" />\n}\n\n// CORRECT\nview! {\n    <label for=\"name\">\"Name\"</label>\n    <input id=\"name\" type=\"text\" />\n}\n```\n\n### ❌ Icon-Only Buttons Without Labels\n```rust\n// FORBIDDEN\nview! {\n    <button><XIcon /></button>\n}\n\n// CORRECT\nview! {\n    <button aria-label=\"Close\">\n        <XIcon />\n    </button>\n}\n```\n\n### ❌ Removing Focus Indicators\n```css\n/* FORBIDDEN */\n*:focus {\n  outline: none;\n}\n```\n\n## Validation Checklist\n- [ ] All components keyboard navigable\n- [ ] All interactive elements have ARIA roles/labels\n- [ ] Focus order is logical\n- [ ] Screen reader announces all content correctly\n- [ ] Forms have proper labels and error handling\n- [ ] Touch targets meet 44×44px minimum\n- [ ] Color contrast passes WCAG AA\n- [ ] No keyboard traps\n- [ ] Skip links present\n- [ ] Headings in correct hierarchy\n\n## References\n- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)\n- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)\n- [WebAIM](https://webaim.org/)\n- [Inclusive Components](https://inclusive-components.design/)\n- [Canon Rule #23: State Tokens](./canon-rule-23-state-tokens.md)\n- [Canon Rule #30: Iconography System](./canon-rule-30-iconography-system.md)"},{"number":32,"slug":"theme-persistence-contract","title":"Theme Persistence Contract","status":"ENFORCED","severity":"MEDIUM","category":"state-reactivity","tags":["theme","cookies","ssr","state"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using client-only storage for theme state causes SSR mismatch and visual flash. Persistence must be SSR-safe.","problem":"theme persistence via client storage breaks ssr and causes flash","solution":"persist theme using http cookies with server synchronization","signals":["flash of wrong theme","hydration mismatch","theme reset"],"search_intent":"how to persist theme ssr safely","keywords":["theme persistence ssr cookies","avoid flash of incorrect theme","leptos theme hydration","cookie based ui state"],"body":"```javascript\nhtml.setAttribute('data-theme', getCookie('theme-preset'));\n```\n\n### Cookie Hijacking\n\n**Not sensitive data** - Theme preference is not a security risk. No HTTPS requirement, but recommended.\n\n## Testing\n\n### SSR Test\n```bash\ncurl -H \"Cookie: theme-mode=dark; theme-preset=ocean\" http://localhost:3000\n# Should return HTML with class=\"dark\" and data-theme=\"ocean\"\n```\n\n### Cookie Persistence Test\n\n1. Set theme to Dark + Ocean preset\n2. Close browser completely\n3. Reopen and navigate to app\n4. Verify theme is Dark + Ocean (no flash)\n\n### Flash Prevention Test\n\n1. Set theme to Dark\n2. Hard reload (Ctrl+Shift+R)\n3. Watch for white flash during load\n4. **PASS:** No flash, dark theme from frame 1\n\n### Validation Checklist\n\n- [ ] Cookies set with correct Max-Age (1 year)\n- [ ] SameSite=Lax for CSRF protection\n- [ ] Anti-flash script in `<head>` before CSS\n- [ ] Script reads cookies (not localStorage)\n- [ ] SSR passes initial theme to ThemeProvider\n- [ ] No flash on page load/reload\n- [ ] Theme persists after browser restart\n- [ ] Server functions only in app layer\n\n## Migration From Localstorage\n\nIf migrating from localStorage-based theme:\n```javascript\n// In anti-flash script, add migration\nconst legacyMode = localStorage.getItem('theme-mode');\nif (legacyMode && !getCookie('theme-mode')) {\n    // First visit after migration - convert to cookie\n    document.cookie = `theme-mode=${legacyMode}; Path=/; Max-Age=31536000; SameSite=Lax`;\n    localStorage.removeItem('theme-mode'); // Clean up\n}\n```\n\n## Prohibited Patterns\n\n### ❌ Client-Only Persistence\n```rust\n// WRONG - causes flash\n#[component]\npub fn ThemeProvider() -> impl IntoView {\n    Effect::new(move |_| {\n        localStorage.setItem('theme', mode);\n    });\n}\n```\n\n### ❌ Server Functions in Design System\n```rust\n// WRONG - violates architecture\n// File: packages-rust/rs-design/src/providers/theme_provider.rs\n#[server]\nfn save_theme() { /* ... */ } // ❌ NO!\n```\n\n### ❌ Async Script Loading\n```html\n<!-- WRONG - script loads after CSS -->\n<link rel=\"stylesheet\" href=\"/app.css\">\n<script async src=\"/theme.js\"></script>\n```\n\n## References\n\n- [MDN: HTTP Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)\n- [SameSite Cookie Explained](https://web.dev/samesite-cookies-explained/)\n- [Leptos SSR Guide](https://book.leptos.dev/ssr/index.html)\n- [Canon Rule #21: Canonical Color Tokens](./canon-rule-21-canonical-color-tokens.md)\n- [Canon Rule #25: Theme Presets Contract](./canon-rule-25-theme-presets-contract.md)"},{"number":33,"slug":"density-accessibility-mapping","title":"Density & Accessibility Mapping","status":"ENFORCED","severity":"HIGH","category":"accessibility","tags":["density","tokens","wcag","ui"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Incorrect density scaling can break accessibility and usability across devices. Density must respect WCAG constraints.","problem":"ui density scaling breaks accessibility requirements and usability constraints","solution":"apply density scaling using tokens while enforcing wcag compliant touch targets and text sizes","signals":["small touch target","text unreadable","layout overflow"],"search_intent":"how to scale ui density accessibly","keywords":["density scaling accessibility wcag","ui density tokens design system","touch target minimum size","responsive density mapping"],"body":"| Density | Row Height | Cell Padding |\n|---------|------------|--------------|\n| Compact | 30px | 6px |\n| Comfortable | 40px | 8px |\n| Spacious | 50px | 10px |\n\n## Accessibility Features\n\n### Focus Indicators\n```css\n.focusable:focus-visible {\n  outline: 2px solid var(--color-ring);\n  outline-offset: calc(2px * var(--density-multiplier));\n  /* Offset scales, width does not */\n}\n```\n\n### Disabled States\n```css\n.disabled {\n  opacity: var(--state-disabled-opacity); /* 0.5 */\n  cursor: not-allowed;\n  /* Opacity is density-independent */\n}\n```\n\n### Hover Active States\n```css\n.interactive:hover {\n  /* Padding increases on hover for better feedback */\n  padding: calc(\n    var(--space-sm) * var(--density-multiplier) * 1.1\n  );\n}\n```\n\n## Responsive Breakpoints\n\n### Mobile First Scaling\n```css\n/* Mobile: Always enforce minimum touch targets */\n@media (max-width: 768px) {\n  :root {\n    --density-multiplier: max(1.1, var(--density-multiplier));\n  }\n}\n\n/* Tablet: Allow comfortable or spacious */\n@media (min-width: 768px) and (max-width: 1024px) {\n  :root {\n    --density-multiplier: max(1.0, var(--density-multiplier));\n  }\n}\n\n/* Desktop: All densities allowed */\n@media (min-width: 1024px) {\n  [data-density=\"compact\"] {\n    --density-multiplier: 0.75;\n  }\n}\n```\n\n## User Preference Detection\n\n### Prefers Reduced Motion\n```css\n@media (prefers-reduced-motion: reduce) {\n  * {\n    /* Density still applies, but animations are disabled */\n    transition-duration: 0.01ms !important;\n  }\n}\n```\n\n### Prefers Contrast\n```css\n@media (prefers-contrast: more) {\n  :root {\n    /* Increase touch targets for high contrast users */\n    --density-multiplier: max(1.1, var(--density-multiplier));\n  }\n}\n```\n\n### Forced Colors Mode\n```css\n@media (forced-colors: active) {\n  .button {\n    /* Maintain density, adjust borders for visibility */\n    border: calc(2px * var(--density-multiplier)) solid CanvasText;\n  }\n}\n```\n\n## Testing Matrix\n\n### Manual Testing\n\n| Test Case | Compact | Comfortable | Spacious |\n|-----------|---------|-------------|----------|\n| Desktop + Mouse | ✅ 30px | ✅ 40px | ✅ 50px |\n| Desktop + Keyboard | ✅ Focus visible | ✅ Focus visible | ✅ Focus visible |\n| Mobile + Touch | ⚠️ Override to 44px | ✅ 44px | ✅ 56px |\n| Screen Reader | ✅ Same semantic | ✅ Same semantic | ✅ Same semantic |\n| Zoom 200% | ✅ No overflow | ✅ No overflow | ✅ No overflow |\n\n### Automated Testing\n```rust\n#[test]\nfn test_density_compliance() {\n    // Compact minimum\n    assert!(calculate_size(\"compact\") >= 30.0);\n\n    // Mobile minimum\n    assert!(calculate_touch_target(\"compact\", \"coarse\") >= 44.0);\n\n    // Spacious maximum (reasonable limit)\n    assert!(calculate_size(\"spacious\") <= 60.0);\n}\n```\n\n## Validation Checklist\n\n- [ ] All interactive elements meet minimum touch target (44px mobile)\n- [ ] Text remains readable at all densities (min 14px)\n- [ ] Focus indicators visible at all densities\n- [ ] Keyboard navigation works at all densities\n- [ ] No horizontal scroll at 200% zoom\n- [ ] Spacing proportional across densities\n- [ ] Typography does NOT scale with density\n- [ ] Colors maintain contrast ratios (4.5:1 minimum)\n\n## Prohibited Patterns\n\n### Scaling Typography with Density\n```css\n/* WRONG - text becomes too small in compact */\nfont-size: calc(var(--font-size-md) * var(--density-multiplier));\n```\n\n### Ignoring Mobile Touch Targets\n```css\n/* WRONG - too small for touch */\n@media (pointer: coarse) {\n  [data-density=\"compact\"] {\n    --density-multiplier: 0.75; /* Results in 30px - too small! */\n  }\n}\n```\n\n### Fixed Pixel Sizes\n```css\n/* WRONG - doesn't scale with density */\n.button {\n  height: 40px; /* Use calc() with density multiplier */\n}\n```\n\n### Inconsistent Scaling\n```css\n/* WRONG - some properties scale, others don't */\n.card {\n  padding: calc(var(--space-md) * var(--density-multiplier)); /* Scales */\n  gap: var(--space-sm); /* Doesn't scale - inconsistent! */\n}\n```\n\n## Implementation Example\n```rust\n// Rust component with density awareness\n#[component]\npub fn Button(\n    #[prop(optional)] density_aware: bool,\n) -> impl IntoView {\n    let base_class = if density_aware {\n        // Scales with data-density attribute\n        \"h-[calc(var(--size-control-md)*var(--density-multiplier))] \\\n         px-[calc(var(--space-lg)*var(--density-multiplier))]\"\n    } else {\n        // Fixed size (density-independent)\n        \"h-10 px-6\"\n    };\n\n    view! {\n        <button class=base_class>\n            {children()}\n        </button>\n    }\n}\n```\n\n## References\n\n- [WCAG 2.1: Target Size](https://www.w3.org/WAI/WCAG21/Understanding/target-size.html)\n- [WCAG 2.1: Contrast Minimum](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)\n- [Apple HIG: Touch Targets](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/)\n- [Material Design: Touch Targets](https://material.io/design/usability/accessibility.html#layout-and-typography)\n- [Canon Rule #24: Density & Size Scaling](./canon-rule-24-density-size-scaling.md)\n- [Canon Rule #31: Accessibility Contract](./canon-rule-31-accessibility-contract.md)"},{"number":34,"slug":"theme-density-enforcement","title":"Theme & Density Enforcement (Lint Rules)","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["lint","tokens","ci","rules"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Manual enforcement of theme and density rules leads to inconsistency and drift. Automated validation is required.","problem":"theme and density rules are not enforced causing inconsistent implementations","solution":"use linting and ci checks to enforce canonical theme and density rules","signals":["rule violation","hardcoded values","ci failure"],"search_intent":"how to enforce design system rules ci","keywords":["design system lint rules","ci enforcement tokens css","frontend rule validation","automated ui compliance checks"],"body":"```rust\n// canon-exempt: migration from localStorage to cookies\nif let Some(legacy) = localStorage.getItem(\"theme\") {\n    set_cookie(legacy);\n}\n```\n\n## References\n\n- [Canon Rule #21: Canonical Color Tokens](./canon-rule-21-canonical-color-tokens.md)\n- [Canon Rule #32: Theme Persistence Contract](./canon-rule-32-theme-persistence-contract.md)\n- [Canon Rule #33: Density & Accessibility Mapping](./canon-rule-33-density-accessibility-mapping.md)"},{"number":35,"slug":"token-usage-validation","title":"Token Usage Validation","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["tokens","validation","css","lint"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Hardcoded visual values break consistency and scalability of the design system. Token usage must be enforced.","problem":"components use hardcoded values instead of tokens causing inconsistency","solution":"validate all visual properties against canonical token system via automated checks","signals":["hardcoded color","px usage","token violation"],"search_intent":"how to validate token usage css","keywords":["design system token validation","css token enforcement rules","frontend style consistency check","avoid hardcoded ui values"],"body":"```rust\n// token-exempt: documentation example\n\"bg-red-500\"  // Shows non-canonical usage for comparison\n```\n\n## Validation Commands\n```bash\n# Full validation\ncd tools/token-validator\ncargo run --release\n\n# Quick check (grep-based)\n./scripts/check-tokens.sh\n\n# Per-component check\n./scripts/check-tokens.sh src/ui/button/\n\n# Generate report\ncargo run --release -- --report > token-report.txt\n```\n\n## Report Output\n```\nToken Usage Validation Report\nGenerated: 2025-01-02 15:30:00\n\nSummary:\n  Total Files: 127\n  Files Checked: 127\n  Violations: 3\n  Compliance: 97.6%\n\nViolations by Category:\n  Color Tokens (Canon #21): 2\n  Typography (Canon #29): 1\n  Spacing (Canon #24): 0\n\nCritical Violations:\n  ❌ src/ui/alert/alert.rs:45:12\n     Found: bg-red-500\n     Expected: bg-destructive\n\n  ❌ src/ui/badge/badge.rs:23:8\n     Found: text-blue-600\n     Expected: text-primary\n\n  ⚠️  src/ui/input/input.rs:67:10\n     Found: text-[14px]\n     Expected: text-sm\n\nAction Required:\n  Fix 3 violations to achieve 100% compliance\n```\n\n## Pre Commit Hook\n```bash\n#!/bin/bash\n# .git/hooks/pre-commit\n\necho \"🔍 Validating token usage...\"\n\ncd tools/token-validator\nif ! cargo run --quiet 2>/dev/null; then\n    echo \"❌ Token validation failed!\"\n    echo \"Run 'cd tools/token-validator && cargo run' for details\"\n    exit 1\nfi\n\necho \"✅ All tokens canonical!\"\n```\n\n## References\n\n- [Canon Rule #21: Canonical Color Tokens](./canon-rule-21-canonical-color-tokens.md)\n- [Canon Rule #24: Density & Size Scaling](./canon-rule-24-density-size-scaling.md)\n- [Canon Rule #29: Typography Contract](./canon-rule-29-typography-contract.md)\n- [Canon Rule #34: Theme & Density Enforcement](./canon-rule-34-theme-density-enforcement.md)"},{"number":36,"slug":"component-compliance-levels","title":"Component Compliance Levels","status":"ENFORCED","severity":"MEDIUM","category":"governance","tags":["components","compliance","rules","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Without defined compliance levels, components cannot balance strict rules and real constraints. Classification is required.","problem":"components lack compliance classification causing inconsistent rule enforcement","solution":"define compliance levels to control rule strictness and allowed exceptions","signals":["inconsistent rules","partial compliance","architecture drift"],"search_intent":"how to classify component compliance","keywords":["component compliance levels design system","frontend rule enforcement levels","ui component governance classification","design system compliance model"],"body":"```rust\n/// @canon-level strict\n/// Compliance: 100%\n#[component]\npub fn Button(...) -> impl IntoView { ... }\n```"},{"number":37,"slug":"provider-taxonomy-boundaries","title":"Provider Taxonomy & Boundaries","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["providers","architecture","state","ssr"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Mixing provider responsibilities causes coupling and SSR issues. Providers must remain orthogonal.","problem":"providers mix responsibilities causing coupling and incorrect state handling","solution":"define strict provider boundaries with stateless design system layer","signals":["state coupling","provider misuse","ssr issue"],"search_intent":"how to design provider architecture","keywords":["provider architecture design system","reactive providers separation","ssr safe providers leptos","frontend context boundaries"],"body":"```html\n<html class=\"dark\" data-theme=\"ocean\">\n```"},{"number":38,"slug":"theme-engine-contract","title":"Theme Engine Contract","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["theme","tokens","engine","css"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Multiple components writing css variables causes inconsistency and conflicts. A single engine must control output.","problem":"multiple components write css variables causing inconsistency","solution":"centralize css variable computation in a single theme engine","signals":["css conflict","variable override","theme inconsistency"],"search_intent":"how to design theme engine css","keywords":["theme engine css variables","design system theme computation","centralized theming architecture","frontend css variable management"],"body":"- ❌ Persist state\n- ❌ Read cookies\n- ❌ Call server functions\n- ❌ Modify providers"},{"number":39,"slug":"settings-ui-compliance","title":"Settings UI Compliance","status":"ENFORCED","severity":"MEDIUM","category":"component-architecture","tags":["ui","settings","providers","callbacks"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Settings components mixing persistence and state logic break separation of concerns. UI must remain stateless.","problem":"settings ui handles persistence and logic causing coupling","solution":"expose state changes via callbacks and delegate persistence to app layer","signals":["coupling","state misuse","side effects"],"search_intent":"how to design settings ui architecture","keywords":["settings ui stateless pattern","callback driven ui design","frontend provider interaction","design system settings components"],"body":""},{"number":40,"slug":"legacy-exception-annotation","title":"canon-rule-40-legacy-exception-annotation","status":"ENFORCED","severity":"LOW","category":"governance","tags":["legacy","exceptions","annotations","rules"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Legacy code without annotation introduces hidden violations and inconsistency. Exceptions must be explicit.","problem":"legacy exceptions are not documented causing hidden rule violations","solution":"require explicit annotation for all legacy exceptions","signals":["hidden violation","legacy drift","rule bypass"],"search_intent":"how to handle legacy exceptions rules","keywords":["legacy exception annotation pattern","code governance exceptions","frontend rule override annotation","technical debt tracking system"],"body":""},{"number":41,"slug":"leptos-resource-consumption","title":"Leptos Resource Consumption Contract","status":"ENFORCED","severity":"MEDIUM","category":"state-reactivity","tags":["leptos","resource","ssr","reactivity"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Treating Resource as Future breaks reactivity and causes silent rendering failures. It must be consumed correctly.","problem":"resource is awaited instead of accessed reactively causing render failure","solution":"consume resource via reactive methods like get and transition","signals":["frozen ui","no render","silent failure"],"search_intent":"how to use leptos resource correctly","keywords":["leptos resource get vs await","reactive resource consumption","ssr resource pattern leptos","transition component usage"],"body":"## Principle\n`Resource` in Leptos is a **reactive signal**, not a `Future`. It must be consumed via reactive tracking (`.get()`, `.read()`, `.with()`), never via `.await`. This rule prevents silent render failures and ensures proper hydration.\n\n## The Problem\n\n### Wrong Treating Resource as Future\n```rust\n#[component]\npub fn MyComponent() -> impl IntoView {\n    let data = Resource::new(|| (), |_| async {\n        fetch_data().await\n    });\n    \n    view! {\n        <Suspense fallback=|| view! { <div>\"Loading...\"</div> }>\n            {move || Suspend::new(async move {\n                // ❌ FATAL ERROR: Resource is not a Future\n                match data.await {\n                    Ok(result) => view! { <div>{result}</div> },\n                    Err(_) => view! { <div>\"Error\"</div> }\n                }\n            })}\n        </Suspense>\n    }\n}\n```\n\n**Why this fails:**\n- `Resource` does NOT implement `Future`\n- `.await` breaks reactive tracking\n- `Suspense` never resolves\n- UI stays frozen on fallback\n- No error, no panic—just silence\n\n### Correct Reactive Consumption\n```rust\n#[component]\npub fn MyComponent() -> impl IntoView {\n    let data = Resource::new(|| (), |_| async {\n        fetch_data().await\n    });\n    \n    view! {\n        <Transition fallback=|| view! { <div>\"Loading...\"</div> }>\n            {move || data.get().map(|result| match result {\n                Ok(value) => view! { <div>{value}</div> }.into_any(),\n                Err(_) => view! { <div>\"Error\"</div> }.into_any()\n            })}\n        </Transition>\n    }\n}\n```\n\n**Why this works:**\n- `data.get()` → reactive signal read\n- `Transition` knows to wait for `None → Some`\n- Leptos controls re-render timing\n- Proper hydration\n- SSR-safe\n\n## Rule Contract\n\n### Resource API Matrix\n\n| Method | Use Case | Works in SSR | Works in Client | Triggers Re-render |\n|--------|----------|--------------|-----------------|-------------------|\n| `.get()` | Read current value | ✅ | ✅ | ✅ |\n| `.read()` | Read with callback | ✅ | ✅ | ✅ |\n| `.with()` | Transform value | ✅ | ✅ | ✅ |\n| `.with_untracked()` | Read without tracking | ✅ | ✅ | ❌ |\n| `.await` | **NEVER USE** | ❌ | ❌ | ❌ |\n| `.refetch()` | Trigger reload | ❌ SSR | ✅ | ✅ |\n\n> ⚠️ `.refetch()` must never be triggered during SSR render phase. It is safe only after hydration, typically via `Action + Effect`.\n\n### Wrapper Components Matrix\n\n| Wrapper | For | Resource Method | Notes |\n|---------|-----|-----------------|-------|\n| `<Transition>` | `Resource` | `.get()` | SSR + Client |\n| `<Suspense>` | Direct `Future` | N/A | Client-only |\n| `<Show>` | Conditional | `.get()` | SSR + Client |\n| `<For>` | Lists | `.get()` | SSR + Client |\n\n## Common Violations\n\n### Violation 1 Using Suspend With Resource\n```rust\n// ❌ WRONG\nview! {\n    <Suspense>\n        {move || Suspend::new(async move {\n            data_resource.await  // Resource is not Future\n        })}\n    </Suspense>\n}\n\n// ✅ CORRECT\nview! {\n    <Transition>\n        {move || data_resource.get().map(|r| ...)}\n    </Transition>\n}\n```\n\n### Violation 2 Mixing Resource And Action\n```rust\n// ❌ WRONG (causes spawn_local panic in SSR)\nlet action = Action::new(|input| async move {\n    resource.await  // Double error: Resource not Future + spawn_local\n});\n\n// ✅ CORRECT\nlet action = Action::new(|input| async move {\n    call_server_fn().await\n});\n\nEffect::new(move |_| {\n    if action.version().get() > 0 {\n        resource.refetch();\n    }\n});\n```\n\n### Violation 3 Server Function As Resource\n```rust\n// ❌ WRONG\nlet data = Resource::new(|| (), |_| async {\n    // Server function already returns Future\n    my_server_fn().await\n});\n\nview! {\n    <Suspense>\n        {move || Suspend::new(async move {\n            data.await  // Nested futures don't work\n        })}\n    </Suspense>\n}\n\n// ✅ CORRECT\nlet data = Resource::new(|| (), |_| async {\n    my_server_fn().await\n});\n\nview! {\n    <Transition>\n        {move || data.get().map(|result| ...)}\n    </Transition>\n}\n```\n\n## Decision Tree\n```\nNeed async data?\n├─ SSR required?\n│  ├─ YES → Use Resource + Transition\n│  └─ NO → Use Resource + Transition (still recommended)\n│\n├─ User interaction triggers fetch?\n│  ├─ YES → Use Action + Effect + Resource.refetch()\n│  └─ NO → Use Resource\n│\n└─ Rendering?\n   ├─ Resource → .get() + Transition\n   ├─ Direct Future → Suspend (client-only)\n   └─ Static data → No wrapper needed\n```\n\n## Complete Working Example\n```rust\nuse leptos::prelude::*;\n\n#[server]\npub async fn fetch_workflow_steps(workflow_id: i64) -> Result<Vec<Step>, ServerFnError> {\n    // Database query\n    Ok(query_db(workflow_id).await?)\n}\n\n#[server]\npub async fn transition_step(workflow_id: i64, step_id: String, new_status: String) \n    -> Result<(), ServerFnError> \n{\n    update_db(workflow_id, step_id, new_status).await?;\n    Ok(())\n}\n\n#[component]\npub fn WorkflowView() -> impl IntoView {\n    // ✅ CORRECT: Resource for initial data\n    let steps = Resource::new(\n        || (), \n        |_| async { fetch_workflow_steps(1).await }\n    );\n    \n    // ✅ CORRECT: Action for mutations\n    let transition = Action::new(\n        move |(wid, sid, status): &(i64, String, String)| {\n            let wid = *wid;\n            let sid = sid.clone();\n            let status = status.clone();\n            async move {\n                transition_step(wid, sid, status).await\n            }\n        }\n    );\n    \n    // ✅ CORRECT: Effect to refetch after mutation\n    Effect::new(move |_| {\n        if transition.version().get() > 0 {\n            steps.refetch();\n        }\n    });\n    \n    view! {\n        // ✅ CORRECT: Transition wraps Resource.get()\n        <Transition fallback=|| view! { <div>\"Loading...\"</div> }>\n            {move || steps.get().map(|result| match result {\n                Ok(data) => view! {\n                    <div>\n                        {data.into_iter().map(|step| {\n                            let step_id = step.id.clone();\n                            view! {\n                                <div>\n                                    <span>{step.label}</span>\n                                    <button on:click=move |_| {\n                                        transition.dispatch((1, step_id.clone(), \"Active\".into()));\n                                    }>\n                                        \"Activate\"\n                                    </button>\n                                </div>\n                            }\n                        }).collect_view()}\n                    </div>\n                }.into_any(),\n                Err(_) => view! { <div>\"Error\"</div> }.into_any()\n            })}\n        </Transition>\n    }\n}\n```\n\n## SSR Implications\n\n### Server Side Rendering Flow\n```\n1. SSR renders component\n   └─ Resource.get() returns None (async not started)\n   └─ Transition shows fallback\n\n2. HTML sent to client with fallback\n\n3. Client hydrates\n   └─ Resource starts async fetch\n   └─ Resource.get() returns Some(result)\n   └─ Transition switches to content\n```\n\n### Hydration Safety\n```rust\n// ✅ SAFE: SSR renders same HTML as client expects\nview! {\n    <Transition fallback=|| view! { <div>\"Loading\"</div> }>\n        {move || resource.get().map(|r| view! { <div>{r}</div> })}\n    </Transition>\n}\n\n// SSR output: <div>Loading</div>\n// Client expects: <div>Loading</div> (initially)\n// ✅ Hydration matches\n\n// ❌ UNSAFE: SSR and client render different trees\nview! {\n    {move || if cfg!(target_arch = \"wasm32\") {\n        view! { <ClientComponent /> }\n    } else {\n        view! { <div>\"SSR placeholder\"</div> }\n    }}\n}\n\n// SSR output: <div>SSR placeholder</div>\n// Client expects: <ClientComponent />\n// ❌ Hydration mismatch panic\n```\n\n## Testing\n\n### Unit Test Template\n```rust\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use leptos::*;\n    \n    #[test]\n    fn resource_consumption_correct() {\n        let runtime = create_runtime();\n        \n        let data = Resource::new(|| (), |_| async {\n            Ok::<_, ()>(\"test data\".to_string())\n        });\n        \n        // ✅ CORRECT: Read via .get()\n        let result = data.get();\n        assert!(result.is_none()); // Not loaded yet\n        \n        // ❌ WRONG: Cannot .await Resource\n        // let result = data.await; // Compile error\n        \n        runtime.dispose();\n    }\n}\n```\n\n## Linter Rules\n```rust\n// tools/leptos-linter/src/resource_rules.rs\n\npub fn check_resource_await(code: &str) -> Vec<Violation> {\n    let mut violations = Vec::new();\n    \n    // Detect: resource_name.await\n    let re = Regex::new(r\"(\\w+)\\.await\").unwrap();\n    for cap in re.captures_iter(code) {\n        let var_name = &cap[1];\n        \n        // Check if variable is a Resource\n        if is_resource_type(var_name, code) {\n            violations.push(Violation {\n                rule: \"CANON-041\",\n                message: format!(\n                    \"Resource `{}` consumed via .await. Use .get() instead.\",\n                    var_name\n                ),\n                severity: Severity::Error,\n            });\n        }\n    }\n    \n    violations\n}\n\npub fn check_suspend_with_resource(code: &str) -> Vec<Violation> {\n    // Detect: Suspend::new(async move { resource.await })\n    if code.contains(\"Suspend::new\") && code.contains(\".await\") {\n        return vec![Violation {\n            rule: \"CANON-041\",\n            message: \"Suspend used with Resource. Use Transition + .get() instead.\".into(),\n            severity: Severity::Error,\n        }];\n    }\n    vec![]\n}\n```\n\n## Migration Guide\n\n### From Suspend To Transition\n```rust\n// BEFORE (broken)\nview! {\n    <Suspense fallback=|| view! { <div>\"Loading\"</div> }>\n        {move || Suspend::new(async move {\n            match my_resource.await {  // ❌\n                Ok(data) => view! { <div>{data}</div> },\n                Err(_) => view! { <div>\"Error\"</div> }\n            }\n        })}\n    </Suspense>\n}\n\n// AFTER (working)\nview! {\n    <Transition fallback=|| view! { <div>\"Loading\"</div> }>\n        {move || my_resource.get().map(|result| match result {  // ✅\n            Ok(data) => view! { <div>{data}</div> }.into_any(),\n            Err(_) => view! { <div>\"Error\"</div> }.into_any()\n        })}\n    </Transition>\n}\n```\n\n## Related Rules\n\n- [Canon Rule #5: SSR Effects](./canon-rule-05-ssr-effects.md) - Effect execution in SSR\n- [Canon Rule #42: Leptos Island Architecture](./canon-rule-42-leptos-island-architecture.md) - Client-only components\n- [Canon Rule #42: Leptos Island Architecture](./canon-rule-42-leptos-island-architecture.md) - Client-only components\n- [Canon Rule #43: spawn_local Prohibition](./canon-rule-43-spawn-local-prohibition.md) - Manual async spawning\n- [Canon Rule #41A: SSR Components Are Read-Only](./canon-rule-41a-ssr-read-only.md) - Derived architectural principle(./canon-rule-43-spawn-local-prohibition.md) - Manual async spawning\n\n## Summary\n\n**Golden Rule:** `Resource` is reactive, not awaitable.\n\n**API:**\n- ✅ `.get()` → read current value\n- ✅ `Transition` → wait for None → Some\n- ❌ `.await` → compile error (not a Future)\n- ❌ `Suspend` → wrong tool (for direct Futures)\n\n**Pattern:**\n```rust\nResource::new() → .get() → Transition → SSR-safe\n```\n\n---\n\n\n---"},{"number":42,"slug":"ui-data-surfaces","title":"UI Data Surfaces","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["ui","data","adapters","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Duplicating components per data strategy creates redundancy and complexity. Data must be abstracted.","problem":"multiple components implement same rendering logic for different data strategies","solution":"use single renderer with adapter based data sources","signals":["duplicate component","code redundancy","complex api"],"search_intent":"how to design ui data abstraction","keywords":["ui adapter pattern data source","single renderer multiple data sources","frontend virtual table architecture","data abstraction components"],"body":"## Principle\nData source strategies (streaming, pagination, infinite scroll) are **adapters**, not components. A single primitive (`VirtualTable`) handles all rendering, while adapters provide data.\n\n## The Problem\n\n### Wrong Strategy As Component\n```\nui/\n├── virtual_table/     // For client-side Vec<T>\n├── streaming_table/   // For server streaming\n├── paginated_table/   // For page/limit API\n└── infinite_table/    // For cursor-based\n```\n\n**Why this fails:**\n- 4 components doing the same job (render rows)\n- Duplicate viewport logic\n- Duplicate row rendering\n- Duplicate column handling\n- Different APIs for same concept\n- Forces domain code to know infrastructure\n\n### Correct Strategy As Adapter\n```\nui/\n└── virtual_table/\n    ├── mod.rs\n    ├── virtual_table.rs      // Single renderer\n    ├── adapters/\n    │   ├── client_vec.rs     // Vec<T> adapter\n    │   ├── server_paged.rs   // page/limit adapter\n    │   ├── server_stream.rs  // streaming adapter\n    │   └── server_cursor.rs  // cursor-based adapter\n    ├── viewport.rs\n    ├── row.rs\n    └── column.rs\n```\n\n**Why this works:**\n- Single component renders ALL strategies\n- Adapters implement `DataSource` trait\n- Domain code agnostic to data strategy\n- Change strategy without changing UI\n- Test rendering independently of data\n\n## Rule Contract\n\n### Component Classification\n\n| Layer | What Goes Here | Never Contains |\n|-------|---------------|----------------|\n| `ui/` | Rendering primitives (table, grid, list) | Domain knowledge, data fetching |\n| `components/` | Domain-aware compositions (workflow, users) | Rendering logic, viewport math |\n| `blocks/` | Opinionated layouts (dashboard_card) | Data sources, business rules |\n\n### Data Surface Pattern\n```rust\n// ui/virtual_table/mod.rs\n\n/// Generic data source trait\npub trait DataSource<T> {\n    fn total_count(&self) -> usize;\n    fn fetch_range(&self, start: usize, end: usize) -> Vec<T>;\n    fn on_scroll(&mut self, position: usize);\n}\n\n/// Single renderer for ALL strategies\n#[component]\npub fn VirtualTable<T, D>(\n    data_source: D,\n    render_row: impl Fn(T) -> View + 'static,\n) -> impl IntoView \nwhere\n    D: DataSource<T>\n{\n    // Viewport calculation\n    // Row rendering\n    // Scroll handling\n    // SAME for all strategies\n}\n```\n\n### Adapter Examples\n\n#### Client Vec Adapter\n```rust\n// ui/virtual_table/adapters/client_vec.rs\n\npub struct ClientVecSource<T> {\n    data: Vec<T>,\n}\n\nimpl<T: Clone> DataSource<T> for ClientVecSource<T> {\n    fn total_count(&self) -> usize {\n        self.data.len()\n    }\n    \n    fn fetch_range(&self, start: usize, end: usize) -> Vec<T> {\n        self.data[start..end].to_vec()\n    }\n    \n    fn on_scroll(&mut self, _position: usize) {\n        // No-op for client data\n    }\n}\n```\n\n#### Server Stream Adapter\n```rust\n// ui/virtual_table/adapters/server_stream.rs\n\npub struct ServerStreamSource<T> {\n    fetched: Vec<T>,\n    total_estimate: usize,\n    fetch_fn: Box<dyn Fn(usize, usize) -> Future<Vec<T>>>,\n}\n\nimpl<T> DataSource<T> for ServerStreamSource<T> {\n    fn total_count(&self) -> usize {\n        self.total_estimate\n    }\n    \n    fn fetch_range(&self, start: usize, end: usize) -> Vec<T> {\n        // Return cached or trigger fetch\n        if start >= self.fetched.len() {\n            // Trigger async fetch (via signal)\n        }\n        self.fetched[start..end].to_vec()\n    }\n    \n    fn on_scroll(&mut self, position: usize) {\n        // Prefetch next page\n        if position > self.fetched.len() * 0.8 {\n            // Trigger fetch\n        }\n    }\n}\n```\n\n## Domain Usage\n\n### Workflow Component Correct\n```rust\n// components/workflow/mod.rs\n\nuse rs_design::ui::virtual_table::{VirtualTable, adapters::ServerStreamSource};\n\n#[component]\npub fn WorkflowAuditLog() -> impl IntoView {\n    let data_source = ServerStreamSource::new(\n        fetch_audit_entries, // Server function\n        1000, // Estimated total\n    );\n    \n    view! {\n        <VirtualTable\n            data_source=data_source\n            render_row=|entry| view! {\n                <div>{entry.timestamp} - {entry.action}</div>\n            }\n        />\n    }\n}\n```\n\n**Key points:**\n- Workflow knows WHAT to display (audit entries)\n- Workflow knows WHERE data comes from (server)\n- Workflow does NOT know HOW to render viewport\n- VirtualTable handles ALL rendering\n\n## Migration Path\n\n### Phase 1 Identify Duplicates\n```sql\n-- Find components doing same job\nSELECT name, location, line_count\nFROM components\nWHERE name LIKE '%table%'\n  AND has_viewport_logic = true;\n```\n\n### Phase 2 Extract Adapters\n```bash\n# For each duplicate table component:\n1. Extract data fetching → adapter\n2. Keep only rendering → primitives\n3. Delete duplicate component\n4. Update imports\n```\n\n### Phase 3 Consolidate\n```\nBEFORE:\n- components/tokens_table.rs (300 lines)\n- components/users_table.rs (280 lines)  \n- components/audit_table.rs (320 lines)\n\nAFTER:\n- ui/virtual_table/ (200 lines)\n- components/tokens.rs (50 lines, uses VirtualTable)\n- components/users.rs (50 lines, uses VirtualTable)\n- components/audit.rs (50 lines, uses VirtualTable)\n```\n\n**Result:** 900 lines → 350 lines\n\n## Violation Examples\n\n### Violation 1 Domain In UI\n```rust\n// ❌ WRONG: ui/tokens_table.rs\n#[component]\npub fn TokensTable() -> impl IntoView {\n    let tokens = Resource::new(|| (), |_| fetch_tokens()); // Domain!\n    \n    view! {\n        <div class=\"viewport\">\n            {/* rendering */}\n        </div>\n    }\n}\n```\n\n**Fix:**\n```rust\n// ✅ CORRECT: components/tokens.rs\nuse rs_design::ui::virtual_table::VirtualTable;\n\n#[component]\npub fn TokensView() -> impl IntoView {\n    let source = ServerStreamSource::new(fetch_tokens, 100);\n    view! { <VirtualTable data_source=source /> }\n}\n```\n\n### Violation 2 Rendering In Components\n```rust\n// ❌ WRONG: components/workflow.rs\n#[component]\npub fn WorkflowSteps() -> impl IntoView {\n    view! {\n        <div class=\"viewport\" style=format!(\"height: {}px\", viewport_height)>\n            <div style=format!(\"transform: translateY({}px)\", offset)>\n                {/* manual viewport math */}\n            </div>\n        </div>\n    }\n}\n```\n\n**Fix:**\n```rust\n// ✅ CORRECT: components/workflow.rs\nuse rs_design::ui::virtual_table::VirtualTable;\n\n#[component]\npub fn WorkflowSteps() -> impl IntoView {\n    view! { <VirtualTable data_source=steps_source /> }\n}\n```\n\n## Testing Strategy\n\n### Test Adapters Independently\n```rust\n#[test]\nfn client_vec_adapter() {\n    let data = vec![1, 2, 3, 4, 5];\n    let source = ClientVecSource::new(data);\n    \n    assert_eq!(source.total_count(), 5);\n    assert_eq!(source.fetch_range(1, 3), vec![2, 3]);\n}\n\n#[test]\nfn server_stream_adapter() {\n    let source = ServerStreamSource::new(mock_fetch, 100);\n    \n    // Test prefetch logic\n    source.on_scroll(80);\n    assert!(source.prefetch_triggered);\n}\n```\n\n### Test Renderer Independently\n```rust\n#[test]\nfn virtual_table_rendering() {\n    let source = MockDataSource::new(vec![1, 2, 3]);\n    \n    let rendered = mount_to_body(|| view! {\n        <VirtualTable\n            data_source=source\n            render_row=|n| view! { <div>{n}</div> }\n        />\n    });\n    \n    assert_eq!(rendered.find_all(\"div\").len(), 3);\n}\n```\n\n## Database Tracking\n```sql\nCREATE TABLE ui_primitives (\n    id INTEGER PRIMARY KEY,\n    name TEXT NOT NULL,\n    category TEXT CHECK(category IN ('table', 'grid', 'list', 'form')),\n    has_adapters BOOLEAN DEFAULT 0,\n    adapter_count INTEGER DEFAULT 0,\n    used_by_components INTEGER DEFAULT 0\n);\n\nCREATE TABLE component_dependencies (\n    component_id INTEGER REFERENCES components(id),\n    primitive_id INTEGER REFERENCES ui_primitives(id),\n    adapter_type TEXT, -- 'client_vec', 'server_stream', etc.\n    PRIMARY KEY (component_id, primitive_id)\n);\n```\n\n## Adapter Registry\n```rust\n// ui/virtual_table/adapters/mod.rs\n\npub mod client_vec;\npub mod server_paged;\npub mod server_stream;\npub mod server_cursor;\n\npub use client_vec::ClientVecSource;\npub use server_paged::ServerPagedSource;\npub use server_stream::ServerStreamSource;\npub use server_cursor::ServerCursorSource;\n\n/// Adapter capabilities matrix\npub const ADAPTERS: &[AdapterInfo] = &[\n    AdapterInfo {\n        name: \"ClientVec\",\n        ssr_safe: true,\n        supports_infinite: false,\n        requires_server: false,\n    },\n    AdapterInfo {\n        name: \"ServerStream\",\n        ssr_safe: true,\n        supports_infinite: true,\n        requires_server: true,\n    },\n    // ...\n];\n```\n\n## Related Rules\n\n- [Canon Rule #3: Lists](./canon-rule-03-lists.md) - List rendering patterns\n- [Canon Rule #14: DataTable vs VirtualTable](./canon-rule-14-datatable-vs-virtualtable.md) - When to virtualize\n- [Canon Rule #15: Pagination vs Virtualization](./canon-rule-15-pagination-vs-virtualization.md) - Strategy selection\n- [Canon Rule #16: Client vs Server Filtering](./canon-rule-16-client-vs-server-filtering.md) - Data location\n\n## Summary\n\n**Golden Rule:** Data strategy is configuration, not identity.\n\n**Pattern:**\n```\nONE renderer + MANY adapters > MANY renderers\n```\n\n**Architecture:**\n```\nui/          → HOW to render\ncomponents/  → WHAT to render  \nadapters/    → WHERE data comes from\n```\n\n**Never:**\n- Duplicate viewport logic\n- Mix domain and rendering\n- Create component per data source\n\n**Always:**\n- Single renderer per primitive type\n- Adapters implement standard trait\n- Components choose adapter, not renderer\n\n---\n\n\n---"},{"number":43,"slug":"domain-components-and-commands","title":"Domain Components and Commands","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["cqrs","components","state","ssr"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Mixing state mutation and rendering inside the same component breaks CQRS and leads to tightly coupled UI logic. Domain components must remain read-only while command execution is delegated to a separate layer.","problem":"domain components execute commands directly causing coupling between ui and state mutation","solution":"separate read and write using domain components command components and orchestrators","signals":["spawn_local in view","button triggers mutation in domain","mixed read write logic"],"search_intent":"how to separate read and write","keywords":["cqrs frontend leptos","separate read write components","domain vs command components","leptos state mutation pattern"],"body":"## Principle\nDomain components **NEVER** execute commands directly. They only **OBSERVE** state. Commands are **ALWAYS** emitted by an explicit \"Command Component\" layer. This rule enforces CQRS at the UI level.\n\n---\n\n## The Problem\n\n### ❌ WRONG: Command Inside Domain Component\n```rust\n#[component]\npub fn Workflow() -> impl IntoView {\n    let steps = Resource::new(|| (), |_| fetch_steps());\n\n    view! {\n        <Transition>\n            {move || steps.get().map(|steps| view! {\n                {steps.into_iter().map(|step| view! {\n                    <div>\n                        <span>{step.label}</span>\n                        <button on:click=move |_| {\n                            // ❌ Command inside domain component\n                            transition_step(step.id, \"Completed\");\n                        }>\n                            \"Complete\"\n                        </button>\n                    </div>\n                }).collect_view()}\n            })}\n        </Transition>\n    }\n}\n```\n\n**Why this is architecturally wrong (even if it works):**\n- UI decides when and how to change state\n- No separation between:\n  - Visualization (what to show)\n  - Intention (what to do)\n  - Authorization (who can do it)\n- Impossible to reuse command outside UI\n- Breaks clean audit trail\n- Breaks future automation\n- Breaks CLI / API / background jobs\n- Component becomes a \"God component\"\n\n### ✅ CORRECT: Explicit Separation\n```rust\n// 1. Domain Component (READ)\n#[component]\npub fn WorkflowView(\n    #[prop(optional)] refresh_trigger: Option<ReadSignal<u32>>\n) -> impl IntoView {\n    let steps = Resource::new(\n        move || refresh_trigger.map(|r| r.get()).unwrap_or(0),\n        |_| async { fetch_steps().await }\n    );\n\n    view! {\n        <Transition>\n            {move || steps.get().map(|steps| view! {\n                {steps.into_iter().map(|step| view! {\n                    <div>\n                        <span>{step.label}</span>\n                        <span class=\"status\">{step.status}</span>\n                    </div>\n                }).collect_view()}\n            })}\n        </Transition>\n    }\n}\n\n// 2. Command Component (WRITE)\n#[component]\npub fn WorkflowActions(\n    step_id: String,\n    current_status: String,\n    #[prop(into)] on_transition_complete: Callback<()>\n) -> impl IntoView {\n    view! {\n        <button on:click=move |_| {\n            #[cfg(target_arch = \"wasm32\")]\n            {\n                use leptos::task::spawn_local;\n                let sid = step_id.clone();\n                spawn_local(async move {\n                    if transition_step(sid, \"Completed\".into()).await.is_ok() {\n                        on_transition_complete.run(());\n                    }\n                });\n            }\n        }>\n            \"Complete\"\n        </button>\n    }\n}\n\n// 3. Orchestrator (GLUE)\n#[component]\npub fn WorkflowClient() -> impl IntoView {\n    let (refresh, set_refresh) = signal(0);\n\n    let on_done = Callback::new(move |_| {\n        set_refresh.update(|n| *n += 1);\n    });\n\n    view! {\n        <WorkflowView refresh_trigger=refresh />\n        <WorkflowActions\n            step_id=\"approval\"\n            current_status=\"Active\"\n            on_transition_complete=on_done\n        />\n    }\n}\n```\n\n---\n\n## Component Classification\n\n### 1. Domain Component (READ MODEL)\n\n**What it is:**\n- Visualizes state\n- Reflects current reality\n- Completely passive\n\n**Rules:**\n- ❌ No `spawn_local`\n- ❌ No mutations\n- ❌ No business rules\n- ✅ SSR-safe\n- ✅ Deterministic\n\n**Example:**\n```rust\n#[component]\npub fn WorkflowView(\n    #[prop(optional)] refresh_trigger: Option<ReadSignal<u32>>\n) -> impl IntoView {\n    let steps = Resource::new(\n        move || refresh_trigger.map(|r| r.get()).unwrap_or(0),\n        |_| async { fetch_workflow_steps(1).await }\n    );\n    \n    view! {\n        <Transition fallback=|| view! { <div>\"Loading...\"</div> }>\n            {move || steps.get().map(|result| match result {\n                Ok(steps) => view! {\n                    <div class=\"workflow-steps\">\n                        {steps.into_iter().map(|step| {\n                            view! {\n                                <div class=\"step\">\n                                    <span>{step.label}</span>\n                                    <span class=\"status\">{step.status}</span>\n                                </div>\n                            }\n                        }).collect_view()}\n                    </div>\n                }.into_any(),\n                Err(_) => view! { <div>\"Error\"</div> }.into_any()\n            })}\n        </Transition>\n    }\n}\n```\n\n### 2. Command Component (WRITE MODEL)\n\n**What it is:**\n- Emits intentions\n- Never renders state\n- Never decides business rules\n\n**Rules:**\n- ❌ Does not read `Resource`\n- ❌ Does not render domain\n- ✅ Only knows about commands\n- ✅ Can be client-only\n- ✅ Can exist without UI (CLI / API)\n\n**Example:**\n```rust\n#[component]\npub fn WorkflowActions(\n    step_id: String,\n    current_status: String,\n    #[prop(into)] on_transition_complete: Callback<()>\n) -> impl IntoView {\n    let actions = match current_status.as_str() {\n        \"Pending\" => vec![(\"Start\", \"Active\")],\n        \"Active\" => vec![(\"Complete\", \"Completed\"), (\"Fail\", \"Failed\")],\n        \"Completed\" => vec![(\"Reset\", \"Pending\")],\n        _ => vec![],\n    };\n    \n    view! {\n        <div class=\"actions\">\n            {actions.into_iter().map(|(label, new_status)| {\n                let sid = step_id.clone();\n                let ns = new_status.to_string();\n                let on_complete = on_transition_complete;\n                \n                view! {\n                    <button on:click=move |_| {\n                        #[cfg(target_arch = \"wasm32\")]\n                        {\n                            use leptos::task::spawn_local;\n                            let sid = sid.clone();\n                            let ns = ns.clone();\n                            spawn_local(async move {\n                                if transition_step(sid, ns).await.is_ok() {\n                                    on_complete.run(());\n                                }\n                            });\n                        }\n                    }>\n                        {label}\n                    </button>\n                }\n            }).collect_view()}\n        </div>\n    }\n}\n```\n\n### 3. Orchestrator (GLUE)\n\n**What it does:**\n- Coordinates READ ↔ WRITE\n- Updates refresh triggers\n- Maintains consistent UX\n\n**Example:**\n```rust\n#[component]\npub fn WorkflowClient() -> impl IntoView {\n    let (refresh, set_refresh) = signal(0u32);\n    \n    let steps_for_actions = Resource::new(\n        move || refresh.get(),\n        |_| async { fetch_workflow_steps(1).await }\n    );\n    \n    let on_done = Callback::new(move |_| {\n        set_refresh.update(|n| *n += 1);\n    });\n\n    view! {\n        <div>\n            <WorkflowView refresh_trigger=refresh />\n            \n            <Transition>\n                {move || steps_for_actions.get().map(|result| match result {\n                    Ok(steps) => view! {\n                        {steps.into_iter().map(|step| view! {\n                            <WorkflowActions\n                                step_id=step.step_id\n                                current_status=step.status\n                                on_transition_complete=on_done\n                            />\n                        }).collect_view()}\n                    }.into_any(),\n                    Err(_) => view! { <div>\"Error\"</div> }.into_any()\n                })}\n            </Transition>\n        </div>\n    }\n}\n```\n\n---\n\n## Component Capability Matrix\n\n| Type | Can Read State | Can Mutate State | SSR-Safe | Example |\n|------|----------------|------------------|----------|---------|\n| **Domain Component** | ✅ | ❌ | ✅ | `WorkflowView` |\n| **Command Component** | ❌ | ✅ | ❌ (usually) | `WorkflowActions` |\n| **Orchestrator** | ⚠️ | ⚠️ | ⚠️ | `WorkflowClient` |\n\n---\n\n## Common Violations\n\n### Violation 1: Button Inside Domain Component\n```rust\n// ❌ WRONG\n#[component]\npub fn WorkflowView() -> impl IntoView {\n    view! {\n        <div>\n            <span>\"Step 1\"</span>\n            <button on:click=move |_| transition_step()>  // ❌\n                \"Complete\"\n            </button>\n        </div>\n    }\n}\n```\n\n**Fix:**\n```rust\n// ✅ CORRECT\n#[component]\npub fn WorkflowView() -> impl IntoView {\n    view! {\n        <div>\n            <span>\"Step 1\"</span>\n            // No button - pure display\n        </div>\n    }\n}\n\n#[component]\npub fn WorkflowActions() -> impl IntoView {\n    view! {\n        <button on:click=move |_| transition_step()>\n            \"Complete\"\n        </button>\n    }\n}\n```\n\n### Violation 2: Resource Inside Command Component\n```rust\n// ❌ WRONG\n#[component]\npub fn WorkflowActions() -> impl IntoView {\n    let steps = Resource::new(|| (), |_| fetch_steps());  // ❌\n    \n    view! {\n        <button>\"Complete\"</button>\n    }\n}\n```\n\n**Fix:**\n```rust\n// ✅ CORRECT\n#[component]\npub fn WorkflowActions(\n    step_id: String,  // Receive data as props\n    current_status: String,\n) -> impl IntoView {\n    view! {\n        <button>\"Complete\"</button>\n    }\n}\n```\n\n### Violation 3: Domain Deciding Business Rules\n```rust\n// ❌ WRONG\n#[component]\npub fn WorkflowView() -> impl IntoView {\n    view! {\n        {if status == \"Active\" {  // ❌ UI deciding rules\n            view! { <button>\"Complete\"</button> }\n        } else {\n            view! { <span>\"Not available\"</span> }\n        }}\n    }\n}\n```\n\n**Fix:**\n```rust\n// ✅ CORRECT - Server decides rules\n#[server]\npub async fn get_available_actions(step_id: String) -> Result<Vec<Action>, ServerFnError> {\n    // Business rules live here\n    match get_step_status(step_id).await? {\n        \"Active\" => Ok(vec![Action::Complete, Action::Fail]),\n        \"Pending\" => Ok(vec![Action::Start]),\n        _ => Ok(vec![]),\n    }\n}\n\n#[component]\npub fn WorkflowActions() -> impl IntoView {\n    let actions = Resource::new(|| (), |_| get_available_actions(\"step1\".into()));\n    \n    view! {\n        <Transition>\n            {move || actions.get().map(|acts| {\n                acts.into_iter().map(|action| view! {\n                    <button>{action.label}</button>\n                }).collect_view()\n            })}\n        </Transition>\n    }\n}\n```\n\n---\n\n## Real Benefits\n\n### ✅ What You Gain\n\n1. **Deterministic UI**\n   - Domain components always render the same for same state\n   - No hidden side effects\n   - Easy to test\n\n2. **Reliable SSR**\n   - Domain components work in SSR\n   - Commands are client-only by design\n   - No hydration mismatches\n\n3. **Clean Audit Trail**\n   - Every command is explicit\n   - Easy to log\n   - Easy to replay\n\n4. **Event Replay**\n   - Commands can be stored as events\n   - Replayable for debugging\n   - Time-travel debugging possible\n\n5. **Shared Core**\n   - CLI, API, UI share same commands\n   - No UI-specific business logic\n   - Single source of truth\n\n6. **Automation Without Refactor**\n   - Background jobs use same commands\n   - Scheduled actions use same code\n   - No duplicate logic\n\n7. **ISO / SOC2 Justifiable**\n   - Clear separation of concerns\n   - Auditable command trail\n   - Provable compliance\n\n---\n\n## Architecture Diagram\n```\n┌─────────────────────────────────────────┐\n│         User Interface Layer            │\n├─────────────────────────────────────────┤\n│                                         │\n│  ┌───────────────┐   ┌──────────────┐  │\n│  │ Domain        │   │ Command      │  │\n│  │ Component     │   │ Component    │  │\n│  │ (READ)        │   │ (WRITE)      │  │\n│  │               │   │              │  │\n│  │ - WorkflowView│   │ - WorkflowActions│\n│  │ - AuditView   │   │ - TransitionButtons│\n│  └───────┬───────┘   └──────┬───────┘  │\n│          │                  │          │\n│          │  ┌───────────────┘          │\n│          │  │                          │\n│  ┌───────▼──▼───────┐                 │\n│  │   Orchestrator   │                 │\n│  │   (GLUE)         │                 │\n│  │                  │                 │\n│  │ - WorkflowClient │                 │\n│  └────────┬─────────┘                 │\n│           │                           │\n└───────────┼───────────────────────────┘\n            │\n┌───────────▼───────────────────────────┐\n│      Application Core / Server        │\n├───────────────────────────────────────┤\n│                                       │\n│  ┌─────────────┐   ┌──────────────┐  │\n│  │ Queries     │   │ Commands     │  │\n│  │ (READ)      │   │ (WRITE)      │  │\n│  │             │   │              │  │\n│  │ fetch_steps │   │ transition_  │  │\n│  │ fetch_audit │   │ step         │  │\n│  └──────┬──────┘   └──────┬───────┘  │\n│         │                 │          │\n│         └────────┬────────┘          │\n│                  │                   │\n│         ┌────────▼────────┐          │\n│         │   Database      │          │\n│         │   - workflows   │          │\n│         │   - audit       │          │\n│         └─────────────────┘          │\n└───────────────────────────────────────┘\n```\n\n---\n\n## Testing Strategy\n\n### Test Domain Components\n```rust\n#[test]\nfn workflow_view_renders_state() {\n    let runtime = create_runtime();\n    \n    let steps = vec![\n        Step { id: 1, label: \"Review\".into(), status: \"Active\".into() },\n    ];\n    \n    let rendered = mount_to_body(|| view! {\n        <WorkflowView steps=steps />\n    });\n    \n    assert!(rendered.inner_html().contains(\"Review\"));\n    assert!(rendered.inner_html().contains(\"Active\"));\n    \n    runtime.dispose();\n}\n```\n\n### Test Command Components\n```rust\n#[test]\nfn workflow_actions_emit_commands() {\n    let runtime = create_runtime();\n    let (called, set_called) = signal(false);\n    \n    let on_complete = Callback::new(move |_| set_called.set(true));\n    \n    let rendered = mount_to_body(|| view! {\n        <WorkflowActions\n            step_id=\"test\"\n            current_status=\"Active\"\n            on_transition_complete=on_complete\n        />\n    });\n    \n    // Simulate click\n    rendered.find(\"button\").click();\n    \n    assert!(called.get());\n    \n    runtime.dispose();\n}\n```\n\n### Test Orchestrator\n```rust\n#[test]\nfn workflow_client_coordinates() {\n    let runtime = create_runtime();\n    \n    let rendered = mount_to_body(|| view! {\n        <WorkflowClient />\n    });\n    \n    // Verify both View and Actions are present\n    assert!(rendered.find(\".workflow-view\").exists());\n    assert!(rendered.find(\".workflow-actions\").exists());\n    \n    runtime.dispose();\n}\n```\n\n---\n\n## Migration Path\n\n### From Monolithic to CQRS\n\n**Before:**\n```rust\n#[component]\npub fn Workflow() -> impl IntoView {\n    let steps = Resource::new(|| (), |_| fetch_steps());\n    \n    view! {\n        {move || steps.get().map(|s| view! {\n            <div>\n                <span>{s.label}</span>\n                <button on:click=move |_| transition()>\n                    \"Complete\"\n                </button>\n            </div>\n        })}\n    }\n}\n```\n\n**After:**\n```rust\n// 1. Extract READ\n#[component]\npub fn WorkflowView(refresh: ReadSignal<u32>) -> impl IntoView {\n    let steps = Resource::new(move || refresh.get(), |_| fetch_steps());\n    \n    view! {\n        {move || steps.get().map(|s| view! {\n            <div><span>{s.label}</span></div>\n        })}\n    }\n}\n\n// 2. Extract WRITE\n#[component]\npub fn WorkflowActions(on_done: Callback<()>) -> impl IntoView {\n    view! {\n        <button on:click=move |_| {\n            spawn_local(async move {\n                transition().await;\n                on_done.run(());\n            });\n        }>\n            \"Complete\"\n        </button>\n    }\n}\n\n// 3. Create Orchestrator\n#[component]\npub fn WorkflowClient() -> impl IntoView {\n    let (refresh, set_refresh) = signal(0);\n    let on_done = Callback::new(move |_| set_refresh.update(|n| *n += 1));\n    \n    view! {\n        <WorkflowView refresh=refresh />\n        <WorkflowActions on_done=on_done />\n    }\n}\n```\n\n---\n\n## Related Rules\n\n- [Canon Rule #41: Leptos Resource Consumption](./canon-rule-41-leptos-resource-consumption.md) - Resource vs Future\n- [Canon Rule #42: UI Data Surfaces](./canon-rule-42-ui-data-surfaces.md) - Component classification\n- [Canon Rule #5: SSR Effects](./canon-rule-05-ssr-effects.md) - SSR safety\n\n---\n\n## Summary\n\n**Golden Rule:** Domain components OBSERVE. Command components ACT. Never mix.\n\n**Pattern:**\n```\nREAD  (Domain)   → Display state\nWRITE (Command)  → Emit intention\nGLUE  (Orchestrator) → Coordinate both\n```\n\n**Never:**\n- Put buttons in domain components\n- Put Resource in command components\n- Decide business rules in UI\n\n**Always:**\n- Separate read from write\n- Keep domain components passive\n- Make commands explicit\n- Coordinate via orchestrator\n\n---\n\n\n---"},{"number":44,"slug":"orchestrators","title":"Orchestrators","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["orchestration","cqrs","signals","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Coupling domain rendering with command execution removes separation of concerns and breaks SSR safety. Orchestrators act as a coordination layer between read and write components without introducing business logic.","problem":"domain components handle commands directly instead of delegating coordination","solution":"use orchestrator layer to connect domain components and command components via callbacks and refresh signals","signals":["command in domain component","resource mixed with mutation","tight coupling read write"],"search_intent":"what is orchestrator pattern in leptos cqrs frontend","keywords":["orchestrator pattern frontend","leptos cqrs coordination","signals refresh pattern","callback coordination ui"],"body":"## Principle\nOrchestrators are the **GLUE** between Domain Components (READ) and Command Components (WRITE). They coordinate the CQRS cycle at the UI level without implementing business logic or rendering complexity.\n\n---\n\n## The Problem\n\n### ❌ WRONG: Tight Coupling\n```rust\n// Domain component knows about commands\n#[component]\npub fn WorkflowView() -> impl IntoView {\n    let steps = Resource::new(|| (), |_| fetch_steps());\n    \n    // ❌ Domain component emitting commands\n    let on_click = move |_| {\n        spawn_local(async { transition_step().await });\n    };\n    \n    view! {\n        <div>\n            {/* rendering */}\n            <button on:click=on_click>\"Complete\"</button>  // ❌\n        </div>\n    }\n}\n```\n\n**Problems:**\n- Domain component has side effects\n- Cannot SSR safely\n- Cannot test READ independently\n- Cannot reuse commands outside this view\n\n### ✅ CORRECT: Orchestrator Pattern\n```rust\n// Domain Component (READ)\n#[component]\npub fn WorkflowView(\n    #[prop(optional)] refresh_trigger: Option<ReadSignal<u32>>\n) -> impl IntoView {\n    let steps = Resource::new(\n        move || refresh_trigger.map(|r| r.get()).unwrap_or(0),\n        |_| async { fetch_steps().await }\n    );\n    \n    view! {\n        <Transition>\n            {move || steps.get().map(|s| {\n                // Pure rendering, no commands\n            })}\n        </Transition>\n    }\n}\n\n// Command Component (WRITE)\n#[component]\npub fn WorkflowActions(\n    step_id: String,\n    #[prop(into)] on_complete: Callback<()>\n) -> impl IntoView {\n    view! {\n        <button on:click=move |_| {\n            #[cfg(target_arch = \"wasm32\")]\n            {\n                use leptos::task::spawn_local;\n                let sid = step_id.clone();\n                spawn_local(async move {\n                    if transition_step(sid).await.is_ok() {\n                        on_complete.run(());\n                    }\n                });\n            }\n        }>\n            \"Complete\"\n        </button>\n    }\n}\n\n// Orchestrator (GLUE)\n#[component]\npub fn WorkflowClient() -> impl IntoView {\n    let (refresh, set_refresh) = signal(0);\n    \n    let on_complete = Callback::new(move |_| {\n        set_refresh.update(|n| *n += 1);\n    });\n    \n    view! {\n        <div>\n            <WorkflowView refresh_trigger=refresh />\n            <WorkflowActions\n                step_id=\"approval\"\n                on_complete=on_complete\n            />\n        </div>\n    }\n}\n```\n\n---\n\n## Orchestrator Contract\n\n### What Orchestrators DO\n\n✅ **Coordinate Refresh Cycle**\n```rust\nlet (refresh, set_refresh) = signal(0);\n\nlet on_command_complete = Callback::new(move |_| {\n    set_refresh.update(|n| *n += 1);\n});\n```\n\n✅ **Pass Callbacks Between Layers**\n```rust\n// Orchestrator creates callback\nlet on_done = Callback::new(move |_| {\n    // Trigger refresh\n    // Log analytics\n    // Show notification\n});\n\n// Passes to command component\n<WorkflowActions on_complete=on_done />\n```\n\n✅ **Manage Layout**\n```rust\nview! {\n    <div class=\"grid grid-cols-2 gap-6\">\n        <WorkflowView refresh_trigger=refresh />\n        <WorkflowActions on_complete=on_done />\n    </div>\n}\n```\n\n✅ **Handle Optimistic Updates**\n```rust\nlet (optimistic_state, set_optimistic) = signal(None);\n\nlet on_command = Callback::new(move |cmd| {\n    // Optimistic update\n    set_optimistic.set(Some(cmd.clone()));\n    \n    // Actual command\n    spawn_local(async move {\n        match execute_command(cmd).await {\n            Ok(_) => {\n                // Server confirmed\n                set_optimistic.set(None);\n                refresh.update(|n| *n += 1);\n            }\n            Err(_) => {\n                // Rollback optimistic\n                set_optimistic.set(None);\n            }\n        }\n    });\n});\n```\n\n### What Orchestrators NEVER DO\n\n❌ **Implement Business Rules**\n```rust\n// ❌ WRONG\nif step.status == \"Active\" && user.role == \"Admin\" {\n    // Business logic in orchestrator\n}\n```\n**Rule:** Business rules live in server/core\n\n❌ **Complex Rendering**\n```rust\n// ❌ WRONG\nview! {\n    <div class=\"complex-layout\">\n        <div class=\"sidebar\">\n            <div class=\"nested\">\n                <div class=\"deep\">\n                    // 100 lines of rendering\n                </div>\n            </div>\n        </div>\n    </div>\n}\n```\n**Rule:** Delegate to domain components\n\n❌ **Direct Database Access**\n```rust\n// ❌ WRONG\nlet conn = Connection::open(\"db.sqlite\");\n```\n**Rule:** Use server functions\n\n❌ **Heavy State Logic**\n```rust\n// ❌ WRONG\nlet complex_derived = Memo::new(move |_| {\n    // 50 lines of computation\n});\n```\n**Rule:** Derive in domain components or server\n\n---\n\n## Orchestrator Patterns\n\n### Pattern 1: Simple Refresh\n```rust\n#[component]\npub fn SimpleOrchestrator() -> impl IntoView {\n    let (refresh, set_refresh) = signal(0);\n    \n    view! {\n        <DomainView refresh=refresh />\n        <Commands on_done=move |_| set_refresh.update(|n| *n += 1) />\n    }\n}\n```\n\n**Use when:**\n- Single domain view\n- Single command source\n- Simple refresh cycle\n\n### Pattern 2: Multiple Resources\n```rust\n#[component]\npub fn MultiResourceOrchestrator() -> impl IntoView {\n    let (steps_refresh, set_steps) = signal(0);\n    let (audit_refresh, set_audit) = signal(0);\n    \n    let on_transition = Callback::new(move |_| {\n        set_steps.update(|n| *n += 1);\n        set_audit.update(|n| *n += 1);\n    });\n    \n    view! {\n        <StepsView refresh=steps_refresh />\n        <AuditView refresh=audit_refresh />\n        <Actions on_complete=on_transition />\n    }\n}\n```\n\n**Use when:**\n- Multiple related resources\n- All need refresh on command\n- Coordinated updates\n\n### Pattern 3: Conditional Commands\n```rust\n#[component]\npub fn ConditionalOrchestrator() -> impl IntoView {\n    let (refresh, set_refresh) = signal(0);\n    \n    let steps_for_actions = Resource::new(\n        move || refresh.get(),\n        |_| async { fetch_steps().await }\n    );\n    \n    view! {\n        <DomainView refresh=refresh />\n        \n        <Transition>\n            {move || steps_for_actions.get().map(|result| match result {\n                Ok(steps) => view! {\n                    {steps.into_iter().map(|step| view! {\n                        <Actions\n                            step_id=step.id\n                            status=step.status\n                            on_done=move |_| set_refresh.update(|n| *n += 1)\n                        />\n                    }).collect_view()}\n                }.into_any(),\n                Err(_) => view! { <div>\"No actions\"</div> }.into_any()\n            })}\n        </Transition>\n    }\n}\n```\n\n**Use when:**\n- Available commands depend on state\n- Dynamic action buttons\n- Server determines what's allowed\n\n### Pattern 4: Cross-Component Coordination\n```rust\n#[component]\npub fn CoordinatedOrchestrator() -> impl IntoView {\n    let (workflow_refresh, set_workflow) = signal(0);\n    let (notifications_refresh, set_notifications) = signal(0);\n    \n    let on_workflow_change = Callback::new(move |_| {\n        set_workflow.update(|n| *n += 1);\n        set_notifications.update(|n| *n += 1);\n    });\n    \n    view! {\n        <WorkflowView refresh=workflow_refresh />\n        <NotificationsView refresh=notifications_refresh />\n        <WorkflowActions on_complete=on_workflow_change />\n    }\n}\n```\n\n**Use when:**\n- Command affects multiple domains\n- Side effects needed (notifications, analytics)\n- Coordinated UI updates\n\n---\n\n## Orchestrator Lifecycle\n```\n┌─────────────────────────────────────┐\n│ 1. User Intent                      │\n│    (button click)                   │\n└────────────┬────────────────────────┘\n             │\n┌────────────▼────────────────────────┐\n│ 2. Command Component                │\n│    - Validates input                │\n│    - Calls server function          │\n└────────────┬────────────────────────┘\n             │\n┌────────────▼────────────────────────┐\n│ 3. Command Callback                 │\n│    - on_complete.run(())            │\n└────────────┬────────────────────────┘\n             │\n┌────────────▼────────────────────────┐\n│ 4. Orchestrator                     │\n│    - set_refresh.update(|n| *n+1)   │\n│    - Triggers resource refetch      │\n└────────────┬────────────────────────┘\n             │\n┌────────────▼────────────────────────┐\n│ 5. Domain Component                 │\n│    - Resource detects change        │\n│    - Refetches data                 │\n│    - Re-renders                     │\n└─────────────────────────────────────┘\n```\n\n---\n\n## Testing Strategy\n\n### Test Domain Components Independently\n```rust\n#[test]\nfn domain_view_renders_state() {\n    let (refresh, _) = signal(0);\n    \n    let rendered = mount_to_body(|| view! {\n        <WorkflowView refresh_trigger=refresh />\n    });\n    \n    assert!(rendered.inner_html().contains(\"Code Review\"));\n}\n```\n\n### Test Command Components Independently\n```rust\n#[test]\nfn command_emits_callback() {\n    let (called, set_called) = signal(false);\n    let on_done = Callback::new(move |_| set_called.set(true));\n    \n    let rendered = mount_to_body(|| view! {\n        <WorkflowActions\n            step_id=\"test\"\n            on_complete=on_done\n        />\n    });\n    \n    rendered.find(\"button\").click();\n    assert!(called.get());\n}\n```\n\n### Test Orchestrator Coordination\n```rust\n#[test]\nfn orchestrator_coordinates_refresh() {\n    let rendered = mount_to_body(|| view! {\n        <WorkflowClient />\n    });\n    \n    // Simulate command\n    rendered.find(\".action-button\").click();\n    \n    // Wait for refresh\n    wait_for(|| {\n        rendered.find(\".workflow-view\").text().contains(\"Updated\")\n    });\n}\n```\n\n---\n\n## Anti Patterns\n\n### Anti-Pattern 1: God Orchestrator\n```rust\n// ❌ WRONG: Orchestrator doing everything\n#[component]\npub fn GodOrchestrator() -> impl IntoView {\n    let (state1, set1) = signal(...);\n    let (state2, set2) = signal(...);\n    let (state3, set3) = signal(...);\n    // ... 20 more signals\n    \n    let complex_logic = Memo::new(move |_| {\n        // 100 lines of business logic\n    });\n    \n    view! {\n        <div>\n            // 500 lines of rendering\n        </div>\n    }\n}\n```\n\n**Fix:** Break into smaller orchestrators + domain components\n\n### Anti-Pattern 2: Callback Hell\n```rust\n// ❌ WRONG: Too many callback layers\nlet on_a = Callback::new(move |_| {\n    let on_b = Callback::new(move |_| {\n        let on_c = Callback::new(move |_| {\n            // ...\n        });\n    });\n});\n```\n\n**Fix:** Flatten with single orchestrator callback\n\n### Anti-Pattern 3: Orchestrator as Data Source\n```rust\n// ❌ WRONG: Orchestrator fetching data\n#[component]\npub fn BadOrchestrator() -> impl IntoView {\n    let data = Resource::new(|| (), |_| fetch_data());  // ❌\n    \n    view! {\n        <div>{move || data.get()}</div>  // ❌\n    }\n}\n```\n\n**Fix:** Move Resource to domain component\n\n---\n\n## Location Rules\n\n| Component Type | Location | Reason |\n|---------------|----------|--------|\n| Domain (READ) | `rs-design/components/` | Reusable across apps |\n| Commands (WRITE) | `apps/*/components/` | App-specific logic |\n| Orchestrators | `apps/*/components/` | App-specific coordination |\n\n**Exception:** Demo orchestrators (like `WorkflowDemo`) can live in `rs-design` for documentation purposes, clearly marked as ephemeral.\n\n---\n\n## Orchestrator Checklist\n\nBefore creating an orchestrator, ask:\n\n- [ ] Does it coordinate READ + WRITE?\n- [ ] Does it manage refresh triggers?\n- [ ] Does it stay under 100 lines?\n- [ ] Does it avoid business logic?\n- [ ] Does it avoid complex rendering?\n- [ ] Can domain/command components work independently?\n\nIf any answer is NO, reconsider the design.\n\n---\n\n## Related Rules\n\n- [Canon Rule #43: Domain Components and Commands](./canon-rule-43-domain-components-and-commands.md) - CQRS in UI\n- [Canon Rule #41: Leptos Resource Consumption](./canon-rule-41-leptos-resource-consumption.md) - Resource patterns\n- [Canon Rule #5: SSR Effects](./canon-rule-05-ssr-effects.md) - SSR safety\n\n---\n\n## Summary\n\n**Golden Rule:** Orchestrators coordinate, never implement.\n\n**Pattern:**\n```\nDomain (READ) ← Orchestrator → Commands (WRITE)\n```\n\n**Orchestrator responsibilities:**\n- ✅ Refresh triggers\n- ✅ Callbacks\n- ✅ Layout\n- ✅ Optimistic updates\n\n**Never:**\n- ❌ Business logic\n- ❌ Complex rendering\n- ❌ Database access\n- ❌ Heavy computation\n\n**Keep orchestrators:**\n- Small (< 100 lines)\n- Simple (coordination only)\n- Testable (mock both sides)\n\n---\n\n\n---"},{"number":45,"slug":"demo-components","title":"Demo Components & Ephemeral State","status":"ENFORCED","severity":"LOW","category":"governance","tags":["demo","state","documentation","patterns"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using production-grade patterns in demos introduces unnecessary complexity and hides the core concept being demonstrated. Demo components should prioritize clarity over architectural correctness.","problem":"demo components mimic production patterns causing unnecessary complexity and confusion","solution":"use simple signals and direct state mutation for demos while isolating them from production code","signals":["resource in demo","fake async logic","over engineered examples"],"search_intent":"how to structure demo components without production complexity","keywords":["demo vs production components","leptos signals demo pattern","avoid over engineering examples","ui documentation patterns"],"body":"## Principle\nDemo components optimize for **clarity**, not realism. Ephemeral state with explicit signals is preferred over production patterns when the goal is documentation, learning, or visual showcase.\n\n---\n\n## The Core Insight\n\n**Demos are NOT mini-production systems.**\n\nThey serve a different purpose:\n\n| Production Components | Demo Components |\n|----------------------|-----------------|\n| Optimize for architecture | Optimize for understanding |\n| Use real data sources | Use hardcoded state |\n| Follow CQRS strictly | Use direct mutations |\n| SSR-critical | SSR-optional |\n| Shared state patterns | Individual signals |\n| Enterprise patterns | Pedagogical patterns |\n\n---\n\n## When to Use Demo Patterns\n\n### ✅ USE Demo Patterns When:\n\n1. **Documentation & Learning**\n   - Component library showcase\n   - Tutorial examples\n   - Interactive documentation\n   - Onboarding materials\n\n2. **Visual Testing**\n   - Storybook-style stories\n   - Visual regression testing\n   - Design system showcase\n   - UX prototyping\n\n3. **Proof of Concept**\n   - Exploring new patterns\n   - Testing visual ideas\n   - Rapid prototyping\n   - Client presentations\n\n### ❌ DON'T Use Demo Patterns When:\n\n1. **Production Features**\n   - User-facing features\n   - Business logic\n   - Data persistence needed\n   - Audit requirements\n\n2. **Reusable Infrastructure**\n   - Shared components\n   - Library components\n   - Framework primitives\n\n---\n\n## Demo Pattern Explicit Signals\n\n### The Pattern\n```rust\n#[component]\npub fn WorkflowDemo() -> impl IntoView {\n    // ✅ One signal per piece of state\n    let (step1_status, set_step1) = signal(StepStatus::Active);\n    let (step2_status, set_step2) = signal(StepStatus::Pending);\n    \n    // ✅ Direct mutation (no async, no server)\n    let transition = move |new_status| {\n        set_step1.set(new_status);\n    };\n    \n    view! {\n        <div>\n            <StepItem status=step1_status />\n            <button on:click=move |_| transition(StepStatus::Completed)>\n                \"Complete\"\n            </button>\n        </div>\n    }\n}\n```\n\n**Why this works for demos:**\n- Crystal clear reactivity (visual A → B)\n- Zero infrastructure noise\n- Easy to understand in 30 seconds\n- No async complications\n- No SSR edge cases\n\n### Anti-Pattern: Simulating Production\n```rust\n// ❌ WRONG for a demo\n#[component]\npub fn WorkflowDemo() -> impl IntoView {\n    let steps = Resource::new(|| (), |_| async {\n        fetch_from_fake_api().await  // Why?\n    });\n    \n    let action = ServerMultiAction::new(...);  // Overkill\n    \n    Effect::new(move |_| {\n        if action.version().get() > 0 {\n            steps.refetch();  // Simulating refresh cycle\n        }\n    });\n    \n    // ... 100 lines later ...\n}\n```\n\n**Why this fails:**\n- Hides the actual component being demoed\n- Adds infrastructure complexity\n- Confuses learners (\"why all this boilerplate?\")\n- Makes simple things look hard\n\n---\n\n## Production Pattern CQRS\n\n### The Pattern\n```rust\n// Domain Component (READ)\n#[component]\npub fn WorkflowView(\n    refresh_trigger: ReadSignal<u32>\n) -> impl IntoView {\n    let steps = Resource::new(\n        move || refresh_trigger.get(),\n        |_| async { fetch_workflow_steps().await }\n    );\n    \n    view! {\n        <Transition>\n            {move || steps.get().map(|s| /* render */)}\n        </Transition>\n    }\n}\n\n// Command Component (WRITE)\n#[component]\npub fn WorkflowActions(\n    on_complete: Callback<()>\n) -> impl IntoView {\n    view! {\n        <button on:click=move |_| {\n            #[cfg(target_arch = \"wasm32\")]\n            spawn_local(async move {\n                transition_step().await;\n                on_complete.run(());\n            });\n        }>\"Complete\"</button>\n    }\n}\n\n// Orchestrator\n#[component]\npub fn WorkflowClient() -> impl IntoView {\n    let (refresh, set_refresh) = signal(0);\n    \n    view! {\n        <WorkflowView refresh_trigger=refresh />\n        <WorkflowActions on_complete=move |_| set_refresh.update(|n| *n += 1) />\n    }\n}\n```\n\n**Why this is production:**\n- Separates concerns\n- SSR-safe domain components\n- Testable in isolation\n- Scales to real requirements\n\n---\n\n## Decision Matrix\n\n| Question | Demo | Production |\n|----------|------|------------|\n| Will this be deployed? | ❌ | ✅ |\n| Needs database? | ❌ | ✅ |\n| Needs SSR? | Maybe | Usually |\n| Shared state across users? | ❌ | ✅ |\n| Audit trail required? | ❌ | ✅ |\n| Used in multiple features? | ❌ | ✅ |\n\n**Rule:** If 3+ answers are \"Production\", don't use demo patterns.\n\n---\n\n## Demo State Patterns\n\n### Pattern 1: Individual Signals (Recommended)\n```rust\nlet (counter, set_counter) = signal(0);\nlet (name, set_name) = signal(\"Alice\".to_string());\nlet (active, set_active) = signal(true);\n```\n\n**Use when:**\n- < 10 pieces of state\n- State is independent\n- Clarity is priority\n\n### Pattern 2: Struct Signal (For Related State)\n```rust\n#[derive(Clone)]\nstruct DemoState {\n    counter: i32,\n    name: String,\n    active: bool,\n}\n\nlet (state, set_state) = signal(DemoState {\n    counter: 0,\n    name: \"Alice\".into(),\n    active: true,\n});\n```\n\n**Use when:**\n- State is tightly coupled\n- Need to reset all at once\n- < 5 fields in struct\n\n### Pattern 3: RwSignal for Simple Mutation\n```rust\nlet state = RwSignal::new(vec![1, 2, 3]);\n\nlet add_item = move |item| {\n    state.update(|s| s.push(item));\n};\n```\n\n**Use when:**\n- Simple list/vec manipulation\n- No complex reactivity needed\n- Mutations are atomic\n\n---\n\n## Common Demo Mistakes\n\n### Mistake 1: Over-Engineering\n```rust\n// ❌ Too much for a demo\n#[component]\npub fn ButtonDemo() -> impl IntoView {\n    let db = use_context::<Database>();  // Why?\n    let auth = use_context::<Auth>();    // Why?\n    \n    view! {\n        <Button>\"Click me\"</Button>\n    }\n}\n```\n\n**Fix:** Just render the button.\n\n### Mistake 2: Hiding the Subject\n```rust\n// ❌ Demo about Button, but wrapped in noise\n#[component]\npub fn ButtonDemo() -> impl IntoView {\n    view! {\n        <ThemeProvider>\n            <AuthGuard>\n                <Layout>\n                    <Sidebar />\n                    <Main>\n                        <Button>\"Click\"</Button>  // Lost in noise\n                    </Main>\n                </Layout>\n            </AuthGuard>\n        </ThemeProvider>\n    }\n}\n```\n\n**Fix:** Minimal wrapper.\n\n### Mistake 3: Fake Async\n```rust\n// ❌ Simulating async for no reason\nlet data = Resource::new(|| (), |_| async {\n    sleep(Duration::from_millis(500)).await;  // Fake delay\n    Ok(\"Hello\")\n});\n```\n\n**Fix:** Just use a signal.\n\n---\n\n## Documentation Guidelines\n\n### Good Demo Documentation\n```rust\n/// WorkflowDemo - Interactive showcase\n/// \n/// **Purpose:** Visual documentation of workflow transitions\n/// **Pattern:** Individual signals (clarity over realism)\n/// **NOT for:** Production use, real workflows, SSR-critical paths\n/// \n/// Each step has independent state for pedagogical clarity.\n#[component]\npub fn WorkflowDemo() -> impl IntoView { ... }\n```\n\n### Bad Demo Documentation\n```rust\n/// Workflow component\n#[component]\npub fn WorkflowDemo() -> impl IntoView { ... }\n```\n\n**Always include:**\n- Purpose statement\n- Pattern used\n- What it's NOT for\n\n---\n\n## Testing Demos\n\n### Unit Test Pattern\n```rust\n#[test]\nfn demo_transitions_work() {\n    let runtime = create_runtime();\n    \n    let rendered = mount_to_body(|| view! {\n        <WorkflowDemo />\n    });\n    \n    // Test the demo itself works\n    rendered.find(\"button.start\").click();\n    assert!(rendered.find(\".active\").exists());\n    \n    runtime.dispose();\n}\n```\n\n**Focus:** Does the demo demonstrate the concept correctly?\n\n---\n\n## Migration Path\n\n### From Demo to Production\n```\nWeek 1: Prototype with demo patterns\n  ✅ Validate UX\n  ✅ Test visual design\n  ✅ Get stakeholder approval\n\nWeek 2: Design production architecture\n  ✅ Identify data sources\n  ✅ Plan CQRS separation\n  ✅ Design audit strategy\n\nWeek 3: Implement production version\n  ✅ Domain components (READ)\n  ✅ Command components (WRITE)\n  ✅ Orchestrators (GLUE)\n\nWeek 4: Replace demo\n  ✅ Swap demo with production\n  ✅ Keep demo in storybook\n  ✅ Update documentation\n```\n\n**Key:** Demo stays in docs, production goes to app.\n\n---\n\n## Component Location\n\n| Type | Location | Reason |\n|------|----------|--------|\n| Demo | `rs-design/components/*/demo.rs` | Documentation purposes |\n| Production Domain | `rs-design/components/*/` | Reusable across apps |\n| Production Commands | `apps/*/components/` | App-specific logic |\n| Production Orchestrators | `apps/*/components/` | App-specific coordination |\n\n---\n\n## Related Rules\n\n- [Canon Rule #43: Domain Components and Commands](./canon-rule-43-domain-components-and-commands.md) - CQRS pattern\n- [Canon Rule #44: Orchestrators](./canon-rule-44-orchestrators.md) - Coordination layer\n- [Canon Rule #41: Leptos Resource Consumption](./canon-rule-41-leptos-resource-consumption.md) - Async patterns\n- [Canon Rule #1: Component Types](./canon-rule-01-types.md) - Component classification\n\n---\n\n## Summary\n\n**Golden Rule:** Demos teach, production ships.\n\n**Demo Principles:**\n- Clarity over correctness\n- Explicit over implicit\n- Simple over scalable\n- Visual over architectural\n\n**When to use demos:**\n- Documentation\n- Learning\n- Prototyping\n- Visual testing\n\n**When NOT to use demos:**\n- Production features\n- Real user data\n- Business requirements\n- Shared infrastructure\n\n**Remember:**\n> \"A demo that looks like production code teaches the wrong lesson.\"\n\nThe best demos make complex things look simple. Production code makes simple things reliable.\n\n---\n\n\n---"},{"number":46,"slug":"command-palette-intent-surfaces","title":"Command Palette & Intent Surfaces","status":"ENFORCED","severity":"MEDIUM","category":"behavior","tags":["commands","ui","keyboard","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Relying solely on buttons for actions leads to UI clutter and poor scalability. A command palette decouples user intent from visual elements and centralizes action execution.","problem":"ui relies on buttons for all actions causing clutter and poor scalability","solution":"introduce command registry and command palette as intent surface separate from ui","signals":["too many buttons","no keyboard navigation","hard to discover actions"],"search_intent":"how to implement command palette architecture frontend","keywords":["command palette frontend architecture","intent surface ui pattern","command registry design","keyboard driven ui commands"],"body":"## Principle\nCommand Palette is not a \"nice-to-have widget\" — it's the **command bus of the UI**. It decouples user intent from visual affordances, enabling discovery, accessibility, automation, and scaling without UI bloat.\n\n---\n\n## The Problem\n\n### ❌ WRONG: Button Proliferation\n```rust\n// Every action becomes a button\nview! {\n    <div>\n        <button on:click=complete>\"Complete Step\"</button>\n        <button on:click=fail>\"Fail Step\"</button>\n        <button on:click=reset>\"Reset Step\"</button>\n        <button on:click=start>\"Start Step\"</button>\n        <button on:click=unblock>\"Unblock Step\"</button>\n        <button on:click=audit>\"View Audit\"</button>\n        <button on:click=export>\"Export Report\"</button>\n        // ... 50 more buttons\n    </div>\n}\n```\n\n**Problems:**\n- UI becomes cluttered\n- Actions hard to discover\n- Cognitive overload\n- No keyboard navigation\n- Doesn't scale\n- Context-switching required\n- No command history\n- Can't automate\n\n### ✅ CORRECT: Intent Surface\n```rust\n// Visual affordances for common actions\nview! {\n    <div>\n        <button on:click=complete>\"Complete\"</button>\n        <button on:click=fail>\"Fail\"</button>\n    </div>\n}\n\n// Command Palette for ALL actions\nCommandPalette {\n    registry: CommandRegistry::new()\n        .add_group(\"workflow\", [\n            complete_command,\n            fail_command,\n            reset_command,\n            start_command,\n            unblock_command,\n            audit_command,\n            export_command,\n            // ... all 50 commands discoverable\n        ])\n}\n```\n\n**Benefits:**\n- UI stays clean\n- All actions discoverable\n- Keyboard-first\n- Scales infinitely\n- Command history possible\n- Automation-ready\n- Consistent UX\n\n---\n\n## What Is An Intent Surface\n\n**Intent Surface** = Interface for declaring \"what user wants to do\"\n\nIt's **NOT:**\n- A menu\n- A shortcut list\n- A search box\n- A modal dialog\n\nIt's a **command registry + execution layer**\n```\nUser Intent → Command Registry → Command Execution → State Change\n```\n\n---\n\n## Command Palette Architecture\n\n### Components\n```\n┌─────────────────────────────────────┐\n│ CommandPalette (Orchestrator)       │\n│ - Opens/closes                      │\n│ - Keyboard shortcuts (Ctrl+K)       │\n│ - Filters registry                  │\n└────────────┬────────────────────────┘\n             │\n┌────────────▼────────────────────────┐\n│ CommandRegistry (State)             │\n│ - Available commands                │\n│ - Search/filter logic               │\n│ - Command groups                    │\n└────────────┬────────────────────────┘\n             │\n┌────────────▼────────────────────────┐\n│ Command (Data)                      │\n│ - id, label, group                  │\n│ - shortcut, icon                    │\n│ - callback (execution)              │\n└─────────────────────────────────────┘\n```\n\n### Separation of Concerns\n\n| Layer | Responsibility | Never Does |\n|-------|---------------|------------|\n| **CommandPalette** (UI) | Rendering, keyboard, focus | Business logic, command execution |\n| **CommandRegistry** (State) | Search, filter, organize | UI rendering, execution |\n| **Command** (Data) | Metadata + callback | State management, UI |\n| **Domain** (Integration) | Register domain commands | Generic UI logic |\n\n---\n\n## Command Pattern\n\n### Command Structure\n```rust\npub struct Command {\n    // Identity\n    pub id: String,              // \"workflow.complete_step\"\n    pub label: String,           // \"Complete Active Step\"\n    pub group: Option<String>,   // \"Workflow\"\n    \n    // UX\n    pub icon: Option<String>,    // \"✅\"\n    pub shortcut: Option<String>,// \"Ctrl+Enter\"\n    \n    // Execution\n    pub callback: CommandCallback,\n}\n```\n\n### Naming Convention\n```\n{domain}.{action}_{object}\n\nExamples:\n- workflow.complete_step\n- workflow.fail_step\n- user.create_account\n- billing.export_invoice\n- navigation.goto_dashboard\n- system.toggle_theme\n```\n\n**Rules:**\n- Lowercase\n- Dots separate domain from action\n- Underscores separate words\n- Verb-first naming (action-oriented)\n\n### Command Groups\n```rust\nCommandGroup::new(\"workflow\", \"Workflow\")\n    .add_command(complete_cmd)\n    .add_command(fail_cmd)\n    .add_command(reset_cmd)\n\nCommandGroup::new(\"navigation\", \"Navigation\")\n    .add_command(goto_dashboard)\n    .add_command(goto_settings)\n\nCommandGroup::new(\"system\", \"System\")\n    .add_command(toggle_theme)\n    .add_command(logout)\n```\n\n**Purpose:**\n- Organize commands logically\n- Visual separation in palette\n- Better discoverability\n- Domain boundaries clear\n\n---\n\n## Integration Patterns\n\n### Pattern 1: Domain Commands\n\n**Where:** `apps/*/components/domain_commands.rs`\n```rust\n// workflow_commands.rs\npub fn create_workflow_commands(\n    on_transition: Callback<()>\n) -> CommandRegistry {\n    CommandRegistry::new()\n        .add_group(\n            CommandGroup::new(\"workflow\", \"Workflow\")\n                .add_command(Command {\n                    id: \"workflow.complete_step\".into(),\n                    label: \"Complete Active Step\".into(),\n                    group: Some(\"Workflow\".into()),\n                    shortcut: Some(\"Ctrl+Enter\".into()),\n                    icon: Some(\"✅\".into()),\n                    callback: CommandCallback::new(move || {\n                        #[cfg(target_arch = \"wasm32\")]\n                        {\n                            use leptos::task::spawn_local;\n                            spawn_local(async move {\n                                transition_workflow_step(...).await;\n                                on_transition.run(());\n                            });\n                        }\n                    }),\n                })\n        )\n}\n```\n\n**Key points:**\n- Domain-specific\n- Uses domain server functions\n- Triggers domain callbacks (refresh)\n- Client-only execution\n\n### Pattern 2: System Commands\n\n**Where:** `apps/*/components/system_commands.rs`\n```rust\npub fn create_system_commands(\n    set_theme: WriteSignal<Theme>,\n    set_lang: WriteSignal<Language>,\n) -> CommandRegistry {\n    CommandRegistry::new()\n        .add_group(\n            CommandGroup::new(\"system\", \"System\")\n                .add_command(Command {\n                    id: \"system.toggle_theme\".into(),\n                    label: \"Toggle Theme\".into(),\n                    shortcut: Some(\"Ctrl+Shift+T\".into()),\n                    callback: CommandCallback::new(move || {\n                        set_theme.update(|t| *t = t.toggle());\n                    }),\n                })\n                .add_command(Command {\n                    id: \"system.change_language\".into(),\n                    label: \"Change Language\".into(),\n                    shortcut: Some(\"Ctrl+Shift+L\".into()),\n                    callback: CommandCallback::new(move || {\n                        set_lang.update(|l| *l = l.next());\n                    }),\n                })\n        )\n}\n```\n\n### Pattern 3: Navigation Commands\n```rust\npub fn create_navigation_commands(\n    navigate: impl Fn(&str) + 'static\n) -> CommandRegistry {\n    CommandRegistry::new()\n        .add_group(\n            CommandGroup::new(\"navigation\", \"Go to...\")\n                .add_command(Command {\n                    id: \"nav.dashboard\".into(),\n                    label: \"Dashboard\".into(),\n                    shortcut: Some(\"Ctrl+Shift+D\".into()),\n                    callback: CommandCallback::new(move || navigate(\"/dashboard\")),\n                })\n        )\n}\n```\n\n### Pattern 4: Composite Registry\n```rust\n// App-level aggregation\npub fn create_app_commands(\n    workflow_on_transition: Callback<()>,\n    set_theme: WriteSignal<Theme>,\n    navigate: impl Fn(&str) + 'static,\n) -> CommandRegistry {\n    let mut registry = CommandRegistry::new();\n    \n    // Merge domain registries\n    for group in create_workflow_commands(workflow_on_transition).groups {\n        registry = registry.add_group(group);\n    }\n    \n    for group in create_system_commands(set_theme).groups {\n        registry = registry.add_group(group);\n    }\n    \n    for group in create_navigation_commands(navigate).groups {\n        registry = registry.add_group(group);\n    }\n    \n    registry\n}\n```\n\n---\n\n## Keyboard Shortcuts\n\n### Global Shortcut\n```rust\n// Ctrl+K or Cmd+K to open palette\nEffect::new(move |_| {\n    let handler = move |ev: web_sys::KeyboardEvent| {\n        if (ev.ctrl_key() || ev.meta_key()) && ev.key() == \"k\" {\n            ev.prevent_default();\n            set_palette_open.update(|open| *open = !*open);\n        }\n    };\n    \n    // Register listener\n    window.add_event_listener(\"keydown\", handler);\n});\n```\n\n### Command Shortcuts\n```rust\nCommand {\n    id: \"workflow.complete\".into(),\n    label: \"Complete Step\".into(),\n    shortcut: Some(\"Ctrl+Enter\".into()),  // Displayed in UI\n    callback: CommandCallback::new(move || { ... }),\n}\n```\n\n**Note:** Shortcuts in commands are **visual hints**, not automatic bindings. Implement actual key handlers separately if needed.\n\n---\n\n## SSR And Hydration\n\n### The Hydration Problem\n```rust\n// ❌ WRONG: Renders different trees in SSR vs client\nview! {\n    {\n        #[cfg(target_arch = \"wasm32\")]\n        {\n            view! { <CommandPalette /> }\n        }\n        \n        #[cfg(not(target_arch = \"wasm32\"))]\n        {\n            view! { <div></div> }\n        }\n    }\n}\n```\n\n**Problem:** SSR renders `<div>`, client expects `<CommandPalette>` → hydration panic\n\n### ✅ CORRECT: Mount After Hydration\n```rust\nlet (palette_mounted, set_palette_mounted) = signal(false);\n\nEffect::new(move |_| {\n    // Register keyboard shortcuts\n    // ...\n    set_palette_mounted.set(true);\n});\n\nview! {\n    <div>\n        <button on:click=open_palette>\"Commands\"</button>\n        \n        <Show when=move || palette_mounted.get()>\n            <CommandPalette />\n        </Show>\n    </div>\n}\n```\n\n**Why this works:**\n- SSR renders button only\n- Client hydrates button (matches)\n- Effect runs AFTER hydration\n- CommandPalette mounts dynamically (no mismatch)\n\n---\n\n## Command Execution\n\n### Sync Commands\n```rust\nCommandCallback::new(move || {\n    set_theme.update(|t| *t = t.toggle());\n})\n```\n\n### Async Commands (Client-Only)\n```rust\nCommandCallback::new(move || {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        use leptos::task::spawn_local;\n        spawn_local(async move {\n            transition_step().await;\n            on_complete.run(());\n        });\n    }\n})\n```\n\n### With Confirmation\n```rust\nCommandCallback::new(move || {\n    if window.confirm(\"Are you sure?\") {\n        delete_item();\n    }\n})\n```\n\n---\n\n## Scaling Commands\n\n### Contextual Commands\n```rust\n// Commands change based on context\nlet commands = Signal::derive(move || {\n    let selected = selected_item.get();\n    \n    if let Some(item) = selected {\n        create_item_commands(item)  // Edit, Delete, Share\n    } else {\n        create_global_commands()    // New, Import\n    }\n});\n\nview! {\n    <CommandPalette registry=commands.get() />\n}\n```\n\n### Dynamic Registration\n```rust\n// Add commands at runtime\nlet (registry, set_registry) = signal(CommandRegistry::new());\n\n// Add workflow commands when workflow loads\nEffect::new(move |_| {\n    if workflow_loaded.get() {\n        set_registry.update(|r| {\n            *r = r.clone().add_group(create_workflow_commands());\n        });\n    }\n});\n```\n\n---\n\n## Testing\n\n### Unit Test Commands\n```rust\n#[test]\nfn command_execution() {\n    let (called, set_called) = signal(false);\n    \n    let cmd = Command {\n        id: \"test\".into(),\n        label: \"Test\".into(),\n        callback: CommandCallback::new(move || set_called.set(true)),\n        ..Default::default()\n    };\n    \n    cmd.callback.call();\n    assert!(called.get());\n}\n```\n\n### Test Registry\n```rust\n#[test]\nfn registry_search() {\n    let registry = CommandRegistry::new()\n        .add_group(\n            CommandGroup::new(\"test\", \"Test\")\n                .add_command(Command {\n                    id: \"test.foo\".into(),\n                    label: \"Foo Action\".into(),\n                    ..Default::default()\n                })\n        );\n    \n    let results = registry.search(\"foo\");\n    assert_eq!(results.len(), 1);\n    assert_eq!(results[0].id, \"test.foo\");\n}\n```\n\n---\n\n## Enterprise Benefits\n\n### 1. Automation\n\nCommands are **data**, not just UI:\n```rust\n// CLI can execute same commands\nfn execute_command(id: &str) {\n    let registry = create_app_commands(...);\n    if let Some(cmd) = registry.find_command(id) {\n        cmd.callback.call();\n    }\n}\n\n// $ app-cli exec workflow.complete_step\n```\n\n### 2. Analytics\n```rust\nCommandCallback::new(move || {\n    analytics::track(\"command_executed\", json!({\n        \"command_id\": \"workflow.complete_step\",\n        \"user_id\": user.id,\n    }));\n    \n    execute_transition();\n})\n```\n\n### 3. A11y\n\n- Keyboard-first by design\n- No mouse required\n- Screen reader friendly (ARIA labels)\n- Discoverable without visual hunt\n\n### 4. Onboarding\n```rust\n// Show \"getting started\" commands on first login\nif is_first_login {\n    registry.add_group(\n        CommandGroup::new(\"tutorial\", \"Getting Started\")\n            .add_command(create_account_cmd)\n            .add_command(import_data_cmd)\n    );\n}\n```\n\n---\n\n## Command Palette Vs Other Patterns\n\n| Pattern | Use Case | Command Palette Replaces? |\n|---------|----------|--------------------------|\n| Toolbar | Common actions | Partially (keep 1-2 most common) |\n| Context Menu | Right-click actions | ✅ Yes |\n| Dropdown Menu | Grouped actions | ✅ Yes |\n| Keyboard Shortcuts | Power users | ✅ Complements (shows shortcuts) |\n| Search | Find items | ❌ Different (find vs execute) |\n\n---\n\n## Related Rules\n\n- [Canon Rule #43: Domain Components and Commands](./canon-rule-43-domain-components-and-commands.md) - CQRS separation\n- [Canon Rule #44: Orchestrators](./canon-rule-44-orchestrators.md) - Coordination patterns\n- [Canon Rule #45: Demo Components](./canon-rule-45-demo-components.md) - When to simplify\n\n---\n\n## Summary\n\n**Golden Rule:** Command Palette is infrastructure, not decoration.\n\n**Architecture:**\n```\nIntent Surface → Command Registry → Execution → State Change\n```\n\n**Benefits:**\n- ✅ Scalable (100s of commands without UI bloat)\n- ✅ Discoverable (search > memory)\n- ✅ Accessible (keyboard-first)\n- ✅ Automatable (CLI, scripts, AI)\n- ✅ Consistent (same commands everywhere)\n- ✅ Testable (commands are data)\n\n**Never:**\n- Replace ALL buttons with palette\n- Hide critical actions behind palette\n- Make palette the only way to act\n\n**Always:**\n- Keep 1-2 most common actions as buttons\n- Make palette a **complement**, not replacement\n- Register ALL actions as commands (even if buttons exist)\n\n**Remember:**\n> \"Buttons are shortcuts to commands, not the commands themselves.\"\n\n---\n\n\n---"},{"number":47,"slug":"tree-context-selection","title":"Tree, Context & Selection","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["tree","selection","context","commands"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Coupling navigation components with actions or business logic leads to rigid and non-scalable systems. Tree structures must expose only hierarchy and selection while delegating actions to command systems.","problem":"tree components execute actions or contain business logic instead of exposing selection","solution":"separate tree selection from actions using selection context and command registry","signals":["tree executes action","button inside tree node","navigation coupled with logic"],"search_intent":"how to decouple tree navigation from","keywords":["tree selection context pattern","frontend navigation decoupling","command registry contextual actions","hierarchical ui design pattern"],"body":"## Principle\nTree components expose **structure and selection**, never actions. Context flows from Tree → SelectionContext → Command Registry, enabling contextual commands without coupling tree navigation to domain logic.\n\n---\n\n## The Core Pattern\n```\n┌──────────────┐\n│ Tree         │ Exposes: structure + selection\n│ (Navigator)  │ Never: executes actions\n└──────┬───────┘\n       │\n       ▼\n┌──────────────┐\n│ Selection    │ Derived: from tree selection\n│ Context      │ Contains: id, type, label, metadata\n└──────┬───────┘\n       │\n       ▼\n┌──────────────┐\n│ Command      │ Filters: based on context\n│ Registry     │ Returns: available commands\n└──────┬───────┘\n       │\n       ▼\n┌──────────────┐\n│ Command      │ Displays: contextual commands\n│ Palette      │ Executes: user intent\n└──────────────┘\n```\n\n**Golden Rule:** Tree never knows commands exist. Commands always know what context they need.\n\n---\n\n## The Problem Tree Solves\n\n### ❌ WRONG: Flat List Navigation\n```rust\n// Everything in one list - no hierarchy\nview! {\n    <div>\n        <div on:click=select_workflow1>\"Workflow 1\"</div>\n        <div on:click=select_step1>\"  Step 1\"</div>\n        <div on:click=select_step2>\"  Step 2\"</div>\n        <div on:click=select_workflow2>\"Workflow 2\"</div>\n        <div on:click=select_step3>\"  Step 3\"</div>\n    </div>\n}\n```\n\n**Problems:**\n- No visual hierarchy\n- Hard to understand relationships\n- Can't collapse/expand\n- Doesn't scale\n- No parent/child concept\n\n### ✅ CORRECT: Tree Navigation\n```rust\nTreeNode::new(\"workflows\", \"Workflows\", \"root\")\n    .with_children(vec![\n        TreeNode::new(\"wf-1\", \"User Onboarding\", \"workflow\")\n            .with_children(vec![\n                TreeNode::new(\"step-1\", \"Review\", \"step\"),\n                TreeNode::new(\"step-2\", \"Approval\", \"step\"),\n            ]),\n        TreeNode::new(\"wf-2\", \"Billing\", \"workflow\")\n            .with_children(vec![\n                TreeNode::new(\"step-3\", \"Charge\", \"step\"),\n            ]),\n    ])\n```\n\n**Benefits:**\n- Visual hierarchy clear\n- Relationships explicit\n- Collapse/expand naturally\n- Scales to any depth\n- Domain structure visible\n\n---\n\n## TreeNode Structure\n\n### Pure Data\n```rust\npub struct TreeNode {\n    /// Unique identifier\n    pub id: String,\n    \n    /// Display label\n    pub label: String,\n    \n    /// Node type (domain-specific)\n    pub node_type: String,\n    \n    /// Optional icon\n    pub icon: Option<String>,\n    \n    /// Child nodes (recursive)\n    pub children: Vec<TreeNode>,\n    \n    /// UI state: expanded or collapsed\n    pub expanded: bool,\n    \n    /// Optional metadata (serialized domain data)\n    pub metadata: Option<String>,\n}\n```\n\n**Key Points:**\n- No callbacks\n- No domain logic\n- No action execution\n- Pure hierarchical data\n- UI state (expanded) separated from domain state\n\n### Builder Pattern\n```rust\nTreeNode::new(\"id\", \"Label\", \"type\")\n    .with_icon(\"📋\")\n    .with_children(vec![...])\n    .with_expanded(true)\n    .with_metadata(\"status:active\")\n```\n\n**Why Builder:**\n- Optional fields clean\n- Chainable construction\n- Self-documenting\n- Easy to extend\n\n---\n\n## Tree Component Contract\n\n### What Tree DOES\n\n✅ **Renders Hierarchy**\n```rust\n#[component]\npub fn Tree(\n    nodes: Signal<Vec<TreeNode>>,\n    selected_id: Signal<Option<String>>,\n    on_select: Callback<String>,\n    on_toggle: Callback<String>,\n) -> impl IntoView\n```\n\n✅ **Manages Visual State**\n- Expand/collapse arrows\n- Indentation by depth\n- Selected node highlight\n- Icon display\n\n✅ **Emits Events**\n- `on_select(id)` - user clicked node\n- `on_toggle(id)` - user expanded/collapsed\n\n✅ **Recursive Rendering**\n```rust\nfn render_node_recursive(\n    node: TreeNode,\n    depth: usize,\n    selected_id: Signal<Option<String>>,\n    on_select: Callback<String>,\n    on_toggle: Callback<String>,\n) -> impl IntoView {\n    view! {\n        <TreeNodeItem node=node depth=depth ... />\n        {if node.expanded {\n            node.children.map(|child| {\n                render_node_recursive(child, depth + 1, ...)\n            })\n        }}\n    }\n}\n```\n\n### What Tree NEVER DOES\n\n❌ **Execute Domain Actions**\n```rust\n// ❌ WRONG\non:click=move |_| {\n    complete_workflow_step();  // Tree shouldn't know this\n}\n```\n\n❌ **Know About Commands**\n```rust\n// ❌ WRONG\nif node.type == \"step\" {\n    show_complete_button();  // Commands come from elsewhere\n}\n```\n\n❌ **Contain Business Rules**\n```rust\n// ❌ WRONG\nif step.status == \"Active\" && user.role == \"Admin\" {\n    allow_transition();  // Server decides this\n}\n```\n\n❌ **Direct Navigation**\n```rust\n// ❌ WRONG\non:click=move |_| navigate(\"/workflows/123\");  // Commands handle this\n```\n\n---\n\n## SelectionContext\n\n### Structure\n```rust\npub struct SelectionContext {\n    /// Currently selected node ID\n    pub selected_id: Option<String>,\n    \n    /// Selected node type\n    pub node_type: Option<String>,\n    \n    /// Selected node label\n    pub label: Option<String>,\n    \n    /// Optional metadata\n    pub metadata: Option<String>,\n}\n```\n\n### Derivation Pattern\n```rust\nlet selection_context = Signal::derive(move || {\n    if let Some(id) = selected_id.get() {\n        let node = nodes.get()\n            .iter()\n            .find_map(|n| n.find(&id))\n            .cloned();\n        \n        if let Some(node) = node {\n            SelectionContext::with_selection(\n                node.id,\n                node.node_type,\n                node.label,\n                node.metadata,\n            )\n        } else {\n            SelectionContext::new()\n        }\n    } else {\n        SelectionContext::new()\n    }\n});\n```\n\n**Why Derived:**\n- Always in sync with tree\n- No manual updates\n- Reactive propagation\n- Single source of truth\n\n### Helper Methods\n```rust\nimpl SelectionContext {\n    pub fn is_type(&self, type_name: &str) -> bool {\n        self.node_type.as_deref() == Some(type_name)\n    }\n    \n    pub fn has_selection(&self) -> bool {\n        self.selected_id.is_some()\n    }\n}\n```\n\n---\n\n## Contextual Commands\n\n### Pattern: Commands Filter by Context\n```rust\npub fn create_contextual_commands(\n    context: Signal<SelectionContext>,\n    on_action: Callback<String>,\n) -> Signal<CommandRegistry> {\n    Signal::derive(move || {\n        let ctx = context.get();\n        let mut registry = CommandRegistry::new();\n        \n        // Commands for \"step\" nodes\n        if ctx.is_type(\"step\") {\n            registry = registry.add_group(\n                CommandGroup::new(\"step-actions\", \"Step Actions\")\n                    .add_command(Command {\n                        id: \"step.complete\".into(),\n                        label: format!(\"Complete: {}\", ctx.label.unwrap_or_default()),\n                        icon: Some(\"✅\".into()),\n                        callback: CommandCallback::new(move || {\n                            on_action.run(format!(\"complete:{}\", ctx.selected_id.unwrap_or_default()))\n                        }),\n                        ..Default::default()\n                    })\n            );\n        }\n        \n        // Commands for \"workflow\" nodes\n        if ctx.is_type(\"workflow\") {\n            registry = registry.add_group(\n                CommandGroup::new(\"workflow-actions\", \"Workflow Actions\")\n                    .add_command(Command {\n                        id: \"workflow.add_step\".into(),\n                        label: \"Add Step\".into(),\n                        icon: Some(\"➕\".into()),\n                        callback: CommandCallback::new(move || {\n                            on_action.run(format!(\"add_step:{}\", ctx.selected_id.unwrap_or_default()))\n                        }),\n                        ..Default::default()\n                    })\n            );\n        }\n        \n        // Global commands (always available)\n        registry = registry.add_group(\n            CommandGroup::new(\"navigation\", \"Navigation\")\n                .add_command(Command {\n                    id: \"nav.dashboard\".into(),\n                    label: \"Go to Dashboard\".into(),\n                    icon: Some(\"🏠\".into()),\n                    callback: CommandCallback::new(move || {\n                        on_action.run(\"nav:dashboard\".into())\n                    }),\n                    ..Default::default()\n                })\n        );\n        \n        registry\n    })\n}\n```\n\n**Key Pattern:**\n- Commands **react** to context changes\n- Context **never** calls commands\n- Tree **never** knows about commands\n- One-way flow: Tree → Context → Commands\n\n---\n\n## Integration Pattern\n\n### Full Integration Example\n```rust\n#[component]\npub fn AppWithTreeAndCommands() -> impl IntoView {\n    let (nodes, set_nodes) = signal(create_tree_structure());\n    let (selected_id, set_selected_id) = signal(None::<String>);\n    let (palette_open, set_palette_open) = signal(false);\n    \n    // 1. Derive context from selection\n    let selection_context = Signal::derive(move || {\n        if let Some(id) = selected_id.get() {\n            let node = nodes.get().iter()\n                .find_map(|n| n.find(&id))\n                .cloned();\n            \n            if let Some(node) = node {\n                SelectionContext::with_selection(\n                    node.id,\n                    node.node_type,\n                    node.label,\n                    node.metadata,\n                )\n            } else {\n                SelectionContext::new()\n            }\n        } else {\n            SelectionContext::new()\n        }\n    });\n    \n    // 2. Create contextual commands\n    let on_action = Callback::new(move |action: String| {\n        leptos::logging::log!(\"Action: {}\", action);\n        // Execute action via server function, etc.\n        set_palette_open.set(false);\n    });\n    \n    let contextual_commands = create_contextual_commands(\n        selection_context,\n        on_action\n    );\n    \n    // 3. Wire tree callbacks\n    let on_select = Callback::new(move |id: String| {\n        set_selected_id.set(Some(id));\n    });\n    \n    let on_toggle = Callback::new(move |id: String| {\n        set_nodes.update(|nodes| {\n            for node in nodes.iter_mut() {\n                if let Some(found) = node.find_mut(&id) {\n                    found.expanded = !found.expanded;\n                    break;\n                }\n            }\n        });\n    });\n    \n    view! {\n        <div>\n            <Tree\n                nodes=nodes\n                selected_id=selected_id\n                on_select=on_select\n                on_toggle=on_toggle\n            />\n            \n            <CommandPalette\n                registry=contextual_commands.get()\n                open=palette_open\n                on_close=Callback::new(move |_| set_palette_open.set(false))\n            />\n        </div>\n    }\n}\n```\n\n---\n\n## Node Type Conventions\n\n### Recommended Types\n\n| Type | Represents | Commands Example |\n|------|------------|------------------|\n| `root` | Top-level container | Add child, Expand all |\n| `collection` | Group of items | Add item, Filter |\n| `workflow` | Workflow instance | Add step, Archive, Export |\n| `step` | Workflow step | Complete, Fail, Reset |\n| `user` | User entity | Edit, Disable, Reset password |\n| `role` | Role entity | Edit permissions, Delete |\n| `file` | File system item | Open, Rename, Delete |\n| `folder` | Directory | Add file, Collapse all |\n\n### Type Naming\n```\nlowercase\nsingle_word_preferred\nuse_underscore_for_compound\n```\n\n**Examples:**\n- ✅ `step`, `workflow`, `user`, `role`\n- ✅ `workflow_step`, `user_group`\n- ❌ `WorkflowStep`, `STEP`, `Workflow-Step`\n\n---\n\n## Tree State Management\n\n### Expand/Collapse\n```rust\nlet on_toggle = Callback::new(move |id: String| {\n    set_nodes.update(|nodes: &mut Vec<TreeNode>| {\n        for node in nodes.iter_mut() {\n            if let Some(found) = node.find_mut(&id) {\n                found.expanded = !found.expanded;\n                break;\n            }\n        }\n    });\n});\n```\n\n### Expand All / Collapse All\n```rust\nfn expand_all(nodes: &mut Vec<TreeNode>) {\n    for node in nodes.iter_mut() {\n        node.expanded = true;\n        expand_all(&mut node.children);\n    }\n}\n\nfn collapse_all(nodes: &mut Vec<TreeNode>) {\n    for node in nodes.iter_mut() {\n        node.expanded = false;\n        collapse_all(&mut node.children);\n    }\n}\n```\n\n### Persist Expand State\n```rust\n// Save to localStorage\n#[cfg(target_arch = \"wasm32\")]\nfn save_expand_state(nodes: &Vec<TreeNode>) {\n    let expanded_ids: Vec<String> = nodes.iter()\n        .flat_map(|n| get_expanded_ids(n))\n        .collect();\n    \n    let json = serde_json::to_string(&expanded_ids).unwrap();\n    window().local_storage()\n        .unwrap()\n        .unwrap()\n        .set_item(\"tree_expanded\", &json)\n        .ok();\n}\n\n// Restore from localStorage\nfn restore_expand_state(nodes: &mut Vec<TreeNode>, expanded_ids: &[String]) {\n    for node in nodes.iter_mut() {\n        if expanded_ids.contains(&node.id) {\n            node.expanded = true;\n        }\n        restore_expand_state(&mut node.children, expanded_ids);\n    }\n}\n```\n\n---\n\n## Keyboard Navigation\n\n### Basic Pattern\n```rust\n#[component]\npub fn TreeWithKeyboard() -> impl IntoView {\n    let (selected_index, set_selected_index) = signal(0usize);\n    \n    let all_ids = Signal::derive(move || {\n        flatten_tree_ids(&nodes.get())\n    });\n    \n    let on_keydown = move |ev: web_sys::KeyboardEvent| {\n        match ev.key().as_str() {\n            \"ArrowDown\" => {\n                ev.prevent_default();\n                set_selected_index.update(|idx| {\n                    *idx = (*idx + 1).min(all_ids.get().len() - 1);\n                });\n            }\n            \"ArrowUp\" => {\n                ev.prevent_default();\n                set_selected_index.update(|idx| {\n                    *idx = idx.saturating_sub(1);\n                });\n            }\n            \"ArrowRight\" => {\n                // Expand if collapsed\n                ev.prevent_default();\n                expand_selected();\n            }\n            \"ArrowLeft\" => {\n                // Collapse if expanded\n                ev.prevent_default();\n                collapse_selected();\n            }\n            \"Enter\" => {\n                // Select/activate\n                ev.prevent_default();\n                activate_selected();\n            }\n            _ => {}\n        }\n    };\n    \n    view! {\n        <div on:keydown=on_keydown tabindex=\"0\">\n            <Tree ... />\n        </div>\n    }\n}\n```\n\n---\n\n## Testing\n\n### Test TreeNode Utilities\n```rust\n#[test]\nfn test_find() {\n    let tree = TreeNode::new(\"root\", \"Root\", \"root\")\n        .with_children(vec![\n            TreeNode::new(\"child\", \"Child\", \"node\"),\n        ]);\n    \n    assert!(tree.find(\"root\").is_some());\n    assert!(tree.find(\"child\").is_some());\n    assert!(tree.find(\"missing\").is_none());\n}\n\n#[test]\nfn test_depth() {\n    let tree = TreeNode::new(\"root\", \"Root\", \"root\")\n        .with_children(vec![\n            TreeNode::new(\"l1\", \"Level 1\", \"node\")\n                .with_children(vec![\n                    TreeNode::new(\"l2\", \"Level 2\", \"node\"),\n                ]),\n        ]);\n    \n    assert_eq!(tree.depth(), 2);\n}\n```\n\n### Test Selection Context\n```rust\n#[test]\nfn test_context_is_type() {\n    let ctx = SelectionContext::with_selection(\n        \"step-1\".into(),\n        \"step\".into(),\n        \"Review\".into(),\n        None,\n    );\n    \n    assert!(ctx.is_type(\"step\"));\n    assert!(!ctx.is_type(\"workflow\"));\n}\n```\n\n### Test Contextual Commands\n```rust\n#[test]\nfn test_commands_change_with_context() {\n    let (context, set_context) = signal(SelectionContext::new());\n    let on_action = Callback::new(|_| {});\n    \n    let commands = create_contextual_commands(context.into(), on_action);\n    \n    // No selection → only global commands\n    assert_eq!(commands.get().get_all_commands().len(), 2);  // Dashboard, Settings\n    \n    // Select step → step commands appear\n    set_context.set(SelectionContext::with_selection(\n        \"step-1\".into(),\n        \"step\".into(),\n        \"Review\".into(),\n        None,\n    ));\n    \n    assert_eq!(commands.get().get_all_commands().len(), 5);  // 3 step + 2 global\n}\n```\n\n---\n\n## Anti Patterns\n\n### Anti-Pattern 1: Tree with Actions\n```rust\n// ❌ WRONG\n#[component]\npub fn TreeNodeItem(node: TreeNode) -> impl IntoView {\n    view! {\n        <div>\n            {node.label}\n            {if node.type == \"step\" {\n                view! { <button on:click=complete>\"Complete\"</button> }  // ❌\n            }}\n        </div>\n    }\n}\n```\n\n**Fix:** Commands come from CommandPalette based on context\n\n### Anti-Pattern 2: Context with Logic\n```rust\n// ❌ WRONG\nimpl SelectionContext {\n    pub fn can_complete(&self) -> bool {\n        self.metadata.as_ref()\n            .map(|m| m.contains(\"active\"))\n            .unwrap_or(false)\n    }\n}\n```\n\n**Fix:** Business logic lives on server, not in context\n\n### Anti-Pattern 3: Mixed Responsibilities\n```rust\n// ❌ WRONG\n#[component]\npub fn Tree() -> impl IntoView {\n    let on_select = move |id| {\n        set_selected(id);\n        fetch_details(id);      // ❌ Side effect\n        update_breadcrumbs(id); // ❌ Side effect\n    };\n}\n```\n\n**Fix:** Tree only updates selection, consumers react\n\n---\n\n## Related Rules\n\n- [Canon Rule #46: Command Palette & Intent Surfaces](./canon-rule-46-command-palette-intent-surfaces.md) - Intent layer\n- [Canon Rule #43: Domain Components and Commands](./canon-rule-43-domain-components-and-commands.md) - CQRS separation\n- [Canon Rule #44: Orchestrators](./canon-rule-44-orchestrators.md) - Coordination\n\n---\n\n## Summary\n\n**Golden Rule:** Tree exposes context, never executes intent.\n\n**Flow:**\n```\nTree → SelectionContext → CommandRegistry → CommandPalette\n```\n\n**Tree Responsibilities:**\n- ✅ Render hierarchy\n- ✅ Expand/collapse\n- ✅ Visual selection\n- ✅ Emit selection events\n\n**Tree NEVER:**\n- ❌ Execute actions\n- ❌ Know about commands\n- ❌ Contain business logic\n- ❌ Trigger side effects\n\n**SelectionContext:**\n- Derived from tree selection\n- Pure data (id, type, label, metadata)\n- No logic, no actions\n\n**Contextual Commands:**\n- Filter based on context\n- One-way dependency (commands → context)\n- Reactive updates\n\n**Result:**\n- Decoupled navigation from actions\n- Scalable command system\n- Testable in isolation\n- Keyboard-first UX\n- Enterprise-grade\n\n---\n\n\n---\n\n\n---\n\n## Breadcrumb Pattern\n\n### What is Breadcrumb?\n\n**Breadcrumb** = Visual representation of the path from root to current selection\n\nIt's **NOT:**\n- A standalone navigation component\n- Independent from tree structure\n- Manually maintained\n\nIt's **DERIVED FROM:**\n- Tree selection\n- Tree hierarchy\n- Navigation context\n```\nTree Selection → build_path() → BreadcrumbItemData[] → Breadcrumb Component\n```\n\n---\n\n\n\n---\n\n\n\n---\n\n\n\n---\n\n\n\n\n\n---\n\n\n\n\n\n\n\n\n\n\n---\n\n\n---\n\n\n\n\n---\n\n\n\n\n\n\n---\n\n\n\n---\n\n## Summary with Breadcrumb\n\n**Updated Golden Rule:** Tree exposes context, Breadcrumb visualizes path, Commands execute intent.\n\n**Complete Flow:**\n```\nTree Selection → build_path() → Breadcrumb (render path)\n                                      ↓\n                              User clicks item\n                                      ↓\n                           Update tree selection\n                                      ↓\n                           Selection Context\n                                      ↓\n                           Command Registry (filtered)\n                                      ↓\n                           Command Palette (display + execute)\n```\n\n**Separation of Concerns:**\n\n| Component | Responsibility | Never Does |\n|-----------|---------------|------------|\n| **Tree** | Structure + Selection | Execute actions, render path |\n| **Breadcrumb** | Path visualization | Know tree structure, execute actions |\n| **SelectionContext** | Current state | Render UI, execute actions |\n| **CommandPalette** | Intent execution | Know tree structure, build paths |\n\n**Result:**\n- Tree for navigation (what exists)\n- Breadcrumb for orientation (where am I)\n- Commands for action (what can I do)\n- All reactive, all decoupled\n\n---\n\n\n---\n\n## Route Synchronization Pattern\n\n### The Problem: Ephemeral State\n\n**Without Route Sync:**\n- Tree selection lost on refresh\n- Can't share \"open at step X\" link\n- Browser back/forward doesn't work\n- SSR renders collapsed tree (bad UX)\n\n**With Route Sync:**\n- ✅ URL is source of truth\n- ✅ Shareable deep links\n- ✅ Browser navigation works\n- ✅ SSR renders expanded state\n- ✅ Refresh preserves context\n\n---\n\n\n\n---\n\n\n\n\n\n---\n\n\n\n\n\n\n\n\n\n---\n\n\n\n\n\n---\n\n\n\n\n\n\n\n\n\n---\n\n\n\n\n---\n\n\n\n---\n\n\n\n\n\n\n\n\n\n\n\n\n---\n\n## Complete Navigation System Summary\n\n**Three Pillars:**\n\n1. **Tree** - Structure + Selection\n2. **Breadcrumb** - Path Visualization\n3. **Command Palette** - Contextual Actions\n\n**With Route Sync:**\n\n4. **URL** - Persistent State (source of truth)\n```\n┌─────────────────────────────────────────────┐\n│ URL (Query Param)                           │\n│ /components?path=/workflows/w1/step-1-2     │\n└────────┬────────────────────────────────────┘\n         │ (source of truth)\n         ↓\n┌────────────────────────────────────────────┐\n│ Tree (renders expanded + selected)         │\n└────────┬───────────────────────────────────┘\n         │\n         ↓\n┌────────────────────────────────────────────┐\n│ Breadcrumb (shows path: Home › W1 › Step)  │\n└────────┬───────────────────────────────────┘\n         │\n         ↓\n┌────────────────────────────────────────────┐\n│ Selection Context (type=step, id=step-1-2) │\n└────────┬───────────────────────────────────┘\n         │\n         ↓\n┌────────────────────────────────────────────┐\n│ Commands (Complete, Fail, Reset)           │\n└────────────────────────────────────────────┘\n```\n\n**Benefits:**\n- ✅ Deep linking\n- ✅ Browser navigation\n- ✅ Shareable state\n- ✅ SSR optimization\n- ✅ No state duplication\n- ✅ Single source of truth\n\n---"},{"number":48,"slug":"context-provider","title":"Context Provider Pattern","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["context","providers","state","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Passing state through multiple component layers creates prop drilling and tightly coupled APIs. Context providers centralize state and allow components to consume it directly.","problem":"prop drilling across components makes state management complex and tightly coupled","solution":"use context providers to share global state and eliminate prop drilling","signals":["prop drilling","deep prop chains","duplicated state passing"],"search_intent":"how to avoid prop drilling using","keywords":["leptos context provider pattern","avoid prop drilling frontend","global state sharing signals","context vs props architecture"],"body":"## Principle\nUse Context Providers to eliminate prop drilling and enable global access to selection state. Components consume context via hooks instead of receiving props.\n\n---\n\n## The Problem Prop Drilling\n\n### ❌ WITHOUT Context Provider\n```rust\n#[component]\npub fn App() -> impl IntoView {\n    let selection_context = Signal::derive(...);\n    \n    view! {\n        <Tree selection_context=selection_context />\n        <Breadcrumb selection_context=selection_context />\n        <Inspector selection_context=selection_context />\n        <StatusBar selection_context=selection_context />\n        <ActionBar selection_context=selection_context />\n        <CommandPalette selection_context=selection_context />\n    }\n}\n```\n\n**Problems:**\n- ❌ Every component needs selection prop\n- ❌ Adding new consumer = update all call sites\n- ❌ Deep nesting = prop drilling hell\n- ❌ Refactoring is painful\n- ❌ Component APIs polluted with passthrough props\n\n### ✅ WITH Context Provider\n```rust\n#[component]\npub fn App() -> impl IntoView {\n    view! {\n        <SelectionContextProvider>\n            <Tree />\n            <Breadcrumb />\n            <Inspector />\n            <StatusBar />\n            <ActionBar />\n            <CommandPalette />\n        </SelectionContextProvider>\n    }\n}\n```\n\n**Benefits:**\n- ✅ Zero prop drilling\n- ✅ Components access context directly\n- ✅ Easy to add new consumers\n- ✅ Clean component APIs\n- ✅ Single source of truth\n\n---\n\n## Context Provider Implementation\n\n### Context Value Structure\n```rust\nuse leptos::prelude::*;\n\n/// SelectionContextValue - Shared selection state\n#[derive(Clone, Copy)]\npub struct SelectionContextValue {\n    pub selected_id: RwSignal<Option<String>>,\n    pub node_type: RwSignal<Option<String>>,\n    pub label: RwSignal<Option<String>>,\n    pub metadata: RwSignal<Option<String>>,\n}\n```\n\n**Key Points:**\n- `Clone + Copy` - Can be copied cheaply\n- All fields are `RwSignal` - Reactive\n- `Option<String>` - Can be empty (no selection)\n\n### Helper Methods\n```rust\nimpl SelectionContextValue {\n    /// Update entire context at once\n    pub fn update_selection(\n        &self,\n        id: Option<String>,\n        node_type: Option<String>,\n        label: Option<String>,\n        metadata: Option<String>,\n    ) {\n        self.selected_id.set(id);\n        self.node_type.set(node_type);\n        self.label.set(label);\n        self.metadata.set(metadata);\n    }\n    \n    /// Clear selection\n    pub fn clear(&self) {\n        self.selected_id.set(None);\n        self.node_type.set(None);\n        self.label.set(None);\n        self.metadata.set(None);\n    }\n    \n    /// Check if has selection\n    pub fn has_selection(&self) -> bool {\n        self.selected_id.get().is_some()\n    }\n    \n    /// Check if node is of type\n    pub fn is_type(&self, type_name: &str) -> bool {\n        self.node_type.get().as_deref() == Some(type_name)\n    }\n}\n```\n\n### Provider Component\n```rust\n#[component]\npub fn SelectionContextProvider(children: Children) -> impl IntoView {\n    // Create reactive signals\n    let selected_id = RwSignal::new(None::<String>);\n    let node_type = RwSignal::new(None::<String>);\n    let label = RwSignal::new(None::<String>);\n    let metadata = RwSignal::new(None::<String>);\n    \n    // Create context value\n    let context = SelectionContextValue {\n        selected_id,\n        node_type,\n        label,\n        metadata,\n    };\n    \n    // Provide to children\n    provide_context(context);\n    \n    view! { {children()} }\n}\n```\n\n**Pattern:**\n1. Create `RwSignal` for each field\n2. Bundle into context struct\n3. `provide_context()`\n4. Render children\n\n### Consumer Hook\n```rust\n/// Hook to consume selection context\npub fn use_selection_context() -> SelectionContextValue {\n    use_context::<SelectionContextValue>()\n        .expect(\"SelectionContext not provided. Wrap in <SelectionContextProvider>\")\n}\n```\n\n**Usage:**\n```rust\n#[component]\npub fn Inspector() -> impl IntoView {\n    let ctx = use_selection_context();\n    \n    view! {\n        <div>\n            {move || ctx.label.get().unwrap_or_default()}\n        </div>\n    }\n}\n```\n\n---\n\n## Producer Pattern\n\nComponents that **update** selection:\n```rust\n#[component]\npub fn Tree() -> impl IntoView {\n    let ctx = use_selection_context();\n    \n    let on_select = Callback::new(move |id: String| {\n        let node = find_node(&id);\n        \n        if let Some(node) = node {\n            ctx.update_selection(\n                Some(node.id),\n                Some(node.node_type),\n                Some(node.label),\n                node.metadata,\n            );\n        }\n    });\n    \n    // ... render tree\n}\n```\n\n**Pattern:**\n1. Get context via `use_selection_context()`\n2. On user action (select, click)\n3. Call `ctx.update_selection()`\n4. All consumers react automatically\n\n---\n\n## Consumer Pattern\n\nComponents that **read** selection:\n```rust\n#[component]\npub fn Inspector() -> impl IntoView {\n    let ctx = use_selection_context();\n    \n    view! {\n        <div>\n            {move || {\n                if ctx.has_selection() {\n                    view! {\n                        <div>\n                            <h3>{move || ctx.label.get().unwrap_or_default()}</h3>\n                            <code>{move || ctx.selected_id.get().unwrap_or_default()}</code>\n                        </div>\n                    }\n                } else {\n                    view! { <p>\"No selection\"</p> }\n                }\n            }}\n        </div>\n    }\n}\n```\n\n**Pattern:**\n1. Get context via `use_selection_context()`\n2. Use signals reactively (`ctx.label.get()`)\n3. No props needed!\n4. Automatically updates when context changes\n\n---\n\n## Contextual Actions Pattern\n\nComponents that show actions based on selection:\n```rust\n#[component]\npub fn InspectorPanel(\n    #[prop(optional)] on_action: Option<Callback<String>>,\n) -> impl IntoView {\n    let ctx = use_selection_context();\n    \n    view! {\n        <div>\n            {move || {\n                if ctx.is_type(\"step\") {\n                    view! {\n                        <button on:click=move |_| {\n                            if let Some(id) = ctx.selected_id.get() {\n                                on_action.unwrap().run(format!(\"complete:{}\", id));\n                            }\n                        }>\n                            \"Complete Step\"\n                        </button>\n                    }\n                } else if ctx.is_type(\"workflow\") {\n                    view! {\n                        <button on:click=move |_| {\n                            if let Some(id) = ctx.selected_id.get() {\n                                on_action.unwrap().run(format!(\"add_step:{}\", id));\n                            }\n                        }>\n                            \"Add Step\"\n                        </button>\n                    }\n                } else {\n                    view! { <p>\"No actions\"</p> }\n                }\n            }}\n        </div>\n    }\n}\n```\n\n**Pattern:**\n1. Read `ctx.is_type()` to check selection type\n2. Render type-specific actions\n3. Use `ctx.selected_id.get()` to get ID\n4. Call action callback with `action:id` format\n\n---\n\n## Integration With Existing Patterns\n\n### With Tree (Rule #47)\n\nTree produces selection → Context → Consumers react\n```rust\n<SelectionContextProvider>\n    <Tree />  // Updates ctx.update_selection()\n    <Inspector />  // Reads ctx.label.get()\n</SelectionContextProvider>\n```\n\n### With Breadcrumb (Rule #47)\n\nBreadcrumb can both read and update context:\n```rust\n#[component]\npub fn Breadcrumb() -> impl IntoView {\n    let ctx = use_selection_context();\n    \n    let items = Signal::derive(move || {\n        if let Some(id) = ctx.selected_id.get() {\n            build_breadcrumb_path(&id)\n        } else {\n            vec![]\n        }\n    });\n    \n    // Click breadcrumb → update context\n    let on_click = move |id: String| {\n        ctx.update_selection(Some(id), ...);\n    };\n}\n```\n\n### With Command Palette (Rule #46)\n\nCommands filter based on context:\n```rust\nlet contextual_commands = Signal::derive(move || {\n    let ctx = use_selection_context();\n    \n    let mut registry = CommandRegistry::new();\n    \n    if ctx.is_type(\"step\") {\n        registry.add_command(Command::new(\"Complete Step\"));\n    }\n    \n    registry\n});\n```\n\n---\n\n## Testing Context\n```rust\n#[test]\nfn test_context_update() {\n    let (selected_id, set_selected_id) = signal(None);\n    let (node_type, set_node_type) = signal(None);\n    let (label, set_label) = signal(None);\n    let (metadata, set_metadata) = signal(None);\n    \n    let ctx = SelectionContextValue {\n        selected_id,\n        node_type,\n        label,\n        metadata,\n    };\n    \n    // Update\n    ctx.update_selection(\n        Some(\"step-1\".into()),\n        Some(\"step\".into()),\n        Some(\"Review\".into()),\n        Some(\"Active\".into()),\n    );\n    \n    // Assert\n    assert_eq!(ctx.selected_id.get(), Some(\"step-1\".to_string()));\n    assert_eq!(ctx.node_type.get(), Some(\"step\".to_string()));\n    assert!(ctx.has_selection());\n    assert!(ctx.is_type(\"step\"));\n    \n    // Clear\n    ctx.clear();\n    assert!(!ctx.has_selection());\n}\n```\n\n---\n\n## Anti Patterns\n\n### Anti-Pattern 1: Prop Drilling with Context Available\n```rust\n// ❌ WRONG: Passing context as prop when provider exists\n#[component]\npub fn App() -> impl IntoView {\n    let ctx = use_selection_context();\n    \n    view! {\n        <SelectionContextProvider>\n            <Inspector ctx=ctx />  // ❌ Don't pass as prop!\n        </SelectionContextProvider>\n    }\n}\n```\n\n**Fix:** Let component access context directly:\n```rust\n// ✅ CORRECT\n<SelectionContextProvider>\n    <Inspector />  // Gets context via use_selection_context()\n</SelectionContextProvider>\n```\n\n### Anti-Pattern 2: Multiple Sources of Truth\n```rust\n// ❌ WRONG: Both context and local state\nlet ctx = use_selection_context();\nlet (local_selection, set_local_selection) = signal(None);\n\n// Which is correct?\n```\n\n**Fix:** Context is ALWAYS source of truth:\n```rust\n// ✅ CORRECT: Only use context\nlet ctx = use_selection_context();\n// No local state needed\n```\n\n### Anti-Pattern 3: Forgetting Provider\n```rust\n// ❌ WRONG: Using hook without provider\n#[component]\npub fn App() -> impl IntoView {\n    view! {\n        <Inspector />  // ❌ Will panic: \"SelectionContext not provided\"\n    }\n}\n```\n\n**Fix:** Wrap in provider:\n```rust\n// ✅ CORRECT\n<SelectionContextProvider>\n    <Inspector />\n</SelectionContextProvider>\n```\n\n### Anti-Pattern 4: Creating Multiple Providers\n```rust\n// ❌ WRONG: Multiple providers = multiple contexts\n<SelectionContextProvider>\n    <Tree />\n</SelectionContextProvider>\n\n<SelectionContextProvider>\n    <Inspector />  // Different context!\n</SelectionContextProvider>\n```\n\n**Fix:** Single provider at top level:\n```rust\n// ✅ CORRECT\n<SelectionContextProvider>\n    <Tree />\n    <Inspector />  // Same context\n</SelectionContextProvider>\n```\n\n---\n\n## When To Use Context Provider\n\n### ✅ USE Context Provider When:\n\n1. **Multiple consumers** - 3+ components need same state\n2. **Deep nesting** - Components are nested 2+ levels deep\n3. **Frequent additions** - New consumers added regularly\n4. **Global state** - State is truly application-wide (selection, theme, user)\n\n### ❌ DON'T Use Context Provider When:\n\n1. **Single consumer** - Only one component needs the data\n2. **Parent-child only** - Direct prop is clearer\n3. **Performance critical** - Context updates ALL consumers (use signals instead)\n4. **Temporary state** - Local form state, modals, etc.\n\n---\n\n## Comparison With Other Frameworks\n\n| Framework | Pattern | API |\n|-----------|---------|-----|\n| **React** | Context API | `createContext`, `useContext` |\n| **Vue** | Provide/Inject | `provide()`, `inject()` |\n| **Solid** | Context | `createContext`, `useContext` |\n| **Leptos** | Context | `provide_context`, `use_context` |\n\n**Leptos Advantage:** Signals + Context = Fine-grained reactivity without extra wrappers.\n\n---\n\n## Migration From Prop Drilling\n\n### Before (Prop Drilling)\n```rust\n#[component]\npub fn App() -> impl IntoView {\n    let selection = signal(...);\n    \n    view! {\n        <Tree selection=selection />\n        <Inspector selection=selection />\n    }\n}\n\n#[component]\npub fn Inspector(selection: Signal<Selection>) -> impl IntoView {\n    // ...\n}\n```\n\n### After (Context Provider)\n```rust\n#[component]\npub fn App() -> impl IntoView {\n    view! {\n        <SelectionContextProvider>\n            <Tree />\n            <Inspector />\n        </SelectionContextProvider>\n    }\n}\n\n#[component]\npub fn Inspector() -> impl IntoView {\n    let ctx = use_selection_context();\n    // ...\n}\n```\n\n**Migration Steps:**\n1. Create provider component\n2. Wrap app in provider\n3. Replace props with `use_selection_context()`\n4. Remove prop from component signatures\n5. Update call sites (remove prop passing)\n\n---\n\n## Summary\n\n**Golden Rule:** Context Provider eliminates prop drilling for global state.\n\n**Pattern:**\n```\nProvider (top) → provide_context()\n    ↓\nConsumer (any depth) → use_context()\n```\n\n**When to Use:**\n- ✅ Multiple consumers\n- ✅ Deep nesting\n- ✅ Global state (selection, theme, user)\n\n**Benefits:**\n- ✅ Zero prop drilling\n- ✅ Clean component APIs\n- ✅ Easy to add consumers\n- ✅ Single source of truth\n\n**Related Rules:**\n- [Rule #47: Tree, Context & Selection](./canon-rule-47-tree-context-selection.md) - Tree produces selection\n- [Rule #46: Command Palette](./canon-rule-46-command-palette-intent-surfaces.md) - Commands consume selection\n\n---\n\n\n---"},{"number":49,"slug":"drag-drop-as-intent","title":"Drag & Drop as Intent (Not Action)","status":"ENFORCED","severity":"MEDIUM","category":"behavior","tags":["drag-drop","commands","events","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Embedding business logic directly inside drag-and-drop handlers couples UI gestures with domain logic. Drag operations should emit intent events that are later converted into commands.","problem":"drag and drop handlers execute business logic directly causing tight coupling","solution":"emit semantic drop events and convert them into commands in application layer","signals":["api call in drag handler","database update in drop","coupled drag logic"],"search_intent":"how to implement drag and drop","keywords":["drag drop command pattern","event driven ui architecture","decoupled drag and drop","undo redo drag operations"],"body":"---\n\n## The Principle\n\n**Drag & Drop is an input gesture, not business logic.**\n\nIt emits **semantic intent**, which the application layer converts into **commands**.\n\n---\n\n## The Problem\n\n### ❌ Wrong Pattern (Coupled)\n```rust\n// DragHandle executes business logic directly\nfn on_drop(item_id: String, target_id: String) {\n    // WRONG: Component knows about database\n    database::move_item(item_id, target_id).await;\n    // WRONG: Component knows about workflow\n    workflow::update_step_order(item_id).await;\n}\n```\n\n**Why this is wrong:**\n- Design system **polluted** with domain logic\n- Impossible to reuse `DragHandle` across contexts\n- Cannot undo/redo (no command history)\n- Cannot test drag without database\n- Violates separation of concerns\n\n---\n\n## The Solution\n\n### ✅ Correct Pattern (Decoupled)\n```rust\n// 1. Design System: Emits events (NO business logic)\nuse rs_design::ui::drag_drop::{DragDropProvider, DragHandle, DropEvent};\n\n#[component]\npub fn MyApp() -> impl IntoView {\n    let on_drop = Callback::new(|event: DropEvent| {\n        // 2. Application Layer: Converts event → command\n        let command = MoveItemCommand {\n            item_id: event.item_id,\n            target_id: event.target_id,\n        };\n        \n        // 3. Execute command (can be undone)\n        command_history.execute(command);\n    });\n\n    view! {\n        <DragDropProvider on_drop=on_drop>\n            <DragHandle item_id=\"task-1\">\n                <div>\"Drag me\"</div>\n            </DragHandle>\n        </DragDropProvider>\n    }\n}\n```\n\n---\n\n## Architecture Layers\n\n### Layer 1: Design System (rs-design)\n**Responsibility:** Input gesture detection  \n**Output:** `DropEvent { item_id, target_id }`\n```rust\n// rs-design/ui/drag_drop/\npub struct DropEvent {\n    pub item_id: DragItemId,\n    pub target_id: DropTargetId,\n    pub data: Option<String>,\n}\n\n// Component ONLY emits events\non_drop.run(DropEvent { ... });\n```\n\n### Layer 2: Application (frontend-leptos)\n**Responsibility:** Convert events → commands\n```rust\nlet on_drop = Callback::new(|event: DropEvent| {\n    match current_context {\n        Context::Workflow => execute_workflow_reorder(event),\n        Context::Tree => execute_tree_move(event),\n        Context::Kanban => execute_kanban_move(event),\n    }\n});\n```\n\n### Layer 3: Command History\n**Responsibility:** Undo/Redo/Audit\n```rust\nstruct MoveItemCommand {\n    item_id: String,\n    from: Position,\n    to: Position,\n}\n\nimpl Command for MoveItemCommand {\n    fn execute(&self) { /* move item */ }\n    fn undo(&self) { /* restore position */ }\n}\n\ncommand_history.push(command);\n```\n\n---\n\n## Real World Example Workflow Steps\n\n### ❌ Wrong (Coupled)\n```rust\n// WRONG: DragHandle knows about workflows\n<DragHandle \n    item_id=\"step-1\"\n    on_drop=move |_| {\n        database::update_workflow_step_order().await; // 🚫\n    }\n>\n```\n\n### ✅ Correct (Decoupled)\n```rust\n// Design System: Pure gesture\n<DragDropProvider on_drop=on_workflow_drop>\n    <SortableList items=steps .../>\n</DragDropProvider>\n\n// Application: Business logic\nlet on_workflow_drop = Callback::new(|event: DropEvent| {\n    let command = ReorderStepsCommand {\n        workflow_id: current_workflow,\n        step_id: event.item_id,\n        new_position: event.target_id,\n    };\n    \n    history.execute(command); // ✅ Undoable\n});\n```\n\n---\n\n## Benefits\n\n### ✅ Reusability\nSame `DragHandle` works for:\n- Workflow steps\n- Tree nodes\n- Kanban cards\n- File managers\n- Task lists\n\n### ✅ Testability\n```rust\n#[test]\nfn test_drag_drop() {\n    let event = DropEvent { \n        item_id: \"task-1\", \n        target_id: \"zone-b\" \n    };\n    \n    // Test WITHOUT rendering component\n    assert_eq!(handle_drop(event), ExpectedState);\n}\n```\n\n### ✅ Undo/Redo\n```rust\n// Free undo/redo if using Command pattern\nuser_presses_ctrl_z();\ncommand_history.undo(); // ✅ Drag is undone\n```\n\n### ✅ Audit Trail\n```rust\n// All drags are logged as commands\nfor command in history.undo_stack {\n    audit_log.push(command.description);\n}\n// \"Moved task-1 from zone-a to zone-b\"\n```\n\n---\n\n## Implementation Checklist\n\nWhen implementing drag & drop:\n\n- [ ] Design system components emit **events only**\n- [ ] Zero database/API calls in drag components\n- [ ] Application layer handles events\n- [ ] Events convertible to Commands\n- [ ] Commands are undoable\n- [ ] Drag works across multiple contexts (workflow, tree, kanban)\n\n---\n\n## Anti Patterns To Avoid\n\n### 🚫 API Calls in DragHandle\n```rust\n// WRONG\n<DragHandle on_drop=move |_| {\n    api::update_position().await; // 🚫\n}>\n```\n\n### 🚫 Database Updates in DropZone\n```rust\n// WRONG\n<DropZone on_drop=move |_| {\n    db.update_item_position(); // 🚫\n}>\n```\n\n### 🚫 Workflow Logic in Design System\n```rust\n// WRONG - in rs-design crate\npub fn DragHandle() {\n    if workflow.is_blocked() { ... } // 🚫\n}\n```\n\n---\n\n## Comparison Canonrs Vs Others\n\n| Framework | Approach | Coupling |\n|-----------|----------|----------|\n| **CanonRS** | Event → Command | ✅ Zero coupling |\n| react-dnd | Imperative handlers | ⚠️ Often coupled |\n| @dnd-kit | Context-based | ⚠️ Moderate coupling |\n| SortableJS | DOM manipulation | 🚫 Tightly coupled |\n\n**Veredito:** CanonRS é **mais rigoroso** e **testável**.\n\n---\n\n## Related Rules\n\n- **Rule #8:** Overlay Islands (Client-Only Architecture)\n- **Rule #XX:** Command Pattern (pending)\n- **Rule #XX:** Separation of Concerns (pending)\n\n---\n\n## Normative Status\n\n- Violations **MUST** block PRs\n- Design system components **MUST NOT** contain business logic\n- All drag & drop **MUST** emit events, not execute actions\n- Events **SHOULD** be convertible to Commands for undo/redo\n\n---\n\n\n---\n\n## Update 2026-01-04: SortableList Pattern\n\n### Best Practice for Reorderable Lists\n\n**Use `SortableList` component instead of manual `DragHandle` + `DropZone`:**\n```rust\nuse rs_design::ui::drag_drop::SortableList;\n\n#[component]\npub fn MyComponent() -> impl IntoView {\n    let (items, set_items) = signal(vec![...]);\n    \n    let on_reorder = Callback::new(move |new_items: Vec<Item>| {\n        set_items.set(new_items);\n        // Convert to Command if needed\n    });\n    \n    view! {\n        <SortableList\n            items=items\n            on_reorder=on_reorder\n            item_id=|item: &Item| item.id.clone()\n            render=move |item: Item| {\n                view! { <div>{item.label}</div> }\n            }\n        />\n    }\n}\n```\n\n**Why this is better:**\n\n✅ Built-in drop indicators (before/after)  \n✅ Automatic drag handle rendering  \n✅ Proper event handling (already tested)  \n✅ SSR-safe implementation  \n✅ Less boilerplate code  \n\n**Working examples:**\n- `drag_drop_tab.rs` - Multi-zone drag & drop\n- `workflow_sortable_tab.rs` - Sortable workflow steps\n\n**When NOT to use SortableList:**\n- Custom drop zones (use `DropZone` directly)\n- Cross-component dragging (use `DragHandle` + callbacks)\n- Non-list layouts (grids, trees)"},{"number":50,"slug":"provider-singleton-pattern","title":"Provider Singleton & Runtime Separation","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["providers","context","runtime","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Duplicating providers creates parallel reactive contexts leading to invisible bugs and broken interactions. Provider lifecycle must be controlled centrally.","problem":"duplicate providers create inconsistent contexts and break interaction flow","solution":"enforce single provider per interaction scope and restrict creation to app layer","signals":["context mismatch","silent failure","drag drop broken","multiple providers"],"search_intent":"why duplicate providers break context","keywords":["provider singleton pattern","leptos context duplication","reactive runtime separation","provider scope architecture"],"body":"---\n\n## The Principle\n\n### Definitions\n\n**Interaction Scope:** A subtree responsible for a continuous unit of interaction (drag operation, editor session, workflow step). Each scope maintains its own reactive runtime.\n\n\n**Runtime Providers must be unique per interaction scope.**\n\nProviders create reactive runtime (signals, state, lifecycle). Duplicating them creates invisible parallel contexts → ghost bugs.\n\n**Corollary:** Design systems NEVER create Providers. Only the Application layer controls Provider lifecycle.\n\n---\n\n## The Problem\n\n### Wrong Pattern Duplicated Provider\n```rust\n// App layer\n<App>\n  <DragDropProvider>  // ✅ Provider 1\n    <Dashboard>\n      <DragDropProvider>  // 🚫 Provider 2 (DUPLICATE!)\n        ...\n      </DragDropProvider>\n    </Dashboard>\n  </DragDropProvider>\n</App>\n```\n\n**Why this breaks:**\n- Two separate `DragContext` instances created\n- `DragHandle` uses Context 1\n- `DropZone` uses Context 2\n- Events emitted to Context 1, callbacks listening on Context 2\n- **Result:** Drag & drop silently fails (no errors, just doesn't work)\n\n### Wrong Pattern Provider In Design System\n```rust\n// rs-design/ui/dashboard/dashboard.rs\n#[component]\npub fn Dashboard() -> impl IntoView {\n    view! {\n        <DragDropProvider>  // 🚫 FORBIDDEN\n            <div>...</div>\n        </DragDropProvider>\n    }\n}\n```\n\n**Why this breaks:**\n- Design system component now controls runtime lifecycle\n- Impossible to reuse `Dashboard` without drag & drop\n- Violates separation of concerns\n- Creates duplicate provider when used in app that already has one\n\n---\n\n## The Solution\n\n### Correct Pattern Singleton Per Scope\n```rust\n// App layer ONLY\n#[component]\npub fn App() -> impl IntoView {\n    view! {\n        <Router>\n            <DragDropProvider>  // ✅ ONE provider at app root\n                <DragDropCallbacksProvider>  // ✅ Nested correctly\n                    <Dashboard />  // ✅ NO provider here\n                    <WorkflowEditor />  // ✅ NO provider here\n                </DragDropCallbacksProvider>\n            </DragDropProvider>\n        </Router>\n    }\n}\n```\n```rust\n// rs-design: Design system component (NO provider)\n#[component]\npub fn Dashboard(\n    widgets: Signal<Vec<WidgetDef>>,\n    on_drop: Callback<DropEvent>,\n) -> impl IntoView {\n    // ✅ Consumes context (does NOT create it)\n    let drag_ctx = use_drag_context();\n    \n    view! {\n        <div>\n            <DragHandle .../>  // ✅ Uses existing context\n            <DropZone .../>    // ✅ Uses existing context\n        </div>\n    }\n}\n```\n\n---\n\n## Architecture Layers\n\n### Layer Responsibilities\n\n| Layer | Creates Providers? | Consumes Context? | Responsibility |\n|-------|-------------------|-------------------|----------------|\n| **App (frontend-leptos)** | ✅ YES | ✅ YES | Lifecycle, orchestration |\n| **Design System (rs-design)** | ❌ NO | ✅ YES | UI primitives, emit events |\n| **Domain/Core** | ❌ NO | ❌ NO | Business logic |\n\n### Canonical Provider Hierarchy\n```\nApp Root\n└── DragDropProvider (runtime: creates signals, state)\n    └── DragDropCallbacksProvider (interprets events → commands)\n        └── LanguageProvider\n            └── ThemeProvider\n                └── UI Components (consume contexts)\n```\n\n**Critical Rule:** Providers that depend on runtime MUST be nested INSIDE the runtime provider.\n\n**Why:** `DragDropCallbacksProvider` interprets events from `DragDropProvider`. If callbacks are outside, they have no runtime to listen to.\n\n---\n\n## Valid Scenarios\n\n### ✅ Multiple Isolated Scopes\n```rust\n// Valid: Each scope has its own runtime\n<App>\n  <Route path=\"/dashboard\">\n    <DragDropProvider>  // ✅ Scope 1\n      <Dashboard />\n    </DragDropProvider>\n  </Route>\n  \n  <Route path=\"/kanban\">\n    <DragDropProvider>  // ✅ Scope 2 (isolated)\n      <KanbanBoard />\n    </DragDropProvider>\n  </Route>\n</App>\n```\n\n**Rule:** Multiple providers are valid if they control **different interaction scopes**.\n\n### ✅ Conditional Provider\n```rust\n// Valid: Feature flag\n{move || {\n    if feature_flags.drag_enabled.get() {\n        view! {\n            <DragDropProvider>\n                <Dashboard />\n            </DragDropProvider>\n        }.into_any()\n    } else {\n        view! {\n            <Dashboard />  // ✅ Works without drag\n        }.into_any()\n    }\n}}\n```\n\n---\n\n## Forbidden Patterns\n\n### Provider in rs design\n```rust\n// rs-design/ui/dashboard.rs\npub fn Dashboard() -> impl IntoView {\n    view! {\n        <DragDropProvider>  // 🚫 FORBIDDEN\n            ...\n        </DragDropProvider>\n    }\n}\n```\n\n### Nested Provider In Same Scope\n```rust\n<DragDropProvider>\n  <Tabs>\n    <TabContent>\n      <DragDropProvider>  // 🚫 DUPLICATE\n        ...\n      </DragDropProvider>\n    </TabContent>\n  </Tabs>\n</DragDropProvider>\n```\n\n### Callbacks Provider Outside Runtime\n```rust\n// WRONG ORDER\n<DragDropCallbacksProvider>  // 🚫 No runtime to listen to!\n  <DragDropProvider>\n    ...\n  </DragDropProvider>\n</DragDropCallbacksProvider>\n```\n\n### Hidden Provider In Reusable Component\n```rust\n// Some reusable component\npub fn ReusableWidget() -> impl IntoView {\n    view! {\n        <DragDropProvider>  // 🚫 Hidden coupling\n            <div>...</div>\n        </DragDropProvider>\n    }\n}\n```\n\n---\n\n## SSR And WASM Rules\n\n### SSR Safe Provider\n```rust\n#[component]\npub fn DragDropProvider(children: Children) -> impl IntoView {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        let context = DragContext::new();  // ✅ Only in WASM\n        provide_context(context);\n    }\n    \n    children()\n}\n```\n\n### Unsafe Signal Creation In SSR\n```rust\npub fn DragDropProvider() -> impl IntoView {\n    // 🚫 Creates RwSignal in SSR → spawn_local panic\n    let context = DragContext::new();\n    provide_context(context);\n}\n```\n\n**Rule:** Reactive signals MUST NOT be created during SSR unless guarded.\n\n---\n\n## Real World Example Dashboard Editor\n\n### Before Broken\n```rust\n// App\n<App>\n  <DragDropProvider>\n    <DashboardTab />\n  </DragDropProvider>\n</App>\n\n// rs-design/dashboard.rs\npub fn Dashboard() -> impl IntoView {\n    view! {\n        <DragDropProvider>  // 🚫 DUPLICATE\n            ...\n        </DragDropProvider>\n    }\n}\n```\n\n**Symptom:** Drag events fire, but widgets don't appear. No errors logged.\n\n**Cause:** Two contexts. `DragHandle` emits to Context A, `DropZone` listens on Context B.\n\n### After Fixed\n```rust\n// App (ONE provider at root)\n<App>\n  <DragDropProvider>\n    <DragDropCallbacksProvider>\n      <DashboardTab />\n    </DragDropCallbacksProvider>\n  </DragDropProvider>\n</App>\n\n// rs-design/dashboard.rs (NO provider)\npub fn Dashboard() -> impl IntoView {\n    let ctx = use_drag_context();  // ✅ Uses existing\n    view! { ... }\n}\n\n// frontend-leptos/dashboard_tab.rs\npub fn DashboardTab() -> impl IntoView {\n    let callbacks = use_context::<DragDropCallbacks>()\n        .expect(\"DragDropCallbacks required\");\n    \n    let on_drop = Callback::new(|event| { ... });\n    callbacks.register_drop(on_drop);  // ✅ Registers to shared context\n}\n```\n\n**Result:** Single context. All components share same runtime. Drag & drop works.\n\n---\n\n## Benefits\n\n### Predictable Context Resolution\n- One provider → one context\n- No ambiguity about which context is active\n- Leptos `use_context()` always finds the right one\n\n### Reusable Components\n```rust\n// Dashboard works with OR without drag\n<Dashboard widgets=.../>  // ✅ No drag (just displays)\n\n<DragDropProvider>\n  <Dashboard widgets=.../>  // ✅ With drag (interactive)\n</DragDropProvider>\n```\n\n### Testable\n```rust\n#[test]\nfn test_dashboard_no_drag() {\n    // ✅ Can test without provider\n    let dashboard = Dashboard::new(widgets);\n    assert!(dashboard.renders());\n}\n```\n\n### SSR Safe\n- Providers only create runtime in WASM\n- No `spawn_local` panics\n- Server can render static HTML\n\n---\n\n## Debug Checklist\n\nWhen drag & drop doesn't work:\n\n- [ ] **Check for duplicate providers**\n```bash\n  grep -r \"DragDropProvider\" src/\n  # Should appear ONCE in app.rs\n```\n\n- [ ] **Verify provider order**\n```\n  Runtime BEFORE Callbacks\n  DragDropProvider > DragDropCallbacksProvider\n```\n\n- [ ] **Check SSR guards**\n```rust\n  #[cfg(target_arch = \"wasm32\")]\n```\n\n- [ ] **Verify context consumption**\n```rust\n  // Should NOT panic\n  let ctx = use_drag_context();\n```\n\n- [ ] **Check for hidden providers in components**\n```bash\n  grep -r \"provide_context\" rs-design/\n  # Should be EMPTY\n```\n\n---\n\n## Comparison CanonRS Vs Others\n\n| Framework | Provider Pattern | Duplication Risk |\n|-----------|------------------|------------------|\n| **CanonRS** | Explicit singleton | ✅ Enforced by rule |\n| React Context | Per-component | ⚠️ Easy to duplicate |\n| Vue Provide/Inject | Hierarchical | ⚠️ Ambiguous scopes |\n| Svelte Context | Module-level | ✅ Natural singleton |\n\n**Veredito:** CanonRS is **more explicit** and **less error-prone** than React/Vue.\n\n---\n\n## Related Rules\n\n- **Rule #49:** Drag & Drop as Intent (Event separation)\n- **Rule #8:** Overlay Islands (Client-only architecture)\n- **Rule #XX:** Separation of Concerns (pending)\n\n---\n\n## Testing Requirements\n\n**E2E Tests:**\n- E2E tests MUST mount the same providers as production\n- Test environment MUST replicate provider hierarchy\n- Mock providers MUST maintain same context structure\n\n\n## Normative Requirements\n\n**MUST:**\n- Runtime providers MUST be unique per interaction scope\n- Design system MUST NOT create providers\n- Providers MUST be guarded for SSR when creating signals\n- Provider order MUST respect dependencies (runtime before callbacks)\n\n**MUST NOT:**\n- Duplicate providers in same functional scope\n- Hide providers inside reusable components\n- Create reactive signals in SSR without guards\n\n**SHOULD:**\n- Place providers at app root or route level\n- Document provider dependencies explicitly\n- Test components with and without providers\n\n---"},{"number":51,"slug":"callbacks-as-commands","title":"Callbacks as Commands","status":"ENFORCED","severity":"MEDIUM","category":"behavior","tags":["callbacks","commands","state","cqrs"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Direct state mutations inside UI callbacks prevent undo, replay, and audit capabilities. This breaks command-based architectures and tightly couples UI with side effects.","problem":"callbacks mutate state directly instead of emitting commands","solution":"callbacks must create commands executed via a command history system","signals":["no undo support","side effects in callback","direct mutation","api call in callback"],"search_intent":"how to use commands in ui callbacks","keywords":["callbacks as commands pattern","command pattern ui architecture","leptos callback command execution","cqrs ui callbacks"],"body":"---\n\n## The Principle\n\n**UI callbacks MUST emit commands, not execute mutations directly.**\n\nEvent → Command → Execution\n\nThis enables:\n- Undo/Redo\n- Audit trails\n- Testing\n- Replay\n- Command composition\n\n---\n\n## The Problem\n\n### Wrong Pattern Direct Mutation\n```rust\n// Dashboard callback executes mutation directly\nlet on_widget_drag = Callback::new(move |event: WidgetDragEvent| {\n    // 🚫 WRONG: Direct state mutation\n    set_widgets.update(|ws| {\n        if let Some(w) = ws.iter_mut().find(|w| w.id == event.widget_id) {\n            w.position = event.to_position;\n        }\n    });\n    \n    // 🚫 WRONG: Direct API call\n    api::update_widget_position(event.widget_id, event.to_position).await;\n});\n```\n\n**Why this breaks:**\n- Cannot undo\n- Cannot replay\n- Cannot audit\n- Cannot test without side effects\n- Violates CQRS\n\n---\n\n## The Solution\n\n### Correct Pattern Command Based\n```rust\n// 1. Define command\n#[derive(Clone, Debug)]\nstruct MoveWidgetCommand {\n    widget_id: String,\n    from_position: WidgetPosition,\n    to_position: WidgetPosition,\n}\n\nimpl Command for MoveWidgetCommand {\n    fn execute(&self, state: &mut AppState) {\n        if let Some(w) = state.widgets.iter_mut().find(|w| w.id == self.widget_id) {\n            w.position = self.to_position.clone();\n        }\n    }\n    \n    fn undo(&self, state: &mut AppState) {\n        if let Some(w) = state.widgets.iter_mut().find(|w| w.id == self.widget_id) {\n            w.position = self.from_position.clone();\n        }\n    }\n    \n    fn description(&self) -> String {\n        format!(\"Move widget {} to ({}, {})\", \n            self.widget_id, self.to_position.x, self.to_position.y)\n    }\n}\n\n// 2. Callback creates command\nlet on_widget_drag = Callback::new(move |event: WidgetDragEvent| {\n    let command = MoveWidgetCommand {\n        widget_id: event.widget_id,\n        from_position: event.from_position,\n        to_position: event.to_position,\n    };\n    \n    // 3. Execute via command history\n    command_history.execute(command);\n});\n```\n\n---\n\n## Architecture\n\n### Command Flow\n```\nUI Event (WidgetDragEvent)\n    ↓\nCallback (on_widget_drag)\n    ↓\nCommand (MoveWidgetCommand)\n    ↓\nCommandHistory.execute()\n    ↓\nState Mutation + Persistence\n```\n\n### Benefits\n1. **Undo/Redo**: Free via command history\n2. **Audit Trail**: All commands logged\n3. **Replay**: Re-execute command sequence\n4. **Testing**: Test commands without UI\n5. **Composition**: Combine commands (MacroCommand)\n\n---\n\n## Command Trait\n```rust\npub trait Command: Clone + Debug {\n    /// Execute the command (mutate state)\n    fn execute(&self, state: &mut AppState);\n    \n    /// Undo the command (restore previous state)\n    fn undo(&self, state: &mut AppState);\n    \n    /// Human-readable description\n    fn description(&self) -> String;\n    \n    /// Optional: Can this command be undone?\n    fn is_undoable(&self) -> bool {\n        true\n    }\n}\n```\n\n---\n\n## Command History\n```rust\npub struct CommandHistory {\n    undo_stack: Vec<Box<dyn Command>>,\n    redo_stack: Vec<Box<dyn Command>>,\n}\n\nimpl CommandHistory {\n    pub fn execute(&mut self, command: impl Command + 'static) {\n        command.execute(&mut app_state);\n        self.undo_stack.push(Box::new(command));\n        self.redo_stack.clear(); // Clear redo on new command\n    }\n    \n    pub fn undo(&mut self) -> Option<String> {\n        if let Some(command) = self.undo_stack.pop() {\n            command.undo(&mut app_state);\n            let desc = command.description();\n            self.redo_stack.push(command);\n            Some(desc)\n        } else {\n            None\n        }\n    }\n    \n    pub fn redo(&mut self) -> Option<String> {\n        if let Some(command) = self.redo_stack.pop() {\n            command.execute(&mut app_state);\n            let desc = command.description();\n            self.undo_stack.push(command);\n            Some(desc)\n        } else {\n            None\n        }\n    }\n}\n```\n\n---\n\n## Real World Examples\n\n### Dashboard Widget Movement\n```rust\n// Command\nstruct MoveWidgetCommand {\n    widget_id: String,\n    from: WidgetPosition,\n    to: WidgetPosition,\n}\n\n// Callback\nlet on_drop = Callback::new(move |event: DropEvent| {\n    command_history.execute(MoveWidgetCommand {\n        widget_id: event.item_id.0,\n        from: event.from_position,\n        to: event.to_position,\n    });\n});\n```\n\n### Form Field Edit\n```rust\n// Command\nstruct UpdateFieldCommand {\n    field_id: String,\n    old_value: String,\n    new_value: String,\n}\n\n// Callback\nlet on_change = Callback::new(move |new_value: String| {\n    command_history.execute(UpdateFieldCommand {\n        field_id: field_id.clone(),\n        old_value: current_value.get(),\n        new_value,\n    });\n});\n```\n\n### Bulk Operations (MacroCommand)\n```rust\nstruct MacroCommand {\n    commands: Vec<Box<dyn Command>>,\n    description: String,\n}\n\nimpl Command for MacroCommand {\n    fn execute(&self, state: &mut AppState) {\n        for cmd in &self.commands {\n            cmd.execute(state);\n        }\n    }\n    \n    fn undo(&self, state: &mut AppState) {\n        for cmd in self.commands.iter().rev() {\n            cmd.undo(state);\n        }\n    }\n}\n\n// Usage\nlet delete_widgets = MacroCommand {\n    commands: vec![\n        Box::new(RemoveWidgetCommand { id: \"w1\" }),\n        Box::new(RemoveWidgetCommand { id: \"w2\" }),\n    ],\n    description: \"Delete 2 widgets\".to_string(),\n};\n```\n\n---\n\n## Integration With Canon Rules\n\n### Rule #49: Drag & Drop as Intent\n```rust\n// ✅ Drag emits event (Rule #49)\n<DragHandle on_drop=on_drop>\n\n// ✅ Callback creates command (Rule #51)\nlet on_drop = Callback::new(|event| {\n    command_history.execute(MoveWidgetCommand { ... });\n});\n```\n\n### Rule #50: Provider Singleton\n```rust\n// ✅ CommandHistory provided globally (Rule #50)\n<CommandHistoryProvider>\n    <Dashboard />\n</CommandHistoryProvider>\n\n// ✅ Callbacks use shared history (Rule #51)\nlet history = use_command_history();\nhistory.execute(command);\n```\n\n---\n\n## Forbidden Patterns\n\n### 🚫 Direct Mutation in Callback\n```rust\nCallback::new(|event| {\n    state.update(|s| s.value = event.value); // 🚫\n});\n```\n\n### 🚫 API Call in Callback\n```rust\nCallback::new(async |event| {\n    api::save(event.data).await; // 🚫\n});\n```\n\n### 🚫 Side Effects in Callback\n```rust\nCallback::new(|event| {\n    log::info!(\"Changed\"); // 🚫 (acceptable for debugging)\n    analytics::track(event); // 🚫\n    toast::show(\"Saved\"); // 🚫\n});\n```\n\n**Rule:** Callbacks create commands. Commands execute effects.\n\n---\n\n## Testing\n\n### ✅ Command Testing (Easy)\n```rust\n#[test]\nfn test_move_widget() {\n    let mut state = AppState::new();\n    let cmd = MoveWidgetCommand { ... };\n    \n    cmd.execute(&mut state);\n    assert_eq!(state.widget(\"w1\").position.x, 5);\n    \n    cmd.undo(&mut state);\n    assert_eq!(state.widget(\"w1\").position.x, 0);\n}\n```\n\n### ❌ Callback Testing (Hard)\n```rust\n#[test]\nfn test_callback() {\n    // 🚫 Requires full UI, events, state\n    let callback = create_callback();\n    callback.run(WidgetDragEvent { ... });\n    // How to assert?\n}\n```\n\n---\n\n## Persistence\n\nCommands can be serialized for:\n- **Save/Load**: Persist command history\n- **Network**: Sync commands across clients\n- **Replay**: Reproduce bug scenarios\n```rust\n#[derive(Serialize, Deserialize)]\nstruct MoveWidgetCommand { ... }\n\n// Save\nlet json = serde_json::to_string(&command_history)?;\nstorage::save(\"history.json\", json);\n\n// Load & Replay\nlet commands: Vec<Box<dyn Command>> = storage::load(\"history.json\")?;\nfor cmd in commands {\n    cmd.execute(&mut state);\n}\n```\n\n---\n\n## Normative Requirements\n\n**MUST:**\n- Callbacks MUST create commands, not mutate state\n- Commands MUST implement `execute()` and `undo()`\n- CommandHistory MUST be provided via context (Rule #50)\n\n**MUST NOT:**\n- Callbacks MUST NOT call APIs directly\n- Callbacks MUST NOT mutate shared state\n- Commands MUST NOT have side effects beyond state mutation\n\n**SHOULD:**\n- Commands SHOULD be serializable\n- Command descriptions SHOULD be user-friendly\n- Undo/Redo SHOULD be available via keyboard shortcuts\n\n---\n\n**Author:** Canon Working Group  \n**Related:** Canon Rule #49 (Drag & Drop as Intent), Canon Rule #50 (Provider Singleton)"},{"number":52,"slug":"command-history-runtime","title":"Command History as First-Class Runtime","status":"ENFORCED","severity":"MEDIUM","category":"state-reactivity","tags":["command-history","providers","state","runtime"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Local command histories fragment state management and prevent global undo and replay. A centralized runtime provider is required for consistency across components.","problem":"local command histories break global undo and state consistency","solution":"provide a single command history provider at the application root","signals":["no global undo","duplicate history","state inconsistency"],"search_intent":"how to implement global command history","keywords":["command history provider pattern","global undo redo architecture","leptos command history context","shared state command runtime"],"body":"---\n\n## The Principle\n\n**Command History MUST be a first-class provider in the application runtime.**\n\nLike DragDropProvider (Rule #50) and other runtime primitives, CommandHistory is provided once at the app root and consumed by all interactive components.\n\nThis enables:\n- Global undo/redo (Ctrl+Z / Ctrl+Y)\n- Audit trail across all user actions\n- Time-travel debugging\n- Command replay\n- Consistent command execution\n\n---\n\n## The Problem\n\n### Wrong Pattern Local Command History\n```rust\n// Each component creates its own history\n#[component]\npub fn Dashboard() -> impl IntoView {\n    let command_history = CommandHistory::new(); // 🚫 Local\n    \n    view! {\n        <div on:click=move |_| {\n            command_history.execute(SomeCommand {});\n        }>\n    }\n}\n```\n\n**Why this breaks:**\n- No global undo/redo\n- Cannot undo across components\n- Duplicate histories waste memory\n- No centralized audit trail\n\n---\n\n## The Solution\n\n### Correct Pattern Global Provider\n```rust\n// App layer (ONE provider at root)\n#[component]\npub fn App() -> impl IntoView {\n    view! {\n        <Router>\n            <DragDropProvider>\n                <DragDropCallbacksProvider>\n                    <CommandHistoryProvider>  // ✅ Global\n                        <Dashboard />\n                        <FormBuilder />\n                        <DataGrid />\n                    </CommandHistoryProvider>\n                </DragDropCallbacksProvider>\n            </DragDropProvider>\n        </Router>\n    }\n}\n\n// Component layer (consume context)\n#[component]\npub fn Dashboard() -> impl IntoView {\n    let history = use_command_history(); // ✅ Consume\n    \n    let on_drop = Callback::new(move |event| {\n        history.execute(MoveWidgetCommand { ... });\n    });\n}\n```\n\n---\n\n## Provider Implementation\n\n### CommandHistory\n```rust\n#[derive(Clone, Copy)]\npub struct CommandHistory {\n    undo_stack: RwSignal<Vec<Arc<dyn Command>>>,\n    redo_stack: RwSignal<Vec<Arc<dyn Command>>>,\n}\n\nimpl CommandHistory {\n    pub fn execute(&self, command: impl Command + 'static);\n    pub fn undo(&self) -> Option<String>;\n    pub fn redo(&self) -> Option<String>;\n    pub fn can_undo(&self) -> Signal<bool>;\n    pub fn can_redo(&self) -> Signal<bool>;\n    pub fn undo_history(&self) -> Signal<Vec<String>>;\n}\n```\n\n### CommandHistoryProvider\n```rust\n#[component]\npub fn CommandHistoryProvider(children: Children) -> impl IntoView {\n    let history = CommandHistory::new();\n    provide_context(history);\n    \n    // ✅ Global keyboard shortcuts\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        // Ctrl+Z → undo\n        // Ctrl+Y → redo\n        // Ctrl+Shift+Z → redo\n    }\n    \n    children()\n}\n```\n\n---\n\n## Keyboard Shortcuts\n\nCommandHistoryProvider automatically binds:\n\n| Shortcut | Action | Platform |\n|----------|--------|----------|\n| Ctrl+Z   | Undo   | Windows/Linux |\n| Cmd+Z    | Undo   | macOS |\n| Ctrl+Y   | Redo   | Windows/Linux |\n| Cmd+Y    | Redo   | macOS |\n| Ctrl+Shift+Z | Redo | Windows/Linux |\n| Cmd+Shift+Z | Redo | macOS |\n\n**Implementation:**\n```rust\nlet closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {\n    if event.ctrl_key() || event.meta_key() {\n        match event.key().as_str() {\n            \"z\" if !event.shift_key() => {\n                event.prevent_default();\n                history.undo();\n            }\n            \"y\" | \"Z\" => {\n                event.prevent_default();\n                history.redo();\n            }\n            _ => {}\n        }\n    }\n}));\n```\n\n---\n\n## Provider Order Canon Rule 50\n```\nRouter\n└── DragDropProvider (runtime)\n    └── DragDropCallbacksProvider (interprets drag events)\n        └── CommandHistoryProvider (executes commands)\n            └── LanguageProvider\n                └── ThemeProvider\n                    └── Components\n```\n\n**Order matters:**\n1. DragDropProvider creates reactive state\n2. DragDropCallbacksProvider interprets events → commands\n3. CommandHistoryProvider executes commands with undo/redo\n\n---\n\n## Real World Examples\n\n### Dashboard Widget Movement\n```rust\n// dashboard_tab.rs\nlet history = use_command_history();\n\nlet on_drop = Callback::new(move |event: DropEvent| {\n    history.execute(MoveWidgetCommand {\n        widgets: placed_widgets,\n        widget_id: event.item_id.0,\n        from_position: /* ... */,\n        to_position: /* ... */,\n    });\n});\n```\n\n### Form Field Edit\n```rust\nlet history = use_command_history();\n\nlet on_change = Callback::new(move |new_value: String| {\n    history.execute(UpdateFieldCommand {\n        field_signal: field_value,\n        old_value: field_value.get(),\n        new_value,\n    });\n});\n```\n\n### Bulk Delete\n```rust\nlet history = use_command_history();\n\nlet delete_selected = move || {\n    let commands: Vec<_> = selected_items.get()\n        .into_iter()\n        .map(|id| RemoveItemCommand { id })\n        .collect();\n    \n    history.execute(MacroCommand {\n        commands,\n        description: format!(\"Delete {} items\", commands.len()),\n    });\n};\n```\n\n---\n\n## Audit Trail UI\n```rust\n#[component]\npub fn AuditTrail() -> impl IntoView {\n    let history = use_command_history();\n    \n    view! {\n        <div class=\"audit-trail\">\n            <h3>\"Command History\"</h3>\n            <For\n                each=move || history.undo_history().get()\n                key=|cmd| cmd.clone()\n                children=move |cmd| view! {\n                    <div class=\"audit-entry\">{cmd}</div>\n                }\n            />\n        </div>\n    }\n}\n```\n\n---\n\n## Testing\n\n### ✅ Command Execution\n```rust\n#[test]\nfn test_command_history() {\n    let history = CommandHistory::new();\n    let state = RwSignal::new(0);\n    \n    history.execute(IncrementCommand { state });\n    assert_eq!(state.get(), 1);\n    \n    history.undo();\n    assert_eq!(state.get(), 0);\n    \n    history.redo();\n    assert_eq!(state.get(), 1);\n}\n```\n\n### ✅ Keyboard Shortcuts\n```rust\n#[wasm_bindgen_test]\nfn test_ctrl_z_undo() {\n    // Mount CommandHistoryProvider\n    // Simulate Ctrl+Z keydown\n    // Assert undo was called\n}\n```\n\n---\n\n## Benefits\n\n### ✅ Global Undo/Redo\n- Works across all components\n- Single keyboard shortcut system\n- Consistent behavior\n\n### ✅ Audit Trail\n- All commands logged\n- Export command history\n- Replay bug scenarios\n\n### ✅ Memory Efficient\n- One history for entire app\n- Shared command storage\n- Automatic cleanup\n\n### ✅ Testable\n- Test commands independently\n- Mock command history\n- Replay sequences\n\n---\n\n## Forbidden Patterns\n\n### 🚫 Local Command History\n```rust\n#[component]\npub fn MyComponent() -> impl IntoView {\n    let history = CommandHistory::new(); // 🚫\n}\n```\n\n### 🚫 Multiple Providers\n```rust\n<CommandHistoryProvider>\n    <Dashboard>\n        <CommandHistoryProvider> // 🚫 Duplicate\n            ...\n        </CommandHistoryProvider>\n    </Dashboard>\n</CommandHistoryProvider>\n```\n\n### 🚫 Provider in Design System\n```rust\n// rs-design/ui/dashboard.rs\npub fn Dashboard() -> impl IntoView {\n    view! {\n        <CommandHistoryProvider> // 🚫 FORBIDDEN\n            ...\n        </CommandHistoryProvider>\n    }\n}\n```\n\n---\n\n## Integration With Canon Rules\n\n### Rule #49: Drag & Drop as Intent\nEvent → Callback → Command\n```rust\n<DragHandle on_drop=on_drop /> // Rule #49\n    ↓\nCallback creates command // Rule #51\n    ↓\nhistory.execute(command) // Rule #52\n```\n\n### Rule #50: Provider Singleton\nONE CommandHistoryProvider per app\n```rust\n<CommandHistoryProvider> // ✅ Once at root\n    <Dashboard />\n    <FormBuilder />\n</CommandHistoryProvider>\n```\n\n### Rule #51: Callbacks as Commands\nCallbacks emit commands executed via history\n```rust\nlet on_event = Callback::new(|e| {\n    history.execute(SomeCommand { ... }); // Rule #51 + #52\n});\n```\n\n---\n\n## Normative Requirements\n\n**MUST:**\n- CommandHistoryProvider MUST be provided once at app root\n- Components MUST use `use_command_history()` to access\n- Keyboard shortcuts MUST be global (Ctrl+Z / Ctrl+Y)\n\n**MUST NOT:**\n- Components MUST NOT create local CommandHistory\n- Design system MUST NOT provide CommandHistoryProvider\n- Multiple CommandHistoryProviders MUST NOT exist in same scope\n\n**SHOULD:**\n- Undo/redo SHOULD show toast notifications\n- Command history SHOULD be exportable\n- Audit trail UI SHOULD be available in dev mode\n\n---\n\n**Author:** Canon Working Group  \n**Related:** Rule #50 (Provider Singleton), Rule #51 (Callbacks as Commands)"},{"number":53,"slug":"client-only-runtime-islands","title":"Client-Only Runtime Islands","status":"ENFORCED","severity":"HIGH","category":"core-runtime","tags":["hydration","csr","wasm","ssr"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Accessing browser APIs during SSR causes panics and hydration mismatches in Leptos. Code that depends on window, document, or wasm-specific features must be isolated into client-only runtime islands.","problem":"browser dependent code executes during ssr causing panic and dom mismatch","solution":"guard client code with cfg wasm32 and provide ssr safe fallback rendering","signals":["unwrap window panic","hydration mismatch","web_sys used in ssr"],"search_intent":"how to prevent leptos ssr panic","keywords":["leptos ssr panic window","wasm32 cfg leptos","hydration mismatch leptos","client only islands rust"],"body":"---\n\n## The Principle\n\n**Runtime code that depends on browser APIs MUST be guarded with `#[cfg(target_arch = \"wasm32\")]`.**\n\nSSR (Server-Side Rendering) runs in a Rust server environment without:\n- DOM APIs (`window`, `document`, `HTMLElement`)\n- Browser events (`MouseEvent`, `KeyboardEvent`)\n- Web APIs (`localStorage`, `fetch`, `WebSocket`)\n- WASM-specific features\n\nClient-only code creates \"islands\" - sections that only exist in the browser.\n\n---\n\n## The Problem\n\n### ❌ Wrong Pattern (Unguarded Client Code)\n```rust\n#[component]\npub fn MyComponent() -> impl IntoView {\n    // 🚫 PANIC in SSR!\n    let window = web_sys::window().unwrap();\n    let document = window.document().unwrap();\n    \n    view! {\n        <div on:click=move |_| {\n            // 🚫 PANIC in SSR!\n            window.alert_with_message(\"Clicked\");\n        }>\n    }\n}\n```\n\n**Why this breaks:**\n- SSR has no `window` or `document`\n- `.unwrap()` panics on server\n- Event handlers execute during SSR (not just client)\n- Hydration mismatches\n\n---\n\n## The Solution\n\n### ✅ Correct Pattern (Guarded Islands)\n```rust\n#[component]\npub fn MyComponent() -> impl IntoView {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        use web_sys::window;\n        \n        let on_click = move |_| {\n            if let Some(w) = window() {\n                let _ = w.alert_with_message(\"Clicked\");\n            }\n        };\n        \n        view! {\n            <div on:click=on_click>\"Click me\"</div>\n        }\n    }\n    \n    #[cfg(not(target_arch = \"wasm32\"))]\n    {\n        view! {\n            <div>\"Click me\"</div>\n        }\n    }\n}\n```\n\n---\n\n## Client Only Patterns\n\n### 1. Event Listeners (ALWAYS Client-Only)\n```rust\n#[component]\npub fn CommandHistoryProvider(children: Children) -> impl IntoView {\n    let history = CommandHistory::new();\n    provide_context(history);\n    \n    // ✅ Keyboard shortcuts - client only\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        use wasm_bindgen::prelude::*;\n        \n        let closure = Closure::wrap(Box::new(move |e: web_sys::KeyboardEvent| {\n            if e.ctrl_key() && e.key() == \"z\" {\n                history.undo();\n            }\n        }) as Box<dyn FnMut(_)>);\n        \n        if let Some(window) = web_sys::window() {\n            let _ = window.add_event_listener_with_callback(\n                \"keydown\",\n                closure.as_ref().unchecked_ref()\n            );\n        }\n        \n        closure.forget();\n    }\n    \n    children()\n}\n```\n\n### 2. DOM Manipulation (ALWAYS Client-Only)\n```rust\n#[component]\npub fn AutoFocus() -> impl IntoView {\n    let input_ref = NodeRef::<html::Input>::new();\n    \n    #[cfg(target_arch = \"wasm32\")]\n    {\n        use leptos::prelude::*;\n        \n        Effect::new(move |_| {\n            if let Some(input) = input_ref.get() {\n                let _ = input.focus();\n            }\n        });\n    }\n    \n    view! {\n        <input node_ref=input_ref />\n    }\n}\n```\n\n### 3. Browser Storage (ALWAYS Client-Only)\n```rust\n#[component]\npub fn PersistentState() -> impl IntoView {\n    let (value, set_value) = signal(String::new());\n    \n    #[cfg(target_arch = \"wasm32\")]\n    {\n        // Load from localStorage\n        Effect::new(move |_| {\n            if let Some(window) = web_sys::window() {\n                if let Ok(Some(storage)) = window.local_storage() {\n                    if let Ok(Some(saved)) = storage.get_item(\"my_key\") {\n                        set_value.set(saved);\n                    }\n                }\n            }\n        });\n        \n        // Save to localStorage\n        Effect::new(move |_| {\n            let v = value.get();\n            if let Some(window) = web_sys::window() {\n                if let Ok(Some(storage)) = window.local_storage() {\n                    let _ = storage.set_item(\"my_key\", &v);\n                }\n            }\n        });\n    }\n    \n    view! {\n        <input\n            prop:value=value\n            on:input=move |e| set_value.set(event_target_value(&e))\n        />\n    }\n}\n```\n\n### 4. Timers & Intervals (ALWAYS Client-Only)\n```rust\n#[component]\npub fn AutoRefresh() -> impl IntoView {\n    let (count, set_count) = signal(0);\n    \n    #[cfg(target_arch = \"wasm32\")]\n    {\n        use wasm_bindgen::prelude::*;\n        \n        let closure = Closure::wrap(Box::new(move || {\n            set_count.update(|c| *c += 1);\n        }) as Box<dyn Fn()>);\n        \n        if let Some(window) = web_sys::window() {\n            let _ = window.set_interval_with_callback_and_timeout_and_arguments_0(\n                closure.as_ref().unchecked_ref(),\n                1000\n            );\n        }\n        \n        closure.forget();\n    }\n    \n    view! {\n        <div>\"Count: \" {count}</div>\n    }\n}\n```\n\n---\n\n## SSR Safe Patterns\n\n### ✅ Reactive Signals (Safe in SSR)\n```rust\n// ✅ Signals work in both SSR and client\nlet (count, set_count) = signal(0);\n```\n\n### ✅ Context Providers (Safe in SSR)\n```rust\n// ✅ Contexts work in both SSR and client\nprovide_context(MyContext::new());\n```\n\n### ✅ View Rendering (Safe in SSR)\n```rust\n// ✅ View macros work in both SSR and client\nview! {\n    <div>\"Hello\"</div>\n}\n```\n\n### ✅ Props & Children (Safe in SSR)\n```rust\n#[component]\npub fn MyComponent(\n    value: String,\n    children: Children,\n) -> impl IntoView {\n    // ✅ Props and children work in both\n    view! {\n        <div>{value} {children()}</div>\n    }\n}\n```\n\n---\n\n## Hydration Safety\n\n### ❌ Wrong (Hydration Mismatch)\n```rust\n#[component]\npub fn DateTime() -> impl IntoView {\n    // 🚫 Different on server vs client!\n    let now = chrono::Local::now();\n    \n    view! {\n        <div>{now.to_string()}</div>\n    }\n}\n```\n\n### ✅ Correct (Client-Only Render)\n```rust\n#[component]\npub fn DateTime() -> impl IntoView {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        let (now, set_now) = signal(chrono::Local::now().to_string());\n        \n        Effect::new(move |_| {\n            set_now.set(chrono::Local::now().to_string());\n        });\n        \n        view! {\n            <div>{now}</div>\n        }\n    }\n    \n    #[cfg(not(target_arch = \"wasm32\"))]\n    {\n        view! {\n            <div>\"Loading...\"</div>\n        }\n    }\n}\n```\n\n---\n\n## Common Client Only APIs\n\n### Browser APIs (ALWAYS Guard)\n- `window()` / `document()`\n- `localStorage` / `sessionStorage`\n- `navigator` / `location`\n- `history.pushState()`\n- `requestAnimationFrame()`\n- Canvas / WebGL\n- WebSocket / WebRTC\n- Geolocation / Camera\n\n### Event Listeners (ALWAYS Guard)\n- `addEventListener()`\n- `removeEventListener()`\n- Global event handlers (keydown, resize, scroll)\n\n### DOM Manipulation (ALWAYS Guard)\n- `.focus()` / `.blur()`\n- `.scrollIntoView()`\n- `.querySelector()`\n- Element properties (`.value`, `.checked`)\n\n### Wasm-Only (ALWAYS Guard)\n- `wasm_bindgen` closures\n- `js_sys` / `web_sys` APIs\n- `Closure::wrap()`\n- `.forget()` / `.into_js_value()`\n\n---\n\n## Testing Strategy\n\n### SSR Tests (No Guards Needed)\n```rust\n#[test]\nfn test_ssr_render() {\n    let html = leptos::ssr::render_to_string(|| view! {\n        <MyComponent />\n    });\n    \n    assert!(html.contains(\"expected content\"));\n}\n```\n\n### Client Tests (WASM Required)\n```rust\n#[wasm_bindgen_test]\nfn test_client_interaction() {\n    // Mount component\n    // Simulate click\n    // Assert state change\n}\n```\n\n---\n\n## Provider Guard Pattern\n\nProviders that use client APIs should guard only the client parts:\n```rust\n#[component]\npub fn DragDropProvider(children: Children) -> impl IntoView {\n    // ✅ Context creation works in SSR\n    let context = DragContext::new();\n    provide_context(context);\n    \n    // ✅ Event setup is client-only\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        setup_drag_listeners(&context);\n    }\n    \n    children()\n}\n```\n\n---\n\n## Real World Examples\n\n### CommandHistoryProvider\n```rust\n#[component]\npub fn CommandHistoryProvider(children: Children) -> impl IntoView {\n    let history = CommandHistory::new();\n    provide_context(history);\n    \n    // ✅ Keyboard shortcuts - client only\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        setup_keyboard_shortcuts(history);\n    }\n    \n    children()\n}\n```\n\n### DragHandle\n```rust\n#[component]\npub fn DragHandle(children: Children) -> impl IntoView {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        let context = use_drag_context();\n        \n        let on_drag_start = move |ev: web_sys::DragEvent| {\n            context.start_drag(item_id.clone(), None);\n        };\n        \n        view! {\n            <div draggable=\"true\" on:dragstart=on_drag_start>\n                {children()}\n            </div>\n        }\n    }\n    \n    #[cfg(not(target_arch = \"wasm32\"))]\n    {\n        view! {\n            <div>{children()}</div>\n        }\n    }\n}\n```\n\n---\n\n## Decision Tree\n```\nDoes code use browser API?\n├─ YES → #[cfg(target_arch = \"wasm32\")]\n└─ NO → Safe in SSR\n\nDoes code create event listener?\n├─ YES → #[cfg(target_arch = \"wasm32\")]\n└─ NO → Safe in SSR\n\nDoes code manipulate DOM?\n├─ YES → #[cfg(target_arch = \"wasm32\")]\n└─ NO → Safe in SSR\n\nDoes code use wasm_bindgen?\n├─ YES → #[cfg(target_arch = \"wasm32\")]\n└─ NO → Safe in SSR\n```\n\n---\n\n## Normative Requirements\n\n**MUST:**\n- Client APIs MUST be guarded with `#[cfg(target_arch = \"wasm32\")]`\n- Event listeners MUST be client-only\n- DOM manipulation MUST be client-only\n- SSR and client MUST render identical initial HTML\n\n**MUST NOT:**\n- MUST NOT use `.unwrap()` on `window()` or `document()`\n- MUST NOT create hydration mismatches\n- MUST NOT call browser APIs in SSR\n- MUST NOT use `wasm_bindgen` without guards\n\n**SHOULD:**\n- Use `if let Some(window) = window()` pattern\n- Provide fallback SSR rendering where appropriate\n- Test both SSR and client paths\n- Document which components are client-only\n\n---\n\n## Anti Patterns\n\n### 🚫 Unguarded DOM Access\n```rust\nlet window = window().unwrap(); // PANIC in SSR!\n```\n\n### 🚫 Hydration Mismatch\n```rust\nlet random = rand::random::<u32>(); // Different on server vs client\nview! { <div>{random}</div> }\n```\n\n### 🚫 Client State in SSR\n```rust\nlocalStorage.get_item(\"key\") // Doesn't exist in SSR\n```\n\n---\n\n**Author:** Canon Working Group  \n**Related:** Leptos SSR, Hydration, WASM"},{"number":54,"slug":"render-must-be-total","title":"canon-rule-54-render-must-be-total","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["render","ssr","hydration","safety"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Render closures in Leptos are executed during SSR and must never panic. Using unwrap or expect inside reactive attributes introduces runtime failures before user interaction.","problem":"render closures assume runtime state and panic during ssr","solution":"make render total by handling all states and move strict assumptions to user actions","signals":["expect in disabled","unwrap in class","panic during ssr"],"search_intent":"why leptos render panics when using","keywords":["leptos render panic","ssr safe closures","reactive attribute unwrap","total rendering pattern"],"body":"## Golden Rule\n\n**Rendering must always be total and non-panicking.  \nActions may be partial and may panic if required runtime is missing.**\n\nAny closure executed during render (`to_html`, SSR, hydration)\nMUST NOT assume the existence of runtime-only providers.\n\n---\n\n## The Problem\n\nIn Leptos/Tachys, **attribute closures are evaluated during rendering**, not interaction.\n\nThis includes closures used in:\n\n- `disabled=`\n- `class=`\n- `style=`\n- `hidden=`\n- any reactive attribute or signal-based expression\n\nIf these closures call:\n\n- `expect`\n- `unwrap`\n- `panic!`\n- runtime-only assumptions\n\n➡️ **The application will panic during SSR or render.**\n\n---\n\n## Forbidden Pattern Render Panic\n\n```rust\nlet history = use_command_history();\n\ndisabled=move || {\n    !history\n        .expect(\"CommandHistory required\")\n        .can_undo()\n        .get()\n}\n```\n\n### Why this is illegal\n\n- Executed during render\n- Render must be total\n- Render must not fail\n- Runtime contexts may not exist yet\n\nThis causes **hard panics during SSR or initial render**.\n\n---\n\n## Correct Pattern Total Render\n\n```rust\nlet history = use_command_history();\n\ndisabled=move || {\n    match history {\n        Some(h) => !h.can_undo().get(),\n        None => true, // degrade safely\n    }\n}\n```\n\n### Why this is correct\n\n- Render handles all possible states\n- No assumptions\n- SSR-safe\n- Hydration-safe\n\n---\n\n## Where Expect Is Allowed\n\n`expect()` is allowed **only inside user-triggered actions**:\n\n- `on:click`\n- `on:submit`\n- keyboard handlers\n- command execution callbacks\n\n```rust\non:click=move |_| {\n    history\n        .expect(\"CommandHistory required\")\n        .undo();\n}\n```\n\n### Reason\n\n- Actions run only after interaction\n- Runtime is guaranteed\n- Explicit failure is acceptable\n\n---\n\n## Canon Principle\n\n> **Render must be total.  \n> Actions may be partial.**\n\n---\n\n## Summary Checklist\n\n- [ ] No `expect()` inside render closures\n- [ ] No `unwrap()` inside reactive attributes\n- [ ] Render degrades safely when context is missing\n- [ ] Actions enforce required runtime explicitly\n- [ ] SSR never panics due to missing providers\n\n---\n\n## Related Rules\n\n- Canon Rule #50 — Provider Singleton Pattern\n- Canon Rule #37 — Provider Taxonomy Boundaries\n- Canon Rule #52 — Command History Runtime"},{"number":55,"slug":"canonical-css-entry-points","title":"Canonical CSS Entry Points","status":"ENFORCED","severity":"HIGH","category":"styling-css","tags":["css","monorepo","design-system","build"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Importing CSS from internal folders creates fragile dependencies and breaks monorepo scalability. Design system styles must be consumed only through canonical workspace packages.","problem":"relative css imports tie consumers to internal repo structure","solution":"use canonical packages that re export css artifacts via workspace resolution","signals":["../../packages-rust import","css breaks after refactor","postcss resolve error"],"search_intent":"how to structure css imports in","keywords":["monorepo css architecture","canonical css packages","postcss resolve imports","design system distribution css"],"body":"---\n\n## Principle\n\nDesign System CSS **MUST** be consumed exclusively through **canonical workspace packages**.  \nInternal directories such as `packages-rust/`, `crates/`, or generated Rust artifacts are **implementation details**, never valid import targets.\n\nThis rule exists to **eliminate fragile relative paths**, ensure **monorepo scalability**, and guarantee **package portability** across environments.\n\n---\n\n## Forbidden Patterns\n\n```css\n/* ❌ NEVER ALLOWED */\n@import \"../../../../packages-rust/rs-tailwind/tokens/theme.css\";\n@import \"../../../crates/rs-design/src/themes/generated.css\";\n```\n\n### Why this is forbidden\n\n- PostCSS uses `enhanced-resolve`, which **breaks on deep relative paths**\n- Relative paths encode repository topology into consumer code\n- Any folder move breaks every consumer silently\n- Makes publishing to npm impossible without refactor\n- Violates separation between **artifact producer** and **artifact consumer**\n\n---\n\n## Canonical Architecture\n\n### Required Workspace Layout\n\n```\npackages/\n├─ canonrs-tailwind/\n│  ├─ package.json\n│  └─ dist/\n│     ├─ tokens.css\n│     └─ globals.css\n│\n├─ canonrs-design/\n│  ├─ package.json\n│  └─ dist/\n│     └─ themes.css\n```\n\n**Key rule:**  \n> Canonical packages **re-export artifacts**.  \n> They do **not** generate them.\n\n---\n\n## Canonical Package Definitions\n\n### `@canonrs/tailwind`\n\n```json\n{\n  \"name\": \"@canonrs/tailwind\",\n  \"private\": true,\n  \"version\": \"0.1.0\",\n  \"exports\": {\n    \"./tokens.css\": \"./dist/tokens.css\",\n    \"./globals.css\": \"./dist/globals.css\"\n  }\n}\n```\n\n### `@canonrs/design`\n\n```json\n{\n  \"name\": \"@canonrs/design\",\n  \"private\": true,\n  \"version\": \"0.1.0\",\n  \"exports\": {\n    \"./themes.css\": \"./dist/themes.css\"\n  }\n}\n```\n\n---\n\n## Mandatory Consumer Pattern\n\n```css\n/* ✅ CANONICAL */\n@import \"@canonrs/tailwind/tokens.css\";\n@import \"@canonrs/tailwind/globals.css\";\n@import \"@canonrs/design/themes.css\";\n```\n\nNo other import source is allowed.\n\n---\n\n## Enforcement Checklist\n\n- [ ] No CSS imports reference `packages-rust/`\n- [ ] No CSS imports reference `crates/`\n- [ ] All CSS imports use `@canonrs/*`\n- [ ] Canonical packages export only `dist/*`\n- [ ] No relative paths longer than `../`\n\n---\n\n## Canonical Justification\n\nThis rule exists because **Design Systems are products**, not side-effects of Rust crates.\n\nA system that cannot be:\n- moved,\n- published,\n- versioned,\n- cached,\n- or reasoned about independently  \n\nis **not enterprise-grade**.\n\n---\n\n## Canon References\n\n- Canon Rule #56 — Monorepo CSS Build Pipeline\n- Canon Rule #57 — PostCSS Canon Configuration"},{"number":56,"slug":"monorepo-css-build-pipeline","title":"Monorepo CSS Build Pipeline","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["css","pipeline","monorepo","artifacts"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Directly importing CSS from generators or crates breaks build consistency and portability. CSS must be generated and copied into canonical packages as immutable artifacts.","problem":"css is consumed directly from generators instead of build artifacts","solution":"enforce build pipeline that copies generated css into canonical dist packages","signals":["css from crates","missing dist css","inconsistent builds"],"search_intent":"how to build css pipeline in","keywords":["css build pipeline monorepo","design tokens build artifacts","copy vs link css","canonical dist css"],"body":"---\n\n## Principle\n\nCSS in a monorepo **MUST** be produced as **build artifacts**, then **copied** into canonical workspace packages.\n\nCSS **MUST NEVER** be imported directly from generators, Rust crates, or internal folders.\n\n---\n\n## Canonical Pipeline\n\n```\nRust / JSON Tokens\n        ↓\nCSS Generation (Rust / Scripts)\n        ↓\nbuild-canon-css.sh\n        ↓\npackages/*/dist/*.css\n        ↓\npnpm workspace resolution\n        ↓\nApplication CSS bundling\n```\n\n---\n\n## Required Script\n\n### `scripts/build-canon-css.sh`\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\necho \"🔨 Building Canon CSS artifacts...\"\n\nmkdir -p packages/canonrs-tailwind/dist\nmkdir -p packages/canonrs-design/dist\n\ncp packages-rust/rs-tailwind/tokens/theme.css \\\n   packages/canonrs-tailwind/dist/tokens.css\n\ncp packages-rust/rs-tailwind/globals.css \\\n   packages/canonrs-tailwind/dist/globals.css\n\ncp packages-rust/rs-design/themes/generated.css \\\n   packages/canonrs-design/dist/themes.css\n\necho \"✅ Canon CSS artifacts ready\"\n```\n\n---\n\n## Critical Constraints\n\n- This script **copies**, never links\n- This script **fails fast**\n- This script is **idempotent**\n- This script runs **before pnpm install**\n\n---\n\n## CI Requirement\n\nCI **MUST** fail if:\n- any dist file is missing\n- any dist file is empty\n- script does not run before CSS bundling\n\n---\n\n## Validation Checklist\n\n- [ ] `packages/*/dist/*.css` exist\n- [ ] No CSS imports internal paths\n- [ ] build-canon-css.sh runs in CI\n- [ ] pnpm install resolves @canonrs/*"},{"number":57,"slug":"postcss-canon-config","title":"PostCSS Canon Configuration","status":"ENFORCED","severity":"MEDIUM","category":"build-tooling","tags":["postcss","css","tailwind","config"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Incorrect PostCSS configuration breaks module resolution and CSS ordering in monorepos. Canon setup requires ESM config, proper plugin order, and strict import sequencing.","problem":"postcss misconfiguration causes unresolved imports and broken css order","solution":"use esm config with postcss import first and enforce canonical ordering","signals":["tailwind import error","css order broken","module resolution fail"],"search_intent":"how to configure postcss esm with","keywords":["postcss esm config","tailwind postcss plugin order","css import resolution node","postcss canonical setup"],"body":"---\n\n## Principle\n\nPostCSS in Canon projects **MUST**:\n\n1. Resolve workspace packages\n2. Be ESM-native\n3. Respect strict CSS import ordering\n4. Treat Tailwind as a PostCSS plugin (not CSS)\n\n---\n\n## Canonical Configuration\n\n### `postcss.config.js` (ESM REQUIRED)\n\n```js\nimport { createRequire } from \"node:module\";\nimport postcssImport from \"postcss-import\";\nimport tailwindcss from \"@tailwindcss/postcss\";\nimport autoprefixer from \"autoprefixer\";\n\nconst require = createRequire(import.meta.url);\n\nexport default {\n  plugins: [\n    postcssImport({\n      resolve(id) {\n        return require.resolve(id);\n      }\n    }),\n    tailwindcss,\n    autoprefixer\n  ]\n};\n```\n\n---\n\n## Mandatory CSS Ordering\n\n```css\n/* 1. IMPORTS FIRST */\n@import \"@canonrs/tailwind/tokens.css\";\n@import \"@canonrs/design/themes.css\";\n\n/* 2. CONFIG */\n@config \"../tailwind.config.js\";\n\n/* 3. TAILWIND */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* 4. LAYERS */\n@layer utilities { }\n```\n\n---\n\n## Absolute Prohibitions\n\n```css\n/* ❌ NEVER */\n@import \"tailwindcss\";\n@tailwind base;\n@import \"@canonrs/tailwind/tokens.css\";\n```\n\nThis will **always** break.\n\n---\n\n## Enforcement Checklist\n\n- [ ] `type: module` in package.json\n- [ ] `createRequire` used\n- [ ] postcss-import FIRST\n- [ ] No JS imported as CSS"},{"number":58,"slug":"leptos-assets-dev-constraint","title":"Leptos Assets Dev Constraint","status":"ENFORCED","severity":"MEDIUM","category":"build-tooling","tags":["leptos","assets","dev-server"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Leptos dev server silently fails to serve nested asset paths, returning placeholder responses. Only first-level files in the assets directory are valid during development.","problem":"nested asset paths are not served causing missing css and silent failures","solution":"copy compiled assets to root of assets directory and reference directly","signals":["1 byte css file","missing styles in dev","assets not loading"],"search_intent":"why leptos dev server does not","keywords":["leptos assets dir limitation","css not loading leptos dev","leptos public folder rules","tailwind leptos dev fix"],"body":"---\n\n## Principle\n\nLeptos dev server **ONLY serves first-level files** from `assets-dir`.\n\nAny subdirectory is silently replaced with a **1-byte placeholder response**.\n\n---\n\n## Canonical Behavior\n\n```toml\n[package.metadata.leptos]\nassets-dir = \"public\"\n```\n\nLeptos dev server serves:\n```\npublic/*.css      ✅\npublic/pkg/*.css  ❌ (1 byte placeholder)\n```\n\n---\n\n## Required Solution\n\n```bash\npostcss input.css -o target/site/pkg/app.css\ncp target/site/pkg/app.css public/app.css\n```\n\nAnd in HTML / Rust:\n\n```rust\n<Stylesheet href=\"/app.css\"/>\n```\n\n---\n\n## Forbidden Patterns\n\n- ❌ Symlinks\n- ❌ Runtime copying\n- ❌ Watching target/site/pkg\n- ❌ Debugging Tailwind / PostCSS\n\nThis is **Leptos behavior**, not a tooling bug.\n\n---\n\n## Validation Checklist\n\n- [ ] CSS file exists in `public/*.css`\n- [ ] No CSS referenced from subfolders\n- [ ] Dev server restarted after adding asset"},{"number":59,"slug":"css-cascade-ownership","title":"CSS Cascade Ownership","status":"ENFORCED","severity":"HIGH","category":"styling-css","tags":["css","cascade","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Multiple CSS bundles without defined precedence create unpredictable styling behavior. A single stylesheet must own the final cascade to ensure deterministic rendering.","problem":"multiple css bundles create undefined cascade precedence","solution":"enforce single stylesheet entrypoint with defined cascade ownership","signals":["style conflicts","random overrides","multiple css files"],"search_intent":"how to manage css cascade ownership","keywords":["css cascade control","single stylesheet architecture","css layering strategy","design system precedence"],"body":"---\n\n## Principle\n\nThere must be **exactly ONE owner** of the final CSS cascade.\n\nMixing multiple style bundles without defined precedence is forbidden.\n\n---\n\n## Canonical Ownership\n\n```\nDesign Tokens  → Lowest\nDesign Themes  → Middle\nApplication    → Highest\n```\n\nDesign system CSS **MUST NOT override application intent**.\n\n---\n\n## Forbidden Pattern\n\n```html\n<link rel=\"stylesheet\" href=\"/canon.css\">\n<link rel=\"stylesheet\" href=\"/app.css\">\n```\n\nOrder ambiguity = undefined behavior.\n\n---\n\n## Required Pattern\n\n```rust\n<Stylesheet id=\"app\" href=\"/app.css\"/>\n```\n\nSingle entrypoint.\n\n---\n\n## Enforcement Checklist\n\n- [ ] One stylesheet in HTML\n- [ ] No parallel CSS bundles\n- [ ] App owns last cascade layer"},{"number":60,"slug":"css-artifacts-are-immutable","title":"CSS Artifacts Are Immutable","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["css","artifacts","build"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Editing generated CSS artifacts introduces drift and breaks reproducibility. All changes must originate from the generator and propagate via rebuild.","problem":"manual edits in generated css create divergence from source","solution":"treat css artifacts as immutable and regenerate on every change","signals":["manual css patch","dist css edited","inconsistent builds"],"search_intent":"why generated css should not be edited manually","keywords":["immutable build artifacts css","design system build consistency","css generation pipeline","no manual patch dist css"],"body":"---\n\n## Principle\n\nGenerated CSS artifacts are **immutable outputs**.\n\nThey are:\n- not edited\n- not post-processed manually\n- not patched in applications\n\n---\n\n## Canon Law\n\nIf CSS needs change:\n→ Fix generator  \n→ Rebuild  \n→ Replace artifact  \n\nNever patch artifacts.\n\n---\n\n## Enforcement Checklist\n\n- [ ] No commits editing `dist/*.css`\n- [ ] No post-build mutation scripts\n- [ ] Artifacts always regenerated"},{"number":61,"slug":"no-relative-css-imports","title":"No Relative CSS Imports","status":"ENFORCED","severity":"MEDIUM","category":"styling-css","tags":["css","imports","monorepo"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Relative CSS imports create brittle dependencies that break with refactors and prevent package portability. Only canonical package imports should be used across boundaries.","problem":"relative css imports couple files to directory structure","solution":"use canonical package imports instead of relative paths","signals":["../../css import","broken imports after move","path fragility"],"search_intent":"why relative css imports are bad in monorepo","keywords":["relative css imports problem","monorepo css imports","canonical css packages","postcss path resolution"],"body":"---\n\n## Principle\n\nRelative imports in CSS **DO NOT SCALE** and are forbidden outside the local package.\n\n---\n\n## Forbidden\n\n```css\n@import \"../../theme.css\";\n@import \"../../../../tokens.css\";\n```\n\n---\n\n## Allowed\n\n```css\n@import \"@canonrs/tailwind/tokens.css\";\n```\n\n---\n\n## Rationale\n\nRelative imports:\n- break monorepos\n- break refactors\n- break publishing\n- hide architectural boundaries\n\nCanonical imports encode **intent**, not topology.\n\n---\n\n## Enforcement Checklist\n\n- [ ] Zero `../` imports across package boundaries\n- [ ] Only `@canonrs/*` used\n- [ ] Reviewed in PR"},{"number":62,"slug":"single-source-of-truth-tokens","title":"Single Source of Truth for Design Tokens","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","design-system","css"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Multiple token definitions across files lead to divergence and inconsistency in design systems. Tokens must exist in a single canonical source and propagate through build artifacts.","problem":"multiple token sources create divergence and inconsistency","solution":"define tokens in one canonical file and propagate via build pipeline","signals":["multiple tokens.css","different values","sync issues"],"search_intent":"how to manage design tokens single","keywords":["design tokens single source","tailwind tokens architecture","css tokens monorepo","design system consistency"],"body":"---\n\n## The Principle\n\n**Design tokens MUST exist in exactly ONE canonical location.**\n\nAll other references are **build artifacts** or **generated outputs**, never sources of truth.\n\n---\n\n## The Problem\n\n### ❌ Wrong Pattern (Multiple Sources)\n```\n├── crates/rs-design/style/tokens.css          ← \"Source 1\"\n├── packages-js/canonrs-design/tokens.css      ← \"Source 2\" \n├── examples/workbench/style/tokens.css        ← \"Source 3\"\n└── frontend/styles/tokens.css                 ← \"Source 4\"\n```\n\n**Why this is wrong:**\n- Tokens diverge between files\n- Impossible to know which is authoritative\n- Changes must be synchronized manually\n- Theme changes require updates in 4+ places\n- No clear ownership\n\n**Symptoms:**\n```bash\ngrep \"color-background:\" */tokens.css\n# Returns 4 different values\n```\n\n---\n\n## The Solution\n\n### ✅ Correct Pattern (Single Source)\n```\nmonorepo/\n├── crates/rs-design/style/\n│   ├── tokens.css       ← ÚNICA FONTE DA VERDADE\n│   ├── main.css\n│   └── theme.css\n│\n└── examples/workbench/\n    ├── style/globals.css\n    │   @import \"/absolute/path/to/crates/rs-design/style/tokens.css\"\n    └── public/workbench.css  ← ARTIFACT (gerado via build)\n```\n\n**Single Source Rules:**\n1. Tokens defined in: `crates/rs-design/style/tokens.css`\n2. Apps import via **absolute paths** or **valid relative paths**\n3. Generated CSS is artifact, never edited manually\n4. Changes propagate via rebuild, not sync\n\n---\n\n## Architecture Layers\n\n### Layer 1: Source of Truth (rs-design)\n**Location:** `crates/rs-design/style/tokens.css`  \n**Responsibility:** Define all design tokens\n```css\n/* ÚNICA FONTE DA VERDADE */\n@theme inline {\n  --color-background: 0 0% 100%;\n  --color-foreground: 0 0% 3.9%;\n  --color-primary: 38 92% 50%;\n  --color-border: 0 0% 89.8%;\n}\n```\n\n### Layer 2: Consumption (Applications)\n**Location:** `examples/*/style/globals.css`  \n**Responsibility:** Import canonical tokens\n```css\n/* NÃO define tokens, apenas importa */\n@import \"tailwindcss\";\n@import \"/opt/docker/monorepo/opensource/canonrs/crates/rs-design/style/tokens.css\";\n\n@layer utilities {\n  .bg-background { background-color: hsl(var(--color-background)); }\n}\n```\n\n### Layer 3: Build Artifact (Generated)\n**Location:** `examples/*/public/workbench.css`  \n**Responsibility:** Compiled CSS for runtime\n```bash\n# Gerado via build, NUNCA editado manualmente\nnpx @tailwindcss/cli -i style/globals.css -o public/workbench.css\n```\n\n---\n\n## Tailwind V4 Requirements\n\n### ⚠️ Critical: Tailwind v4 Does NOT Resolve Node Modules\n\n**This FAILS silently:**\n```css\n@import \"@canonrs/design/tokens.css\";  /* 🚫 Not resolved */\n```\n\n**This WORKS:**\n```css\n@import \"/absolute/path/to/tokens.css\";  /* ✅ Resolved */\n```\n\n**Why:** Tailwind v4 uses native CSS `@import`, which:\n- Does NOT use Node module resolution\n- Does NOT support `node_modules/` lookups\n- Requires filesystem paths (absolute or valid relative)\n\n---\n\n## Implementation Checklist\n\nWhen setting up design tokens:\n\n- [ ] Tokens exist in ONE file: `crates/rs-design/style/tokens.css`\n- [ ] Apps use **absolute paths** in `@import`\n- [ ] `postcss.config.js` NOT used for path resolution (doesn't work)\n- [ ] Build script generates `public/*.css` artifact\n- [ ] Test: `grep \"color-background:\" public/*.css` returns values\n- [ ] NO token definitions duplicated across files\n\n---\n\n## Health Check Commands\n```bash\n# 1. Verify single source exists\nls crates/rs-design/style/tokens.css\n# ✅ Should exist\n\n# 2. Verify no duplicates\nfind . -name \"tokens.css\" -type f\n# ✅ Should return ONLY crates/rs-design/style/tokens.css\n\n# 3. Verify build artifact has tokens\ngrep \"color-background:\" examples/workbench/public/workbench.css\n# ✅ Should return: --color-background: 0 0% 100%;\n\n# 4. Verify no @import remains unresolved\ngrep \"@import.*tokens\" examples/workbench/public/workbench.css\n# ✅ Should return NOTHING (all imports processed)\n\n# 5. Verify runtime CSS is served\ncurl http://localhost:3003/workbench.css | head -20\n# ✅ Should return compiled CSS with tokens\n```\n\n---\n\n## Anti Patterns\n\n### 🚫 Copying Tokens to Multiple Files\n```bash\n# WRONG\ncp crates/rs-design/style/tokens.css examples/app1/tokens.css\ncp crates/rs-design/style/tokens.css examples/app2/tokens.css\n```\n\n### 🚫 Node Module Path Resolution\n```css\n/* WRONG - Tailwind v4 ignores this */\n@import \"@canonrs/design/tokens.css\";\n```\n\n### 🚫 Editing Generated CSS\n```bash\n# WRONG\nvim public/workbench.css  # 🚫 This is an artifact\n```\n\n### 🚫 Relative Paths Without Validation\n```css\n/* WRONG - breaks if file moves */\n@import \"../../tokens.css\";  /* 🚫 Fragile */\n```\n\n---\n\n## Correct Build Pipeline\n\n### Step 1: Define Tokens (Once)\n```css\n/* crates/rs-design/style/tokens.css */\n@theme inline {\n  --color-background: 0 0% 100%;\n}\n```\n\n### Step 2: Import in App (Absolute Path)\n```css\n/* examples/workbench/style/globals.css */\n@import \"tailwindcss\";\n@import \"/opt/docker/monorepo/opensource/canonrs/crates/rs-design/style/tokens.css\";\n```\n\n### Step 3: Build CSS (Generates Artifact)\n```bash\nnpx @tailwindcss/cli \\\n  -i style/globals.css \\\n  -o public/workbench.css \\\n  --minify\n```\n\n### Step 4: Serve Artifact\n```html\n<!-- examples/workbench/index.html -->\n<link rel=\"stylesheet\" href=\"/workbench.css\">\n```\n\n---\n\n## Comparison\n\n| Approach | Source Files | Build Required | Tailwind v4 Compatible |\n|----------|--------------|----------------|------------------------|\n| **CanonRS** | 1 (canonical) | ✅ Yes | ✅ Yes |\n| Turborepo shared | Multiple | ✅ Yes | ⚠️ Needs config |\n| Nx libraries | Multiple | ✅ Yes | ⚠️ Needs config |\n| Copy-paste | Many (diverge) | ❌ No | ✅ Yes (but breaks) |\n\n**Veredito:** CanonRS é **mais rigoroso** mas **menos frágil**.\n\n---\n\n## Debugging Guide\n\n### Problem: \"CSS has no tokens\"\n```bash\n# Check if tokens are in source\ncat crates/rs-design/style/tokens.css | grep \"color-background\"\n# ✅ Should exist\n\n# Check if build ran\nls -lh public/workbench.css\n# ✅ Should exist and be recent\n\n# Check if tokens made it to artifact\ngrep \"color-background\" public/workbench.css\n# ✅ Should return token definition\n```\n\n### Problem: \"@import not resolved\"\n```bash\n# Check import path\ncat style/globals.css | grep \"@import.*tokens\"\n# ❌ If shows @import, path is wrong\n\n# Fix: Use absolute path\n@import \"/opt/docker/monorepo/opensource/canonrs/crates/rs-design/style/tokens.css\";\n```\n\n### Problem: \"Changes don't appear\"\n```bash\n# Rebuild CSS\nnpx @tailwindcss/cli -i style/globals.css -o public/workbench.css\n\n# Hard refresh browser\nCtrl+Shift+R\n```\n\n---\n\n## Related Rules\n\n- **Rule #1:** Design System as Source of Truth\n- **Rule #64:** CSS Build Pipeline is Mandatory\n- **Rule #65:** data-theme Sync Responsibility\n\n---\n\n## Normative Status\n\n- Violations **MUST** block PRs\n- Multiple token sources **MUST NOT** exist\n- Apps **MUST** import from canonical location\n- Build artifacts **MUST NOT** be committed to git (add to `.gitignore`)\n- Token changes **MUST** propagate via rebuild only\n\n---\n\n**Author:** Canon Working Group  \n**Replaces:** None\n\n---\n\n## Economic Impact\n\n**Time saved per incident:** ~2 hours  \n**Frequency without rule:** Every new app setup  \n**Annual savings (10 apps):** ~20 hours\n\n**Root causes eliminated:**\n- ❌ Token divergence\n- ❌ \"Which file is source?\"\n- ❌ Sync failures\n- ❌ Tailwind import errors"},{"number":63,"slug":"leptos-reactivity-closures","title":"Leptos Reactivity - No .get() Outside Closures","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["leptos","signals","memo","reactivity"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Accessing reactive values outside tracked closures breaks dependency tracking in Leptos. Calling .get() outside reactive contexts results in stale UI and prevents automatic re-rendering.","problem":"reactive values accessed outside tracking context prevent updates","solution":"wrap memo and signal access inside closures to ensure reactive tracking","signals":["outside reactive context error","stale ui","memo not updating"],"search_intent":"how to fix leptos memo not updating","keywords":["leptos memo get closure","reactive tracking leptos","signal vs memo leptos","leptos stale ui fix"],"body":"---\n\n## The Principle\n\n**Reactive values MUST be accessed inside closures when used in reactive contexts.**\n\n`Signal<T>` and `Memo<T>` are NOT the same type. Their usage patterns differ critically.\n\n---\n\n## The Problem\n\n### ❌ Wrong Pattern (Breaks Reactivity)\n```rust\nlet tokens = Memo::new(move |_| fetch_tokens());\n\n// WRONG: .get() outside tracking context\nlet count = tokens.get().len();\n\nview! {\n    <p>{count}</p>  // 🚫 Never updates\n}\n```\n\n**Error message:**\n```\nyou access a reactive_graph::computed::arc_memo::ArcMemo<T> \noutside a reactive tracking context\n```\n\n**Why this fails:**\n- `.get()` was called during component initialization\n- Leptos didn't track the dependency\n- Changes to `tokens` never trigger re-render\n- UI becomes stale\n\n---\n\n## The Solution\n\n### ✅ Correct Pattern (Reactive)\n```rust\nlet tokens = Memo::new(move |_| fetch_tokens());\n\nview! {\n    <p>{move || tokens.get().len()}</p>  // ✅ Reactive\n}\n```\n\n**Why this works:**\n- `move ||` creates closure\n- Leptos tracks `tokens` as dependency\n- Changes trigger re-render automatically\n- UI stays synchronized\n\n---\n\n## Signal vs Memo Critical Difference\n\n### Signal<T> (Callable)\n```rust\nlet count = RwSignal::new(0);\n\n// ✅ BOTH work for Signal<T>\nview! {\n    <p>{count}</p>           // ✅ Signal is Fn() -> T\n    <p>{move || count.get()}</p>  // ✅ Also works\n}\n```\n\n### Memo<T> (NOT Callable)\n```rust\nlet doubled = Memo::new(move |_| count.get() * 2);\n\n// ❌ WRONG\nview! {\n    <p>{doubled}</p>  // 🚫 Memo is NOT Fn()\n}\n\n// ✅ CORRECT\nview! {\n    <p>{move || doubled.get()}</p>  // ✅ Must use closure\n}\n```\n\n---\n\n## Common Scenarios\n\n### 1. Component Props\n\n#### ❌ Wrong: Passing Memo directly\n```rust\n#[component]\npub fn DataTable<T>(\n    data: Vec<T>,  // Expects Vec, not Memo\n) -> impl IntoView { ... }\n\n// WRONG usage\nlet filtered = Memo::new(move |_| filter_items());\nview! {\n    <DataTable data=filtered/>  // 🚫 Type mismatch\n}\n```\n\n#### ✅ Correct: Extract value in closure\n```rust\nview! {\n    <DataTable data=move || filtered.get()/>  // ✅ Passes Vec<T>\n}\n```\n\nOr better:\n```rust\n// Change DataTable to accept reactive prop\n#[component]\npub fn DataTable<T>(\n    #[prop(into)] data: Signal<Vec<T>>,  // Accepts reactive types\n) -> impl IntoView { ... }\n```\n\n### 2. Show Component\n\n#### ❌ Wrong: Condition without closure\n```rust\nlet has_data = Memo::new(move |_| items.get().len() > 0);\n\n// WRONG\nview! {\n    <Show when={has_data}>  // 🚫 Memo is not Fn() -> bool\n        <Table/>\n    </Show>\n}\n```\n\n#### ✅ Correct: Wrap in closure\n```rust\nview! {\n    <Show when={move || has_data.get()}>  // ✅ Closure returns bool\n        <Table/>\n    </Show>\n}\n```\n\n### 3. Event Handlers\n\n#### ❌ Wrong: Access outside handler\n```rust\nlet selected = Memo::new(move |_| get_selected_items());\n\n// WRONG\nlet count = selected.get().len();  // 🚫 Called at init time\nlet on_click = move |_| {\n    log!(\"Selected: {}\", count);  // Stale value\n};\n```\n\n#### ✅ Correct: Access inside handler\n```rust\nlet on_click = move |_| {\n    let count = selected.get().len();  // ✅ Fresh value\n    log!(\"Selected: {}\", count);\n};\n```\n\n### 4. Suspense Fallback\n\n#### ❌ Wrong: Eager evaluation\n```rust\n<Suspense fallback=|| view! { <p>\"Loading...\"</p> }>\n    {move || {\n        if tokens.get().is_empty() {  // ✅ Inside closure (correct)\n            view! { <p>\"No data\"</p> }\n        } else {\n            view! { <DataTable data=tokens/> }  // 🚫 Memo passed directly\n        }\n    }}\n</Suspense>\n```\n\n#### ✅ Correct: Extract value properly\n```rust\n<Suspense fallback=|| view! { <p>\"Loading...\"</p> }>\n    {move || {\n        let items = tokens.get();  // Extract once\n        if items.is_empty() {\n            view! { <p>\"No data\"</p> }\n        } else {\n            view! { <DataTable data=items/>}  // ✅ Pass Vec<T>\n        }\n    }}\n</Suspense>\n```\n\n---\n\n## Implementation Checklist\n\nWhen using reactive values:\n\n- [ ] `Signal<T>` can be used directly in `view!`\n- [ ] `Memo<T>` MUST be wrapped in `move ||` closure\n- [ ] Props expecting `Vec<T>` get `memo.get()`, not `memo`\n- [ ] `<Show when={}>` always receives `Fn() -> bool`\n- [ ] Event handlers access reactive values inside closure\n- [ ] Test: No \"outside reactive tracking context\" errors\n\n---\n\n## Debugging Guide\n\n### Error: \"accessed outside reactive tracking context\"\n\n**Step 1: Find the line**\n```\nAt src/components/table.rs:42:21, you access a \nreactive_graph::computed::arc_memo::ArcMemo<Vec<Item>>\n```\n\n**Step 2: Check for naked .get()**\n```rust\n// Line 42\nlet items = filtered_items.get();  // 🚫 Found it\n```\n\n**Step 3: Wrap in closure**\n```rust\n// Fix\nview! {\n    <div>{move || filtered_items.get().len()}</div>\n}\n```\n\n### Error: \"Type mismatch: expected Vec<T>, found Memo\"\n\n**Step 1: Find component call**\n```rust\n<DataTable data=my_memo/>  // 🚫 Wrong type\n```\n\n**Step 2: Extract value**\n```rust\n<DataTable data=my_memo.get()/>  // ✅ Fixed\n```\n\n**Step 3: Or change prop type**\n```rust\n#[component]\nfn DataTable(\n    #[prop(into)] data: Signal<Vec<T>>  // Accepts Memo now\n) { ... }\n```\n\n---\n\n## Anti Patterns\n\n### 🚫 Eager Computation\n```rust\n// WRONG: Computed at init time\nlet count = items.get().len();\n\nview! {\n    <p>{count}</p>  // Never updates\n}\n```\n\n### 🚫 Memo as Callable\n```rust\n// WRONG: Memo is not Fn()\n<Show when={is_visible}>  // 🚫 Memo<bool> is not Fn() -> bool\n```\n\n### 🚫 Untracked in Reactive Context\n```rust\n// WRONG: Breaks reactivity intentionally\nview! {\n    <p>{items.get_untracked().len()}</p>  // 🚫 Never updates\n}\n```\n\n---\n\n## Correct Patterns Reference\n\n### Pattern 1: Display Reactive Value\n```rust\nview! {\n    <p>{move || memo.get()}</p>\n}\n```\n\n### Pattern 2: Conditional Rendering\n```rust\n<Show\n    when={move || memo.get().len() > 0}\n    fallback={|| view! { <p>\"Empty\"</p> }}\n>\n    <List/>\n</Show>\n```\n\n### Pattern 3: Pass to Component\n```rust\n// Option A: Extract value\n<MyComponent data=memo.get()/>\n\n// Option B: Accept Signal\n#[component]\nfn MyComponent(#[prop(into)] data: Signal<Vec<T>>) { ... }\n<MyComponent data=memo/>\n```\n\n### Pattern 4: Event Handler\n```rust\nlet on_click = move |_| {\n    let value = memo.get();  // Fresh value\n    do_something(value);\n};\n```\n\n---\n\n## Testing\n```rust\n#[test]\nfn test_reactivity() {\n    let count = RwSignal::new(0);\n    let doubled = Memo::new(move |_| count.get() * 2);\n    \n    // ✅ Test that Memo reacts to Signal changes\n    assert_eq!(doubled.get(), 0);\n    count.set(5);\n    assert_eq!(doubled.get(), 10);\n}\n```\n\n---\n\n## Related Rules\n\n- **Rule #49:** Drag & Drop as Intent (uses Callback pattern)\n- **Rule #XX:** Component Props (pending)\n- **Rule #XX:** Event Handlers (pending)\n\n---\n\n## Normative Status\n\n- Violations **MUST** block PRs\n- Compiler errors related to reactivity **MUST** be fixed before merge\n- `Memo<T>` **MUST NOT** be used where `Fn() -> T` is expected\n- All reactive values in `view!` **MUST** be in closures (except `Signal<T>`)\n\n---\n\n**Author:** Canon Working Group  \n**Replaces:** None\n\n---\n\n## Economic Impact\n\n**Time saved per incident:** ~1 hour  \n**Frequency without rule:** Every component with Memo  \n**Annual savings (50 components):** ~50 hours\n\n**Root causes eliminated:**\n- ❌ \"outside reactive tracking context\" errors\n- ❌ Stale UI that doesn't update\n- ❌ Type mismatches with component props\n- ❌ Confusion between Signal and Memo"},{"number":64,"slug":"css-build-pipeline-mandatory","title":"CSS Build Pipeline is Mandatory","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["css","tailwind","build","pipeline"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Leptos does not generate CSS automatically, leading to missing styles when Tailwind is not built explicitly. CSS must be produced through a dedicated build step before runtime.","problem":"css is not generated because build step is missing","solution":"run explicit tailwind build pipeline before serving application","signals":["unstyled page","missing css file","tailwind not applied"],"search_intent":"why leptos app has no css","keywords":["leptos css missing","tailwind build pipeline leptos","css not generated leptos","tailwind cli build step"],"body":"---\n\n## The Principle\n\n**CSS is a build-time artifact, not a runtime feature.**\n\nTailwind v4 does NOT run in the browser. A build step is **mandatory**.\n\n---\n\n## The Problem\n\n### ❌ Wrong Assumption (No Build)\n```bash\n# Developer assumes:\ncargo leptos serve\n# → CSS auto-generates ✅\n\n# Reality:\ncargo leptos serve\n# → Only compiles Rust\n# → NO CSS generated 🚫\n```\n\n**What happens:**\n```html\n<!-- HTML references CSS -->\n<link rel=\"stylesheet\" href=\"/workbench.css\">\n\n<!-- But file doesn't exist -->\n$ ls public/workbench.css\nls: cannot access 'public/workbench.css': No such file or directory\n```\n\n**Symptoms:**\n- White unstyled page\n- Tailwind classes in HTML but no styling\n- `getComputedStyle()` returns empty values\n- No errors in console (CSS just missing)\n\n---\n\n## The Solution\n\n### ✅ Correct Pipeline (Explicit Build)\n```bash\n# Step 1: Generate CSS (MANDATORY)\nnpx @tailwindcss/cli \\\n  -i style/globals.css \\\n  -o public/workbench.css \\\n  --minify\n\n# Step 2: Compile Rust\ncargo leptos build\n\n# Step 3: Serve\ncargo leptos serve\n```\n\n---\n\n## Why Cargo Leptos Does Not Generate CSS\n\n### Leptos Responsibility\n- ✅ Compiles Rust → WASM\n- ✅ Bundles JavaScript\n- ✅ Sets up SSR server\n- ❌ Does NOT run Tailwind\n- ❌ Does NOT process PostCSS\n- ❌ Does NOT generate CSS\n\n### Tailwind v4 Requirement\n**Tailwind is NOT a runtime library.**\n\nIt's a **build tool** that:\n1. Reads your CSS input file\n2. Scans your source files for classes\n3. Generates optimized CSS output\n4. Exits\n\n**No build = No CSS = No styling.**\n\n---\n\n## Correct Setup Step By Step\n\n### 1. Install Tailwind CLI\n```bash\nnpm install -D @tailwindcss/cli@latest\n```\n\n### 2. Create Input CSS\n```css\n/* style/globals.css */\n@import \"tailwindcss\";\n@import \"/absolute/path/to/crates/rs-design/style/tokens.css\";\n\n@layer utilities {\n  .bg-background { background-color: hsl(var(--color-background)); }\n}\n```\n\n### 3. Add Build Script\n```json\n// package.json\n{\n  \"scripts\": {\n    \"build:css\": \"tailwindcss -i style/globals.css -o public/workbench.css --minify\",\n    \"watch:css\": \"tailwindcss -i style/globals.css -o public/workbench.css --watch\"\n  }\n}\n```\n\n### 4. Configure Leptos Assets\n```toml\n# Leptos.toml (optional but recommended)\n[site]\nassets-dir = \"public\"\n```\n\n### 5. Link CSS in HTML\n```rust\n// src/app.rs\nview! {\n    <link rel=\"stylesheet\" href=\"/workbench.css\"/>\n}\n```\n\n### 6. Build Workflow\n```bash\n# Development\nnpm run watch:css &  # Background process\ncargo leptos serve\n\n# Production\nnpm run build:css\ncargo leptos build --release\n```\n\n---\n\n## Health Check Commands\n```bash\n# 1. Verify Tailwind CLI installed\nnpx @tailwindcss/cli --help\n# ✅ Should show help text\n\n# 2. Build CSS manually\nnpx @tailwindcss/cli -i style/globals.css -o public/workbench.css\n# ✅ Should complete without errors\n\n# 3. Verify output exists\nls -lh public/workbench.css\n# ✅ Should exist and have size > 0\n\n# 4. Verify output has content\nhead -20 public/workbench.css\n# ✅ Should show CSS rules\n\n# 5. Verify tokens present\ngrep \"color-background\" public/workbench.css\n# ✅ Should return token definitions\n\n# 6. Test runtime serving\ncurl http://localhost:3003/workbench.css | head -20\n# ✅ Should return CSS content\n```\n\n---\n\n## Common Mistakes\n\n### ❌ Mistake 1: Assuming cargo leptos Handles CSS\n```bash\n# WRONG: Expects CSS to auto-generate\ncargo leptos serve\n# 🚫 No CSS created\n```\n\n**Fix:**\n```bash\nnpm run build:css  # Generate CSS first\ncargo leptos serve\n```\n\n### ❌ Mistake 2: Using Wrong Tailwind Package\n```bash\n# WRONG: Old Tailwind v3\nnpm install tailwindcss\n```\n\n**Fix:**\n```bash\n# CORRECT: Tailwind v4 CLI\nnpm install @tailwindcss/cli\n```\n\n### ❌ Mistake 3: No Watch Mode\n```bash\n# WRONG: Must rebuild manually on CSS changes\nnpm run build:css\n# Edit style/globals.css\n# ... no update\n```\n\n**Fix:**\n```bash\n# CORRECT: Auto-rebuild on changes\nnpm run watch:css &  # Runs in background\n```\n\n### ❌ Mistake 4: Wrong Output Path\n```bash\n# WRONG: CSS generated in wrong location\ntailwindcss -o dist/output.css\n\n# HTML links to different path\n<link href=\"/public/workbench.css\">  # 404\n```\n\n**Fix:**\n```bash\n# Align output path with HTML link\ntailwindcss -o public/workbench.css\n```\n\n---\n\n## Debugging Guide\n\n### Problem: \"Page has no styling\"\n\n**Check 1: Does CSS file exist?**\n```bash\nls public/workbench.css\n# If missing → Run build:css\n```\n\n**Check 2: Is CSS being served?**\n```bash\ncurl http://localhost:3003/workbench.css\n# If 404 → Check assets-dir in Leptos.toml\n```\n\n**Check 3: Does CSS have content?**\n```bash\nwc -l public/workbench.css\n# If 0 lines → Build failed, check errors\n```\n\n**Check 4: Are tokens present?**\n```bash\ngrep \"color-background\" public/workbench.css\n# If missing → Check @import paths\n```\n\n### Problem: \"CSS outdated after changes\"\n\n**Solution: Use watch mode**\n```bash\n# Kill old process\npkill -f \"tailwindcss.*watch\"\n\n# Start new watch\nnpm run watch:css &\n```\n\n### Problem: \"Build succeeds but CSS empty\"\n\n**Cause: Input file has errors**\n```bash\n# Check for syntax errors\nnpx @tailwindcss/cli -i style/globals.css -o /tmp/test.css\n# Read error output carefully\n```\n\n---\n\n## Integration With Hot Reload\n\n### Leptos Hot Reload (Rust Only)\n```bash\ncargo leptos watch\n# ✅ Reloads on .rs changes\n# ❌ Does NOT reload on .css changes\n```\n\n### CSS Hot Reload (Separate Process)\n```bash\nnpm run watch:css\n# ✅ Rebuilds on .css changes\n# ⚠️ Browser needs manual refresh (Ctrl+Shift+R)\n```\n\n### Full Stack Development\n```bash\n# Terminal 1: CSS watch\nnpm run watch:css\n\n# Terminal 2: Rust watch\ncargo leptos watch\n\n# Edit .rs → Auto reload\n# Edit .css → Manual refresh\n```\n\n---\n\n## Production Build\n```bash\n#!/bin/bash\n# build.sh\n\nset -e\n\necho \"Building CSS...\"\nnpm run build:css\n\necho \"Building Rust...\"\ncargo leptos build --release\n\necho \"Build complete!\"\necho \"CSS: public/workbench.css\"\necho \"Binary: target/release/my-app\"\n```\n\n---\n\n## Anti Patterns\n\n### 🚫 No Build Script\n```json\n// WRONG: No CSS build defined\n{\n  \"scripts\": {}\n}\n```\n\n### 🚫 Committing Generated CSS\n```bash\n# WRONG: Generated files in git\ngit add public/workbench.css\n```\n\n**Fix:**\n```bash\n# Add to .gitignore\necho \"public/*.css\" >> .gitignore\n```\n\n### 🚫 Manual CSS Editing\n```bash\n# WRONG: Editing generated file\nvim public/workbench.css  # 🚫 Gets overwritten\n```\n\n---\n\n## Comparison\n\n| Framework | CSS Auto-Generated | Build Required | Watch Mode |\n|-----------|-------------------|----------------|------------|\n| **CanonRS (Leptos)** | ❌ No | ✅ Yes | ✅ Separate |\n| Next.js | ✅ Yes | ❌ No (built-in) | ✅ Built-in |\n| SvelteKit | ✅ Yes | ❌ No (built-in) | ✅ Built-in |\n| Remix | ✅ Yes | ❌ No (built-in) | ✅ Built-in |\n\n**Veredito:** Leptos requires **explicit CSS pipeline** (not worse, just different).\n\n---\n\n## Related Rules\n\n- **Rule #62:** Single Source of Truth for Design Tokens\n- **Rule #65:** data-theme Sync Responsibility\n\n---\n\n## Normative Status\n\n- New Leptos apps **MUST** have `build:css` script in `package.json`\n- CSS output **MUST** be in `.gitignore`\n- Production builds **MUST** run CSS build before Rust build\n- Documentation **MUST** include CSS build step\n- PRs adding Tailwind **MUST** include build pipeline\n\n---\n\n**Author:** Canon Working Group  \n**Replaces:** None\n\n---\n\n## Economic Impact\n\n**Time saved per incident:** ~30 minutes  \n**Frequency without rule:** Every new app setup  \n**Annual savings (10 apps):** ~5 hours\n\n**Root causes eliminated:**\n- ❌ \"Why is my page white?\"\n- ❌ \"Tailwind classes not working\"\n- ❌ Assuming cargo leptos handles CSS\n- ❌ Missing build:css script"},{"number":65,"slug":"theme-provider-dom-sync","title":"data-theme Sync is ThemeProvider Responsibility","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["theming","tokens","dom","signals"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Theme state in Rust signals does not affect CSS unless synchronized to the DOM. Without updating attributes like data-theme, styles based on selectors never activate.","problem":"theme state is not reflected in dom attributes so css does not apply","solution":"sync theme signals to dom attributes using client side effect","signals":["theme not applying","missing data-theme","css tokens inactive"],"search_intent":"why data-theme not applied leptos","keywords":["leptos themeprovider dom sync","data theme attribute css","leptos theming signals dom","css tokens not applying"],"body":"---\n\n## The Principle\n\n**The ThemeProvider MUST synchronize theme state to the DOM.**\n\nControlling Rust signals is NOT enough. CSS selectors like `[data-theme=\"...\"]` require actual DOM attributes.\n\n---\n\n## The Problem\n\n### ❌ Wrong Pattern (State Only)\n```rust\n#[component]\npub fn ThemeProvider(children: Children) -> impl IntoView {\n    let mode = RwSignal::new(ThemeMode::Light);\n    let preset = RwSignal::new(\"amber-minimal\".to_string());\n    \n    provide_context(ThemeContext { mode, preset });\n    \n    children()  // 🚫 NO DOM sync\n}\n```\n\n**CSS expects:**\n```css\n[data-theme=\"amber-minimal\"] {\n  --color-background: 0 0% 100%;\n}\n```\n\n**But HTML has:**\n```html\n<html\n\n**Result:**\n- Tokens defined ✅\n- State controlled ✅\n- CSS never activates ❌\n- Theme doesn't apply ❌\n\n---\n\n## The Solution\n\n### ✅ Correct Pattern (State + DOM Sync)\n```rust\n#[component]\npub fn ThemeProvider(\n    #[prop(optional)] initial_preset: Option<String>,\n    children: Children,\n) -> impl IntoView {\n    let mode = RwSignal::new(ThemeMode::Light);\n    let preset = RwSignal::new(initial_preset.unwrap_or(\"amber-minimal\".to_string()));\n    \n    // ✅ CRITICAL: Sync to DOM\n    #[cfg(target_arch = \"wasm32\")]\n    Effect::new(move |_| {\n        if let Some(window) = web_sys::window() {\n            if let Some(document) = window.document() {\n                if let Some(html) = document.document_element() {\n                    // Sync preset\n                    let _ = html.set_attribute(\"data-theme\", &preset.get());\n                    \n                    // Sync mode\n                    let is_dark = matches!(mode.get(), ThemeMode::Dark);\n                    let _ = html.class_list().toggle_with_force(\"dark\", is_dark);\n                }\n            }\n        }\n    });\n    \n    provide_context(ThemeContext { mode, preset });\n    children()\n}\n```\n\n**Result:**\n```html\n<html data-theme=\"amber-minimal\" class=\"\">  <!-- ✅ Synced -->\n```\n\n---\n\n## Architecture Layers\n\n### Layer 1: Rust State (ThemeContext)\n**Responsibility:** Store theme in reactive signals\n```rust\npub struct ThemeContext {\n    pub mode: RwSignal<ThemeMode>,\n    pub preset: RwSignal<String>,\n}\n```\n\n### Layer 2: DOM Sync (Effect)\n**Responsibility:** Mirror state to HTML attributes\n```rust\nEffect::new(move |_| {\n    html.set_attribute(\"data-theme\", &preset.get());\n    html.class_list().toggle_with_force(\"dark\", is_dark);\n});\n```\n\n### Layer 3: CSS Consumption\n**Responsibility:** Apply styles based on attributes\n```css\n[data-theme=\"amber-minimal\"] {\n  --color-background: 0 0% 100%;\n}\n\n.dark {\n  --color-background: 0 0% 9%;\n}\n```\n\n---\n\n## Why Both Are Required\n\n### Rust State Alone (Insufficient)\n```rust\nlet preset = RwSignal::new(\"amber-minimal\");\nprovide_context(preset);\n```\n\n**Can do:**\n- ✅ UI components react to changes\n- ✅ Conditional rendering works\n- ✅ Settings panel updates\n\n**Cannot do:**\n- ❌ CSS `[data-theme]` selectors don't match\n- ❌ Tokens don't activate\n- ❌ Theme doesn't apply visually\n\n### DOM Attribute Alone (Insufficient)\n```rust\nhtml.set_attribute(\"data-theme\", \"amber-minimal\");\n```\n\n**Can do:**\n- ✅ CSS selectors match\n- ✅ Theme applies visually\n\n**Cannot do:**\n- ❌ No reactive updates\n- ❌ UI components don't know current theme\n- ❌ Settings panel out of sync\n\n### Both Together (Correct)\n```rust\nlet preset = RwSignal::new(\"amber-minimal\");\n\nEffect::new(move |_| {\n    html.set_attribute(\"data-theme\", &preset.get());\n});\n```\n\n**Result:**\n- ✅ Rust state for reactivity\n- ✅ DOM attribute for CSS\n- ✅ Changes propagate everywhere\n- ✅ System stays in sync\n\n---\n\n## Implementation Checklist\n\nWhen implementing ThemeProvider:\n\n- [ ] Store theme in `RwSignal` (for reactivity)\n- [ ] Use `Effect::new` to sync to DOM\n- [ ] Set `data-theme` attribute on `<html>`\n- [ ] Toggle `.dark` class based on mode\n- [ ] Use `#[cfg(target_arch = \"wasm32\")]` guard\n- [ ] Test: `curl localhost | grep data-theme` shows value\n\n---\n\n## Health Check Commands\n```bash\n# 1. Verify Rust state exists\n# (In component code)\nlet ctx = use_context::<ThemeContext>();\n# ✅ Should not panic\n\n# 2. Verify DOM attribute set\ncurl -s http://localhost:3003 | grep 'data-theme='\n# ✅ Should return: data-theme=\"amber-minimal\"\n\n# 3. Verify dark mode class\ncurl -s http://localhost:3003 | grep '<html.*class=\"dark\"'\n# ✅ Should match when dark mode active\n\n# 4. Test in browser console\ndocument.documentElement.getAttribute('data-theme')\n# ✅ Should return: \"amber-minimal\"\n\n# 5. Test dark mode toggle\ndocument.documentElement.classList.contains('dark')\n# ✅ Should return: true or false\n```\n\n---\n\n## Common Mistakes\n\n### ❌ Mistake 1: No Effect\n```rust\n// WRONG: State exists but never syncs\nlet preset = RwSignal::new(\"amber\");\nprovide_context(preset);\n// 🚫 DOM never updated\n```\n\n**Fix:**\n```rust\nEffect::new(move |_| {\n    html.set_attribute(\"data-theme\", &preset.get());\n});\n```\n\n### ❌ Mistake 2: Missing wasm32 Guard\n```rust\n// WRONG: Runs on server (panics)\nEffect::new(move |_| {\n    let window = web_sys::window().unwrap();  // 🚫 Panics in SSR\n});\n```\n\n**Fix:**\n```rust\n#[cfg(target_arch = \"wasm32\")]\nEffect::new(move |_| {\n    if let Some(window) = web_sys::window() { ... }\n});\n```\n\n### ❌ Mistake 3: One-Time Sync\n```rust\n// WRONG: Only sets attribute once\nhtml.set_attribute(\"data-theme\", \"amber\");\n\n// User changes theme → Nothing happens\n```\n\n**Fix:**\n```rust\n// CORRECT: Reactive sync\nEffect::new(move |_| {\n    html.set_attribute(\"data-theme\", &preset.get());\n});\n```\n\n### ❌ Mistake 4: Forgetting Dark Mode\n```rust\n// WRONG: Only syncs preset\nEffect::new(move |_| {\n    html.set_attribute(\"data-theme\", &preset.get());\n    // 🚫 Missing dark mode class\n});\n```\n\n**Fix:**\n```rust\nEffect::new(move |_| {\n    html.set_attribute(\"data-theme\", &preset.get());\n    let is_dark = matches!(mode.get(), ThemeMode::Dark);\n    html.class_list().toggle_with_force(\"dark\", is_dark);\n});\n```\n\n---\n\n## Debugging Guide\n\n### Problem: \"Theme not applying\"\n\n**Step 1: Check Rust state**\n```rust\n// Add debug log\nEffect::new(move |_| {\n    log!(\"Current preset: {}\", preset.get());\n});\n```\n\n**Step 2: Check DOM attribute**\n```bash\ncurl -s http://localhost:3003 | grep data-theme\n# If missing → Effect not running\n```\n\n**Step 3: Check CSS selector**\n```css\n/* Verify selector matches attribute */\n[data-theme=\"amber-minimal\"] { ... }\n```\n\n**Step 4: Check browser**\n```javascript\ngetComputedStyle(document.documentElement).getPropertyValue('--color-background')\n// If empty → CSS not loading or selector wrong\n```\n\n### Problem: \"Theme changes don't update UI\"\n\n**Cause: Effect not reactive**\n```rust\n// WRONG: Captures value at init\nlet theme = preset.get();\nEffect::new(move |_| {\n    html.set_attribute(\"data-theme\", &theme);  // Stale\n});\n```\n\n**Fix: Access signal inside Effect**\n```rust\nEffect::new(move |_| {\n    let theme = preset.get();  // Fresh value\n    html.set_attribute(\"data-theme\", &theme);\n});\n```\n\n---\n\n## SSR Considerations\n\n### Client-Only Sync\n```rust\n// Effect only runs in browser\n#[cfg(target_arch = \"wasm32\")]\nEffect::new(move |_| {\n    // Safe: only executes client-side\n    let html = document.document_element().unwrap();\n    html.set_attribute(\"data-theme\", &preset.get());\n});\n```\n\n### SSR Hydration\n```rust\n// Option: Inject initial theme in SSR HTML\npub fn render_shell(preset: &str) -> String {\n    format!(\n        r#\"<!DOCTYPE html>\n<html data-theme=\"{}\">\n<head>...</head>\n<body>...</body>\n</html>\"#,\n        preset\n    )\n}\n```\n\n**Then client-side Effect takes over after hydration.**\n\n---\n\n## Testing\n```rust\n#[cfg(test)]\nmod tests {\n    use super::*;\n    \n    #[test]\n    fn theme_provider_syncs_to_dom() {\n        // This test requires browser environment\n        // Use wasm-bindgen-test\n        \n        let preset = RwSignal::new(\"amber\".to_string());\n        \n        // Verify Effect syncs\n        Effect::new(move |_| {\n            assert_eq!(\n                document.document_element()\n                    .unwrap()\n                    .get_attribute(\"data-theme\"),\n                Some(\"amber\".to_string())\n            );\n        });\n    }\n}\n```\n\n---\n\n## Anti Patterns\n\n### 🚫 Manual DOM Updates Outside Provider\n```rust\n// WRONG: Random component mutates DOM\n#[component]\nfn MyButton() -> impl IntoView {\n    let on_click = move |_| {\n        document.document_element()\n            .unwrap()\n            .set_attribute(\"data-theme\", \"blue\");  // 🚫 Bypasses state\n    };\n}\n```\n\n### 🚫 Multiple Theme Providers\n```rust\n// WRONG: Conflicting providers\n<ThemeProvider initial_preset=\"amber\">\n    <ThemeProvider initial_preset=\"blue\">  // 🚫 Which wins?\n        <App/>\n    </ThemeProvider>\n</ThemeProvider>\n```\n\n---\n\n## Comparison\n\n| Framework | State Management | DOM Sync | Auto-Sync |\n|-----------|-----------------|----------|-----------|\n| **CanonRS (Leptos)** | RwSignal | Manual Effect | ❌ No |\n| Next.js (next-themes) | useState | useEffect | ✅ Yes |\n| SvelteKit | writable | $: reactive | ✅ Yes |\n| Solid.js | createSignal | createEffect | ❌ No |\n\n**Veredito:** Leptos requires **explicit Effect**, similar to Solid.js.\n\n---\n\n## Related Rules\n\n- **Rule #62:** Single Source of Truth for Design Tokens\n- **Rule #63:** Leptos Reactivity (Effect usage)\n- **Rule #64:** CSS Build Pipeline\n\n---\n\n## Normative Status\n\n- ThemeProvider **MUST** sync state to DOM\n- Effect **MUST** be guarded with `#[cfg(target_arch = \"wasm32\")]`\n- Both `data-theme` and `.dark` class **MUST** be synced\n- PRs adding ThemeProvider **MUST** include DOM sync\n- Test: `curl localhost | grep data-theme` **MUST** pass\n\n---\n\n**Author:** Canon Working Group  \n**Replaces:** None\n\n---\n\n## Economic Impact\n\n**Time saved per incident:** ~45 minutes  \n**Frequency without rule:** Every theme implementation  \n**Annual savings (5 themes):** ~4 hours\n\n**Root causes eliminated:**\n- ❌ \"CSS has tokens but theme doesn't apply\"\n- ❌ State works but DOM out of sync\n- ❌ Theme toggle updates UI but not styles\n- ❌ Confusion between Rust state and DOM state"},{"number":66,"slug":"workbench-setup-checklist","title":"Workbench Setup Checklist","status":"ENFORCED","severity":"LOW","category":"governance","tags":["setup","validation","workspace"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Skipping initial environment validation leads to hidden configuration issues such as missing CSS pipelines. A mandatory checklist ensures system readiness before development begins.","problem":"developers start without validating setup leading to hidden configuration failures","solution":"enforce pre development checklist validating tokens css and assets pipeline","signals":["unstyled app","missing css pipeline","invalid setup"],"search_intent":"how to validate leptos project setup before development","keywords":["leptos setup checklist","css pipeline validation","tailwind setup verification","monorepo environment validation"],"body":"---\n\n## The Principle\n\n**Every new workbench/app MUST pass a health check before development begins.**\n\nAssumptions kill productivity. Validation saves hours.\n\n---\n\n## The Problem\n\n### ❌ Wrong Approach (Assume Everything Works)\n```bash\n# Developer starts coding\nvim src/app.rs\n\n# Hours later: \"Why is nothing styled?\"\n# Reason: CSS pipeline was never set up\n```\n\n**Time wasted:** 2-4 hours debugging what should have been validated upfront.\n\n---\n\n## The Solution\n\n### ✅ Correct Approach (Validate Before Coding)\n```bash\n# Run checklist FIRST\n./scripts/validate-setup.sh\n\n# ✅ All checks pass → Safe to develop\n# ❌ Any check fails → Fix before proceeding\n```\n\n---\n\n## Mandatory Checklist\n\n### Step 1: Design System Tokens Exist\n**What:** Verify canonical token source exists\n```bash\nls crates/rs-design/style/tokens.css\n```\n\n**Expected:** File exists  \n**If fails:** Clone rs-design or create tokens.css  \n**Time saved:** 30 minutes\n\n---\n\n### Step 2: Tailwind CLI Installed\n**What:** Verify build tool available\n```bash\nnpx @tailwindcss/cli --help\n```\n\n**Expected:** Help text displayed  \n**If fails:** `npm install -D @tailwindcss/cli`  \n**Time saved:** 15 minutes\n\n---\n\n### Step 3: Build Script Exists\n**What:** Verify CSS can be generated\n```bash\ncat package.json | grep \"build:css\"\n```\n\n**Expected:** Script defined  \n**If fails:** Add to `package.json`:\n```json\n{\n  \"scripts\": {\n    \"build:css\": \"tailwindcss -i style/globals.css -o public/workbench.css --minify\",\n    \"watch:css\": \"tailwindcss -i style/globals.css -o public/workbench.css --watch\"\n  }\n}\n```\n**Time saved:** 20 minutes\n\n---\n\n### Step 4: CSS Builds Successfully\n**What:** Verify CSS generation works\n```bash\nnpm run build:css\nls public/workbench.css\n```\n\n**Expected:** File exists with size > 0  \n**If fails:** Check import paths in `style/globals.css`  \n**Time saved:** 1 hour\n\n---\n\n### Step 5: CSS Has Tokens\n**What:** Verify tokens were included in build\n```bash\ngrep \"color-background:\" public/workbench.css\n```\n\n**Expected:** Returns token definitions  \n**If fails:** Fix `@import` paths (use absolute paths)  \n**Time saved:** 1 hour\n\n---\n\n### Step 6: Assets Directory Configured\n**What:** Verify Leptos serves public folder\n```bash\ncat Cargo.toml | grep \"leptos\"\n# or\ncat Leptos.toml | grep \"assets-dir\"\n```\n\n**Expected:** Configuration exists  \n**If fails:** Create `Leptos.toml`:\n```toml\n[site]\nassets-dir = \"public\"\n```\n**Time saved:** 15 minutes\n\n---\n\n### Step 7: CSS Served at Runtime\n**What:** Verify CSS accessible via HTTP\n```bash\ncargo leptos serve &\nsleep 5\ncurl -I http://localhost:3003/workbench.css\n```\n\n**Expected:** `200 OK` response  \n**If fails:** Check assets-dir path or HTML link  \n**Time saved:** 30 minutes\n\n---\n\n### Step 8: HTML Links CSS Correctly\n**What:** Verify page loads CSS\n```bash\ncurl -s http://localhost:3003 | grep 'href=\"/workbench.css\"'\n```\n\n**Expected:** Link tag present  \n**If fails:** Add to `src/app.rs`:\n```rust\nview! {\n    <link rel=\"stylesheet\" href=\"/workbench.css\"/>\n}\n```\n**Time saved:** 15 minutes\n\n---\n\n### Step 9: DOM Has data-theme\n**What:** Verify theme system active\n```bash\ncurl -s http://localhost:3003 | grep 'data-theme='\n```\n\n**Expected:** Attribute present with value  \n**If fails:** Implement ThemeProvider DOM sync (Rule #65)  \n**Time saved:** 45 minutes\n\n---\n\n### Step 10: Styles Apply in Browser\n**What:** End-to-end verification\n\n**Manual test:**\n1. Open http://localhost:3003\n2. Open DevTools Console\n3. Run:\n```javascript\ngetComputedStyle(document.documentElement).getPropertyValue('--color-background')\n```\n\n**Expected:** Returns value (e.g., `\"0 0% 100%\"`)  \n**If fails:** Review Rules #62-#65  \n**Time saved:** Variable\n\n---\n\n## Automation Script\n\n### validate-setup.sh\n```bash\n#!/bin/bash\nset -e\n\necho \"🔍 Canon Workbench Setup Validation\"\necho \"====================================\"\n\n# Check 1: Tokens exist\necho -n \"✓ Design tokens exist... \"\ntest -f crates/rs-design/style/tokens.css && echo \"PASS\" || (echo \"FAIL\" && exit 1)\n\n# Check 2: Tailwind CLI\necho -n \"✓ Tailwind CLI installed... \"\nnpx @tailwindcss/cli --help &>/dev/null && echo \"PASS\" || (echo \"FAIL\" && exit 1)\n\n# Check 3: Build script\necho -n \"✓ Build script defined... \"\ngrep -q \"build:css\" package.json && echo \"PASS\" || (echo \"FAIL\" && exit 1)\n\n# Check 4: CSS builds\necho -n \"✓ CSS builds successfully... \"\nnpm run build:css &>/dev/null && test -f public/workbench.css && echo \"PASS\" || (echo \"FAIL\" && exit 1)\n\n# Check 5: Tokens in CSS\necho -n \"✓ Tokens in generated CSS... \"\ngrep -q \"color-background:\" public/workbench.css && echo \"PASS\" || (echo \"FAIL\" && exit 1)\n\n# Check 6: Assets configured\necho -n \"✓ Assets directory configured... \"\n(grep -q \"assets-dir\" Leptos.toml 2>/dev/null || grep -q \"leptos\" Cargo.toml) && echo \"PASS\" || (echo \"FAIL\" && exit 1)\n\n# Check 7: Server starts\necho -n \"✓ Leptos server starts... \"\ncargo leptos build &>/dev/null && echo \"PASS\" || (echo \"FAIL\" && exit 1)\n\n# Check 8: CSS served (requires server running)\nif pgrep -f \"cargo leptos\" &>/dev/null; then\n    echo -n \"✓ CSS served at runtime... \"\n    curl -sfI http://localhost:3003/workbench.css &>/dev/null && echo \"PASS\" || (echo \"FAIL\" && exit 1)\n    \n    echo -n \"✓ HTML links CSS... \"\n    curl -s http://localhost:3003 | grep -q 'href=\"/workbench.css\"' && echo \"PASS\" || (echo \"FAIL\" && exit 1)\n    \n    echo -n \"✓ data-theme present... \"\n    curl -s http://localhost:3003 | grep -q 'data-theme=' && echo \"PASS\" || (echo \"FAIL\" && exit 1)\nelse\n    echo \"⚠ Server not running - skipping runtime checks\"\nfi\n\necho \"\"\necho \"✅ All checks passed! Safe to develop.\"\n```\n\n---\n\n## When To Run Checklist\n\n### Mandatory Scenarios\n- [ ] Creating new app/example\n- [ ] Cloning repository (first time)\n- [ ] After major dependency updates\n- [ ] Before starting new feature\n- [ ] After merge conflicts in config files\n\n### Optional but Recommended\n- [ ] Weekly (catches drift)\n- [ ] Before production deploy\n- [ ] After CSS-related changes\n\n---\n\n## Checklist For Different Stages\n\n### Stage 1: Project Init (New App)\n```bash\n# Required checks\n✓ Tokens exist\n✓ Tailwind CLI installed\n✓ Build script defined\n```\n\n### Stage 2: First Build\n```bash\n# Add to Stage 1\n✓ CSS builds successfully\n✓ Tokens in CSS\n✓ Assets configured\n```\n\n### Stage 3: Development Ready\n```bash\n# Add to Stage 2\n✓ Server starts\n✓ CSS served\n✓ HTML links CSS\n✓ data-theme present\n```\n\n### Stage 4: Production Ready\n```bash\n# Add to Stage 3\n✓ Styles apply in browser\n✓ No console errors\n✓ Performance acceptable\n```\n\n---\n\n## Integration With CI CD\n\n### GitHub Actions Example\n```yaml\nname: Validate Setup\n\non: [push, pull_request]\n\njobs:\n  validate:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n      - run: npm install\n      - run: ./scripts/validate-setup.sh\n```\n\n---\n\n## Troubleshooting Guide\n\n### Common Failure: \"CSS builds but has no tokens\"\n\n**Diagnosis:**\n```bash\ncat style/globals.css | grep \"@import.*tokens\"\n# Shows: @import \"../../wrong/path/tokens.css\"\n```\n\n**Fix:**\n```css\n/* Use absolute path */\n@import \"/opt/docker/monorepo/opensource/canonrs/crates/rs-design/style/tokens.css\";\n```\n\n---\n\n### Common Failure: \"CSS not served at runtime\"\n\n**Diagnosis:**\n```bash\ncurl -I http://localhost:3003/workbench.css\n# Returns: 404 Not Found\n```\n\n**Fix:**\n```toml\n# Add to Leptos.toml\n[site]\nassets-dir = \"public\"\n```\n\n---\n\n### Common Failure: \"data-theme missing\"\n\n**Diagnosis:**\n```bash\ncurl -s http://localhost:3003 | grep data-theme\n# Returns: (empty)\n```\n\n**Fix:** Implement ThemeProvider DOM sync (Rule #65)\n\n---\n\n## Related Rules\n\n- **Rule #62:** Single Source of Truth for Design Tokens\n- **Rule #63:** Leptos Reactivity\n- **Rule #64:** CSS Build Pipeline\n- **Rule #65:** data-theme Sync\n\n---\n\n## Normative Status\n\n- All new apps **MUST** pass checklist before first commit\n- PRs adding new examples **MUST** include passing validation\n- CI **SHOULD** run validation on every push\n- Checklist script **MUST** be in repository\n\n---\n\n**Author:** Canon Working Group  \n**Replaces:** None\n\n---\n\n## Economic Impact\n\n**Time saved per new app:** ~4 hours  \n**Frequency:** Every new workbench/example  \n**Annual savings (10 apps):** ~40 hours\n\n**Root causes eliminated:**\n- ❌ \"Why is page white?\" (70% of issues)\n- ❌ Hours debugging missing CSS pipeline\n- ❌ Assumption that cargo leptos handles everything\n- ❌ Discovering problems late in development\n- ❌ Incomplete setup blocking progress\n\n**ROI:** Extremely high (prevents most common setup failures)"},{"number":67,"slug":"leptos-csr-css-loading","title":"Leptos CSR Does NOT Load CSS via `<Stylesheet />`","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["leptos","csr","css","ssr"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"In CSR mode, Leptos does not inject CSS using the Stylesheet component. Applications relying on it fail to load styles because no HTML link tag is created.","problem":"stylesheet component does not inject css in csr mode","solution":"load css via html link tags instead of stylesheet component in csr","signals":["no styles loaded","stylesheet ignored","unstyled csr app"],"search_intent":"why leptos stylesheet not working in csr","keywords":["leptos csr stylesheet issue","css not loading leptos csr","leptos meta stylesheet csr","html link css leptos"],"body":"---\n\n## Principle\n\nIn Client-Side Rendering (CSR) mode, `leptos_meta::<Stylesheet />` is **NOT** the source of truth for global CSS.\n\n> **CSR apps mount to existing HTML.**  \n> `<Stylesheet />` only works with SSR hydration.\n\n---\n\n## The Problem\n\nDevelopers coming from SSR frameworks assume:\n```rust\nview! {\n    <Stylesheet href=\"/styles.css\"/>\n    <App />\n}\n```\n\nWill inject `<link>` tags into the document. **In CSR, it does not.**\n\n**Why:**\n- CSR uses `mount_to_body()` which replaces `<body>` content\n- `<Stylesheet />` generates meta tags during SSR render\n- Without SSR, there's no server to inject the `<link>`\n- The component renders but produces no DOM effect\n\n**Result:** CSS never loads, app renders unstyled.\n\n---\n\n## Forbidden Pattern\n```rust\n// ❌ NEVER IN CSR\nuse leptos_meta::*;\n\n#[component]\npub fn App() -> impl IntoView {\n    provide_meta_context();\n    \n    view! {\n        <Stylesheet href=\"/styles.css\"/>  // Does nothing in CSR\n        <div class=\"bg-primary\">\n            \"Hello\"\n        </div>\n    }\n}\n```\n\n**This compiles. It runs. It silently fails.**\n\n---\n\n## Canonical Pattern\n\n### Option 1: HTML `<link>` (Recommended)\n```html\n<!-- index.html -->\n<!DOCTYPE html>\n<html>\n<head>\n    <link data-trunk rel=\"css\" href=\"dist/styles.css\" />\n</head>\n<body></body>\n</html>\n```\n\n### Option 2: Direct `<link>` in HTML (No Trunk)\n```html\n<!DOCTYPE html>\n<html>\n<head>\n    <link rel=\"stylesheet\" href=\"/styles.css\" />\n</head>\n<body></body>\n</html>\n```\n\n**Key difference:**\n- `data-trunk` → Trunk handles hashing and copying\n- Direct `<link>` → Manual cache busting required\n\n---\n\n## When Stylesheet Works\n\n### SSR with Hydration\n```rust\n// ✅ WORKS IN SSR\nuse leptos::*;\nuse leptos_meta::*;\n\n#[component]\npub fn App() -> impl IntoView {\n    provide_meta_context();\n    \n    view! {\n        <Stylesheet href=\"/styles.css\"/>  // Injected during SSR\n        <div>\"Hello\"</div>\n    }\n}\n\n// Server renders HTML with <link> already present\n// Client hydrates existing DOM\n```\n\n**Why it works:**\n- Server generates complete HTML including `<head>`\n- `<link>` exists before WASM loads\n- Client hydration preserves existing elements\n\n---\n\n## Detection Protocol\n\n**Symptom:** Styles not loading in CSR app\n\n**Verification:**\n```bash\n# 1. Check if using CSR\ngrep \"mount_to_body\" src/main.rs\n# If yes → CSR mode\n\n# 2. Check HTML source (before JS runs)\ncurl http://localhost:3001/ | grep \"stylesheet\"\n# Should see <link> in HTML\n# If not → CSS not loaded\n\n# 3. Check component code\ngrep \"Stylesheet\" src/lib.rs\n# If present in CSR → WRONG\n```\n\n---\n\n## Correct CSR Configuration\n\n### `main.rs`\n```rust\nuse leptos::mount::mount_to_body;\n\nfn main() {\n    console_error_panic_hook::set_once();\n    mount_to_body(App);\n}\n```\n\n### `index.html`\n```html\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>App</title>\n    <link data-trunk rel=\"rust\" data-bin=\"app-name\" />\n    <link data-trunk rel=\"css\" href=\"dist/styles.css\" />\n</head>\n<body></body>\n</html>\n```\n\n### `lib.rs`\n```rust\nuse leptos::prelude::*;\n\n#[component]\npub fn App() -> impl IntoView {\n    // NO provide_meta_context() needed for CSS\n    // NO <Stylesheet /> needed\n    \n    view! {\n        <div class=\"bg-primary text-white p-4\">\n            \"Styled content\"\n        </div>\n    }\n}\n```\n\n---\n\n## Framework Comparison\n\n| Framework | CSR CSS Method | SSR CSS Method |\n|-----------|---------------|----------------|\n| Leptos CSR | HTML `<link>` | `<Stylesheet />` |\n| Leptos SSR | HTML `<link>` | `<Stylesheet />` (both work) |\n| SolidStart | HTML `<link>` | `<Link />` component |\n| Next.js | `globals.css` import | Same |\n\n**Pattern:** CSR always requires HTML-level CSS loading.\n\n---\n\n## Canonical Justification\n\n> **Components that mount after HTML loads cannot modify `<head>`.**  \n> This is browser behavior, not framework limitation.\n\nCSR applications must:\n- Load critical CSS before JavaScript\n- Prevent flash of unstyled content (FOUC)\n- Work with disabled JavaScript (progressive enhancement)\n\nThe `<link>` in HTML is the only guarantee.\n\n---\n\n## Canon References\n\n- Canon Rule #47 — Asset Must Exist in Final dist/\n- Canon Rule #68 — CSS Build is Explicit, Never Implicit\n- Canon Rule #71 — Debug Theme by Verifying File First"},{"number":68,"slug":"asset-must-exist-in-dist","title":"Asset Must Exist in Final dist/","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["assets","dist","build"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Referencing assets that do not exist in the final dist directory causes silent runtime failures. Build outputs must be physically verified before deployment.","problem":"assets referenced in app do not exist in final build output","solution":"verify all referenced assets physically exist in dist before runtime","signals":["404 asset","missing css file","silent build failure"],"search_intent":"how to ensure assets exist in dist build","keywords":["dist assets verification","missing css dist","build output validation","trunk asset missing"],"body":"---\n\n## Principle\n\nEvery asset referenced by the application **MUST** physically exist in the final `dist/` directory that is served to users.\n\n> **Build determinism requires physical verification.**  \n> References without artifacts are contract violations.\n\n---\n\n## The Problem\n\nApplications often reference assets that:\n\n- Were never generated\n- Were generated in the wrong location\n- Failed silently during build\n- Exist in source but not in output\n\n**Result:** 404 errors, missing styles, broken functionality — all silent until runtime.\n\n---\n\n## Forbidden Patterns\n\n```html\n<!-- ❌ NEVER ALLOWED -->\n<link href=\"/styles.css\" />\n<!-- But dist/styles.css does not exist -->\n\n<script src=\"/app.js\"></script>\n<!-- But build failed silently -->\n```\n\n### Why this is forbidden\n\n- Silent 404s break functionality without errors\n- Build tools can fail without surfacing issues\n- Developers assume \"the tool handles it\"\n- CI/CD passes but production is broken\n\n---\n\n## Mandatory Verification Checklist\n\n**Before considering any build successful:**\n\n```bash\n# 1. Asset physically exists\nls dist/styles*.css || exit 1\n\n# 2. Asset is served correctly\ncurl -I http://localhost:3001/styles-HASH.css | grep \"200 OK\"\n\n# 3. Asset contains expected content\ngrep \".dark\" dist/styles*.css || exit 1\ngrep \"bg-background\" dist/styles*.css || exit 1\n```\n\n**If ANY check fails → build is INVALID.**\n\n---\n\n## Enforcement Rules\n\n### Rule 1: No Phantom References\n\nEvery `<link>`, `<script>`, or asset reference must point to a file that exists in `dist/`.\n\n### Rule 2: Explicit Build Steps\n\nAsset generation must be:\n\n- Explicitly defined in build pipeline\n- Verifiable before runtime\n- Failed loudly if unsuccessful\n\n### Rule 3: Pre-Runtime Validation\n\nCI/CD must verify asset existence:\n\n```yaml\n# Example CI check\n- name: Verify assets\n  run: |\n    test -f dist/styles.css || exit 1\n    test -f dist/app.js || exit 1\n```\n\n---\n\n## Canonical Pattern\n\n### Build Pipeline (Correct)\n\n```json\n{\n  \"scripts\": {\n    \"build:css\": \"tailwindcss -i style/globals.css -o dist/styles.css\",\n    \"build:app\": \"trunk build\",\n    \"verify\": \"test -f dist/styles.css && grep '.dark' dist/styles.css\"\n  }\n}\n```\n\n### Trunk Configuration (Correct)\n\n```toml\n[[hooks]]\nstage = \"pre_build\"\ncommand = \"npm\"\ncommand_arguments = [\"run\", \"build:css\"]\n\n[build]\ndist = \"dist\"\n```\n\n### HTML Reference (Correct)\n\n```html\n<!-- ✅ CANONICAL -->\n<link data-trunk rel=\"css\" href=\"dist/styles.css\" />\n```\n\n**Trunk will:**\n\n1. Copy `dist/styles.css` to `dist/styles-HASH.css`\n2. Update HTML reference automatically\n3. Asset is guaranteed to exist\n\n---\n\n## Debugging Protocol\n\nWhen assets are missing, follow this exact order:\n\n```bash\n# 1. Verify physical file\nls -lh dist/styles.css\n# If missing → build failed\n\n# 2. Verify build command ran\nnpm run build:css\n# Check for errors\n\n# 3. Verify output location\n# Is CSS being generated to dist/ or somewhere else?\n\n# 4. Verify trunk configuration\ncat Trunk.toml\n# Does it reference the correct path?\n```\n\n---\n\n## Real World Case Study\n\n**Symptom:** Dark mode toggle not working  \n**Investigation:** Checked tokens, signals, Effect, DOM manipulation  \n**Actual cause:** `styles.css` returned 404 — file never existed\n\n**Time wasted:** 2+ hours on wrong layer  \n**Fix time:** 2 minutes once asset verified\n\n**Lesson:** Always verify the asset FIRST.\n\n---\n\n## Canonical Justification\n\n> **Assets that don't exist can't fail loudly.**  \n> They fail silently at runtime, in production, in front of users.\n\nEnterprise systems require:\n\n- Deterministic builds\n- Verifiable outputs\n- Fail-fast validation\n- No phantom dependencies\n\nThis rule enforces physical reality over assumptions.\n\n---\n\n## Canon References\n\n- Canon Rule #69 — CSS Build is Explicit, Never Implicit\n- Canon Rule #70 — Trunk Only Serves What's in dist/\n- Canon Rule #71 — CSS Pipeline Requires Health Checks"},{"number":69,"slug":"trunk-only-serves-dist","title":"Trunk Only Serves What's in dist/","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["trunk","dist","assets"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Trunk serves only files present in the dist directory, ignoring source folders like public or style. Misunderstanding this leads to missing assets at runtime.","problem":"assets outside dist are not served causing 404 errors","solution":"ensure all assets are built or copied into dist before serving","signals":["404 styles.css","public not served","asset not found"],"search_intent":"why trunk not serving files from public folder","keywords":["trunk dist assets","leptos trunk css 404","trunk asset pipeline","dist only serving trunk"],"body":"---\n\n## Principle\n\nTrunk serves **ONLY** the content that exists in the final `dist/` directory.\n\n> **`public/`, `style/`, `assets/` are NOT served automatically.**  \n> If it's not in `dist/`, it doesn't exist to the browser.\n\n---\n\n## The Problem\n\nDevelopers assume Trunk works like Vite or Webpack Dev Server:\n- \"Put assets in `public/` and they're served\"\n- \"CSS in `style/` is automatically available\"\n- \"Trunk will find it\"\n\n**Wrong.** Trunk has a different model:\n\n1. Processes `index.html` and its `data-trunk` directives\n2. Copies/generates assets to `dist/`\n3. Serves **only** `dist/`\n\n**Result:** Assets in `public/styles.css` return 404 because they're not in `dist/`.\n\n---\n\n## Forbidden Assumptions\n```bash\n# ❌ WRONG ASSUMPTIONS\nproject/\n├─ public/\n│  └─ styles.css          # \"Trunk will serve this\" ← NO\n├─ style/\n│  └─ globals.css         # \"Available at /style/globals.css\" ← NO\n├─ assets/\n│  └─ logo.png            # \"Browser can access this\" ← NO\n└─ dist/                  # Only this directory is served\n```\n\n### What Actually Happens\n```bash\n# Request\nGET /styles.css\n\n# Trunk looks for\ndist/styles.css           # Not public/styles.css\n\n# Result\n404 Not Found\n```\n\n---\n\n## Canonical Asset Flow\n\n### 1. Source Files (NOT Served)\n```\nproject/\n├─ style/\n│  └─ globals.css         # Source\n├─ assets/\n│  └─ logo.png            # Source\n```\n\n### 2. Build Process\n```toml\n# Trunk.toml\n[[hooks]]\nstage = \"pre_build\"\ncommand = \"npm\"\ncommand_arguments = [\"run\", \"build:css\"]\n```\n```json\n// package.json\n{\n  \"scripts\": {\n    \"build:css\": \"tailwindcss -i style/globals.css -o dist/styles.css\"\n  }\n}\n```\n\n### 3. Final Output (Served)\n```\ndist/\n├─ styles-4aad46ee.css    # Generated by Trunk from dist/styles.css\n├─ logo-a3f2b1c4.png      # Copied by Trunk if referenced\n├─ index.html\n└─ app-HASH.wasm\n```\n\n**Only files in `dist/` are accessible to browsers.**\n\n---\n\n## Trunk Directives Explained\n\n### CSS Assets\n```html\n<!-- index.html -->\n<link data-trunk rel=\"css\" href=\"dist/styles.css\" />\n```\n\n**What happens:**\n1. Trunk reads `dist/styles.css` (must exist before Trunk runs)\n2. Copies it to `dist/styles-HASH.css`\n3. Updates HTML reference to hashed version\n4. Browser requests `/styles-HASH.css` (exists in `dist/`)\n\n### Static Assets\n```html\n<link data-trunk rel=\"copy-dir\" href=\"public\" />\n```\n\n**What happens:**\n1. Trunk copies entire `public/` directory to `dist/`\n2. `public/logo.png` becomes `dist/logo.png`\n3. Browser can request `/logo.png`\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Generating to `public/`\n```json\n// ❌ WRONG\n{\n  \"scripts\": {\n    \"build:css\": \"tailwindcss -o public/styles.css\"\n  }\n}\n```\n\n**Problem:** `public/styles.css` is never copied to `dist/`.\n\n**Fix:**\n```json\n// ✅ CORRECT\n{\n  \"scripts\": {\n    \"build:css\": \"tailwindcss -o dist/styles.css\"\n  }\n}\n```\n\n### Mistake 2: Assuming `style/` is Served\n```html\n<!-- ❌ WRONG -->\n<link href=\"/style/globals.css\" />\n```\n\n**Problem:** `style/` is a source directory, not served.\n\n**Fix:**\n```html\n<!-- ✅ CORRECT -->\n<link data-trunk rel=\"css\" href=\"dist/styles.css\" />\n```\n\n### Mistake 3: Direct References Without Trunk\n```html\n<!-- ❌ WRONG in Trunk projects -->\n<link href=\"/styles.css\" />\n```\n\n**Problem:** No cache busting, no copy to `dist/`.\n\n**Fix:**\n```html\n<!-- ✅ CORRECT -->\n<link data-trunk rel=\"css\" href=\"dist/styles.css\" />\n```\n\n---\n\n## Verification Protocol\n\n### Check 1: Physical File Exists\n```bash\nls dist/styles.css\n# Must exist BEFORE trunk serve\n```\n\n### Check 2: File is Served\n```bash\ncurl -I http://localhost:3001/styles-HASH.css\n# Should return 200 OK\n```\n\n### Check 3: Source Files NOT Served\n```bash\ncurl -I http://localhost:3001/style/globals.css\n# Should return 404 (correct)\n\ncurl -I http://localhost:3001/public/styles.css\n# Should return 404 (correct)\n```\n\n---\n\n## Canonical Build Pipeline\n```bash\n# 1. Generate assets to dist/\nnpm run build:css\n# Output: dist/styles.css\n\n# 2. Trunk processes and serves\ntrunk serve --port 3001\n# Reads: dist/styles.css\n# Creates: dist/styles-HASH.css\n# Serves: only dist/\n```\n\n---\n\n## Architecture Diagram\n```\nSource Layer (NOT served)\n├─ style/globals.css\n├─ assets/logo.png\n└─ public/\n\n        ↓ Build Process\n        \nBuild Layer (MUST exist before Trunk)\n└─ dist/\n   ├─ styles.css\n   └─ logo.png\n\n        ↓ Trunk Processing\n        \nServed Layer (Browser accessible)\n└─ dist/\n   ├─ styles-HASH.css\n   ├─ logo-HASH.png\n   ├─ index.html\n   └─ app-HASH.wasm\n```\n\n---\n\n## Canonical Justification\n\n> **Trunk is a static asset bundler, not a development server.**  \n> It transforms and serves `dist/`, nothing else.\n\nThis constraint ensures:\n- **Deterministic builds** — output is always `dist/`\n- **Cache busting** — assets get content hashes\n- **Production parity** — dev and prod serve same structure\n- **Clear separation** — source vs. output\n\nTrying to serve `public/` or `style/` directly breaks this model.\n\n---\n\n## Canon References\n\n- Canon Rule #68 — Asset Must Exist in Final dist/\n- Canon Rule #70 — CSS Pipeline Requires Health Checks\n- Canon Rule #67 — Leptos CSR Does NOT Load CSS via `<Stylesheet />`"},{"number":70,"slug":"css-pipeline-health-checks","title":"CSS Pipeline Requires Health Checks","status":"ENFORCED","severity":"MEDIUM","category":"build-tooling","tags":["css","validation","pipeline"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"CSS build pipelines can fail silently, producing empty or incomplete outputs. Automated validation is required to guarantee correctness before runtime.","problem":"css pipeline failures are silent and only detected at runtime","solution":"implement automated checks to validate css existence content and size","signals":["empty css file","missing classes","silent failure"],"search_intent":"how to validate css build pipeline automatically","keywords":["css pipeline validation","tailwind verify build","css health check script","missing css classes detection"],"body":"---\n\n## Principle\n\nEvery example, application, and component library **MUST** have automated validation that CSS is correctly built and contains expected content.\n\n> **Untested pipelines fail silently.**  \n> Health checks are not optional.\n\n---\n\n## The Problem\n\nCSS build pipelines fail in subtle ways:\n- Build command runs but outputs nothing\n- File is generated but is empty\n- Classes are missing due to purge errors\n- Theme tokens not compiled\n- Path resolution fails silently\n\n**Without validation, these failures only surface at runtime in production.**\n\n---\n\n## Forbidden Pattern\n```json\n// ❌ NO VALIDATION\n{\n  \"scripts\": {\n    \"build:css\": \"tailwindcss -i style/globals.css -o dist/styles.css\"\n  }\n}\n```\n\n**Problem:** If this fails, subsequent steps still run.\n\n---\n\n## Mandatory Health Check Script\n```json\n// ✅ REQUIRED\n{\n  \"scripts\": {\n    \"build:css\": \"tailwindcss -i style/globals.css -o dist/styles.css\",\n    \"verify:css\": \"node scripts/verify-css.js\"\n  }\n}\n```\n\n### Minimum Verification Script\n```javascript\n// scripts/verify-css.js\nimport { readFileSync, statSync } from 'fs';\nimport { exit } from 'process';\n\nconst CSS_PATH = 'dist/styles.css';\nconst REQUIRED_CLASSES = [\n  '.bg-background',\n  '.text-foreground',\n  '.dark'\n];\n\n// Check 1: File exists\ntry {\n  statSync(CSS_PATH);\n} catch {\n  console.error(`❌ ${CSS_PATH} does not exist`);\n  exit(1);\n}\n\n// Check 2: File is not empty\nconst stats = statSync(CSS_PATH);\nif (stats.size === 0) {\n  console.error(`❌ ${CSS_PATH} is empty`);\n  exit(1);\n}\n\n// Check 3: Required classes present\nconst css = readFileSync(CSS_PATH, 'utf-8');\nfor (const className of REQUIRED_CLASSES) {\n  if (!css.includes(className)) {\n    console.error(`❌ ${CSS_PATH} missing required class: ${className}`);\n    exit(1);\n  }\n}\n\nconsole.log('✅ CSS health check passed');\n```\n\n---\n\n## Bash Alternative\n```bash\n#!/bin/bash\n# scripts/verify-css.sh\n\nCSS_FILE=\"dist/styles.css\"\n\n# Check 1: File exists\nif [ ! -f \"$CSS_FILE\" ]; then\n  echo \"❌ $CSS_FILE does not exist\"\n  exit 1\nfi\n\n# Check 2: File is not empty\nif [ ! -s \"$CSS_FILE\" ]; then\n  echo \"❌ $CSS_FILE is empty\"\n  exit 1\nfi\n\n# Check 3: Required classes\nREQUIRED=(\n  \".bg-background\"\n  \".text-foreground\"\n  \".dark\"\n)\n\nfor class in \"${REQUIRED[@]}\"; do\n  if ! grep -q \"$class\" \"$CSS_FILE\"; then\n    echo \"❌ Missing class: $class\"\n    exit 1\n  fi\ndone\n\necho \"✅ CSS health check passed\"\n```\n\n---\n\n## Integration Points\n\n### Pre-Serve Hook\n```toml\n# Trunk.toml\n[[hooks]]\nstage = \"pre_build\"\ncommand = \"npm\"\ncommand_arguments = [\"run\", \"build:css\"]\n\n[[hooks]]\nstage = \"pre_build\"\ncommand = \"bash\"\ncommand_arguments = [\"scripts/verify-css.sh\"]\n```\n\n**Effect:** Trunk refuses to serve if CSS validation fails.\n\n### CI/CD Integration\n```yaml\n# .github/workflows/ci.yml\n- name: Build CSS\n  run: npm run build:css\n\n- name: Verify CSS\n  run: npm run verify:css\n\n- name: Build App\n  run: trunk build\n```\n\n**Effect:** CI fails fast if CSS is broken.\n\n### Make Target\n```makefile\n# Makefile\n.PHONY: verify\nverify:\n\t@bash scripts/verify-css.sh\n\n.PHONY: dev\ndev: verify\n\tnpm run watch:css & trunk serve --port 3001\n```\n\n**Effect:** Dev server won't start with broken CSS.\n\n---\n\n## Comprehensive Health Check\n\nFor design systems and critical apps:\n```javascript\n// scripts/verify-css.js (comprehensive)\nimport { readFileSync, statSync } from 'fs';\n\nconst CSS_PATH = 'dist/styles.css';\n\nconst checks = [\n  {\n    name: 'File exists',\n    test: () => {\n      try {\n        statSync(CSS_PATH);\n        return true;\n      } catch {\n        return false;\n      }\n    }\n  },\n  {\n    name: 'File is not empty',\n    test: () => statSync(CSS_PATH).size > 0\n  },\n  {\n    name: 'Contains theme tokens',\n    test: () => {\n      const css = readFileSync(CSS_PATH, 'utf-8');\n      return css.includes('--color-background') &&\n             css.includes('--color-foreground');\n    }\n  },\n  {\n    name: 'Contains utility classes',\n    test: () => {\n      const css = readFileSync(CSS_PATH, 'utf-8');\n      return ['.bg-background', '.text-foreground', '.flex', '.p-4']\n        .every(c => css.includes(c));\n    }\n  },\n  {\n    name: 'Dark mode variant exists',\n    test: () => {\n      const css = readFileSync(CSS_PATH, 'utf-8');\n      return css.includes('.dark');\n    }\n  },\n  {\n    name: 'No duplicate declarations',\n    test: () => {\n      const css = readFileSync(CSS_PATH, 'utf-8');\n      const bgCount = (css.match(/\\.bg-background\\{/g) || []).length;\n      return bgCount === 1;\n    }\n  },\n  {\n    name: 'File size reasonable',\n    test: () => {\n      const size = statSync(CSS_PATH).size;\n      return size > 1000 && size < 1000000; // 1KB - 1MB\n    }\n  }\n];\n\nlet failed = false;\nfor (const check of checks) {\n  const passed = check.test();\n  console.log(passed ? `✅ ${check.name}` : `❌ ${check.name}`);\n  if (!passed) failed = true;\n}\n\nif (failed) {\n  console.error('\\n❌ CSS health check failed');\n  process.exit(1);\n}\n\nconsole.log('\\n✅ All CSS health checks passed');\n```\n\n---\n\n## Example Specific Checks\n\nDifferent apps require different validations:\n\n### Component Library\n```javascript\nconst REQUIRED = [\n  '.btn', '.btn-primary', '.btn-secondary',\n  '.card', '.card-header', '.card-body',\n  '.input', '.input-error'\n];\n```\n\n### Landing Page\n```javascript\nconst REQUIRED = [\n  '.hero', '.section', '.cta',\n  '.bg-gradient', '.text-gradient'\n];\n```\n\n### Design System\n```javascript\nconst REQUIRED_TOKENS = [\n  '--spacing-1', '--spacing-2', '--spacing-4',\n  '--color-primary', '--color-secondary',\n  '--radius-sm', '--radius-md'\n];\n```\n\n---\n\n## Failure Examples\n\n### Silent Build Failure\n```bash\n$ npm run build:css\n# No output, no error\n$ ls dist/styles.css\n# File doesn't exist\n```\n\n**Without health check:** Continues to `trunk serve`, app loads unstyled  \n**With health check:** Fails immediately with clear error\n\n### Empty Output\n```bash\n$ npm run build:css\n# Runs successfully\n$ cat dist/styles.css\n# Empty file\n```\n\n**Without health check:** App loads, no styles applied  \n**With health check:** Detects empty file, fails build\n\n### Missing Classes\n```bash\n$ npm run build:css\n# Tailwind purges too aggressively\n$ grep \".dark\" dist/styles.css\n# No results\n```\n\n**Without health check:** Dark mode silently broken  \n**With health check:** Fails with \"Missing class: .dark\"\n\n---\n\n## Canonical Justification\n\n> **Build pipelines are code.**  \n> Code without tests is broken by default.\n\nCSS build failures are:\n- **Silent** — no runtime errors\n- **Subtle** — partial functionality works\n- **Persistent** — caching hides issues\n- **Production-critical** — affects all users\n\nHealth checks are the only way to guarantee correctness.\n\n---\n\n## Canon References\n\n- Canon Rule #68 — Asset Must Exist in Final dist/\n- Canon Rule #69 — Trunk Only Serves What's in dist/\n- Canon Rule #71 — Debug Theme by Verifying File First"},{"number":71,"slug":"debug-theme-verify-file-first","title":"Debug Theme by Verifying File First","status":"ENFORCED","severity":"MEDIUM","category":"governance","tags":["debugging","css","theme"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Most theming issues originate from missing or broken CSS files, not application logic. Debugging must start by verifying file existence and loading.","problem":"developers debug application logic before verifying css file existence","solution":"always validate css file existence serving and content before debugging code","signals":["missing css","404 stylesheet","theme not applying"],"search_intent":"how to debug theme issues in leptos","keywords":["debug css file first","leptos theme debugging","missing css file fix","verify stylesheet loading"],"body":"---\n\n## Principle\n\nBefore debugging tokens, signals, Effects, or DOM manipulation in theme-related issues, **ALWAYS** verify that the CSS file exists and was loaded correctly.\n\n> **The filesystem is the source of truth.**  \n> Code can lie. Files cannot.\n\n---\n\n## The Problem\n\nWhen dark mode, theming, or styling doesn't work, developers instinctively debug:\n- React/Leptos state management\n- CSS-in-JS token generation\n- DOM class application\n- JavaScript event handlers\n- Framework reactivity systems\n\n**95% of the time, the CSS file is missing or broken.**\n\n---\n\n## Mandatory Debug Order\n\n### Step 1: Verify File Exists (Filesystem)\n```bash\nls -lh dist/styles*.css\n```\n\n**Expected:** File exists with reasonable size (> 1KB)  \n**If missing:** Build pipeline is broken, stop here\n\n### Step 2: Verify File is Served (HTTP)\n```bash\ncurl -I http://localhost:3001/styles-HASH.css\n```\n\n**Expected:** `200 OK`  \n**If 404:** Asset not in dist/ or Trunk not serving correctly\n\n### Step 3: Verify File Contents (Grep)\n```bash\ngrep \".dark\" dist/styles*.css\ngrep \"bg-background\" dist/styles*.css\ngrep \"--color-background\" dist/styles*.css\n```\n\n**Expected:** All required classes/tokens present  \n**If missing:** Tailwind purge or compilation issue\n\n### Step 4: Verify Browser Loaded CSS (DevTools)\n```javascript\n// Browser console\ndocument.styleSheets.length\n// Should be > 0\n\nArray.from(document.styleSheets).map(s => s.href)\n// Should include styles-HASH.css\n\nArray.from(document.styleSheets)\n  .flatMap(s => Array.from(s.cssRules))\n  .find(r => r.selectorText === '.dark')\n// Should return a CSSStyleRule\n```\n\n**Expected:** Stylesheet loaded and contains `.dark` rule  \n**If missing:** CSS file exists but wasn't linked in HTML\n\n### Step 5: ONLY THEN Debug Application Code\n```javascript\n// Now it's safe to check application logic\ndocument.documentElement.classList.contains('dark')\n// Is the class being applied?\n\ngetComputedStyle(document.documentElement).backgroundColor\n// Does it change when .dark is toggled?\n```\n\n---\n\n## Decision Tree\n```\nTheme not working\n  │\n  ├─→ ls dist/styles.css → Missing?\n  │     └─→ FIX: Build pipeline (Rule #68, #69, #70)\n  │\n  ├─→ curl /styles-HASH.css → 404?\n  │     └─→ FIX: Asset not in dist/ or wrong Trunk config\n  │\n  ├─→ grep \".dark\" dist/styles.css → Not found?\n  │     └─→ FIX: Tailwind config or purge issue\n  │\n  ├─→ document.styleSheets.length → 0?\n  │     └─→ FIX: HTML missing <link> tag\n  │\n  └─→ All above pass?\n        └─→ NOW debug application code\n```\n\n---\n\n## Real World Case Study\n\n### Reported Issue\n\"Dark mode toggle doesn't work in Leptos app\"\n\n### Developer's Initial Investigation (2 hours wasted)\n1. Checked `Effect::new` logic ✓\n2. Verified `classList.add(\"dark\")` is called ✓\n3. Tested signal reactivity ✓\n4. Inspected DOM mutations ✓\n5. Checked CSS variable definitions ✓\n6. Tried different browsers ✓\n\n### Actual Root Cause (found in 30 seconds)\n```bash\n$ curl -I http://localhost:3001/styles.css\nHTTP/1.1 404 Not Found\n```\n\n**File never existed. All debugging was on the wrong layer.**\n\n---\n\n## Automated Verification Script\n```bash\n#!/bin/bash\n# scripts/debug-theme.sh\n\necho \"=== Theme Debug Protocol ===\"\necho \"\"\n\n# Step 1: File exists\necho \"1️⃣ Checking if CSS file exists...\"\nif ls dist/styles*.css 1>/dev/null 2>&1; then\n  SIZE=$(ls -lh dist/styles*.css | awk '{print $5}')\n  echo \"   ✅ File exists ($SIZE)\"\nelse\n  echo \"   ❌ CSS file missing in dist/\"\n  echo \"   → Run: npm run build:css\"\n  exit 1\nfi\n\n# Step 2: File is served\necho \"\"\necho \"2️⃣ Checking if CSS is served...\"\nHASH_FILE=$(ls dist/styles*.css | head -1 | xargs basename)\nHTTP_CODE=$(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3001/$HASH_FILE)\nif [ \"$HTTP_CODE\" = \"200\" ]; then\n  echo \"   ✅ CSS served correctly (200 OK)\"\nelse\n  echo \"   ❌ CSS returns $HTTP_CODE\"\n  echo \"   → Check if trunk serve is running\"\n  exit 1\nfi\n\n# Step 3: Required classes present\necho \"\"\necho \"3️⃣ Checking for required theme classes...\"\nREQUIRED=(\".dark\" \".bg-background\" \"--color-background\")\nfor class in \"${REQUIRED[@]}\"; do\n  if grep -q \"$class\" dist/styles*.css; then\n    echo \"   ✅ Found: $class\"\n  else\n    echo \"   ❌ Missing: $class\"\n    echo \"   → Check Tailwind config or globals.css\"\n    exit 1\n  fi\ndone\n\necho \"\"\necho \"✅ All file-level checks passed\"\necho \"\"\necho \"4️⃣ Now check browser (open DevTools console):\"\necho \"   document.styleSheets.length\"\necho \"   Array.from(document.styleSheets).map(s => s.href)\"\necho \"   document.documentElement.classList.toggle('dark')\"\n```\n\n**Usage:**\n```bash\nbash scripts/debug-theme.sh\n```\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Debugging Framework First\n```rust\n// ❌ WRONG PRIORITY\nEffect::new(move |_| {\n    // Spending hours here when CSS doesn't exist\n    web_sys::console::log_1(&\"Effect running\".into());\n    let is_dark = dark_mode.get();\n    // ...\n});\n```\n\n**Correct approach:**\n```bash\n# ✅ VERIFY FILE FIRST\nls dist/styles.css && grep \".dark\" dist/styles.css\n# Only then debug Effect\n```\n\n### Mistake 2: Trusting Build Logs\n```bash\n$ npm run build:css\n✓ Done in 142ms\n```\n\n**Assumption:** \"It says 'Done', so it worked\"  \n**Reality:** Could be empty file or wrong output location\n\n**Correct approach:**\n```bash\nnpm run build:css && ls -lh dist/styles.css\n```\n\n### Mistake 3: Assuming Hot Reload Works\n\n**Developer:** \"I changed globals.css, saved, but nothing changed\"  \n**Assumption:** Hot reload is broken\n\n**Reality:**\n```bash\n$ ls -lh dist/styles.css\n-rw-r--r-- 1 user user 5.2K Jan 08 20:15 dist/styles.css\n# File timestamp hasn't changed → build didn't run\n```\n\n**Correct approach:**\n```bash\n# Verify file was regenerated\nnpm run build:css && ls -lh dist/styles.css\n```\n\n---\n\n## Browser Based Verification\n\n### Check 1: Stylesheet Count\n```javascript\nconsole.log('Loaded stylesheets:', document.styleSheets.length);\n// Expected: At least 1\n```\n\n### Check 2: Stylesheet URLs\n```javascript\nArray.from(document.styleSheets).forEach(sheet => {\n  console.log('Stylesheet:', sheet.href);\n});\n// Expected: Should include styles-HASH.css\n```\n\n### Check 3: Dark Mode Rule Exists\n```javascript\nconst darkRule = Array.from(document.styleSheets)\n  .flatMap(s => {\n    try {\n      return Array.from(s.cssRules);\n    } catch {\n      return [];\n    }\n  })\n  .find(r => r.selectorText === '.dark');\n\nconsole.log('Dark mode rule:', darkRule);\n// Expected: CSSStyleRule object\n```\n\n### Check 4: Computed Styles\n```javascript\nconst html = document.documentElement;\n\n// Before\nconsole.log('Before:', getComputedStyle(html).backgroundColor);\n\n// Toggle\nhtml.classList.toggle('dark');\n\n// After\nconsole.log('After:', getComputedStyle(html).backgroundColor);\n// Expected: Different values\n```\n\n---\n\n## Integration With Development Workflow\n\n### Before Starting Work\n```bash\nmake verify  # Runs CSS health checks\nmake dev     # Only starts if verify passes\n```\n\n### During Development\n```bash\n# Terminal 1: Watch CSS\nnpm run watch:css\n\n# Terminal 2: Monitor output\nwatch -n 1 ls -lh dist/styles.css\n\n# Terminal 3: Dev server\ntrunk serve --port 3001\n```\n\n### When Issues Arise\n```bash\n# Don't debug code first\n# Run the debug protocol\nbash scripts/debug-theme.sh\n```\n\n---\n\n## Canonical Justification\n\n> **Debugging is deterministic.**  \n> Start with what can be verified objectively.\n\nThe filesystem and HTTP responses are:\n- **Deterministic** — files exist or they don't\n- **Observable** — can be inspected directly\n- **Independent** — not affected by framework bugs\n- **Fast** — verification takes seconds\n\nApplication code is:\n- **Stateful** — depends on runtime conditions\n- **Complex** — many moving parts\n- **Framework-dependent** — different debugging per framework\n- **Slow** — requires reproduction steps\n\n**Always verify the foundation before debugging the application.**\n\n---\n\n## Time Savings Analysis\n\n| Issue | Wrong Debug Path | Correct Debug Path | Time Saved |\n|-------|-----------------|-------------------|------------|\n| CSS 404 | 2h debugging Effect | 30s checking file | 1h 59m |\n| Empty CSS | 1h checking tokens | 10s checking file size | 50m |\n| Missing .dark | 1h debugging classList | 20s grepping file | 40m |\n| Wrong output path | 3h debugging Tailwind | 1m checking dist/ | 2h 59m |\n\n**Average time wasted per theme issue: 1-3 hours**  \n**Time with verification-first: < 5 minutes**\n\n---\n\n## Canon References\n\n- Canon Rule #68 — Asset Must Exist in Final dist/\n- Canon Rule #69 — Trunk Only Serves What's in dist/\n- Canon Rule #70 — CSS Pipeline Requires Health Checks\n- Canon Rule #67 — Leptos CSR Does NOT Load CSS via `<Stylesheet />`"},{"number":72,"slug":"layout-h1-prohibition","title":"Layout H1 Prohibition","status":"ENFORCED","severity":"MEDIUM","category":"accessibility","tags":["html","accessibility","semantics"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using multiple h1 elements across layout and page components breaks semantic structure and accessibility. Layouts must not define document identity elements.","problem":"layouts include h1 causing multiple h1 elements per page","solution":"restrict h1 usage to page components only and use alternative tags in layouts","signals":["multiple h1","seo issues","screen reader confusion"],"search_intent":"why only one h1 per page html","keywords":["html h1 best practice","multiple h1 accessibility issue","semantic heading structure","wcag heading hierarchy"],"body":"## Rule\n\n**Layouts MUST NOT contain `<h1>` elements. The `<h1>` tag is reserved exclusively for Page components.**\n\n## Classification\n\n| Aspect | Value |\n|--------|-------|\n| Category | HTML Semantics |\n| Severity | ❌ Critical |\n| Scope | Layouts |\n| Enforcement | Manual Review + Linting |\n\n## Rationale\n\n### Semantic HTML Hierarchy\n- One `<h1>` per page is a fundamental web standard\n- Multiple `<h1>` elements confuse:\n  - Screen readers\n  - Search engines\n  - Document outline algorithms\n\n### Separation of Concerns\n- **Layout** provides structure (shell)\n- **Page** provides content (identity)\n- `<h1>` is content identity, not structure\n\n## Anti Pattern\n```rust\n// ❌ WRONG - Layout with h1\n#[component]\npub fn AppLayout() -> impl IntoView {\n    view! {\n        <header>\n            <h1>\"My App\"</h1>  // ❌ Layout choosing h1\n        </header>\n        <main>\n            <Outlet/>\n        </main>\n    }\n}\n\n// Result: Page also has h1\n#[component]\npub fn DashboardPage() -> impl IntoView {\n    view! {\n        <h1>\"Dashboard\"</h1>  // Conflict! Multiple h1\n    }\n}\n```\n\n**Problems:**\n- Two `<h1>` elements on same page\n- Violates WCAG 2.1\n- SEO penalty\n- Screen reader confusion\n\n## Correct Pattern\n```rust\n// ✅ CORRECT - Layout without h1\n#[component]\npub fn AppLayout() -> impl IntoView {\n    view! {\n        <header role=\"banner\">\n            <div class=\"text-lg font-semibold\">\n                \"My App\"  // ✅ div, not h1\n            </div>\n        </header>\n        <main>\n            <Outlet/>\n        </main>\n    }\n}\n\n// Page owns the h1\n#[component]\npub fn DashboardPage() -> impl IntoView {\n    view! {\n        <h1>\"Dashboard\"</h1>  // ✅ Single h1 per page\n    }\n}\n```\n\n**Benefits:**\n- One `<h1>` per page (semantic)\n- Layout provides branding via `<div>` or `<h2>`\n- Page controls document identity\n\n## Alternatives For Layout Branding\n\n### Option 1: Plain `<div>` (Recommended)\n```rust\n<div class=\"text-lg font-semibold\">\"App Name\"</div>\n```\n\n### Option 2: `<h2>` with visual parity\n```rust\n<h2 class=\"text-lg font-semibold\">\"App Name\"</h2>\n```\n\n### Option 3: ARIA landmark\n```rust\n<div role=\"banner\" aria-label=\"Application branding\">\n    <span class=\"text-lg font-semibold\">\"App Name\"</span>\n</div>\n```\n\n## Detection\n\n### Manual Check\n```bash\n# Search for h1 in layouts\ngrep -r \"<h1\" src/layouts/\n```\n\n### Expected Output\n```\n# Should return: (no matches)\n```\n\n## Enforcement\n\n### Pre-commit Hook\n```bash\n#!/bin/bash\nif grep -r \"<h1\" src/layouts/; then\n    echo \"❌ Canon Rule #72 violated: h1 found in layout\"\n    exit 1\nfi\n```\n\n### Clippy Extension (Future)\n```rust\n// Deny h1 in layout scope\n#[deny(layout_h1)]\n```\n\n## Exceptions\n\n**None.** This rule has no exceptions.\n\n- Layouts never need `<h1>`\n- If you think you need it, you're mixing concerns\n\n## Related Rules\n\n- **Canon Rule #74**: Block Semantic HTML\n- **Canon Rule #31**: Accessibility Contract\n\n## References\n\n- [WCAG 2.1 - Headings and Labels](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels.html)\n- [MDN - Using HTML sections and outlines](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements)\n\n## Version\n\n- **Created**: 2026-01-12\n- **Status**: ✅ Active"},{"number":73,"slug":"componentpage-template-contract","title":"ComponentPage Template Contract","status":"ENFORCED","severity":"LOW","category":"component-architecture","tags":["design-system","template","ssot"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Reimplementing documentation page structure in product code creates duplication and inconsistency across components. Component pages must delegate structure to a centralized template to maintain a single source of truth.","problem":"component pages duplicate layout structure instead of using centralized template","solution":"use ComponentPage template and pass only data as props from product layer","signals":["duplicate layout code","inconsistent component pages","template drift"],"search_intent":"how to enforce template usage for","keywords":["componentpage template leptos","design system ssot template","avoid duplicated page layout","leptos component documentation pattern"],"body":"## Rule\n\n**Component documentation pages MUST use the `ComponentPage` template from the design system. Product code MUST only pass data as props, never reimplementing page structure.**\n\n## Classification\n\n| Aspect | Value |\n|--------|-------|\n| Category | Architecture |\n| Severity | ❌ Critical |\n| Scope | Component Documentation |\n| Enforcement | Code Review |\n\n## Rationale\n\n### Single Source of Truth\n- Template changes propagate to all component pages\n- 50+ pages → 1 file to maintain\n- Zero code duplication\n\n### Separation of Structure and Content\n- **Template** (rs-design): defines structure + blocks\n- **Page** (product): provides data only\n- Never mix concerns\n\n## Template Location\n```\nrs-design/\n├── src/blocks/canonrs/        # Blocks used by template\n│   ├── component_header.rs\n│   ├── symbol_badges.rs\n│   ├── usage_block.rs\n│   └── ...\n└── src/pages/canonrs/         # Template itself\n    └── component_page.rs\n```\n\n**Rule:** Template lives in design system, not product.\n\n## Anti Pattern\n```rust\n// ❌ WRONG - Reimplementing structure in product\n#[component]\npub fn ButtonPage() -> impl IntoView {\n    view! {\n        <div class=\"page\">\n            <div class=\"main\">\n                <h1>\"Button\"</h1>\n                <p>\"Description\"</p>\n                <div class=\"preview\">\n                    // Preview content\n                </div>\n                // ... more structure\n            </div>\n        </div>\n    }\n}\n```\n\n**Problems:**\n- Structure duplicated across 50+ pages\n- Template changes require 50+ file edits\n- Inconsistent styling\n- Token governance violation\n\n## Correct Pattern\n```rust\n// ✅ CORRECT - Using template\nuse rs_design::pages::canonrs::ComponentPage;\nuse rs_design::blocks::canonrs::*;\n\n#[component]\npub fn ButtonPage() -> impl IntoView {\n    let preview = view! {\n        <div class=\"flex gap-4\">\n            <Button>\"Primary\"</Button>\n            <Button variant=ButtonVariant::Outline>\"Outline\"</Button>\n        </div>\n    }.into_any();\n\n    view! {\n        <ComponentPage\n            name=\"Button\".to_string()\n            description=\"Primary action trigger\".to_string()\n            symbols=vec![\n                Symbol { \n                    label: \"SSR\".to_string(), \n                    variant: SymbolVariant::SSR \n                },\n            ]\n            when_to_use=vec![\"Primary actions\".to_string()]\n            when_not_to_use=vec![\"Navigation\".to_string()]\n            preview=preview\n        />\n    }\n}\n```\n\n**Benefits:**\n- Page only provides data\n- Template handles structure\n- Single point of change\n- Token governance maintained\n\n## Template API Contract\n\n### Required Props\n```rust\nname: String           // Component name\ndescription: String    // Short description\n```\n\n### Optional Props\n```rust\nsymbols: Option<Vec<Symbol>>              // Technical badges\npreview: Option<AnyView>                  // Component preview\nwhen_to_use: Option<Vec<String>>          // Use cases\nwhen_not_to_use: Option<Vec<String>>      // Anti-use cases\napi_props: Option<Vec<ApiProp>>           // API documentation\ncomparison: Option<Vec<ComparisonRow>>    // Technical comparison\nrules: Option<Vec<String>>                // Canon rules\nanti_patterns: Option<Vec<String>>        // Anti-patterns\n```\n\n## Preview Content Pattern\n\n### Converting View to AnyView\n```rust\n// Pattern: .into_any()\nlet preview = view! {\n    <div class=\"space-y-4\">\n        // Your preview content\n    </div>\n}.into_any();\n```\n\n**Rule:** Preview content must use `.into_any()` for type erasure.\n\n## Data Structures\n\n### Symbol\n```rust\nSymbol {\n    label: String,           // \"SSR\", \"Stable\", etc.\n    variant: SymbolVariant,  // Semantic variant\n}\n\nenum SymbolVariant {\n    SSR,\n    Hydration,\n    ClientOnly,\n    Stable,\n    Beta,\n}\n```\n\n### ApiProp\n```rust\nApiProp {\n    name: String,          // Prop name\n    prop_type: String,     // Rust type\n    description: String,   // Purpose\n    required: bool,        // Mandatory?\n}\n```\n\n### ComparisonRow\n```rust\nComparisonRow {\n    aspect: String,       // What's compared\n    traditional: String,  // Old way\n    canon: String,        // Canon way\n}\n```\n\n## Maintenance\n\n### Updating All Pages\n```bash\n# Change template once\nvim rs-design/src/pages/canonrs/component_page.rs\n\n# Rebuild\ncargo build\n\n# All 50+ pages updated automatically\n```\n\n### Adding New Block\n```rust\n// 1. Create block in rs-design\n// rs-design/src/blocks/canonrs/new_block.rs\n\n// 2. Add to template\n// rs-design/src/pages/canonrs/component_page.rs\n{new_data.map(|d| view! { <NewBlock data=d /> })}\n\n// 3. Pages opt-in via prop\nnew_data=vec![...]\n```\n\n## Enforcement\n\n### Code Review Checklist\n- [ ] Page imports `ComponentPage` from rs-design\n- [ ] Page only passes props (no structure)\n- [ ] Preview uses `.into_any()`\n- [ ] No duplicate HTML structure\n\n## Exceptions\n\n**None.** All component documentation pages use the template.\n\n- If template is insufficient, extend it\n- Never create parallel structure\n\n## Related Rules\n\n- **Canon Rule #74**: Block Semantic HTML\n- **Canon Rule #75**: Token Family Boundaries\n- **Canon Rule #59**: CSS Cascade Ownership\n\n## Version\n\n- **Created**: 2026-01-12\n- **Status**: ✅ Active"},{"number":74,"slug":"block-semantic-html","title":"Block Semantic HTML","status":"ENFORCED","severity":"HIGH","category":"accessibility","tags":["html","aria","wcag"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Improper HTML semantics in design system blocks break accessibility, screen reader navigation, and document hierarchy. Blocks must enforce semantic structure with ARIA attributes and correct heading levels.","problem":"blocks lack semantic html and proper aria leading to accessibility issues","solution":"use section landmarks aria-labelledby and strict heading hierarchy","signals":["axe violations","screen reader confusion","invalid heading hierarchy"],"search_intent":"how to implement accessible semantic html in components","keywords":["wcag semantic html blocks","aria labelledby section pattern","heading hierarchy accessibility","accessible table markup"],"body":"## Rule\n\n**Blocks MUST use semantic HTML with proper ARIA attributes, heading hierarchy, and accessible table markup. All documentation blocks MUST follow WCAG 2.1 guidelines.**\n\n## Classification\n\n| Aspect | Value |\n|--------|-------|\n| Category | HTML Semantics + A11y |\n| Severity | ❌ Critical |\n| Scope | Design System Blocks |\n| Enforcement | Manual Review + Axe |\n\n## Rationale\n\n### Accessibility is Not Optional\n- Screen readers depend on semantic structure\n- Keyboard navigation requires proper landmarks\n- SEO benefits from document outline\n\n### Enterprise Compliance\n- WCAG 2.1 AA compliance required\n- Legal requirements (ADA, Section 508)\n- Audit-ready markup\n\n## Block Structure Pattern\n\n### General Pattern\n```rust\n#[component]\npub fn SomeBlock() -> impl IntoView {\n    view! {\n        <section \n            class=\"block-class\" \n            aria-labelledby=\"block-heading\"\n        >\n            <h2 id=\"block-heading\" class=\"sr-only\">\n                \"Screen reader description\"\n            </h2>\n            \n            // Block content\n        </section>\n    }\n}\n```\n\n**Requirements:**\n- ✅ `<section>` for semantic grouping\n- ✅ `aria-labelledby` pointing to heading\n- ✅ `<h2>` with unique id\n- ✅ `.sr-only` for visual hiding\n\n## Heading Hierarchy\n\n### Correct Hierarchy\n```\n<h1> → Page title (ComponentHeader)\n  <h2> → Block titles (UsageBlock, ApiBlock, etc.)\n    <h3> → Sub-sections within blocks\n```\n\n### Anti-Pattern\n```rust\n// ❌ WRONG - Skipping h2\nview! {\n    <section>\n        <h3>\"API\"</h3>  // ❌ Should be h2\n    </section>\n}\n```\n\n### Correct Pattern\n```rust\n// ✅ CORRECT\nview! {\n    <section aria-labelledby=\"api-heading\">\n        <h2 id=\"api-heading\">\"API\"</h2>\n        \n        <div>\n            <h3>\"Props\"</h3>      // ✅ Nested correctly\n            <h3>\"Methods\"</h3>     // ✅ Same level\n        </div>\n    </section>\n}\n```\n\n## Table Semantics\n\n### Required Elements\n```rust\n<table>\n    <caption class=\"sr-only\">\n        \"Descriptive table purpose\"\n    </caption>\n    \n    <thead>\n        <tr>\n            <th scope=\"col\">Column 1</th>\n            <th scope=\"col\">Column 2</th>\n        </tr>\n    </thead>\n    \n    <tbody>\n        <tr>\n            <th scope=\"row\">Row header</th>\n            <td>Data</td>\n        </tr>\n    </tbody>\n</table>\n```\n\n**Rules:**\n- ✅ `<caption class=\"sr-only\">` always present\n- ✅ `scope=\"col\"` on column headers\n- ✅ `scope=\"row\"` on row headers\n- ✅ `<thead>` and `<tbody>` explicit\n\n### Anti-Pattern\n```rust\n// ❌ WRONG - No scope, no caption\n<table>\n    <tr>\n        <th>Name</th>      // ❌ Missing scope\n        <th>Type</th>\n    </tr>\n    <tr>\n        <td>variant</td>   // ❌ Should be th scope=\"row\"\n        <td>string</td>\n    </tr>\n</table>\n```\n\n### Correct Pattern\n```rust\n// ✅ CORRECT - Full semantics\n<table>\n    <caption class=\"sr-only\">\"Component API properties\"</caption>\n    <thead>\n        <tr>\n            <th scope=\"col\">\"Name\"</th>\n            <th scope=\"col\">\"Type\"</th>\n        </tr>\n    </thead>\n    <tbody>\n        <tr>\n            <th scope=\"row\">\"variant\"</th>\n            <td>\"string\"</td>\n        </tr>\n    </tbody>\n</table>\n```\n\n## Screen Reader Only Content\n\n### The `.sr-only` Utility\n```css\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border: 0;\n}\n```\n\n**Usage:**\n```rust\n<h2 class=\"sr-only\">\"Section description\"</h2>\n<caption class=\"sr-only\">\"Table purpose\"</caption>\n```\n\n**Rule:** Use `.sr-only` for semantic headings that aren't needed visually.\n\n## Block Examples\n\n### UsageBlock\n```rust\n#[component]\npub fn UsageBlock(\n    when_to_use: Vec<String>,\n    when_not_to_use: Vec<String>,\n) -> impl IntoView {\n    view! {\n        <section class=\"usage-block\" aria-labelledby=\"usage-heading\">\n            <h2 id=\"usage-heading\" class=\"sr-only\">\"Usage\"</h2>\n            \n            <div class=\"usage-section\">\n                <h3>\"When to use\"</h3>\n                <ul class=\"usage-text\">\n                    {when_to_use.into_iter().map(|item| {\n                        view! { <li>{item}</li> }\n                    }).collect_view()}\n                </ul>\n            </div>\n        </section>\n    }\n}\n```\n\n### ApiBlock\n```rust\n#[component]\npub fn ApiBlock(props: Vec<ApiProp>) -> impl IntoView {\n    view! {\n        <section class=\"api-block\" aria-labelledby=\"api-heading\">\n            <h2 id=\"api-heading\">\"API\"</h2>\n            \n            <table>\n                <caption class=\"sr-only\">\n                    \"Component API properties\"\n                </caption>\n                <thead>\n                    <tr>\n                        <th scope=\"col\">\"Prop\"</th>\n                        <th scope=\"col\">\"Type\"</th>\n                        <th scope=\"col\">\"Required\"</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    {props.into_iter().map(|prop| {\n                        view! {\n                            <tr>\n                                <th scope=\"row\"><code>{prop.name}</code></th>\n                                <td><code>{prop.prop_type}</code></td>\n                                <td>{if prop.required { \"Yes\" } else { \"No\" }}</td>\n                            </tr>\n                        }\n                    }).collect_view()}\n                </tbody>\n            </table>\n        </section>\n    }\n}\n```\n\n## Validation\n\n### Axe DevTools\n```bash\n# Install\nnpm install -D @axe-core/cli\n\n# Run\naxe http://localhost:3000/button\n```\n\n### Expected Output\n```\n✅ 0 violations found\n```\n\n### Manual Checklist\n- [ ] Each block is a `<section>`\n- [ ] Each section has `aria-labelledby`\n- [ ] Headings follow h1 → h2 → h3 hierarchy\n- [ ] Tables have `<caption class=\"sr-only\">`\n- [ ] Tables have `scope` on all headers\n- [ ] `.sr-only` content is meaningful\n\n## Exceptions\n\n**None.** All blocks must follow semantic HTML rules.\n\n## Related Rules\n\n- **Canon Rule #72**: Layout H1 Prohibition\n- **Canon Rule #31**: Accessibility Contract\n- **Canon Rule #76**: Accessibility Utilities Location\n\n## References\n\n- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)\n- [MDN - ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA)\n- [WebAIM - Screen Reader Testing](https://webaim.org/articles/screenreader_testing/)\n\n## Version\n\n- **Created**: 2026-01-12\n- **Status**: ✅ Active"},{"number":75,"slug":"primitive-css-prohibition","title":"Primitive CSS Prohibition","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["primitives","css","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Adding CSS or visual logic to primitives breaks architectural separation and portability. Primitives must remain pure structural HTML without styling or responsive logic.","problem":"primitives include css and viewport logic violating separation of concerns","solution":"restrict primitives to semantic html and move styling to ui layer","signals":["tailwind in primitives","viewport logic in context","style leakage"],"search_intent":"why primitives should not contain css","keywords":["primitive vs ui separation","no css in primitives","design system layering","leptos primitive architecture"],"body":"---\n\n## Principle\n\nPrimitives provide **semantic HTML structure only**. They MUST NOT contain:\n- CSS classes (Tailwind or custom)\n- Viewport logic (breakpoints, is_mobile)\n- Debug logging\n- Visual styling decisions\n\nThis separation ensures **portability**, **testability**, and **clean architectural boundaries**.\n\n---\n\n## Forbidden Patterns\n```rust\n// ❌ NEVER ALLOWED IN PRIMITIVES\n\n// CSS Classes\n#[component]\npub fn SidebarPrimitive(children: Children) -> impl IntoView {\n    view! {\n        <aside \n            class=\"w-64 transition-all duration-300 overflow-hidden\"\n            class:w-0=move || !ctx.open.get()\n        >\n            {children()}\n        </aside>\n    }\n}\n\n// Viewport Logic\npub struct SidebarContext {\n    pub is_mobile: bool,  // ❌ NO\n    pub breakpoint: String,  // ❌ NO\n}\n\n// Debug Logging\nimpl SidebarContext {\n    pub fn toggle(&self) {\n        console::log_1(&format!(\"Toggle called\"));  // ❌ NO\n        self.open.update(|open| *open = !*open);\n    }\n}\n```\n\n### Why This is Forbidden\n\n1. **CSS in Primitives breaks portability**\n   - Tailwind classes tie primitive to specific CSS framework\n   - Width/animations are visual decisions, not structure\n   - Makes primitive unusable without Tailwind loaded\n\n2. **Viewport logic breaks separation of concerns**\n   - Primitives don't know about screens, devices, or breakpoints\n   - Layout layer handles responsive behavior\n   - Prevents SSR/testing without browser globals\n\n3. **Logging breaks production builds**\n   - Debug code leaks into production\n   - No feature flag control\n   - Violates \"primitives are dumb HTML\" principle\n\n---\n\n## Canonical Architecture\n\n### Primitive Layer (HTML Only)\n```rust\n// ✅ CORRECT PRIMITIVE\n\n#[component]\npub fn SidebarPrimitive(children: Children) -> impl IntoView {\n    view! {\n        <aside data-sidebar=\"\">\n            {children()}\n        </aside>\n    }\n}\n\npub struct SidebarContext {\n    pub open: RwSignal<bool>,\n    pub variant: SidebarVariant,\n}\n\nimpl SidebarContext {\n    pub fn toggle(&self) {\n        self.open.update(|open| *open = !*open);\n    }\n}\n```\n\n**Primitives provide:**\n- Semantic HTML tags (`<aside>`, `<nav>`, `<button>`)\n- `data-*` attributes for state (`data-open`, `data-active`)\n- Structural context (open/closed state)\n\n**Primitives do NOT provide:**\n- Visual appearance\n- Responsive behavior\n- Animation timing\n- Debug information\n\n---\n\n### UI Layer (Styling + Tokens)\n```rust\n// ✅ CORRECT UI COMPONENT\n\n#[component]\npub fn Sidebar(children: Children) -> impl IntoView {\n    let ctx = expect_context::<SidebarContext>();\n    \n    view! {\n        <SidebarPrimitive>\n            <div \n                class=\"sidebar-root\"\n                data-open=move || if ctx.open.get() { \"true\" } else { \"false\" }\n                style=\"\n                    width: var(--sidebar-width);\n                    padding: var(--space-lg);\n                    background: var(--color-bg-surface);\n                    transition: width var(--motion-duration-normal);\n                \"\n            >\n                {children()}\n            </div>\n        </SidebarPrimitive>\n    }\n}\n```\n\n**UI layer provides:**\n- All CSS (classes, inline styles, tokens)\n- Responsive wrappers (if needed)\n- Animation timing\n- Visual state binding (`data-open=\"true\"`)\n\n---\n\n## Enforcement Checklist\n\n### Primitive Files Must Have\n\n- [ ] Only HTML tags\n- [ ] Only `data-*` attributes\n- [ ] Zero CSS classes\n- [ ] Zero inline `style=\"\"`\n- [ ] Zero viewport/breakpoint logic\n- [ ] Zero console.log or debug output\n- [ ] Structural context only (open/closed, active/inactive)\n\n### UI Files Must Have\n\n- [ ] Consumes primitive as base\n- [ ] Applies all tokens\n- [ ] Handles visual state\n- [ ] Manages responsive behavior\n\n---\n\n## State Representation\n\n### ✅ Correct (Primitive)\n```rust\n// State via data-attribute\n<button \n    data-sidebar-menu-button=\"\"\n    data-active=is_active\n>\n```\n\n### ❌ Incorrect (Primitive)\n```rust\n// Visual styling in primitive\n<button \n    class=\"bg-primary text-white\"\n    class:bg-gray=!is_active\n>\n```\n\n---\n\n## Real World Example\n\n### Before (Violates Rule)\n```rust\n// ❌ PRIMITIVE with CSS\n#[component]\npub fn SidebarPrimitive(children: Children) -> impl IntoView {\n    let ctx = expect_context::<SidebarContext>();\n    \n    view! {\n        <aside\n            class:w-64=move || ctx.open.get()\n            class:w-0=move || !ctx.open.get()\n            class=\"transition-all duration-300\"\n        >\n            {children()}\n        </aside>\n    }\n}\n```\n\n**Problems:**\n- Width decision in primitive\n- Tailwind dependency\n- Animation timing hardcoded\n- No token usage\n\n### After (Compliant)\n```rust\n// ✅ PRIMITIVE: Structure only\n#[component]\npub fn SidebarPrimitive(children: Children) -> impl IntoView {\n    view! {\n        <aside data-sidebar=\"\">\n            {children()}\n        </aside>\n    }\n}\n\n// ✅ UI: Styling with tokens\n#[component]\npub fn Sidebar(children: Children) -> impl IntoView {\n    let ctx = expect_context::<SidebarContext>();\n    \n    view! {\n        <SidebarPrimitive>\n            <div \n                class=\"sidebar-root\"\n                data-open=move || if ctx.open.get() { \"true\" } else { \"false\" }\n            >\n                {children()}\n            </div>\n        </SidebarPrimitive>\n    }\n}\n```\n\n**CSS (separate file):**\n```css\n.sidebar-root[data-open=\"true\"] {\n    width: var(--sidebar-width);\n    transition: width var(--motion-duration-normal);\n}\n\n.sidebar-root[data-open=\"false\"] {\n    width: 0;\n}\n```\n\n---\n\n## Exceptions\n\n**NONE.**\n\nThis rule has no exceptions. Even for:\n- \"Quick prototypes\"\n- \"Internal components\"\n- \"It's just one class\"\n\nIf styling is needed, move to UI layer.\n\n---\n\n## Violation Consequences\n\n1. **Immediate:** Component cannot be styled independently\n2. **Short-term:** Breaks reusability across projects\n3. **Long-term:** Design system becomes unmaintainable\n4. **Enterprise:** Cannot pass audit/review\n\n---\n\n## Canonical Justification\n\nPrimitives are **contracts**, not implementations.\n\nA primitive that dictates visual appearance:\n- Cannot be themed\n- Cannot be customized\n- Cannot be tested without DOM\n- Cannot be ported to non-Tailwind projects\n\n**Canon mandates:** Structure and style are orthogonal concerns.\n\n---\n\n## Canon References\n\n- Canon Rule #74 — Block Semantic HTML\n- Canon Rule #76 — Navigation vs Action Contract\n- Canon Rule #81 — Flex Layout Ownership\n\n---\n\n## Related Symptoms\n\nIf you see:\n- Tailwind classes in `primitives/` folder\n- `is_mobile` or `breakpoint` in context\n- `console.log` in primitive toggle functions\n\n→ **This rule is violated.**\n\nGo to: **SYMPTOMS.md → PRIMITIVE VIOLATIONS**"},{"number":76,"slug":"navigation-vs-action-contract","title":"Navigation vs Action Component Contract","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["html","a11y","semantics"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Mixing navigation and action semantics creates invalid HTML and accessibility issues. Components must strictly separate link navigation from button actions.","problem":"anchors nested in buttons causing invalid html and interaction conflicts","solution":"separate navigation components using a tags and action components using button tags","signals":["button link nesting","keyboard navigation issues","aria conflicts"],"search_intent":"when to use a vs button","keywords":["a vs button html semantics","invalid nested interactive elements","navigation vs action components","accessibility button link rules"],"body":"---\n\n## Principle\n\n**Navigation uses `<a>`.**\n**Actions use `<button>`.**\n\nNEVER nest anchor tags inside buttons (`<button><a>`). This creates invalid HTML, breaks accessibility, and violates semantic web standards.\n\nComponents MUST be architecturally separated by their intent:\n- **Navigation** → Changes URL → `<a href>`\n- **Action** → Triggers behavior → `<button>`\n\n---\n\n## Forbidden Patterns\n```rust\n// ❌ NEVER ALLOWED\n\n#[component]\npub fn SidebarMenuButton(children: Children) -> impl IntoView {\n    view! {\n        <button data-sidebar-menu-button=\"\">\n            <a href=\"/tokens\">  // ❌ INVALID HTML\n                {children()}\n            </a>\n        </button>\n    }\n}\n```\n\n### Why This is Forbidden\n\n1. **HTML Validity**\n   - W3C specification prohibits interactive content inside buttons\n   - Browsers handle click events inconsistently\n   - Some browsers fire both button and link events\n\n2. **Accessibility**\n   - Screen readers announce \"button link\" (ambiguous)\n   - Keyboard navigation breaks (Tab? Enter? Space?)\n   - ARIA roles conflict\n\n3. **Click Event Conflicts**\n   - `onClick` on button fires\n   - `href` navigation triggers\n   - JavaScript event.preventDefault() workarounds required\n   - Race conditions in event bubbling\n\n4. **SEO Impact**\n   - Search engines ignore nested links\n   - Site structure becomes invisible\n   - Crawlers cannot follow navigation\n\n---\n\n## Canonical Architecture\n\n### Separate Components by Intent\n```rust\n// ✅ NAVIGATION COMPONENT\n#[component]\npub fn SidebarMenuLink(\n    href: String,\n    children: Children,\n    #[prop(default = false)] is_active: bool,\n) -> impl IntoView {\n    view! {\n        \n            href=href\n            data-sidebar-menu-link=\"\"\n            data-active=if is_active { \"true\" } else { \"false\" }\n            style=\"\n                display: block;\n                height: var(--nav-item-height);\n                padding: var(--space-sm) var(--space-md);\n                color: var(--color-fg-default);\n                text-decoration: none;\n            \"\n        >\n            {children()}\n        </a>\n    }\n}\n\n// ✅ ACTION COMPONENT\n#[component]\npub fn SidebarMenuButton(\n    children: Children,\n    on_click: Callback<()>,\n    #[prop(default = false)] is_active: bool,\n) -> impl IntoView {\n    view! {\n        <button\n            data-sidebar-menu-button=\"\"\n            data-active=if is_active { \"true\" } else { \"false\" }\n            on:click=move |_| on_click.call(())\n            style=\"\n                display: block;\n                height: var(--nav-item-height);\n                padding: var(--space-sm) var(--space-md);\n            \"\n        >\n            {children()}\n        </button>\n    }\n}\n```\n\n---\n\n## Usage Decision Tree\n```\nDoes this component change the URL?\n│\n├─ YES → Use <a> (SidebarMenuLink)\n│         Examples: /dashboard, /settings, /profile\n│\n└─ NO → Does it trigger an action?\n         │\n         ├─ YES → Use <button> (SidebarMenuButton)\n         │         Examples: toggle sidebar, open modal, submit form\n         │\n         └─ NO → Use <div> or <span> (informational only)\n```\n\n---\n\n## Real World Example\n\n### Before (Violates Rule)\n```rust\n// ❌ HTML INVALID\n<SidebarMenu>\n    <SidebarMenuItem>\n        <SidebarMenuButton>\n            <A href=\"/tokens\">Tokens</A>  // ❌ button > a\n        </SidebarMenuButton>\n    </SidebarMenuItem>\n</SidebarMenu>\n```\n\n**Problems:**\n- Invalid HTML\n- Accessibility broken\n- Click events conflict\n- Cannot style link independently\n\n### After (Compliant)\n```rust\n// ✅ CORRECT: Separate components\n<SidebarMenu>\n    <SidebarMenuItem>\n        <SidebarMenuLink href=\"/tokens\">\n            Tokens\n        </SidebarMenuLink>\n    </SidebarMenuItem>\n    \n    <SidebarMenuItem>\n        <SidebarMenuButton on_click=handle_collapse>\n            Collapse All\n        </SidebarMenuButton>\n    </SidebarMenuItem>\n</SidebarMenu>\n```\n\n---\n\n## Token Application\n\nBoth components share tokens but differ semantically:\n```rust\n// Navigation (Link)\nstyle=\"\n    height: var(--nav-item-height);\n    padding: var(--space-sm) var(--space-md);\n    color: var(--color-fg-default);\n    background: var(--color-bg-surface);\n    text-decoration: none;  // ← Link-specific\n\"\n\n// Action (Button)\nstyle=\"\n    height: var(--nav-item-height);\n    padding: var(--space-sm) var(--space-md);\n    color: var(--color-fg-default);\n    background: var(--color-bg-surface);\n    cursor: pointer;  // ← Button-specific\n\"\n```\n\n**Shared tokens:** height, padding, color, background\n**Different properties:** text-decoration vs cursor\n\n---\n\n## Accessibility Requirements\n\n### Navigation Links MUST Have\n\n- [ ] Valid `href` attribute\n- [ ] Clear, descriptive text (not \"click here\")\n- [ ] `aria-current=\"page\"` when active\n- [ ] Keyboard accessible (Tab)\n- [ ] Focus visible\n\n### Action Buttons MUST Have\n\n- [ ] Descriptive text or `aria-label`\n- [ ] `type=\"button\"` (prevents form submission)\n- [ ] `disabled` when action unavailable\n- [ ] Keyboard accessible (Tab + Enter/Space)\n- [ ] Focus visible\n\n---\n\n## Enforcement Checklist\n\n- [ ] No `<a>` inside `<button>`\n- [ ] No `<button>` inside `<a>`\n- [ ] Navigation components use `href`\n- [ ] Action components use `on:click`\n- [ ] Separate component names (Link vs Button)\n- [ ] Shared styling via tokens, not inheritance\n\n---\n\n## Component Naming Convention\n```rust\n// ✅ CORRECT NAMING\nSidebarMenuLink      // Navigation\nSidebarMenuButton    // Action\n\nCardLink             // Navigation\nCardButton           // Action\n\nDropdownLink         // Navigation\nDropdownButton       // Action\n```\n\n**Rule:** Component name MUST reflect HTML element used.\n\n---\n\n## Exceptions\n\n**NONE.**\n\nThis rule has no exceptions. Even for:\n- \"Just this one screen\"\n- \"Our designer said so\"\n- \"It looks the same\"\n\nHTML validity is non-negotiable.\n\n---\n\n## Violation Consequences\n\n1. **Immediate:** Browser console warnings\n2. **Short-term:** Screen reader users cannot navigate\n3. **Long-term:** Fails WCAG 2.1 compliance\n4. **Enterprise:** Legal liability (ADA violations)\n\n---\n\n## Testing\n```rust\n// ✅ Test navigation\nassert!(page.find(\"a[href='/tokens']\").exists());\nassert_eq!(page.find(\"a[href='/tokens']\").text(), \"Tokens\");\n\n// ✅ Test action\npage.find(\"button[data-sidebar-trigger]\").click();\nassert!(sidebar.is_collapsed());\n```\n\n---\n\n## Canonical Justification\n\nThe web has two fundamental interactions:\n1. **Go somewhere** (navigation)\n2. **Do something** (action)\n\nMixing them creates **ambiguity at every level**:\n- Semantic (what does this do?)\n- Technical (which event fires?)\n- Legal (is this accessible?)\n\n**Canon mandates:** One intent, one element.\n\n---\n\n## Canon References\n\n- Canon Rule #74 — Block Semantic HTML\n- Canon Rule #75 — Primitive CSS Prohibition\n- Canon Rule #31 — Accessibility Contract\n\n---\n\n## Related Symptoms\n\nIf you see:\n- `<button><a>` or `<a><button>` in DOM inspector\n- \"Interactive element inside interactive element\" console warning\n- Screen reader announces \"button link\"\n- Keyboard navigation skips elements\n\n→ **This rule is violated.**\n\nGo to: **SYMPTOMS.md → HTML VALIDITY VIOLATIONS**"},{"number":77,"slug":"monorepo-path-stability","title":"Monorepo Path Stability","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["monorepo","paths","config"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Hardcoded paths in monorepo configurations break builds during refactors and environment changes. Path resolution must be relative and dynamically calculated.","problem":"hardcoded paths break when monorepo structure changes","solution":"calculate paths relative to config files and avoid absolute paths","signals":["file not found after refactor","broken imports","postcss path errors"],"search_intent":"how to manage paths in monorepo configurations","keywords":["monorepo path resolution","relative path config js","postcss path issues","cargo workspace path dependency"],"body":"---\n\n## Principle\n\nConfiguration files MUST NOT hardcode internal monorepo paths.\n\nWhen packages move within the monorepo (e.g., `crates/` → `packages-rust/`), ALL configuration references must update simultaneously.\n\n**Critical insight:** Paths are relative, not absolute. Configuration must calculate paths from its own location, not assume global structure.\n\n---\n\n## Problem Statement\n\n### What Happens During Package Moves\n```bash\n# Initial structure\nmonorepo/\n├─ crates/\n│  └─ rs-design/\n└─ examples/\n   └─ workbench/\n      ├─ Cargo.toml  (path = \"../../crates/rs-design\")\n      └─ postcss.config.js  (path = \"../../crates/rs-design/style\")\n\n# After refactor\nmonorepo/\n├─ packages-rust/  ← MOVED\n│  └─ rs-design/\n└─ examples/\n   └─ workbench/\n      ├─ Cargo.toml  (path = \"../../crates/rs-design\")  ❌ BROKEN\n      └─ postcss.config.js  (../../crates/...)          ❌ BROKEN\n```\n\n**Result:**\n- Build fails with \"file not found\"\n- PostCSS cannot resolve imports\n- Developer spends hours debugging\n\n---\n\n## Forbidden Patterns\n\n### Hardcoded Absolute Paths\n```javascript\n// ❌ NEVER ALLOWED\nexport default {\n  plugins: {\n    'postcss-import': {\n      resolve: (id) => {\n        return '/opt/docker/monorepo/crates/rs-design/style/tokens.css';\n      }\n    }\n  }\n}\n```\n\n**Problems:**\n- Machine-specific path\n- Breaks on CI/CD\n- Breaks on other developers' machines\n- Survives find/replace during refactor\n\n### Deep Relative Paths Without Context\n```toml\n# ❌ FRAGILE\n[dependencies]\nrs-design = { path = \"../../../../../crates/rs-design\" }\n```\n\n**Problems:**\n- Hard to audit during refactor\n- Easy to miss in search\n- No indication of what `../../../../../` points to\n\n---\n\n## Canonical Architecture\n\n### Path Calculation Strategy\n```javascript\n// ✅ CORRECT: Calculate from current file\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport default {\n  plugins: {\n    'postcss-import': {\n      resolve: (id) => {\n        if (id === '@canonrs/tailwind/tokens.css') {\n          // Calculate relative to THIS config file\n          return path.resolve(__dirname, '../../../../../packages-rust/rs-design/style/tokens.css');\n        }\n        return id;\n      }\n    }\n  }\n}\n```\n\n**Benefits:**\n- Relative to config location\n- Works on any machine\n- Clear refactor path\n\n---\n\n## Configuration Update Checklist\n\nWhen moving packages, update ALL of:\n\n### 1. Cargo.toml Dependencies\n```toml\n# Find all references\n[dependencies]\nrs-design = { path = \"../../crates/rs-design\" }  # ← Update this\n\n# Search command\ngrep -r 'path.*rs-design' examples/*/Cargo.toml\n```\n\n### 2. PostCSS Config\n```javascript\n// postcss.config.js\nresolve: (id) => {\n  if (id === '@canonrs/tailwind/tokens.css') {\n    return path.resolve(__dirname, '../../crates/rs-design/...');  // ← Update\n  }\n}\n```\n\n### 3. CSS Imports\n```css\n/* tailwind.css */\n@import \"../../crates/rs-design/style/canonrs.css\";  /* ← Update */\n```\n\n### 4. Package.json Scripts\n```json\n{\n  \"scripts\": {\n    \"copy-tokens\": \"cp ../../crates/rs-design/tokens.json public/\"  // ← Update\n  }\n}\n```\n\n### 5. Workspace Configuration\n```toml\n# Root Cargo.toml\n[workspace]\nmembers = [\n    \"crates/rs-design\",  # ← Update\n    \"examples/workbench\"\n]\n```\n\n---\n\n## Systematic Update Procedure\n```bash\n# Step 1: Identify all hardcoded paths\ngrep -r \"crates/rs-design\" . --include=\"*.toml\" --include=\"*.js\" --include=\"*.css\"\n\n# Step 2: Calculate new relative paths\n# From each config file to new location\n\n# Step 3: Update in batches by file type\nfind . -name \"Cargo.toml\" -exec sed -i 's|crates/rs-design|packages-rust/rs-design|g' {} +\nfind . -name \"postcss.config.js\" -exec sed -i 's|crates/rs-design|packages-rust/rs-design|g' {} +\nfind . -name \"*.css\" -exec sed -i 's|crates/rs-design|packages-rust/rs-design|g' {} +\n\n# Step 4: Verify all references updated\ngrep -r \"crates/rs-design\" . --include=\"*.toml\" --include=\"*.js\" --include=\"*.css\"\n# Should return 0 results\n\n# Step 5: Test each example builds\ncd examples/minimal && cargo build\ncd examples/workbench && cargo build\ncd examples/leptos-tailwindv4 && cargo build\n```\n\n---\n\n## Path Calculation Examples\n\n### From Workbench to rs-design\n```\nmonorepo/\n├─ packages-rust/\n│  └─ rs-design/\n│     └─ src/\n└─ examples/\n   └─ _wip/\n      └─ workbench/\n         └─ Cargo.toml  ← Starting point\n\nRelative path: ../../../../../packages-rust/rs-design\n```\n\n### Verification Command\n```bash\ncd examples/_wip/workbench\nls -la ../../../../../packages-rust/rs-design\n# Should list directory contents, not error\n```\n\n---\n\n## Real World Refactor: crates → packages-rust\n\n### Files That Needed Updates\n\n1. **3× Cargo.toml** (workbench, minimal, leptos-tailwindv4)  \n2. **1× postcss.config.js** (workbench)  \n3. **1× tailwind.css** (workbench)  \n4. **1× Root Cargo.toml** (workspace exclude)\n\n### Commands Used\n```bash\n# Update Cargo.toml files\nsed -i 's|path = \"../../crates/rs-design\"|path = \"../../../../packages-rust/rs-design\"|' examples/minimal/Cargo.toml\nsed -i 's|path = \"../../crates/rs-design\"|path = \"../../../../packages-rust/rs-design\"|' examples/leptos-tailwindv4/Cargo.toml\nsed -i 's|path = \"../../../crates/rs-design\"|path = \"../../../../../packages-rust/rs-design\"|' examples/_wip/workbench/Cargo.toml\n\n# Update PostCSS\nsed -i 's|../../../crates|../../../../packages-rust|g' examples/_wip/workbench/postcss.config.js\n\n# Update CSS imports\nsed -i 's|/opt/docker/monorepo/opensource/canonrs/crates/rs-design|/opt/docker/monorepo/packages-rust/rs-design|g' examples/_wip/workbench/style/tailwind.css\n\n# Remove old exclude\nsed -i 's|exclude = \\[\"crates/rs-design\", \"examples/_wip\"\\]|exclude = \\[\"examples/_wip\"\\]|' Cargo.toml\n```\n\n---\n\n## Prevention: Canonical Package References\n\n### Use Workspace Dependencies (Future)\n```toml\n# Root Cargo.toml\n[workspace.dependencies]\nrs-design = { path = \"packages-rust/rs-design\" }\n\n# Example Cargo.toml\n[dependencies]\nrs-design.workspace = true  # ← Single source of truth\n```\n\n### Use Package Aliases\n```javascript\nconst DESIGN_SYSTEM_PATH = path.resolve(__dirname, '../../../../../packages-rust/rs-design');\n\nresolve: (id) => {\n  if (id === '@canonrs/tailwind/tokens.css') {\n    return path.join(DESIGN_SYSTEM_PATH, 'style/tokens.css');\n  }\n}\n```\n\n---\n\n## Canonical Justification\n\n**Monorepos must survive refactoring.**\n\nA system that breaks when folders move:\n- Cannot scale beyond toy projects\n- Cannot integrate with enterprise tooling\n- Cannot be maintained by multiple teams\n\n**Canon mandates:** Path resilience through relative, calculated references.\n\n---\n\n## Related Symptoms\n\nIf you see:\n- \"File not found\" after moving packages\n- Build works for some examples, fails for others\n- PostCSS import resolution errors\n- `cargo metadata` failures\n\n→ **This rule is violated.**\n\nGo to: **SYMPTOMS.md → MONOREPO PATH VIOLATIONS**"},{"number":78,"slug":"token-definition-completeness","title":"Token Definition Completeness","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["tokens","theming","css"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Partial use of design tokens creates inconsistent UI and breaks theming. Components must fully adopt all applicable tokens without hardcoded values.","problem":"components use partial tokens and hardcoded values breaking consistency","solution":"enforce 100 percent token usage for all applicable properties","signals":["hardcoded values","theme inconsistencies","partial token usage"],"search_intent":"how to enforce design token usage in components","keywords":["design token completeness","no hardcoded css values","token based theming","css variables system design"],"body":"---\n\n## Principle\n\nEvery UI component MUST use **100% of applicable canonical and contextual tokens**.\n\nUsing only a subset of tokens creates **inconsistent visual systems**, breaks **theme inheritance**, and violates **Canon's deterministic design contract**.\n\nIf a token exists and applies to your component, it MUST be used.\n\n---\n\n## Token Categories\n\n### Canonical Tokens (Global)\n\n**Always applicable to all components:**\n```css\n/* Core */\nspace.xs/sm/md/lg/xl\nradius.sm/md/lg\nshadow.sm/md/lg\nborder.width.hairline/thin\nz.base/popover/dropdown/modal\n\n/* Color */\ncolor.bg.surface/muted/elevated\ncolor.fg.default/muted/inverted\ncolor.border.default/muted\ncolor.primary.bg/fg/border\ncolor.danger/success/warning.bg/fg\n\n/* Typography */\nfont.family.sans/mono\nfont.size.xs/sm/md/lg/xl\nfont.weight.regular/medium/semibold\nline.height.tight/normal/relaxed\n\n/* Motion */\nmotion.duration.fast/normal\nmotion.ease.standard/emphasized\n\n/* State */\nstate.hover.opacity\nstate.active.opacity\nstate.disabled.opacity\nstate.focus.ring\n```\n\n### Contextual Tokens (Family-Specific)\n\n**Family D (Navigation & Structure):**\n```css\nnav.item.height\nnav.indicator.thickness\nsidebar.width\npagination.size\n```\n\n**Family B (Forms & Input):**\n```css\ncontrol.height.sm/md/lg\ninput.padding\nfield.gap\n```\n\n*(Other families: A, C, E, F)*\n\n---\n\n## Forbidden Patterns\n```rust\n// ❌ INCOMPLETE TOKEN USAGE\n\n#[component]\npub fn Sidebar(children: Children) -> impl IntoView {\n    view! {\n        <div style=\"\n            padding: var(--space-lg);        // ✓ Used\n            background: var(--color-bg-surface);  // ✓ Used\n            width: 16rem;                    // ❌ Hardcoded, should use --sidebar-width\n        \">\n            {children()}\n        </div>\n    }\n}\n```\n\n### Why This is Forbidden\n\n1. **Breaks Theme Consistency**\n   - Hardcoded `16rem` doesn't respond to token changes\n   - Different components use different widths\n   - Theme switch doesn't affect this value\n\n2. **Prevents Customization**\n   - Enterprise customers cannot override\n   - Design iterations require code changes\n   - No central control point\n\n3. **Violates Canon Contract**\n   - Tokens exist to be used\n   - Selective usage = partial system\n   - Partial system = not a system\n\n---\n\n## Mandatory Token Application\n\n### Sidebar Component (Family D)\n```rust\n// ✅ 100% TOKEN USAGE\n\n#[component]\npub fn Sidebar(children: Children) -> impl IntoView {\n    let ctx = expect_context::<SidebarContext>();\n    \n    view! {\n        <SidebarPrimitive>\n            <div \n                class=\"sidebar-root\"\n                data-open=move || if ctx.open.get() { \"true\" } else { \"false\" }\n                style=\"\n                    /* Core */\n                    padding: var(--space-lg);\n                    border-radius: var(--radius-md);\n                    box-shadow: var(--shadow-md);\n                    z-index: var(--z-base);\n                    \n                    /* Color */\n                    background: var(--color-bg-surface);\n                    border-right: var(--border-width-hairline) solid var(--color-border-default);\n                    color: var(--color-fg-default);\n                    \n                    /* Typography */\n                    font-family: var(--font-family-sans);\n                    font-size: var(--font-size-sm);\n                    font-weight: var(--font-weight-regular);\n                    line-height: var(--line-height-normal);\n                    \n                    /* Motion */\n                    transition-duration: var(--motion-duration-normal);\n                    transition-timing-function: var(--motion-ease-standard);\n                    \n                    /* Family D */\n                    width: var(--sidebar-width);\n                \"\n            >\n                {children()}\n            </div>\n        </SidebarPrimitive>\n    }\n}\n```\n\n**Tokens Used:** 16/16 applicable ✓\n\n---\n\n## Token Applicability Matrix\n\n| Component Type | Canonical Tokens | Family Tokens | Total Required |\n|----------------|------------------|---------------|----------------|\n| Sidebar | Core, Color, Typography, Motion, State | Family D | ~18 tokens |\n| Button | Core, Color, Typography, Motion, State | Family A | ~15 tokens |\n| Input | Core, Color, Typography, State | Family B | ~12 tokens |\n| Card | Core, Color, Typography | - | ~10 tokens |\n| Modal | Core, Color, Motion, State | Family E | ~14 tokens |\n\n---\n\n## Token Definition Location\n\n### `:root` Declaration (Mandatory)\n```css\n/* canonrs.css or tokens.css */\n:root {\n  /* Family D Tokens */\n  --sidebar-width: 16rem;\n  --nav-item-height: 2.5rem;\n  --nav-indicator-thickness: 2px;\n  \n  /* Canonical Tokens */\n  --space-lg: 1.5rem;\n  --color-bg-surface: hsl(0 0% 100%);\n  --font-family-sans: system-ui, -apple-system, sans-serif;\n  --motion-duration-normal: 300ms;\n  /* ... all other tokens */\n}\n```\n\n**If token is not in `:root`, it does not exist.**\n\n---\n\n## Enforcement Checklist\n\n### Component Review\n\n- [ ] All applicable canonical tokens used\n- [ ] All applicable family tokens used\n- [ ] Zero hardcoded values (colors, sizes, timing)\n- [ ] All tokens exist in `:root`\n- [ ] Token values follow naming convention\n\n### Token Audit\n```bash\n# Find hardcoded values\ngrep -r \"16rem\\|300ms\\|#fff\\|rgba(\" src/ui/*.rs\n\n# Should return zero results\n```\n\n---\n\n## Real World Example: Before/After\n\n### Before (Violates Rule)\n```rust\n// ❌ Partial token usage\n<div style=\"\n    width: 16rem;                          // ❌ Hardcoded\n    padding: var(--space-lg);              // ✓\n    background: white;                     // ❌ Hardcoded\n    font-size: 14px;                       // ❌ Hardcoded\n    transition: all 300ms ease;            // ❌ Hardcoded\n\">\n```\n\n**Problems:**\n- 5 properties, 2 use tokens (40% compliant)\n- Theme switch won't affect hardcoded values\n- Customization requires code changes\n\n### After (Compliant)\n```rust\n// ✅ 100% token usage\n<div style=\"\n    width: var(--sidebar-width);           // ✓\n    padding: var(--space-lg);              // ✓\n    background: var(--color-bg-surface);   // ✓\n    font-size: var(--font-size-sm);        // ✓\n    transition-duration: var(--motion-duration-normal);  // ✓\n    transition-timing-function: var(--motion-ease-standard);  // ✓\n\">\n```\n\n**Result:** 6/6 properties use tokens (100% compliant)\n\n---\n\n## Missing Token Detection\n\n### Lint Rule (Future)\n```rust\n// Pseudo-code for lint\nfn check_token_usage(component: &Component) -> Result<()> {\n    let hardcoded = find_hardcoded_values(component);\n    let applicable_tokens = get_applicable_tokens(component.family);\n    let used_tokens = find_used_tokens(component);\n    \n    if used_tokens.len() < applicable_tokens.len() {\n        return Err(\"Incomplete token usage\");\n    }\n    \n    if !hardcoded.is_empty() {\n        return Err(format!(\"Hardcoded values found: {:?}\", hardcoded));\n    }\n    \n    Ok(())\n}\n```\n\n---\n\n## Token Naming Convention\n```\n{category}.{subcategory}.{variant}\n\nExamples:\ncolor.bg.surface       // Category: color, Sub: bg, Variant: surface\nspace.lg               // Category: space, Variant: lg (no sub)\nmotion.duration.fast   // Category: motion, Sub: duration, Variant: fast\nsidebar.width          // Category: sidebar, Variant: width (Family D)\n```\n\n---\n\n## Exceptions\n\n**Allowed hardcoded values (ONLY):**\n\n1. **Layout primitives:** `display: flex`, `position: relative`\n2. **Functional values:** `overflow: hidden`, `cursor: pointer`\n3. **Resets:** `margin: 0`, `padding: 0`\n\n**NOT allowed:**\n- Any dimension (width, height, padding, margin with value)\n- Any color (hex, rgb, named colors)\n- Any timing (ms, s)\n- Any typography (px, rem font sizes)\n\n---\n\n## Violation Consequences\n\n1. **Immediate:** Visual inconsistency across components\n2. **Short-term:** Theme switching partially broken\n3. **Long-term:** Design system becomes patchwork\n4. **Enterprise:** Cannot pass design audit\n\n---\n\n## Canonical Justification\n\nA token that exists but is not used is **technical debt**.\n\nA component that doesn't use tokens is **not part of the system**.\n\n**Canon mandates:** 100% or 0%. Partial usage is worse than no usage.\n\n---\n\n## Canon References\n\n- Canon Rule #7 — Token Governance\n- Canon Rule #21 — Canonical Color Tokens\n- Canon Rule #62 — Single Source of Truth Tokens\n- Canon Rule #75 — Primitive CSS Prohibition\n\n---\n\n## Related Symptoms\n\nIf you see:\n- Hardcoded `16rem`, `300ms`, `#ffffff` in component styles\n- Theme switch doesn't affect all components equally\n- Inconsistent spacing/colors across UI\n- `var(--token-name)` returns empty\n\n→ **This rule is violated.**\n\nGo to: **SYMPTOMS.md → TOKEN COMPLETENESS VIOLATIONS**"},{"number":79,"slug":"componentpage-template-contract","title":"ComponentPage Template Contract","status":"ENFORCED","severity":"LOW","category":"design-system","tags":["layout","template","css"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Using generic layout classes in templates causes inconsistent page structure and styling conflicts. ComponentPage must enforce canonical layout classes for centralized control.","problem":"generic classes break layout consistency across component pages","solution":"use canon-page and canon-rail classes as mandatory layout contract","signals":["content not centered","inconsistent page width","layout drift"],"search_intent":"how to enforce consistent layout in component templates","keywords":["canon page layout classes","centralized layout template","component page css contract","design system layout consistency"],"body":"---\n\n## Principle\n\n`ComponentPage` is a **template contract** that provides canonical layout structure for component documentation.\n\nIt MUST use:\n- `.canon-page` (outer container)\n- `.canon-rail` (content rail with max-width)\n\nIt MUST NOT use:\n- `.page` (generic, conflicts with other systems)\n- `.main` (semantic tag, not layout class)\n\nThis ensures **visual consistency** and **centralization** across all component documentation pages.\n\n---\n\n## Forbidden Patterns\n```rust\n// ❌ NEVER ALLOWED\n\n#[component]\npub fn ComponentPage(...) -> impl IntoView {\n    view! {\n        <div class=\"page\">  // ❌ Generic class name\n            <div class=\"main\">  // ❌ Semantic tag as class\n                {children.run()}\n            </div>\n        </div>\n    }\n}\n```\n\n### Why This is Forbidden\n\n1. **Name Conflicts**\n   - `.page` is too generic\n   - Clashes with application-level `.page` classes\n   - No namespace protection\n\n2. **Semantic Confusion**\n   - `.main` is an HTML tag (`<main>`)\n   - Using as class creates ambiguity\n   - Screen readers may misinterpret\n\n3. **No Centralization**\n   - Generic classes don't have associated CSS\n   - Each page implements own centering\n   - Inconsistent max-width across pages\n\n4. **Breaks Canon Convention**\n   - Canon uses `canon-*` prefix for system classes\n   - Unprefixed classes are application-level\n\n---\n\n## Canonical Architecture\n\n### Template Structure\n```rust\n// ✅ CORRECT TEMPLATE\n\n#[component]\npub fn ComponentPage(\n    name: String,\n    description: String,\n    #[prop(into)] children: ViewFn,\n    // ... other props\n) -> impl IntoView {\n    view! {\n        <div class=\"canon-page\">  // ← Outer container\n            <div class=\"canon-rail\">  // ← Content rail\n                <ComponentHeader name=name description=description />\n                \n                <PreviewBlock>\n                    {children.run()}\n                </PreviewBlock>\n                \n                <ApiBlock props=api_props />\n                <RulesBlock rules=rules anti_patterns=anti_patterns />\n            </div>\n        </div>\n    }\n}\n```\n\n### CSS Contract\n```css\n/* canonrs.css */\n\n.canon-page {\n    display: flex;\n    justify-content: center;\n    width: 100%;\n    padding: var(--space-lg);\n}\n\n.canon-rail {\n    width: 100%;\n    max-width: 72rem;  /* 1152px */\n    display: flex;\n    flex-direction: column;\n    gap: var(--space-xl);\n}\n```\n\n---\n\n## Layout Hierarchy\n```\nComponentPage\n└─ .canon-page (flex container, centers content)\n   └─ .canon-rail (max-width container)\n      ├─ ComponentHeader\n      ├─ PreviewBlock\n      ├─ ApiBlock\n      └─ RulesBlock\n```\n\n**Key insight:** `.canon-page` provides centering, `.canon-rail` provides max-width.\n\n---\n\n## Usage Pattern\n\n### Button Page Example\n```rust\n#[component]\npub fn ButtonPage() -> impl IntoView {\n    view! {\n        <ComponentPage\n            name=\"Button\".to_string()\n            description=\"Primary interactive element\".to_string()\n            symbols=vec![Symbol { label: \"SSR\".to_string(), variant: SymbolVariant::SSR }]\n            when_to_use=vec![\"Primary actions\".to_string()]\n            api_props=vec![\n                ApiProp {\n                    name: \"variant\".to_string(),\n                    prop_type: \"ButtonVariant\".to_string(),\n                    description: \"Visual style\".to_string(),\n                    required: false,\n                }\n            ]\n            rules=vec![\"Clear labels\".to_string()]\n        >\n            <div class=\"space-y-4\">\n                <Button>\"Solid\"</Button>\n                <Button variant=ButtonVariant::Outline>\"Outline\"</Button>\n            </div>\n        </ComponentPage>\n    }\n}\n```\n\n**Note:** Content goes in `children` slot, NOT as separate props.\n\n---\n\n## Children Slot Contract\n\n### ✅ Correct: ViewFn Slot\n```rust\n#[component]\npub fn ComponentPage(\n    #[prop(into)] children: ViewFn,  // ← Slot for preview content\n) -> impl IntoView {\n    view! {\n        <div class=\"canon-page\">\n            <PreviewBlock>\n                {children.run()}  // ← Render slot content\n            </PreviewBlock>\n        </div>\n    }\n}\n\n// Usage\n<ComponentPage>\n    <Button>\"Preview Content\"</Button>\n</ComponentPage>\n```\n\n### ❌ Incorrect: Separate Prop\n```rust\n#[component]\npub fn ComponentPage(\n    preview: AnyView,  // ❌ Type incompatibility\n) -> impl IntoView {\n    view! {\n        <PreviewBlock>{preview}</PreviewBlock>\n    }\n}\n\n// Usage - causes type errors\nlet preview_content = view! { <Button/> }.into_any();\n<ComponentPage preview=preview_content />\n```\n\n---\n\n## Centralization Mechanics\n\n### How Centering Works\n```css\n/* Parent (canon-page) centers child via flexbox */\n.canon-page {\n    display: flex;\n    justify-content: center;  /* Horizontally center */\n    width: 100%;\n}\n\n/* Child (canon-rail) has max-width */\n.canon-rail {\n    width: 100%;\n    max-width: 72rem;  /* Prevents content from being too wide */\n}\n```\n\n**Result:**\n- Viewport < 1152px: Content fills viewport\n- Viewport > 1152px: Content centered with max 1152px width\n\n### Visual Diagram\n```\n┌────────────────────────────────────────────────────┐\n│ .canon-page (viewport width)                       │\n│                                                     │\n│        ┌─────────────────────────────┐            │\n│        │ .canon-rail (max 72rem)     │            │\n│        │                             │            │\n│        │  Component Content          │            │\n│        │                             │            │\n│        └─────────────────────────────┘            │\n│                                                     │\n└────────────────────────────────────────────────────┘\n```\n\n---\n\n## Real World Migration\n\n### Before (Broken)\n```rust\n// ❌ Old template\n#[component]\npub fn ComponentPage(...) -> impl IntoView {\n    view! {\n        <div class=\"page\">\n            <div class=\"main\">\n                <ComponentHeader />\n                <PreviewBlock>{children.run()}</PreviewBlock>\n            </div>\n        </div>\n    }\n}\n```\n\n**Problems:**\n- Content stuck to left\n- No centralization CSS\n- Generic class names\n\n### After (Fixed)\n```rust\n// ✅ New template\n#[component]\npub fn ComponentPage(...) -> impl IntoView {\n    view! {\n        <div class=\"canon-page\">\n            <div class=\"canon-rail\">\n                <ComponentHeader />\n                <PreviewBlock>{children.run()}</PreviewBlock>\n            </div>\n        </div>\n    }\n}\n```\n\n**CSS added:**\n```css\n.canon-page { display: flex; justify-content: center; }\n.canon-rail { max-width: 72rem; }\n```\n\n**Result:** Content properly centered ✓\n\n---\n\n## Block Components\n\n### PreviewBlock\n```rust\n// Shows component variants and states\n<PreviewBlock>\n    <Button>\"Variant 1\"</Button>\n    <Button variant=ButtonVariant::Outline>\"Variant 2\"</Button>\n</PreviewBlock>\n```\n\n### ApiBlock\n```rust\n// Documents component props\n<ApiBlock props=vec![\n    ApiProp {\n        name: \"variant\".to_string(),\n        prop_type: \"ButtonVariant\".to_string(),\n        description: \"Visual style\".to_string(),\n        required: false,\n    }\n] />\n```\n\n### RulesBlock\n```rust\n// Shows usage rules and anti-patterns\n<RulesBlock \n    rules=vec![\"Use clear, action-oriented labels\".to_string()]\n    anti_patterns=vec![\"Multiple primary buttons\".to_string()]\n/>\n```\n\n---\n\n## Template Props Reference\n```rust\n#[component]\npub fn ComponentPage(\n    name: String,                             // Component name (e.g., \"Button\")\n    description: String,                      // Short description\n    #[prop(optional)] symbols: Option<Vec<Symbol>>,  // SSR, Hydration badges\n    #[prop(into)] children: ViewFn,           // Preview content\n    #[prop(optional)] when_to_use: Option<Vec<String>>,\n    #[prop(optional)] when_not_to_use: Option<Vec<String>>,\n    #[prop(optional)] api_props: Option<Vec<ApiProp>>,\n    #[prop(optional)] comparison: Option<Vec<ComparisonRow>>,\n    #[prop(optional)] rules: Option<Vec<String>>,\n    #[prop(optional)] anti_patterns: Option<Vec<String>>,\n) -> impl IntoView\n```\n\n**Required:** `name`, `description`, `children`  \n**Optional:** All other props\n\n---\n\n## Enforcement Checklist\n\n- [ ] Uses `.canon-page` as outer container\n- [ ] Uses `.canon-rail` for content width\n- [ ] No `.page` or `.main` classes\n- [ ] `children` is `ViewFn` type\n- [ ] CSS defines centralization rules\n- [ ] Max-width is 72rem (1152px)\n\n---\n\n## Testing Centralization\n```javascript\n// DevTools Console\nconst page = document.querySelector('.canon-page');\nconst rail = document.querySelector('.canon-rail');\n\nconsole.log('Page width:', page.offsetWidth);\nconsole.log('Rail width:', rail.offsetWidth);\nconsole.log('Rail max-width:', getComputedStyle(rail).maxWidth);\nconsole.log('Page justify-content:', getComputedStyle(page).justifyContent);\n\n// Expected:\n// - Page width: full viewport\n// - Rail width: min(viewport, 1152px)\n// - Rail max-width: 1152px\n// - Page justify-content: center\n```\n\n---\n\n## Canonical Justification\n\n**Templates are contracts, not implementations.**\n\nA template that uses generic class names:\n- Cannot guarantee visual consistency\n- Cannot be styled centrally\n- Cannot evolve without breaking pages\n\n**Canon mandates:** Namespaced, semantic class names with CSS contract.\n\n---\n\n## Canon References\n\n- Canon Rule #73 — ComponentPage Template Contract (update existing)\n- Canon Rule #74 — Block Semantic HTML\n- Canon Rule #81 — Flex Layout Ownership\n\n---\n\n## Related Symptoms\n\nIf you see:\n- Content stuck to left edge\n- Inconsistent max-width across pages\n- `.page` or `.main` classes in ComponentPage\n- Centralization works randomly\n\n→ **This rule is violated.**\n\nGo to: **SYMPTOMS.md → COMPONENTPAGE VIOLATIONS**"},{"number":80,"slug":"workspace-watch-configuration","title":"Workspace Watch Configuration","status":"ENFORCED","severity":"MEDIUM","category":"build-tooling","tags":["watch","cargo","leptos"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"cargo-leptos does not monitor external path dependencies by default, causing stale builds during development. Explicit watch configuration is required for local packages.","problem":"changes in path dependencies do not trigger recompilation","solution":"configure watch-additional-files for all local dependencies","signals":["stale build","no recompilation","changes ignored"],"search_intent":"why cargo leptos watch not detecting changes","keywords":["cargo leptos watch additional files","rust watch path dependency","leptos hot reload issue","monorepo watch config"],"body":"---\n\n## Principle\n\n`cargo-leptos watch` does NOT automatically monitor path dependencies outside the current workspace.\n\nWhen using local design system packages (e.g., `rs-design`), changes will NOT trigger recompilation unless explicitly configured via `watch-additional-files`.\n\n---\n\n## Problem Statement\n\n### Default Behavior\n```toml\n# workbench/Cargo.toml\n[dependencies]\nrs-design = { path = \"../../../../../packages-rust/rs-design\" }\n```\n\n**What happens:**\n1. Edit file in `packages-rust/rs-design/src/ui/sidebar.rs`\n2. `cargo-leptos watch` does NOT detect change\n3. No recompilation occurs\n4. Browser shows stale code\n5. Developer wastes time debugging \"why isn't this working?\"\n\n**Root cause:** `cargo-leptos` only watches files listed in workspace members by default.\n\n---\n\n## Canonical Solution\n\n### Add to Cargo.toml\n```toml\n[package.metadata.leptos]\n# ... other config ...\nwatch-additional-files = [\"../../../../../packages-rust/rs-design/src\"]\n```\n\n**Effect:** Now changes in `rs-design/src` trigger recompilation.\n\n---\n\n## Configuration Syntax\n\n### Single Path\n```toml\n[package.metadata.leptos]\nwatch-additional-files = [\"../../../../../packages-rust/rs-design/src\"]\n```\n\n### Multiple Paths\n```toml\n[package.metadata.leptos]\nwatch-additional-files = [\n    \"../../../../../packages-rust/rs-design/src\",\n    \"../../../../../packages-rust/rs-tailwind/dist\",\n    \"../../shared-components/src\"\n]\n```\n\n### Path Requirements\n\n- **MUST** be relative to Cargo.toml location\n- **MUST** point to directories (not individual files)\n- **SHOULD** point to `src/` for Rust packages\n- **CAN** include multiple dependency paths\n\n---\n\n## Path Calculation\n```bash\n# From: /monorepo/examples/_wip/workbench/Cargo.toml\n# To:   /monorepo/packages-rust/rs-design/src\n\n# Calculate relative path:\ncd /monorepo/examples/_wip/workbench\nrealpath --relative-to=. /monorepo/packages-rust/rs-design/src\n\n# Result: ../../../../../packages-rust/rs-design/src\n```\n\n### Common Monorepo Layouts\n```\nmonorepo/\n├─ packages-rust/\n│  └─ rs-design/\n│     └─ src/  ← Watch this\n└─ examples/\n   └─ _wip/\n      └─ workbench/\n         └─ Cargo.toml  ← From here\n\nRelative path: ../../../../../packages-rust/rs-design/src\n```\n\n---\n\n## Verification\n\n### Before Configuration\n```bash\n# 1. Start watch\ncargo leptos watch\n\n# 2. In another terminal, edit dependency\nvim ../../../../../packages-rust/rs-design/src/ui/sidebar.rs\n\n# 3. Save file\n# Result: No recompilation ❌\n```\n\n### After Configuration\n```bash\n# 1. Add watch-additional-files to Cargo.toml\n\n# 2. Restart watch\npkill -f \"cargo leptos\"\ncargo leptos watch\n\n# 3. Edit dependency\nvim ../../../../../packages-rust/rs-design/src/ui/sidebar.rs\n\n# 4. Save file\n# Result: Recompilation triggered ✓\n# Output: \"Compiling rs-design v0.1.0\"\n```\n\n---\n\n## Complete Configuration Example\n```toml\n[package]\nname = \"canonrs-workbench\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nrs-design = { path = \"../../../../../packages-rust/rs-design\" }\nleptos = \"0.8\"\n\n[package.metadata.leptos]\noutput-name = \"workbench\"\nsite-root = \"target/site\"\nsite-pkg-dir = \"pkg\"\nsite-addr = \"127.0.0.1:3003\"\nassets-dir = \"public\"\nreload-port = 3004\nenv = \"DEV\"\nbin-features = [\"ssr\"]\nlib-features = [\"hydrate\"]\n\n# ✅ CRITICAL: Watch local dependencies\nwatch-additional-files = [\"../../../../../packages-rust/rs-design/src\"]\n```\n\n---\n\n## When To Use\n\n### Always Required For\n\n- Local design system packages (`rs-design`, `rs-ui`, etc.)\n- Shared component libraries\n- Local utility crates used across examples\n- Any path dependency outside workspace\n\n### NOT Required For\n\n- Published crates from crates.io\n- Dependencies within same workspace\n- Generated code in `target/`\n- Third-party npm packages\n\n---\n\n## Multiple Workbench Setup\n```toml\n# Example 1: minimal/Cargo.toml\n[package.metadata.leptos]\nwatch-additional-files = [\"../../../packages-rust/rs-design/src\"]\n\n# Example 2: workbench/Cargo.toml\n[package.metadata.leptos]\nwatch-additional-files = [\"../../../../../packages-rust/rs-design/src\"]\n\n# Example 3: leptos-tailwindv4/Cargo.toml\n[package.metadata.leptos]\nwatch-additional-files = [\"../../../packages-rust/rs-design/src\"]\n```\n\n**Note:** Paths differ based on example location in monorepo.\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Absolute Paths\n```toml\n# ❌ WRONG\nwatch-additional-files = [\"/opt/docker/monorepo/packages-rust/rs-design/src\"]\n```\n\n**Problem:** Breaks on different machines\n**Solution:** Always use relative paths\n\n### Mistake 2: Wrong Base Directory\n```toml\n# ❌ WRONG (from workspace root instead of Cargo.toml)\nwatch-additional-files = [\"packages-rust/rs-design/src\"]\n```\n\n**Problem:** Path doesn't exist relative to Cargo.toml\n**Solution:** Calculate from Cargo.toml location\n\n### Mistake 3: Watching `target/`\n```toml\n# ❌ WRONG\nwatch-additional-files = [\"../../../../../packages-rust/rs-design/target\"]\n```\n\n**Problem:** Causes infinite recompile loops\n**Solution:** Only watch `src/`, never `target/`\n\n### Mistake 4: Individual Files\n```toml\n# ❌ SUBOPTIMAL\nwatch-additional-files = [\n    \"../../../../../packages-rust/rs-design/src/ui/sidebar.rs\",\n    \"../../../../../packages-rust/rs-design/src/ui/button.rs\"\n]\n```\n\n**Problem:** Maintenance nightmare\n**Solution:** Watch entire `src/` directory\n\n---\n\n## Debugging Watch Issues\n\n### Check if path is correct\n```bash\ncd examples/_wip/workbench\nls -la ../../../../../packages-rust/rs-design/src\n# Should list files, not error\n```\n\n### Check cargo-leptos output\n```bash\ncargo leptos watch --verbose\n# Look for \"Watching additional paths: ...\"\n```\n\n### Manually trigger recompilation\n```bash\n# Force rebuild to test if code change works\ncargo leptos build\n\n# If build succeeds but watch fails, it's a watch config issue\n```\n\n### Test with simple file change\n```bash\n# Add comment to watched file\necho \"// test\" >> ../../../../../packages-rust/rs-design/src/lib.rs\n\n# Watch terminal for \"Compiling rs-design\"\n```\n\n---\n\n## Performance Considerations\n\n### Impact on Build Times\n\n- **Minimal:** Only checks file timestamps\n- **No CPU overhead** when files unchanged\n- **Instant detection** when files change\n\n### Number of Watch Paths\n```toml\n# ✅ GOOD: Specific packages\nwatch-additional-files = [\n    \"../../../../../packages-rust/rs-design/src\",\n    \"../../../../../packages-rust/rs-components/src\"\n]\n\n# ⚠️ AVOID: Watching too many paths\nwatch-additional-files = [\n    \"../../../../../packages-rust\",  # Watches EVERYTHING\n]\n```\n\n**Guideline:** Watch only packages you actively edit.\n\n---\n\n## Integration With Other Tools\n\n### With CSS Watch\n```bash\n# Terminal 1: Watch Rust (includes rs-design)\ncargo leptos watch\n\n# Terminal 2: Watch CSS\nnpm run watch:css\n```\n\nBoth can run simultaneously.\n\n### With VS Code\n```json\n// .vscode/tasks.json\n{\n  \"label\": \"Watch Workbench + rs-design\",\n  \"type\": \"shell\",\n  \"command\": \"cargo leptos watch\",\n  \"problemMatcher\": [\"$rustc\"],\n  \"isBackground\": true\n}\n```\n\n---\n\n## Enforcement Checklist\n\n- [ ] All local path dependencies listed\n- [ ] Paths are relative to Cargo.toml\n- [ ] Paths point to `src/` directories\n- [ ] Paths verified with `ls -la`\n- [ ] Watch detects changes (test with edit)\n- [ ] No infinite recompile loops\n\n---\n\n## Canonical Justification\n\n**Developer experience is a feature.**\n\nWaiting 10 seconds to realize code didn't recompile wastes:\n- Developer focus\n- Iteration speed\n- Debugging time\n\nConfiguring watch once saves hours over project lifetime.\n\n**Canon mandates:** Explicit watch configuration for all local dependencies.\n\n---\n\n## Canon References\n\n- Canon Rule #64 — CSS Build Pipeline Mandatory\n- Canon Rule #82 — CSS Build Pipeline Health\n- Canon Rule #56 — Monorepo CSS Build Pipeline\n\n---\n\n## Related Symptoms\n\nIf you see:\n- Code changes don't trigger recompilation\n- `cargo leptos watch` ignores dependency edits\n- Manual `cargo build` works, watch doesn't\n- Recompilation only happens on workbench file changes\n\n→ **This rule is violated.**\n\nGo to: **SYMPTOMS.md → WATCH CONFIGURATION FAILURES**"},{"number":81,"slug":"flex-layout-ownership","title":"Flex Layout Ownership","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["flexbox","layout","ui"],"language":"EN","version":"1.0.0","date":"2026-01-12","intro":"Incorrect flex ownership causes layout inconsistencies and unpredictable sizing. Flex growth must be controlled by parent and UI layers, not primitives.","problem":"components define flex behavior internally instead of parent context","solution":"assign flex responsibility to ui wrappers and parent containers","signals":["wrong width calculation","flex not growing","layout mismatch"],"search_intent":"why flex child not growing in layout","keywords":["flex grow not working","flexbox parent child contract","ui wrapper flex pattern","leptos flex layout issue"],"body":"## Principle\n\nIn flex container hierarchies, **child growth is determined by the parent**, not the child itself.\n\nA component that needs to grow (`flex: 1`) MUST have this property applied **in its immediate parent context**, not internally.\n\n**Critical distinction:**\n- Primitives provide structure\n- UI wrappers provide flex behavior\n- Layout components orchestrate space distribution\n\n---\n\n## Forbidden Patterns\n```rust\n// ❌ NEVER ALLOWED\n\n// Primitive trying to grow itself\n#[component]\npub fn SidebarInsetPrimitive(children: Children) -> impl IntoView {\n    view! {\n        <div data-sidebar-inset=\"\" class=\"flex-1\">  // ❌ Primitive has flex\n            {children()}\n        </div>\n    }\n}\n\n// UI component expecting parent to know its needs\n#[component]\npub fn SidebarInset(children: ChildrenFn) -> impl IntoView {\n    view! {\n        <SidebarInsetPrimitive>\n            <main>{children()}</main>  // ❌ No flex wrapper in UI\n        </SidebarInsetPrimitive>\n    }\n}\n```\n\n### Why This is Forbidden\n\n1. **Violates Separation of Concerns**\n   - Primitive decides its own layout behavior\n   - Breaks \"dumb HTML\" contract\n   - Cannot be reused in non-flex contexts\n\n2. **Creates Invisible Dependencies**\n   - Parent must be flex container (not documented)\n   - Component doesn't work if parent changes\n   - Debugging requires understanding CSS cascade\n\n3. **Breaks Composability**\n   - Cannot wrap in additional containers\n   - Cannot conditionally apply flex\n   - Hard to override in specific contexts\n\n---\n\n## Canonical Architecture\n\n### Three-Layer Flex Contract\n```\n┌─────────────────────────────────────┐\n│ Parent (Flex Container)              │\n│ display: flex;                       │\n│                                      │\n│  ┌────────────────────────────────┐ │\n│  │ UI Wrapper (Flex Child)        │ │\n│  │ flex: 1;  ← OWNS GROWTH        │ │\n│  │                                │ │\n│  │  ┌──────────────────────────┐ │ │\n│  │  │ Primitive (Structure)    │ │ │\n│  │  │ No flex properties       │ │ │\n│  │  └──────────────────────────┘ │ │\n│  └────────────────────────────────┘ │\n└─────────────────────────────────────┘\n```\n\n**Rule:** Each layer has ONE responsibility.\n\n---\n\n## Correct Implementation\n\n### Primitive (No Flex)\n```rust\n// ✅ CORRECT: Primitive is pure structure\n#[component]\npub fn SidebarInsetPrimitive(children: Children) -> impl IntoView {\n    view! {\n        <div data-sidebar-inset=\"\">\n            {children()}\n        </div>\n    }\n}\n```\n\n### UI Wrapper (Adds Flex)\n```rust\n// ✅ CORRECT: UI layer adds flex behavior\n#[component]\npub fn SidebarInset(\n    children: ChildrenFn,\n    #[prop(default = String::new(), into)] class: String,\n) -> impl IntoView {\n    view! {\n        <div class=\"flex-1\">  // ← UI layer owns flex\n            <SidebarInsetPrimitive>\n                <main class=format!(\"flex-1 {}\", class)>\n                    {children()}\n                </main>\n            </SidebarInsetPrimitive>\n        </div>\n    }\n}\n```\n\n### Layout Context (Flex Container)\n```rust\n// ✅ CORRECT: Parent establishes flex context\n#[component]\npub fn SidebarProvider(children: Children) -> impl IntoView {\n    view! {\n        <SidebarProviderPrimitive>\n            <div class=\"flex min-h-svh w-full\">  // ← Flex container\n                <Sidebar/>  // Child 1\n                <SidebarInset/>  // Child 2 (will grow via flex-1)\n            </div>\n        </SidebarProviderPrimitive>\n    }\n}\n```\n\n---\n\n## Real World Problem\n\n### Symptom\n```\nViewport: 1493px\nSidebar: 256px\nExpected Inset: 1237px (1493 - 256)\nActual Inset: 521px ❌\n```\n\n### Root Cause\n```javascript\nconst inset = document.querySelector('[data-sidebar-inset]');\ngetComputedStyle(inset).flex; // \"0 1 auto\" ❌\n```\n\n`flex: 0 1 auto` means:\n- `flex-grow: 0` → **Does not grow**\n- `flex-shrink: 1` → Can shrink\n- `flex-basis: auto` → Use content size\n\n**Result:** Inset stays at minimum content width (521px).\n\n### Solution\n```rust\n// Add flex-1 wrapper in UI layer\n<div class=\"flex-1\">  // ← This makes flex: 1 1 0%\n    <SidebarInsetPrimitive>\n        ...\n    </SidebarInsetPrimitive>\n</div>\n```\n\n**Result:** Inset grows to 1237px ✓\n\n---\n\n## Flex Property Decision Tree\n```\nIs this a primitive?\n│\n├─ YES → NO flex properties allowed\n│         (Not even flex-1)\n│\n└─ NO → Is this a UI component?\n         │\n         ├─ YES → Should it grow in flex container?\n         │         │\n         │         ├─ YES → Add <div class=\"flex-1\"> wrapper\n         │         └─ NO → No flex needed\n         │\n         └─ NO → Is this a layout component?\n                  │\n                  └─ YES → Use display: flex on container\n                           Let children decide growth via UI wrappers\n```\n\n---\n\n## Common Flex Mistakes\n\n### Mistake 1: Flex in Primitive\n```rust\n// ❌ WRONG\n#[component]\npub fn CardPrimitive(children: Children) -> impl IntoView {\n    view! {\n        <div class=\"flex-1\">  // ❌ Primitive assumes flex parent\n            {children()}\n        </div>\n    }\n}\n```\n\n**Problem:** Breaks in non-flex contexts.\n\n### Mistake 2: No Wrapper in UI\n```rust\n// ❌ WRONG\n#[component]\npub fn Card(children: Children) -> impl IntoView {\n    view! {\n        <CardPrimitive>  // ❌ No flex wrapper\n            {children()}\n        </CardPrimitive>\n    }\n}\n```\n\n**Problem:** Card won't grow even in flex parent.\n\n### Mistake 3: Conflicting Flex Values\n```rust\n// ❌ WRONG\n<div class=\"flex-1\" style=\"flex: 0 0 auto;\">  // ❌ Inline overrides class\n    <Content/>\n</div>\n```\n\n**Problem:** Inline styles win, class ignored.\n\n---\n\n## Enforcement Checklist\n\n### Primitive Files\n\n- [ ] Zero `flex` properties\n- [ ] Zero `flex-1` classes\n- [ ] Zero `flex-grow/shrink/basis`\n- [ ] Only structural HTML\n\n### UI Files\n\n- [ ] Flex behavior added via wrapper\n- [ ] Wrapper is immediate child of primitive\n- [ ] No conflicting inline styles\n- [ ] Growth documented in component docs\n\n### Layout Files\n\n- [ ] Parent uses `display: flex`\n- [ ] Children growth controlled via UI wrappers\n- [ ] Flex direction documented\n- [ ] Gap/spacing uses tokens\n\n---\n\n## Testing Flex Layout\n```javascript\n// ✅ Verify flex setup\nconst parent = document.querySelector('.flex-container');\nconst child = document.querySelector('[data-sidebar-inset]');\nconst wrapper = child.querySelector('.flex-1');\n\nconsole.assert(\n    getComputedStyle(parent).display === 'flex',\n    'Parent must be flex container'\n);\n\nconsole.assert(\n    getComputedStyle(wrapper).flex === '1 1 0%',\n    'Wrapper must have flex: 1'\n);\n\nconsole.assert(\n    getComputedStyle(child).flex === '0 1 auto',\n    'Primitive should NOT have flex: 1'\n);\n```\n\n---\n\n## Flex Math Reference\n```css\n/* flex: [grow] [shrink] [basis] */\n\nflex: 0 1 auto;  /* Default - don't grow, can shrink, auto size */\nflex: 1;         /* Shorthand for: 1 1 0% */\nflex: 1 1 0%;    /* Grow to fill, shrink if needed, start at 0 */\nflex: 0 0 auto;  /* Fixed size, no grow/shrink */\n```\n\n**Canon preference:** Use `class=\"flex-1\"` (Tailwind) instead of inline `flex: 1`.\n\n---\n\n## Architecture Diagram\n```\nSidebarProvider (Layout)\n└─ <div class=\"flex\">  ← Flex container\n   ├─ Sidebar\n   │  └─ SidebarPrimitive\n   │     └─ <div class=\"sidebar-root\">  ← Fixed width\n   │\n   └─ SidebarInset (UI)\n      └─ <div class=\"flex-1\">  ← GROWS ✓\n         └─ SidebarInsetPrimitive\n            └─ <div data-sidebar-inset>  ← Structure only\n               └─ <main>\n                  └─ Content\n```\n\n**Key insight:** `flex-1` lives **between** UI component and primitive.\n\n---\n\n## Exceptions\n\n**NONE for primitives.**\n\nPrimitives NEVER have flex properties, even if:\n- \"It's always used in flex containers\"\n- \"It makes the code shorter\"\n- \"Everyone does it this way\"\n\n**Conditional for UI:**\n- If component needs to grow → Add flex wrapper\n- If component is fixed size → No flex needed\n\n---\n\n## Violation Consequences\n\n1. **Immediate:** Layout breaks in production\n2. **Short-term:** Impossible to debug without DevTools\n3. **Long-term:** Cannot reuse components in different contexts\n4. **Enterprise:** Fails responsive design audit\n\n---\n\n## Canonical Justification\n\nFlex is a **parent-child contract**.\n\nA child that declares its own flex behavior:\n- Assumes knowledge of parent context\n- Breaks when parent changes\n- Cannot be composed flexibly\n\n**Canon mandates:** Explicit flex ownership in UI layer.\n\n---\n\n## Canon References\n\n- Canon Rule #75 — Primitive CSS Prohibition\n- Canon Rule #79 — ComponentPage Template Contract\n- Canon Rule #1 — Types (Primitive vs UI separation)\n\n---\n\n## Related Symptoms\n\nIf you see:\n- Component width doesn't match available space\n- `flex: 0 1 auto` on element that should grow\n- Layout works randomly in some screens, not others\n- DevTools shows \"expected 1222px, got 521px\"\n\n→ **This rule is violated.**\n\nGo to: **SYMPTOMS.md → FLEX LAYOUT VIOLATIONS**"},{"number":82,"slug":"css-build-pipeline-health","title":"CSS Build Pipeline Health","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["css","pipeline","postcss"],"language":"EN","version":"1.0.0","date":"2026-01-12","intro":"CSS changes do not propagate automatically without explicit compilation, leading to stale styles in development. The build pipeline must be executed and validated for every change.","problem":"css changes not reflected because build pipeline is not executed","solution":"run css build pipeline and verify compiled output before testing","signals":["stale styles","css not updating","postcss not triggered"],"search_intent":"why css changes not applying in leptos","keywords":["postcss build required","css not updating leptos","tailwind build pipeline issue","compiled css not refreshed"],"body":"## Principle\n\nCSS changes MUST be **explicitly compiled** before they take effect in the application.\n\nThe build pipeline is NOT automatic. Changes to:\n- `canonrs.css`\n- `tokens.css`\n- `theme.css`\n- Any imported CSS\n\n**REQUIRE** running `npm run build:css` before `cargo leptos watch` will reflect them.\n\n---\n\n## Forbidden Assumptions\n```bash\n# ❌ WRONG WORKFLOW\n\n# 1. Edit canonrs.css\nvim packages-rust/rs-design/style/canonrs.css\n\n# 2. Reload browser\n# Expectation: Changes appear\n# Reality: Nothing happens ❌\n```\n\n### Why This Fails\n\n1. **Leptos serves static CSS**\n   - CSS is compiled to `public/workbench.css`\n   - Browser loads compiled artifact\n   - Source changes don't affect artifact\n\n2. **PostCSS is not automatic**\n   - PostCSS runs via npm script\n   - cargo-leptos doesn't trigger npm\n   - Watch mode only monitors Rust files\n\n3. **Cached artifacts persist**\n   - Old CSS stays in memory\n   - Hard refresh (Ctrl+Shift+R) needed\n   - Multiple layers of caching\n\n---\n\n## Canonical Build Order\n```bash\n# ✅ CORRECT WORKFLOW\n\n# 1. Edit CSS source\nvim packages-rust/rs-design/style/canonrs.css\n\n# 2. Compile CSS (MANDATORY)\ncd examples/_wip/workbench\nnpm run build:css\n\n# 3. Verify compilation\ngrep \"sidebar-root\" public/workbench.css  # Should find new rules\n\n# 4. Hard refresh browser\n# Ctrl + Shift + R (or Cmd + Shift + R on Mac)\n\n# 5. If still not working, restart cargo-leptos\npkill -f \"cargo leptos\"\nmake dev\n```\n\n---\n\n## CSS Build Pipeline Architecture\n```\nSource Files                    Build Process              Output\n─────────────────────────────────────────────────────────────────────\ncanonrs.css                 →   PostCSS                 →  workbench.css\ntokens.css                  →   @import resolution      →  (compiled)\ntheme.css                   →   Tailwind v4             →\ntailwind.css (entry)        →   Token substitution      →\n                                                            ↓\n                                                         public/\n                                                         └─ workbench.css\n                                                            (served to browser)\n```\n\n**Critical path:** Source → PostCSS → Output → Browser\n\n**Break at any step = changes invisible**\n\n---\n\n## PostCSS Configuration Requirements\n\n### package.json Scripts\n```json\n{\n  \"scripts\": {\n    \"build:css\": \"postcss style/tailwind.css -o public/workbench.css\",\n    \"watch:css\": \"postcss style/tailwind.css -o public/workbench.css --watch\"\n  }\n}\n```\n\n### postcss.config.js\n```javascript\nexport default {\n  plugins: {\n    'postcss-import': {\n      resolve: (id) => {\n        if (id === '@canonrs/tailwind/tokens.css') {\n          return path.resolve(__dirname, '../../../../../packages-rust/rs-design/style/tokens.css');\n        }\n        // ... other resolvers\n        return id;\n      }\n    },\n    '@tailwindcss/postcss': {}\n  }\n}\n```\n\n**Critical:** Resolvers MUST use correct relative paths.\n\n---\n\n## Verification Steps\n\n### Step 1: Verify Source Change\n```bash\n# Check that your edit is saved\ncat packages-rust/rs-design/style/canonrs.css | grep \"your-new-rule\"\n# Should return your change\n```\n\n### Step 2: Verify Compilation\n```bash\n# Run build\nnpm run build:css\n\n# Check output\ngrep \"your-new-rule\" public/workbench.css\n# Should return your change in compiled CSS\n```\n\n### Step 3: Verify Browser Loaded New CSS\n```javascript\n// DevTools Console\nconst link = document.querySelector('link[href*=\"workbench.css\"]');\nconsole.log('CSS URL:', link.href);\n\n// Force reload CSS\nlink.href = link.href.split('?')[0] + '?v=' + Date.now();\n```\n\n### Step 4: Verify Rule Applied\n```javascript\n// Check computed styles\nconst element = document.querySelector('.sidebar-root');\nconst styles = getComputedStyle(element);\nconsole.log('Your property:', styles.getPropertyValue('your-property'));\n```\n\n---\n\n## Common Build Failures\n\n### Failure 1: PostCSS Path Resolution\n```bash\n# ❌ ERROR\n[Error: ENOENT: no such file or directory, stat '.../tokens.css']\n```\n\n**Cause:** Incorrect path in postcss.config.js\n**Solution:** Use `path.resolve(__dirname, 'correct/relative/path')`\n\n### Failure 2: Import Order\n```bash\n# ❌ ERROR\nPostCSS: @import must precede all other statements\n```\n\n**Cause:** `@import` after CSS rules\n**Solution:** Move all `@import` to top of file\n\n### Failure 3: Missing Output Directory\n```bash\n# ❌ ERROR\nENOENT: no such file or directory, open 'public/workbench.css'\n```\n\n**Cause:** `public/` folder doesn't exist\n**Solution:** `mkdir -p public`\n\n### Failure 4: Stale Cache\n```bash\n# Changes compiled but not visible in browser\n```\n\n**Cause:** Browser cached old CSS\n**Solution:** Hard refresh (Ctrl+Shift+R)\n\n---\n\n## Automated Watch\n```bash\n# Terminal 1: Watch CSS\nnpm run watch:css\n\n# Terminal 2: Watch Rust\nmake dev\n```\n\n**Benefits:**\n- Automatic recompilation on CSS changes\n- Faster iteration\n- Fewer manual steps\n\n**Limitations:**\n- Still requires browser refresh\n- Doesn't catch PostCSS config errors\n- Can get out of sync with Rust watch\n\n---\n\n## Build Health Checks\n\n### Pre-flight Checklist\n```bash\n# Run before starting development\ncd examples/_wip/workbench\n\n# 1. Check PostCSS installed\nnpx postcss --version  # Should return version number\n\n# 2. Check output directory exists\nls -la public/  # Should show workbench.css\n\n# 3. Run initial build\nnpm run build:css  # Should complete without errors\n\n# 4. Verify output size\nls -lh public/workbench.css  # Should be >50KB (has content)\n\n# 5. Check for syntax errors\ngrep \"syntax error\" public/workbench.css  # Should return nothing\n```\n\n### Runtime Health Check\n```javascript\n// Add to browser console for debugging\nfunction checkCSSHealth() {\n    const link = document.querySelector('link[href*=\"workbench.css\"]');\n    \n    if (!link) {\n        console.error('❌ workbench.css not loaded');\n        return;\n    }\n    \n    fetch(link.href)\n        .then(r => r.text())\n        .then(css => {\n            console.log('✓ CSS size:', css.length, 'bytes');\n            console.log('✓ Contains tokens:', css.includes('--sidebar-width'));\n            console.log('✓ Contains canon:', css.includes('.canon-page'));\n        });\n}\n```\n\n---\n\n## Error Recovery Procedure\n```bash\n# If CSS is completely broken:\n\n# 1. Clean everything\nrm -rf public/workbench.css\nrm -rf node_modules/.cache\n\n# 2. Reinstall dependencies\nnpm install\n\n# 3. Rebuild from scratch\nnpm run build:css\n\n# 4. Verify output\nls -lh public/workbench.css\nhead -50 public/workbench.css\n\n# 5. Restart dev server\npkill -f \"cargo leptos\"\nmake dev\n\n# 6. Hard refresh browser\n# Ctrl + Shift + R\n```\n\n---\n\n## Build Order Dependencies\n```\nCSS Source Change\n    ↓\nnpm run build:css (MUST RUN)\n    ↓\npublic/workbench.css updated\n    ↓\nBrowser hard refresh (MUST DO)\n    ↓\nNew styles visible\n```\n\n**Any step skipped = changes invisible**\n\n---\n\n## Enforcement Checklist\n\n- [ ] PostCSS installed (`npm list postcss`)\n- [ ] Build script exists in package.json\n- [ ] Output directory exists (`public/`)\n- [ ] postcss.config.js has correct paths\n- [ ] Initial build runs without errors\n- [ ] Output file has content (>50KB)\n- [ ] Browser loads from `public/` directory\n\n---\n\n## Canonical Justification\n\n**Design systems are compiled artifacts.**\n\nExpecting CSS to \"just work\" without compilation is like expecting Rust to run without `cargo build`.\n\nThe pipeline exists to:\n- Resolve imports\n- Apply transformations\n- Optimize output\n- Ensure consistency\n\n**Canon mandates:** Explicit compilation over implicit magic.\n\n---\n\n## Canon References\n\n- Canon Rule #55 — Canonical CSS Entry Points\n- Canon Rule #56 — Monorepo CSS Build Pipeline\n- Canon Rule #57 — PostCSS Canon Config\n- Canon Rule #64 — CSS Build Pipeline Mandatory\n\n---\n\n## Related Symptoms\n\nIf you see:\n- CSS changes don't appear in browser\n- \"File not found\" errors in PostCSS\n- Stale styles persist after edits\n- Some tokens work, others don't\n- Build succeeds but browser shows old CSS\n\n→ **This rule is violated.**\n\nGo to: **SYMPTOMS.md → CSS BUILD PIPELINE FAILURES**"},{"number":83,"slug":"layout-zones-contract","title":"Layout Zones Contract","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["layout","zones","hierarchy"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Unstructured layouts lead to inconsistent UI hierarchy and unpredictable responsive behavior. Canon enforces fixed spatial zones to standardize component placement and eliminate layout ambiguity.","problem":"components are placed without spatial rules causing layout inconsistency","solution":"enforce five canonical layout zones with strict nesting and responsibilities","signals":["layout chaos","content outside container","z-index conflicts"],"search_intent":"how to structure layout zones in ui architecture","keywords":["layout zones architecture","ui spatial hierarchy system","canonical layout containers","component placement rules"],"body":"## The Principle\n\n**Every element knows where it belongs.**\n\nCanon defines **5 spatial zones** that control WHERE components can exist. This prevents layout chaos, ensures consistent spatial hierarchy, and makes responsive behavior predictable.\n\n---\n\n## The 5 Layout Zones\n```\n┌─────────────────────────────────────────────────────────┐\n│ OVERLAY (z-index layers)                                │\n├─────────────────────────────────────────────────────────┤\n│ ┌─────────┬─────────────────────────────────────────┐  │\n│ │         │ PAGE (application container)            │  │\n│ │         │ ┌───────────────────────────────────┐   │  │\n│ │ PANEL   │ │ RAIL (content width container)    │   │  │\n│ │         │ │ ┌─────────────────────────────┐   │   │  │\n│ │ (side   │ │ │ CONTENT (actual information)│   │   │  │\n│ │  bar)   │ │ │                             │   │   │  │\n│ │         │ │ └─────────────────────────────┘   │   │  │\n│ └─────────┴─┴───────────────────────────────────┴───┴──┘\n└─────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Zone 1 PAGE\n\n**Purpose:** Application-level container  \n**Token:** `--color-bg-surface`  \n**Max instances:** 1 per route\n\n### Characteristics\n- Full viewport width/height\n- Background for entire application\n- Sets base surface color\n- Contains all other zones\n\n### CSS Contract\n```css\n.canon-page {\n  display: flex;\n  justify-content: center;\n  width: 100%;\n  min-height: 100vh;\n  background: var(--color-bg-surface);\n}\n```\n\n### What Lives Here\n- ✅ RAIL zone\n- ✅ PANEL zone (sidebar)\n- ❌ NEVER: Direct content\n- ❌ NEVER: Components\n\n### Example: AppLayout\n```rust\nview! {\n    <div class=\"canon-page\">\n        <Sidebar />  // PANEL zone\n        <main class=\"canon-rail\">\n            // RAIL zone content\n        </main>\n    </div>\n}\n```\n\n---\n\n## Zone 2 PANEL\n\n**Purpose:** Persistent navigation/tooling  \n**Token:** `--color-bg-muted`  \n**Max instances:** 2 (left + right)\n\n### Characteristics\n- Vertical slice\n- Fixed or collapsible width\n- Muted background (distinct from content)\n- Independent scroll\n\n### CSS Contract\n```css\n.canon-panel {\n  width: var(--sidebar-width); /* 16rem default */\n  height: 100vh;\n  background: var(--color-bg-muted);\n  border-right: var(--border-width-hairline) solid var(--color-border-default);\n  overflow-y: auto;\n}\n```\n\n### What Lives Here\n- ✅ Navigation menus\n- ✅ Tool palettes\n- ✅ Filters\n- ✅ Inspector panels\n- ❌ NEVER: Primary content\n- ❌ NEVER: Forms (use CONTENT)\n\n### Positioning Rules\n| Side | Use Case |\n|------|----------|\n| Left | Primary navigation |\n| Right | Context panels (inspector, properties) |\n\n**Golden Rule:** If it's persistent across routes → PANEL\n\n---\n\n## Zone 3 RAIL\n\n**Purpose:** Content width constraint  \n**Token:** N/A (structural only)  \n**Max width:** `72rem` (1152px)\n\n### Characteristics\n- Centers content horizontally\n- Provides reading-optimal width\n- Responsive padding\n- Contains CONTENT zone\n\n### CSS Contract\n```css\n.canon-rail {\n  width: 100%;\n  max-width: 72rem; /* 1152px */\n  padding: var(--space-xl);\n  display: flex;\n  flex-direction: column;\n  gap: var(--space-xl);\n}\n```\n\n### What Lives Here\n- ✅ CONTENT zone\n- ✅ Section spacing\n- ✅ Vertical rhythm\n- ❌ NEVER: Direct text/components\n- ❌ NEVER: Fixed width content\n\n### Responsive Behavior\n```css\n@media (max-width: 768px) {\n  .canon-rail {\n    padding: var(--space-md);\n  }\n}\n```\n\n**Golden Rule:** If it needs reading width → RAIL\n\n---\n\n## Zone 4 CONTENT\n\n**Purpose:** Actual information display  \n**Token:** `--color-bg-elevated` (when highlighted)  \n**Max instances:** Unlimited\n\n### Characteristics\n- Information-bearing\n- Can be elevated surface\n- Semantic HTML\n- Accessible structure\n\n### CSS Contract (Elevated)\n```css\n.canon-content-elevated {\n  background: var(--color-bg-elevated);\n  border: var(--border-width-hairline) solid var(--color-border-muted);\n  border-radius: var(--radius-lg);\n  box-shadow: var(--shadow-sm);\n  padding: var(--space-xl);\n}\n```\n\n### CSS Contract (Flat)\n```css\n.canon-content-flat {\n  background: transparent;\n  /* Inherits PAGE surface */\n}\n```\n\n### What Lives Here\n- ✅ Component previews\n- ✅ Documentation blocks\n- ✅ Tables\n- ✅ Forms\n- ✅ Cards\n- ✅ API docs\n- ✅ Text content\n\n### Content Types\n\n| Type | Surface | Border | Shadow |\n|------|---------|--------|--------|\n| Hero | `base` | No | No |\n| Preview | `elevated` | Yes | Yes |\n| API Block | `elevated` | Yes | Yes |\n| Paragraph | `base` | No | No |\n\n**Golden Rule:** If users read/interact with it → CONTENT\n\n---\n\n## Zone 5 OVERLAY\n\n**Purpose:** Temporary UI above main flow  \n**Token:** `--z-*` (z-index hierarchy)  \n**Max instances:** Stacked (modal > dropdown > tooltip)\n\n### Characteristics\n- Positioned above PAGE\n- Uses z-index system\n- Temporary visibility\n- Backdrop when modal\n\n### Z-Index Hierarchy\n```css\n:root {\n  --z-base: 0;          /* PAGE/RAIL/PANEL */\n  --z-popover: 1000;    /* Dropdowns, popovers */\n  --z-dropdown: 2000;   /* Context menus */\n  --z-modal: 3000;      /* Dialogs */\n  --z-toast: 4000;      /* Notifications */\n  --z-tooltip: 5000;    /* Tooltips */\n}\n```\n\n### CSS Contract (Modal)\n```css\n.canon-overlay-modal {\n  position: fixed;\n  inset: 0;\n  z-index: var(--z-modal);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background: hsl(0 0% 0% / 0.5); /* Backdrop */\n}\n\n.canon-overlay-content {\n  background: var(--color-bg-elevated);\n  border-radius: var(--radius-xl);\n  box-shadow: var(--shadow-2xl);\n  max-width: 32rem;\n  padding: var(--space-2xl);\n}\n```\n\n### What Lives Here\n- ✅ Modals\n- ✅ Dropdowns\n- ✅ Tooltips\n- ✅ Command palette\n- ✅ Toast notifications\n- ❌ NEVER: Persistent UI\n- ❌ NEVER: Primary navigation\n\n**Golden Rule:** If it blocks interaction temporarily → OVERLAY\n\n---\n\n## Zone Nesting Rules\n\n### ✅ Valid Nesting\n```\nPAGE\n└── PANEL (sidebar)\n└── RAIL\n    └── CONTENT (elevated)\n    └── CONTENT (flat)\n```\n\n### ❌ Invalid Nesting\n```\nRAIL\n└── PAGE  /* WRONG - PAGE must be root */\n\nCONTENT\n└── RAIL  /* WRONG - RAIL contains CONTENT, not reverse */\n\nPANEL\n└── RAIL  /* WRONG - Both are siblings under PAGE */\n```\n\n---\n\n## Component Zone Mapping\n\n| Component | Zone | Surface |\n|-----------|------|---------|\n| AppLayout | PAGE | `base` |\n| Sidebar | PANEL | `muted` |\n| ComponentHeader | CONTENT (flat) | `base` |\n| PreviewBlock | CONTENT (elevated) | `elevated` |\n| ApiBlock | CONTENT (elevated) | `elevated` |\n| ComparisonBlock | CONTENT (elevated) | `elevated` |\n| RulesBlock | CONTENT (elevated) | `elevated` |\n| Modal | OVERLAY | `elevated` |\n| Dropdown | OVERLAY | `elevated` |\n| Toast | OVERLAY | `elevated` |\n\n---\n\n## Real World Example\n\n### Structure\n```rust\n// PAGE zone\n<div class=\"canon-page\">\n    \n    // PANEL zone (sidebar)\n    <Sidebar />\n    \n    // RAIL zone\n    <main class=\"canon-rail\">\n        \n        // CONTENT zone (hero - flat)\n        <ComponentHeader\n            name=\"Button\"\n            description=\"Primary interactive element\"\n        />\n        \n        // CONTENT zone (elevated)\n        <PreviewBlock>\n            <ButtonShowcase />\n        </PreviewBlock>\n        \n        // CONTENT zone (elevated)\n        <ApiBlock props=api_props />\n        \n        // CONTENT zone (elevated)\n        <ComparisonBlock rows=comparison />\n        \n    </main>\n</div>\n```\n\n### CSS Mapping\n```css\n/* PAGE */\n.canon-page {\n  background: var(--color-bg-surface);\n}\n\n/* PANEL */\n.sidebar {\n  background: var(--color-bg-muted);\n}\n\n/* RAIL */\n.canon-rail {\n  max-width: 72rem;\n  padding: var(--space-xl);\n}\n\n/* CONTENT (flat) */\n.canon-component-hero {\n  background: var(--color-bg-surface); /* Inherits PAGE */\n}\n\n/* CONTENT (elevated) */\n.canon-preview-surface {\n  background: var(--color-bg-elevated);\n  border: 1px solid var(--color-border-muted);\n  box-shadow: var(--shadow-xl);\n}\n```\n\n---\n\n## Responsive Behavior\n\n### PANEL Collapse\n```css\n@media (max-width: 1024px) {\n  .canon-panel {\n    position: fixed;\n    transform: translateX(-100%);\n    transition: transform var(--motion-duration-normal);\n  }\n  \n  .canon-panel[data-open=\"true\"] {\n    transform: translateX(0);\n  }\n}\n```\n\n### RAIL Padding\n```css\n@media (max-width: 768px) {\n  .canon-rail {\n    padding: var(--space-md);\n    max-width: 100%;\n  }\n}\n```\n\n### CONTENT Stacking\n```css\n@media (max-width: 640px) {\n  .canon-content-elevated {\n    border-radius: var(--radius-md); /* Smaller radius on mobile */\n    padding: var(--space-lg);\n  }\n}\n```\n\n---\n\n## Anti Patterns\n\n### ❌ Content Outside RAIL\n```rust\n// WRONG\n<div class=\"canon-page\">\n    <h1>Title</h1>  /* Floating content - no RAIL */\n</div>\n```\n\n**Fix:** Wrap in RAIL.\n\n### ❌ RAIL Inside CONTENT\n```rust\n// WRONG\n<PreviewBlock>\n    <div class=\"canon-rail\">  /* RAIL nested in CONTENT */\n        ...\n    </div>\n</PreviewBlock>\n```\n\n**Fix:** RAIL contains CONTENT, not reverse.\n\n### ❌ Hardcoded Widths in CONTENT\n```css\n/* WRONG */\n.my-content {\n  max-width: 800px; /* Hardcoded */\n}\n```\n\n**Fix:** Let RAIL control width, CONTENT fills available space.\n\n### ❌ Z-Index Without Semantic Token\n```css\n/* WRONG */\n.my-modal {\n  z-index: 9999; /* Arbitrary */\n}\n```\n\n**Fix:** Use `var(--z-modal)`.\n\n### ❌ Multiple RAILs\n```rust\n// WRONG\n<div class=\"canon-page\">\n    <div class=\"canon-rail\">Content 1</div>\n    <div class=\"canon-rail\">Content 2</div>  /* Duplicate RAIL */\n</div>\n```\n\n**Fix:** One RAIL per PAGE. Multiple CONTENT sections inside RAIL.\n\n---\n\n## Benefits\n\n### ✅ Predictable Layout\n- Every component knows its zone\n- No ambiguity about placement\n- Consistent spatial hierarchy\n\n### ✅ Responsive by Default\n- PANEL collapses\n- RAIL adapts padding\n- CONTENT reflows\n\n### ✅ Accessible Structure\n```html\n<div role=\"application\" class=\"canon-page\">\n  <nav role=\"navigation\" class=\"canon-panel\">...</nav>\n  <main role=\"main\" class=\"canon-rail\">...</main>\n</div>\n```\n\n### ✅ Prevents Layout Bugs\n- Can't nest zones incorrectly (linter enforces)\n- Can't create rogue widths\n- Can't break z-index hierarchy\n\n---\n\n## Validation Checklist\n\n### Zone Audit\n```bash\n# Check for content outside RAIL\ngrep -r \"canon-page\" src/ | grep -v \"canon-rail\"\n\n# Check for RAIL inside CONTENT\ngrep -r \"canon-content\" src/ | grep \"canon-rail\"\n\n# Check for arbitrary z-index\ngrep -r \"z-index: [0-9]\" src/ | grep -v \"var(--z-\"\n```\n\n### Accessibility Test\n```javascript\n// Verify semantic structure\ndocument.querySelector('[role=\"main\"]') // Should be RAIL\ndocument.querySelector('[role=\"navigation\"]') // Should be PANEL\n```\n\n---\n\n## System Status After Rule 83\n\n| Aspect | Status |\n|--------|--------|\n| Spatial Zones | 🟢 DEFINED |\n| Nesting Rules | 🟢 ENFORCED |\n| Responsive Behavior | 🟢 PREDICTABLE |\n| Z-Index Hierarchy | 🟢 CLOSED |\n| Layout Chaos | 🟢 IMPOSSIBLE |\n\n---\n\n## Related Rules\n\n- **Rule #82:** Visual Surfaces Contract\n- **Rule #81:** Token Architecture & Theme Specificity\n- **Rule #50:** Provider Singleton Pattern\n\n---\n\n## Normative Requirements\n\n**MUST:**\n- Every route MUST have exactly one PAGE zone\n- RAIL MUST be direct child of PAGE (or sibling of PANEL)\n- CONTENT MUST be inside RAIL\n- OVERLAY MUST use semantic z-index tokens\n- Zones MUST NOT be nested incorrectly\n\n**MUST NOT:**\n- Create content outside RAIL\n- Hardcode widths in CONTENT\n- Use arbitrary z-index values\n- Nest RAIL inside CONTENT\n- Create multiple RAIL zones per PAGE\n\n**SHOULD:**\n- Use PANEL for persistent navigation\n- Collapse PANEL on mobile\n- Use semantic roles (main, nav)\n\n---\n\n**Author:** Canon Working Group  \n**Supersedes:** Implicit layout patterns  \n**Related:** Canon Rule #82 (Visual Surfaces)"},{"number":84,"slug":"svg-dark-mode-contract","title":"SVG Dark Mode Contract","status":"ENFORCED","severity":"HIGH","category":"styling-css","tags":["svg","dark-mode","css"],"language":"EN","version":"1.0.0","date":"2026-01-13","intro":"SVG files loaded via img tags do not inherit CSS properties like currentColor, causing incorrect colors in dark mode. Proper strategies are required to ensure theming compatibility.","problem":"external svg images do not inherit css color leading to dark mode issues","solution":"use css filters inline svg or masks to control svg coloring","signals":["icons stay black","dark mode contrast issues","svg color mismatch"],"search_intent":"why svg not adapting to dark mode","keywords":["svg currentcolor not working","img svg css inheritance","dark mode svg fix","svg color css filter"],"body":"## Principle\n\nSVG assets referenced via `<img>` tags **CANNOT** inherit `currentColor` or CSS properties.\n\n> **`<img src=\"*.svg\">` creates an isolated document context.**\n> Color inheritance requires inline SVG or CSS filters.\n\n---\n\n## The Problem\n\nDevelopers assume SVG strokes will adapt to dark mode by using `stroke=\"currentColor\"`:\n```svg\n<!-- ❌ DOES NOT WORK with <img> -->\n<svg>\n  <path stroke=\"currentColor\" />\n</svg>\n```\n```html\n<!-- ❌ currentColor is NOT inherited -->\n<img src=\"icon.svg\" />\n```\n\n**Result:** Icons remain black in dark mode, creating poor contrast and broken UX.\n\n---\n\n## Root Cause\n\n`<img>` treats SVG as an **external document**:\n- No access to parent CSS context\n- No inheritance of `color` property\n- `currentColor` resolves to SVG's internal default (black)\n\n---\n\n## Canonical Solutions\n\n### Solution 1: CSS Filter (Recommended for `<img>`)\n\nUse `filter: invert()` to adapt existing black strokes to dark mode.\n```css\n.icon {\n  width: 24px;\n  height: 24px;\n}\n\nhtml.dark .icon {\n  filter: invert(1) hue-rotate(180deg);\n}\n```\n\n**Pros:**\n- Works with `<img>` tags\n- No SVG file modification\n- Simple implementation\n\n**Cons:**\n- Inverts ALL colors (including fills)\n- May need `hue-rotate()` adjustments\n- Not precise color control\n\n---\n\n### Solution 2: Inline SVG (Most Control)\n\nEmbed SVG directly in HTML/components to inherit CSS context.\n```rust\n// Leptos component\nview! {\n  <svg class=\"icon\" viewBox=\"0 0 24 24\">\n    <path stroke=\"currentColor\" d=\"...\" />\n  </svg>\n}\n```\n```css\n.icon {\n  color: var(--color-fg-default);\n  width: 24px;\n  height: 24px;\n}\n```\n\n**Pros:**\n- Full CSS inheritance\n- Precise color control\n- Token-based theming\n\n**Cons:**\n- Larger bundle size\n- Cannot cache SVG separately\n- More verbose code\n\n---\n\n### Solution 3: CSS Mask (Advanced)\n\nUse SVG as a mask with CSS background color.\n```css\n.icon {\n  width: 24px;\n  height: 24px;\n  background: var(--color-fg-default);\n  mask: url('/icons/check.svg') no-repeat center;\n  mask-size: contain;\n}\n```\n\n**Pros:**\n- `<img>`-like simplicity\n- Full color control via CSS\n- Caches like normal images\n\n**Cons:**\n- Only works for single-color icons\n- Requires mask-compatible SVG\n- Less browser support (older IE)\n\n---\n\n## Decision Matrix\n\n| Use Case | Solution | Reason |\n|----------|----------|--------|\n| Simple icons, black strokes | CSS Filter | Easiest, works with `<img>` |\n| Token-based theming | Inline SVG | Full design system integration |\n| Performance-critical | CSS Mask | Best of both worlds |\n| Multi-color illustrations | Keep as `<img>`, manual dark variants | Filters distort complex colors |\n\n---\n\n## Anti Patterns\n\n### ❌ Anti-Pattern 1: `currentColor` in External SVG\n```svg\n<!-- icons/check.svg -->\n<svg>\n  <path stroke=\"currentColor\" />\n</svg>\n```\n```html\n<img src=\"/icons/check.svg\" class=\"icon\" />\n```\n**Problem:** `currentColor` resolves to black, ignoring parent CSS.\n\n---\n\n### ❌ Anti-Pattern 2: Hardcoded Colors\n```svg\n<svg>\n  <path stroke=\"#000000\" />\n</svg>\n```\n**Problem:** Cannot adapt to any theme or dark mode.\n\n---\n\n### ❌ Anti-Pattern 3: JavaScript Color Injection\n```js\nfetch('icon.svg')\n  .then(r => r.text())\n  .then(svg => {\n    const colored = svg.replace('black', theme.color);\n    element.innerHTML = colored;\n  });\n```\n**Problem:** Bypasses SSR, breaks hydration, performance overhead.\n\n---\n\n## Canonical Implementation\n\n### Step 1: Choose Strategy\nFor icon systems with `<img>` tags, use **CSS Filter**.\n\n### Step 2: Create CSS\n```css\n/* /style/components/icon-strip.css */\n.icon-item {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.icon-image {\n  width: 24px;\n  height: 24px;\n}\n\nhtml.dark .icon-image {\n  filter: invert(1) hue-rotate(180deg);\n}\n```\n\n### Step 3: Ensure Import Order\n```css\n/* /style/canonrs.css */\n@import \"./components/icon-strip.css\";\n```\n\n### Step 4: Verify Compilation\n```bash\nnpm run build:css\ngrep \"html.dark .icon-image\" public/workbench.css\n```\n\n---\n\n## Testing Protocol\n\n1. **Light Mode Test**\n   - Icons appear black with good contrast\n   - No color distortion\n\n2. **Dark Mode Test**\n   - Icons appear white/light with good contrast\n   - Filter does not affect surrounding elements\n\n3. **Edge Cases**\n   - Multi-color SVGs may need manual variants\n   - Gradients may require inline approach\n\n---\n\n## Performance Considerations\n\n- **CSS Filter:** Minimal performance impact\n- **Inline SVG:** Increases HTML size, but enables caching at component level\n- **CSS Mask:** Similar to `<img>`, good caching\n\n---\n\n## Canonical Justification\n\n> **External SVGs are isolated documents.**\n> They cannot inherit CSS context by design for security and encapsulation.\n\nThis rule enforces:\n- **Predictable theming** — Dark mode always works\n- **Performance** — Choose right strategy for use case\n- **Maintainability** — No runtime color injection hacks\n\n---\n\n## Canon References\n\n- Canon Rule #7 — Token Governance\n- Canon Rule #21 — Canonical Color Tokens\n- Canon Rule #58 — Leptos Assets Dev Constraint\n- Canon Rule #69 — Trunk Only Serves What's in dist/"},{"number":85,"slug":"leptos-asset-pipeline-dev","title":"Leptos Asset Pipeline in Dev Mode","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["leptos","assets","pipeline"],"language":"EN","version":"1.0.0","date":"2026-01-13","intro":"Leptos development mode does not use Trunk for asset handling, leading to missing files when relying on Trunk hooks. Assets must be served from the configured public directory.","problem":"assets not served in dev because trunk hooks are ignored","solution":"place assets in public directory and configure leptos asset serving","signals":["404 assets","missing files in dev","trunk hooks ignored"],"search_intent":"why assets not loading in leptos dev mode","keywords":["leptos asset pipeline dev","trunk ignored leptos watch","public assets directory leptos","leptos asset serving config"],"body":"## Principle\n\n`cargo leptos watch` (via `make dev`) uses **Leptos asset serving**, NOT Trunk.\n\n> **Trunk hooks and `data-trunk` directives are IGNORED in Leptos dev mode.**\n> Assets MUST be in `public/` directory specified in `Cargo.toml`.\n\n---\n\n## The Problem\n\nDevelopers assume `Trunk.toml` hooks will copy assets during `make dev`:\n```toml\n# Trunk.toml\n[[hooks]]\nstage = \"pre_build\"\ncommand = \"cp\"\ncommand_arguments = [\"-r\", \"assets\", \"dist/\"]\n```\n\n**Result:** Hook never executes. Assets return 404. Developer wastes hours debugging Trunk.\n\n---\n\n## Root Cause\n\n**Leptos has TWO build modes:**\n\n1. **Development (`cargo leptos watch`):**\n   - Uses Leptos built-in dev server\n   - Serves from `assets-dir` in `Cargo.toml`\n   - **IGNORES** `Trunk.toml` completely\n\n2. **Production (`cargo leptos build`):**\n   - Uses Trunk for bundling\n   - Processes `Trunk.toml` hooks\n   - Outputs to `dist/`\n\n**Most developers run `make dev`, which uses mode #1.**\n\n---\n\n## Canonical Asset Pipeline Development\n\n### Step 1: Check Leptos Configuration\n```toml\n# Cargo.toml\n[package.metadata.leptos]\nassets-dir = \"public\"\nsite-root = \"target/site\"\n```\n\n**Assets MUST be in `public/` directory.**\n\n### Step 2: Copy Assets to `public/`\n```bash\n# Manual copy (for development)\nmkdir -p public/assets\ncp -r /path/to/source/assets/symbols public/assets/\n```\n\n### Step 3: Reference in Code\n```rust\n// Correct path relative to public/\n<img src=\"/assets/symbols/icon.svg\" />\n```\n\n### Step 4: Verify Serving\n```bash\n# With make dev running\ncurl http://localhost:3003/assets/symbols/icon.svg\n# Should return SVG content, not 404\n```\n\n---\n\n## Canonical Asset Pipeline Production\n\n### Step 1: Configure Trunk Hooks\n```toml\n# Trunk.toml\n[[hooks]]\nstage = \"build\"\ncommand = \"cp\"\ncommand_arguments = [\"-r\", \"public/assets\", \"dist/\"]\n```\n\n### Step 2: Build for Production\n```bash\ncargo leptos build --release\n```\n\n### Step 3: Verify Output\n```bash\nls -la dist/assets/symbols/\n# Assets should exist in dist/\n```\n\n---\n\n## Decision Matrix\n\n| Context | Asset Location | Tool | Hook Execution |\n|---------|---------------|------|----------------|\n| `make dev` | `public/` | Leptos dev server | ❌ No |\n| `cargo leptos watch` | `public/` | Leptos dev server | ❌ No |\n| `cargo leptos build` | `dist/` | Trunk | ✅ Yes |\n| `trunk serve` | `dist/` | Trunk | ✅ Yes |\n\n---\n\n## Anti Patterns\n\n### ❌ Anti-Pattern 1: Relying on Trunk Hooks in Dev\n```toml\n# Trunk.toml\n[[hooks]]\nstage = \"pre_build\"\ncommand = \"cp\"\ncommand_arguments = [\"assets\", \"public/\"]\n```\n```bash\nmake dev  # Hook does NOT run\n```\n\n**Problem:** Trunk is not involved in `cargo leptos watch`.\n\n---\n\n### ❌ Anti-Pattern 2: Assets in `dist/` During Dev\n```rust\n<img src=\"/dist/assets/icon.svg\" />\n```\n\n**Problem:** Leptos serves from `public/`, not `dist/`.\n\n---\n\n### ❌ Anti-Pattern 3: `data-trunk` Directives in Dev\n```html\n<link data-trunk rel=\"copy-dir\" href=\"assets\" />\n```\n\n**Problem:** Leptos dev server ignores `data-trunk` attributes.\n\n---\n\n## Debugging Protocol\n\nWhen assets return 404 during `make dev`:\n```bash\n# 1. Verify Leptos config\ncat Cargo.toml | grep -A 5 \"\\[package.metadata.leptos\\]\"\n# Check assets-dir value\n\n# 2. Check assets physically exist\nls -la public/assets/\n\n# 3. Verify path in code matches public/\ngrep -r \"src=\\\"/assets\" src/\n\n# 4. Test direct access\ncurl http://localhost:3003/assets/symbols/icon.svg\n\n# 5. If still failing, restart dev server\n# Ctrl+C and run make dev again\n```\n\n---\n\n## Canonical Workflow\n\n### For Development (Daily Work)\n```bash\n# One-time setup\nmkdir -p public/assets\ncp -r /source/assets/symbols public/assets/\n\n# Daily workflow\nmake dev\n# Assets served from public/\n```\n\n### For Production Builds\n```bash\n# Trunk handles asset pipeline\ncargo leptos build --release\n# Assets copied to dist/ via hooks\n```\n\n---\n\n## Integration With Monorepo\n\nWhen assets are in shared packages:\n```bash\n# Development: Manual copy to public/\ncp -r /opt/docker/monorepo/packages-rust/rs-design/assets/symbols \\\n     public/assets/\n\n# Production: Trunk hook copies from source\n[[hooks]]\nstage = \"build\"\ncommand = \"cp\"\ncommand_arguments = [\n  \"-r\",\n  \"/opt/docker/monorepo/packages-rust/rs-design/assets/symbols\",\n  \"dist/assets/\"\n]\n```\n\n---\n\n## Makefile Integration\n```makefile\n.PHONY: dev setup-assets\n\nsetup-assets:\n\tmkdir -p public/assets\n\tcp -r /source/assets/symbols public/assets/\n\ndev: setup-assets\n\tcargo leptos watch\n```\n\n**Benefit:** Assets automatically updated on `make dev`.\n\n---\n\n## Canonical Justification\n\n> **Leptos and Trunk are separate tools with different asset models.**\n> Development speed requires lightweight serving without full bundling.\n\nThis rule enforces:\n- **No wasted time** debugging Trunk in dev mode\n- **Clear separation** between dev and prod pipelines\n- **Predictable behavior** across environments\n\n---\n\n## Canon References\n\n- Canon Rule #58 — Leptos Assets Dev Constraint (subfolder limitation)\n- Canon Rule #69 — Trunk Only Serves What's in dist/\n- Canon Rule #68 — Asset Must Exist in Final dist/\n- Canon Rule #56 — Monorepo CSS Build Pipeline"},{"number":86,"slug":"children-childrenfn-contract","title":"Children vs ChildrenFn Contract","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["leptos","children","ownership"],"language":"EN","version":"1.0.0","date":"2026-01-14","intro":"Using the wrong children type in Leptos components causes FnOnce errors and broken reactivity. Components must correctly distinguish between Children and ChildrenFn.","problem":"incorrect children type leads to fnonce errors and reactivity issues","solution":"use childrenfn for reactive wrappers and children for static slots","signals":["expected fn found fnonce","sync trait error","render failure"],"search_intent":"when to use childrenfn vs children leptos","keywords":["leptos childrenfn vs children","fnonce vs fn leptos","leptos wrapper component children","leptos reactive children pattern"],"body":"## Principle\n\n`Children` and `ChildrenFn` are **NOT interchangeable**.\n\n> **`Children` = `FnOnce` (call once)**\n> **`ChildrenFn` = `Fn` (call multiple times)**\n\nUsing the wrong type causes `FnOnce` errors, `Sync` violations, and broken reactivity.\n\n---\n\n## The Problem\n\nDevelopers encounter this error repeatedly:\n```\nerror[E0525]: expected a closure that implements the `Fn` trait, \nbut this closure only implements `FnOnce`\n```\n\n**Root causes:**\n1. Using `Children` in wrapper components\n2. Trying to call `children()` multiple times\n3. Attempting `StoredValue<Children>` (not `Sync`)\n4. Writing `{children}` instead of `{children()}`\n\n---\n\n## Always Use ChildrenFn When\n\n### 1. Wrapper Components\nComponents that wrap other content and may re-render:\n```rust\n#[component]\npub fn Dialog(\n    children: ChildrenFn, // ✅ Can re-render\n    #[prop(into)] open: RwSignal<bool>,\n) -> impl IntoView {\n    view! {\n        <DialogPrimitive open=open>\n            {children()} // ✅ Always call with ()\n        </DialogPrimitive>\n    }\n}\n```\n\n### 2. Components Used in `Show`\nAny component that appears inside reactive boundaries:\n```rust\nview! {\n    <Show when=move || open.get()>\n        <Dialog> // ✅ Dialog uses ChildrenFn\n            {children()} // ✅ Can re-render\n        </Dialog>\n    </Show>\n}\n```\n\n### 3. Components with Reactive Visibility\nAny component that conditionally renders:\n```rust\n#[component]\npub fn Collapsible(\n    children: ChildrenFn, // ✅ May show/hide\n    #[prop(into)] expanded: RwSignal<bool>,\n) -> impl IntoView {\n    view! {\n        <Show when=move || expanded.get()>\n            {children()} // ✅ Reactive\n        </Show>\n    }\n}\n```\n\n---\n\n## Always Use Children When\n\n### 1. Static Layout Slots\nComponents that render once and never change:\n```rust\n#[component]\npub fn DialogHeader(\n    children: Children, // ✅ Static slot\n) -> impl IntoView {\n    view! {\n        <div data-dialog-header=\"\">\n            {children()} // ✅ Called once\n        </div>\n    }\n}\n```\n\n### 2. Simple Wrappers\nDivs, sections, or containers without logic:\n```rust\n#[component]\npub fn Card(\n    children: Children, // ✅ No reactivity\n) -> impl IntoView {\n    view! {\n        <div class=\"card\">\n            {children()}\n        </div>\n    }\n}\n```\n\n---\n\n## Never Do These\n\n### ❌ Anti-Pattern 1: `{children}` Without `()`\n```rust\nview! {\n    <div>{children}</div> // ❌ WRONG\n}\n```\n\n**Why it fails:**\n`ChildrenFn` is `Arc<dyn Fn() -> AnyView>` which does NOT implement `RenderHtml`.\n\n**Correct:**\n```rust\nview! {\n    <div>{children()}</div> // ✅ Always call\n}\n```\n\n---\n\n### ❌ Anti-Pattern 2: `StoredValue<Children>`\n```rust\nlet children_stored = StoredValue::new(children); // ❌ WRONG\n```\n\n**Error:**\n```\nthe trait `Sync` is not implemented for `dyn FnOnce() -> AnyView + Send`\n```\n\n**Why it fails:**\n`Children` is `Box<dyn FnOnce()>` which is NOT `Sync`.\n\n**Correct:**\nUse `ChildrenFn` instead, or don't store at all.\n\n---\n\n### ❌ Anti-Pattern 3: Cloning Children\n```rust\nlet children_clone = children.clone(); // ❌ Does not exist\n```\n\n**Why it fails:**\nNeither `Children` nor `ChildrenFn` implement `Clone`.\n\n**Correct:**\nUse `ChildrenFn` which can be called multiple times without cloning.\n\n---\n\n### ❌ Anti-Pattern 4: Children in Wrapper Components\n```rust\n#[component]\npub fn Dialog(\n    children: Children, // ❌ WRONG - will break in Show\n    #[prop(into)] open: RwSignal<bool>,\n) -> impl IntoView {\n    view! {\n        <DialogPrimitive open=open>\n            {children()} // ❌ FnOnce error\n        </DialogPrimitive>\n    }\n}\n```\n\n**Why it fails:**\n`DialogPrimitive` expects `Fn + Send + Sync`, but `Children` is `FnOnce`.\n\n**Correct:**\n```rust\n#[component]\npub fn Dialog(\n    children: ChildrenFn, // ✅ Fn trait\n    #[prop(into)] open: RwSignal<bool>,\n) -> impl IntoView {\n    view! {\n        <DialogPrimitive open=open>\n            {children()} // ✅ Works\n        </DialogPrimitive>\n    }\n}\n```\n\n---\n\n## Decision Matrix\n\n| Use Case | Type | Reason |\n|----------|------|--------|\n| Wrapper (Dialog, Sheet, Button) | `ChildrenFn` | May re-render |\n| Inside `Show`, `Suspense` | `ChildrenFn` | Reactive boundary |\n| Static slot (Header, Footer) | `Children` | Renders once |\n| Simple div wrapper | `Children` | No reactivity |\n| Conditional visibility | `ChildrenFn` | Show/hide logic |\n\n---\n\n## Debugging Protocol\n\nWhen you see:\n```\nerror[E0525]: expected a closure that implements the `Fn` trait, \nbut this closure only implements `FnOnce`\n```\n\n**Check:**\n1. Is the component used inside `Show`? → Use `ChildrenFn`\n2. Is it a wrapper component? → Use `ChildrenFn`\n3. Does it have reactive state? → Use `ChildrenFn`\n4. Is it just a static slot? → Use `Children`\n\n---\n\n## Canonical Examples\n\n### ✅ Correct: Dialog (Wrapper)\n```rust\n#[component]\npub fn Dialog(\n    children: ChildrenFn, // ✅\n    #[prop(into)] open: RwSignal<bool>,\n) -> impl IntoView {\n    view! {\n        <DialogPrimitive open=open>\n            {children()} // ✅\n        </DialogPrimitive>\n    }\n}\n```\n\n### ✅ Correct: DialogHeader (Static Slot)\n```rust\n#[component]\npub fn DialogHeader(\n    children: Children, // ✅\n) -> impl IntoView {\n    view! {\n        <div data-dialog-header=\"\">\n            {children()} // ✅\n        </div>\n    }\n}\n```\n\n### ✅ Correct: Usage with Show\n```rust\nview! {\n    <Dialog open=modal_open> // ChildrenFn internally\n        <Show when=move || modal_open.get()>\n            <DialogContent> // ChildrenFn internally\n                {content()} // ✅\n            </DialogContent>\n        </Show>\n    </Dialog>\n}\n```\n\n---\n\n## Common Mistakes And Fixes\n\n### Mistake 1: Wrong Type in Wrapper\n```rust\n// ❌ WRONG\n#[component]\npub fn Sheet(children: Children, ...) -> impl IntoView { ... }\n\n// ✅ CORRECT\n#[component]\npub fn Sheet(children: ChildrenFn, ...) -> impl IntoView { ... }\n```\n\n### Mistake 2: Forgot `()`\n```rust\n// ❌ WRONG\nview! { <div>{children}</div> }\n\n// ✅ CORRECT\nview! { <div>{children()}</div> }\n```\n\n### Mistake 3: Trying to Store\n```rust\n// ❌ WRONG\nlet stored = StoredValue::new(children);\n\n// ✅ CORRECT\n// Don't store - just call children() when needed\n```\n\n---\n\n## Canonical Justification\n\n> **Leptos enforces ownership rules strictly.**\n> `FnOnce` can only be called once.\n> `Fn` can be called multiple times.\n> Wrapper components need `Fn` because they may re-render.\n\nThis rule enforces:\n- **Correct ownership** — No `FnOnce` violations\n- **Proper reactivity** — Components re-render when needed\n- **Type safety** — Compiler catches errors early\n- **SSR compatibility** — `Fn + Send + Sync` works everywhere\n\n---\n\n## Canon References\n\n- Canon Rule #2 — Ownership Rules (StoredValue, reactive access)\n- Canon Rule #50 — Provider Singleton Pattern (context + children)\n- Canon Rule #4 — Hydration Safety (SSR + reactivity)\n\n---\n\n## Mental Model\n```\nComponent Type → Children Type → Call Pattern\n\nWrapper         → ChildrenFn   → {children()}\nStatic Slot     → Children     → {children()}\nReactive        → ChildrenFn   → {children()}\nSimple Div      → Children     → {children()}\n```\n\n**Rule of thumb:**\n> When in doubt, use `ChildrenFn`.\n> It works everywhere `Children` works, plus reactivity.\n\n---\n\n## Related Errors\n\nIf you see these errors, you have a Children/ChildrenFn problem:\n\n1. `expected Fn, found FnOnce`\n2. `Sync is not implemented for dyn FnOnce`\n3. `method 'clone' not found`\n4. `ToChildren` trait bound not satisfied\n\n**Solution:** Check if you're using the right type for your use case."},{"number":87,"slug":"leptos-ssr-script-placement","title":"Leptos SSR Script Placement","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["ssr","hydration","scripts"],"language":"EN","version":"1.0.0","date":"2026-01-14","intro":"Scripts placed in index.html execute before hydration and bind to stale DOM nodes, causing inconsistent behavior. Proper placement ensures scripts run against the hydrated DOM.","problem":"scripts execute before hydration leading to broken event bindings","solution":"inject scripts inside shell after app rendering","signals":["event not firing","phantom behavior","hydration mismatch"],"search_intent":"where to place scripts in leptos ssr","keywords":["leptos script placement ssr","hydration event binding issue","index html script problem","leptos shell script injection"],"body":"## Principle\n\n**Imperative JavaScript MUST be injected in `shell()`, NOT in `index.html`.**\n\nIn Leptos SSR apps, `index.html` is a static template that does NOT participate in the hydration cycle. Scripts placed there execute before the real DOM exists, causing \"phantom behavior\" where events are registered but never fire.\n\n---\n\n## The Problem\n\n### ❌ Anti-Pattern: Script in `index.html`\n```html\n<!-- index.html -->\n<!DOCTYPE html>\n<html>\n<head>...</head>\n<body>\n    <script>\n    document.addEventListener(\"click\", (e) => {\n        const btn = e.target.closest(\"[data-copy-button]\");\n        // ❌ btn is never found, even though HTML has the button\n    });\n    </script>\n</body>\n</html>\n```\n\n**Why it fails:**\n- Script runs before SSR DOM is hydrated\n- Event listener attaches to wrong/stale DOM tree\n- After hydration, the real DOM has no listener\n- Result: clicks detected but nothing happens\n\n**Symptoms:**\n- Event seems to fire intermittently\n- `console.log` shows click detected but action fails\n- DOM inspection shows correct attributes but no behavior\n- Works in production build but not dev\n\n---\n\n## Canonical Solution\n\n### ✅ Pattern: Script in `shell()`\n```rust\n#[cfg(feature = \"ssr\")]\npub fn shell(options: leptos::config::LeptosOptions) -> impl IntoView {\n    use leptos_meta::*;\n\n    view! {\n        <!DOCTYPE html>\n        <html lang=\"en\">\n            <head>\n                <meta charset=\"utf-8\"/>\n                <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n                <AutoReload options=options.clone()/>\n                <HydrationScripts options/>\n                <MetaTags/>\n            </head>\n            <body>\n                <App/>\n                \n                // ✅ Script AFTER <App/>, inside shell()\n                <script>\n                document.addEventListener(\"click\", (e) => {{\n                    const btn = e.target.closest(\"[data-copy-button]\");\n                    if (!btn) return;\n                    \n                    const text = btn.getAttribute(\"data-copy-text\");\n                    if (!text) return;\n                    \n                    // clipboard logic...\n                }}, true); // capture phase for hydrated apps\n                </script>\n            </body>\n        </html>\n    }\n}\n```\n\n**Why it works:**\n- Script executes AFTER SSR DOM is rendered\n- Has access to actual hydrated elements\n- Event delegation survives hydration\n- Works in both dev and production\n\n---\n\n## Decision Matrix\n\n| Script Location | SSR | Hydration | Event Access | Verdict |\n|----------------|-----|-----------|--------------|---------|\n| `index.html` | ❌ Ignored | ❌ Replaced | ❌ Stale DOM | **FORBIDDEN** |\n| `shell()` inline | ✅ Rendered | ✅ Preserved | ✅ Real DOM | **CORRECT** |\n| `shell()` external | ✅ Rendered | ✅ Preserved | ✅ Real DOM | **ACCEPTABLE** |\n\n---\n\n## Scope Of Application\n\n**Scripts that MUST be in `shell()`:**\n- Event delegation (clicks, keyboard shortcuts)\n- Clipboard operations\n- Global observers (IntersectionObserver, MutationObserver)\n- Analytics / tracking\n- Third-party integrations (e.g., Stripe, Google Analytics)\n\n**Scripts that CAN be in `index.html`:**\n- Theme flash prevention (must run before render)\n- Critical path CSS loading\n- Feature detection that doesn't touch DOM\n\n---\n\n## Real World Example\n\n### Problem Statement\nCopy button with `data-copy-button` and `data-copy-text` attributes renders correctly in DOM, but clicking does nothing. Console shows no errors.\n\n### Root Cause\nEvent listener in `index.html` attached to pre-hydration DOM. After hydration, listener was orphaned.\n\n### Solution\n```rust\n// src/app.rs\npub fn shell(options: LeptosOptions) -> impl IntoView {\n    view! {\n        <!DOCTYPE html>\n        <html>\n            <body>\n                <App/>\n                <script>\n                document.addEventListener(\"click\", (e) => {{\n                    const btn = e.target.closest(\"[data-copy-button]\");\n                    if (!btn) return;\n                    \n                    const text = btn.getAttribute(\"data-copy-text\");\n                    const textarea = document.createElement(\"textarea\");\n                    textarea.value = text;\n                    textarea.style.position = \"fixed\";\n                    textarea.style.opacity = \"0\";\n                    \n                    document.body.appendChild(textarea);\n                    textarea.select();\n                    document.execCommand(\"copy\");\n                    document.body.removeChild(textarea);\n                    \n                    btn.textContent = \"Copied!\";\n                    setTimeout(() => btn.textContent = \"Copy\", 1500);\n                }}, true);\n                </script>\n            </body>\n        </html>\n    }\n}\n```\n\n**Result:** Copy functionality works reliably in dev and production.\n\n---\n\n## Debugging Protocol\n\nWhen imperative JS \"doesn't work\" in Leptos SSR:\n```bash\n# 1. Check script location\ngrep -r \"<script>\" src/app.rs\n# Should be in shell(), not index.html\n\n# 2. Verify shell() is being used\ngrep \"pub fn shell\" src/app.rs\n\n# 3. Confirm SSR feature is enabled\ncargo check --features ssr\n\n# 4. Test in browser console\ndocument.querySelectorAll('[data-copy-button]')\n# Should return NodeList with elements\n\n# 5. Manually test event\ndocument.addEventListener('click', e => console.log(e.target), true)\n# Should log all clicks\n```\n\n---\n\n## Integration With Monorepo\n\nWhen scripts are shared across multiple Leptos apps:\n```rust\n// Option A: Inline in each shell()\npub fn shell() -> impl IntoView {\n    view! {\n        <body>\n            <App/>\n            <script>{include_str!(\"../scripts/runtime.js\")}</script>\n        </body>\n    }\n}\n\n// Option B: External with correct path\npub fn shell() -> impl IntoView {\n    view! {\n        <body>\n            <App/>\n            <script src=\"/scripts/runtime.js\"></script>\n        </body>\n    }\n}\n```\n\n**IMPORTANT:** External scripts must be in `assets-dir` (see Canon Rule #85).\n\n---\n\n## Canonical Justification\n\n> **Leptos SSR has a specific hydration model.**  \n> Scripts that don't respect this model create \"phantom behaviors\" where the app appears broken despite correct DOM structure.\n\nThis rule enforces:\n- **Predictable script execution** relative to hydration\n- **No \"works sometimes\" bugs** caused by race conditions\n- **Clear separation** between static templates and dynamic rendering\n- **Maintainable debugging** by eliminating invisible failure modes\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #85 — Leptos Asset Pipeline in Dev Mode\n- Canon Rule #58 — Leptos Assets Dev Constraint (subfolder limitation)\n- Canon Rule #04 — Hydration Safety in SSR\n\n---\n\n## Version History\n\n- **1.0.0** (2026-01-14): Initial rule based on Copy Button implementation debugging"},{"number":89,"slug":"primitives-no-browser-apis","title":"Primitives Must Never Touch Browser APIs","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["primitives","ssr","browser"],"language":"EN","version":"1.0.0","date":"2026-01-14","intro":"Accessing browser APIs inside primitives breaks SSR compatibility and violates architectural separation. Primitives must remain environment-agnostic.","problem":"primitives access browser apis causing ssr and hydration issues","solution":"restrict primitives to html and delegate behavior to runtime js","signals":["window usage in primitives","hydration errors","ssr breakage"],"search_intent":"why primitives should not use browser apis","keywords":["no browser api primitives","ssr safe components design","leptos primitives architecture","separation runtime js"],"body":"## Principle\n\n**UI Primitives are structural only. They MUST NOT access browser APIs.**\n\n---\n\n## Absolute Prohibitions\n\nPrimitives MUST NOT reference:\n\n- window\n- document\n- navigator\n- clipboard\n- web_sys\n- wasm_bindgen\n- execCommand\n\n---\n\n## Correct Responsibility Split\n\n```text\nPrimitive → HTML + data-* attributes\nRuntime JS → browser APIs + behavior\n```\n\n---\n\n## Justification\n\n- Preserves SSR compatibility\n- Prevents hydration breakage\n- Maintains architectural layering\n- Enables deterministic testing\n\n---\n\n## Enforcement\n\nAny browser API reference inside `primitives/` is a hard violation.\n\n---\n\n## Related Rules\n\n- Canon Rule #87 — Leptos SSR Script Placement\n- Canon Rule #91 — Markdown Is Render-Only"},{"number":90,"slug":"hydration-is-dom-replacement","title":"Hydration Is DOM Replacement, Not Enhancement","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","ssr","dom"],"language":"EN","version":"1.0.0","date":"2026-01-14","intro":"Assuming hydration enhances existing DOM leads to broken interactions and lost event bindings. Hydration replaces nodes entirely, invalidating pre-existing references.","problem":"developers assume dom persists after hydration causing broken behavior","solution":"treat hydration as dom replacement and avoid relying on node identity","signals":["event lost after hydration","inconsistent behavior","silent failures"],"search_intent":"why hydration breaks dom references","keywords":["hydration dom replacement","leptos hydration behavior","ssr dom identity issue","event listener lost hydration"],"body":"## Principle\n\n**Hydration replaces the DOM. It does not enhance it.**\n\nAny assumption that pre-hydration DOM nodes persist is invalid.\n\n---\n\n## Consequences\n\n- Node identity is not preserved\n- Direct JS bindings are lost\n- Pre-hydration listeners are discarded\n\n---\n\n## Common Failure Symptoms\n\n- Works in prod, fails in dev\n- Click detected but no action\n- Correct DOM, broken behavior\n- Silent runtime failures\n\n---\n\n## Canonical Response\n\n- Use event delegation\n- Inject JS after SSR render\n- Never depend on node identity\n\n---\n\n## Related Rules\n\n- Canon Rule #87 — Leptos SSR Script Placement\n- Canon Rule #88 — Event Delegation Only"},{"number":91,"slug":"markdown-render-only","title":"Markdown and Code Blocks Are Render-Only","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["markdown","rendering","ui"],"language":"EN","version":"1.0.0","date":"2026-01-14","intro":"Embedding behavior inside markdown components creates SSR coupling and unreliable runtime behavior. Markdown must remain purely declarative.","problem":"markdown components include behavior instead of pure rendering","solution":"limit markdown components to structure and delegate behavior externally","signals":["event handlers in markdown","clipboard logic inside render","ssr coupling"],"search_intent":"why markdown components should not have behavior","keywords":["markdown render only pattern","separate behavior from content","leptos markdown components","ssr safe markdown rendering"],"body":"## Principle\n\n**Markdown components MUST only render structure, never behavior.**\n\n---\n\n## Forbidden Responsibilities\n\nMarkdown components MUST NOT:\n\n- Copy to clipboard\n- Register click handlers\n- Manage imperative state\n- Call browser APIs\n\n---\n\n## Correct Pattern\n\n```html\n<pre data-copy-button data-copy-text=\"...\">\n```\n\nBehavior is handled by runtime JS via event delegation.\n\n---\n\n## Justification\n\n- Markdown is static content\n- Behavior must survive hydration\n- Prevents SSR/runtime coupling\n\n---\n\n## Related Rules\n\n- Canon Rule #87 — Leptos SSR Script Placement\n- Canon Rule #89 — Primitives No Browser APIs"},{"number":92,"slug":"ssr-debug-heuristic","title":"If It Works in Prod but Not Dev, Suspect Hydration Order","status":"ENFORCED","severity":"MEDIUM","category":"governance","tags":["debugging","ssr","hydration"],"language":"EN","version":"1.0.0","date":"2026-01-14","intro":"Differences between development and production behavior often indicate hydration timing issues. Recognizing this pattern reduces debugging time.","problem":"developers misdiagnose issues caused by hydration order differences","solution":"use heuristic to identify hydration timing problems early","signals":["works in prod fails in dev","no errors","inconsistent behavior"],"search_intent":"why app works in prod but","keywords":["ssr debug heuristic","hydration timing issue","dev vs prod behavior","leptos hydration debugging"],"body":"## Principle\n\n**Behavior differences between dev and prod indicate hydration or script timing issues.**\n\n---\n\n## Heuristic\n\n```text\nWorks in prod\nFails in dev\nNo JS errors\n→ Hydration order violation\n```\n\n---\n\n## Immediate Checks\n\n1. Script location (must be in shell)\n2. Event delegation usage\n3. Primitive touching browser APIs\n4. Pre-hydration execution\n\n---\n\n## Purpose\n\n- Prevents false bug hunts\n- Reduces debugging time\n- Encodes institutional knowledge\n\n---\n\n## Related Rules\n\n- Canon Rule #87 — Leptos SSR Script Placement\n- Canon Rule #90 — Hydration Is DOM Replacement"},{"number":93,"slug":"leptos-wasm-dev-builds-must-use-release-mode","title":"Leptos WASM Dev Builds Must Use Release Mode","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["wasm","leptos","performance"],"language":"EN","version":"1.0.0","date":"2026-01-14","intro":"Debug-mode WASM builds are too large and slow for interactive development, causing browser freezes and misleading debugging. Development must use optimized builds.","problem":"debug wasm builds cause large binaries and browser stalls","solution":"use release mode for leptos wasm development builds","signals":["browser freeze","slow hydration","large wasm size"],"search_intent":"why wasm dev build is slow leptos","keywords":["leptos wasm release mode","wasm debug build size issue","browser stall wasm","cargo leptos watch release"],"body":"## Principle\n\n**Interactive Leptos applications that compile to WASM MUST run dev/watch in `--release` mode.**\n\nDebug-mode WASM builds are prohibitively large, cause browser-side compilation stalls, and make the application unusable during development.\n\nThis is a toolchain constraint, not an application bug.\n\n---\n\n## The Problem\n\nWhen using default dev/watch mode:\n\n- WASM is built with full debug symbols\n- Binary size explodes to hundreds of megabytes\n- Browser must download, validate, compile, and instantiate the module\n- Main thread is blocked for tens of seconds to minutes\n- App appears frozen after refresh with no errors\n\n### Real Symptoms\n\n- Page loads, then freezes for ~30–60s\n- No console errors\n- Hydration logs appear only after long delay\n- Hot reload WebSocket disconnects\n- Developers misdiagnose SSR, routing, or JS issues\n\n---\n\n## Anti Pattern\n\n```bash\n# ❌ FORBIDDEN for medium/large apps\ncargo leptos watch\n```\n\nThis command produces debug WASM and must not be used for interactive development in CanonRS-scale apps.\n\n---\n\n## Canonical Pattern\n\n```bash\n# ✅ REQUIRED\ncargo leptos watch --release\n```\n\nRelease mode:\n- Strips debug symbols\n- Applies LLVM optimizations\n- Produces small, browser-friendly WASM\n- Enables instant refresh and hydration\n\n---\n\n## Expected Results\n\n| Mode | Typical WASM Size | Browser Behavior |\n|----|-------------------|------------------|\n| Debug | 100–300 MB | Freeze / Stall |\n| Release | 1–8 MB | Instant load |\n\nAny WASM larger than ~10 MB in dev indicates a rule violation.\n\n---\n\n## Approved Alternatives\n\nIf `--release` cannot be used temporarily:\n\n```bash\n# Acceptable fallback (not ideal)\nRUSTFLAGS=\"-C debuginfo=0 -C opt-level=2\" cargo leptos watch\n```\n\nThis is a mitigation, not a replacement for `--release`.\n\n---\n\n## Enforcement\n\n- All CanonRS Leptos apps MUST document `--release` as the default dev command\n- Scripts (`dev.sh`, `Makefile`, `Justfile`) MUST use release mode\n- Debug WASM usage is allowed ONLY for:\n  - minimal demos\n  - reproduction of compiler bugs\n  - non-interactive examples\n\n---\n\n## Canonical Justification\n\n> **WASM debug builds are incompatible with interactive development.**  \n> The cost of browser-side compilation exceeds acceptable latency, creating false negatives and wasted debugging effort.\n\nThis rule exists to:\n- Eliminate phantom performance bugs\n- Align dev experience with production reality\n- Encode toolchain limitations as governance\n- Prevent repeated misdiagnosis\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #87 — Leptos SSR Script Placement\n- Canon Rule #90 — Hydration Is DOM Replacement\n- Canon Rule #92 — SSR Debugging Heuristic\n\n---\n\n## Version History\n\n- **1.0.0** (2026-01-14): Initial rule based on Workbench WASM stall diagnosis"},{"number":94,"slug":"leptos-workspace-features-must-be-explicit","title":"Leptos Workspace Features Must Be Explicit","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["leptos","workspace","features","cargo"],"language":"EN","version":"1.0.0","date":"2025-01-15","intro":"Leptos macro expansion fails when required features like nightly are not declared at the workspace level. Missing workspace feature flags cause cascading E0282 errors in view! macros despite correct code.","problem":"workspace level features are missing causing leptos macro type inference failures","solution":"declare leptos features explicitly in workspace dependencies instead of member crates","signals":["E0282 error","cannot infer type","view macro failure"],"search_intent":"how to fix leptos E0282 view macro","keywords":["leptos workspace features nightly","cargo workspace feature resolution","leptos E0282 error fix","view macro type inference leptos"],"body":"## Principle\n\n**Leptos features (especially `nightly`) MUST be declared explicitly in `[workspace.dependencies]`, not delegated to member crates.**\n\nWithout workspace-level feature declaration, the Leptos macro expansion fails with cascading E0282 \"type annotations needed\" errors in every `view!` macro, despite correct code.\n\nThis is a toolchain requirement, not a code quality issue.\n\n---\n\n## The Problem\n\nWhen `features = [\"nightly\"]` is declared only in member crate dependencies:\n\n- Leptos macros cannot infer types in closures\n- Every `view!` block fails with E0282\n- Error messages are misleading (point to view! code, not features)\n- No compilation succeeds despite syntactically correct code\n\n### Real Symptoms\n```rust\nview! {\n    <Sidebar>\n        <SidebarContent>\n            {items.iter().map(|item| {  // ← E0282 here\n                view! { <div>{item.label}</div> }\n            }).collect::<Vec<_>>()}\n        </SidebarContent>\n    </Sidebar>\n}\n```\n\nError:\n```\nerror[E0282]: type annotations needed\n  --> src/components/sidebar.rs:12:21\n   |\n12 |                     view! {\n   |                     ^^^^^^ cannot infer type\n```\n\n---\n\n## Anti Pattern\n```toml\n# ❌ FORBIDDEN: Features only in member crate\n[workspace.dependencies]\nleptos = \"0.8\"  # NO FEATURES\n\n# shell/Cargo.toml\n[dependencies]\nleptos = { workspace = true, features = [\"nightly\"] }\n```\n\nThis fails because macro expansion happens at workspace resolution time, before member features are applied.\n\n---\n\n## Canonical Pattern\n```toml\n# ✅ REQUIRED: Features in workspace root\n[workspace.dependencies]\nleptos = { version = \"0.8\", features = [\"nightly\"] }\nleptos_router = { version = \"0.8\", features = [\"nightly\"] }\n\n# Member crates inherit correctly\n[dependencies]\nleptos = { workspace = true }\nleptos_router = { workspace = true }\n```\n\n---\n\n## Why This Happens\n\nLeptos `view!` macro uses advanced type inference that relies on:\n1. Nightly Rust features (type system extensions)\n2. Workspace-level feature resolution\n3. Consistent feature flags across all crates\n\nWhen `nightly` is not workspace-level:\n- Macro sees stable Rust type system\n- Cannot infer closure return types\n- Cascades to all nested view! blocks\n\n---\n\n## Diagnostic Checklist\n\nIf you see E0282 in view! macros:\n```bash\n# 1. Check workspace features\ngrep -A 3 \"leptos.*=\" Cargo.toml\n\n# 2. Verify nightly is present\n# Should see: features = [\"nightly\"]\n\n# 3. Clean and rebuild\ncargo clean\ncargo build\n\n# 4. If still failing, check rust-toolchain\ncat rust-toolchain.toml  # Should be \"nightly\"\n```\n\n---\n\n## Enforcement\n\n- All Leptos workspaces MUST declare `features = [\"nightly\"]` in workspace root\n- Member crates MUST NOT redeclare features (inherit from workspace)\n- CI MUST fail if workspace.dependencies lacks nightly feature\n- New projects MUST use canonical template with correct feature declaration\n\n---\n\n## Canonical Justification\n\n> **Macro expansion is workspace-scoped, not crate-scoped.**  \n> Feature resolution happens before member crate dependencies are processed.  \n> This is Cargo's designed behavior, not a Leptos bug.\n\nThis rule exists to:\n- Eliminate 2+ hours of debugging time per setup\n- Encode Cargo's workspace feature resolution as governance\n- Prevent misdiagnosis of \"type inference bugs\"\n- Align with Leptos 0.8's architecture assumptions\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #93 — Leptos WASM Dev Builds Must Use Release Mode\n- Canon Rule #95 — SSR Requires Complete HTML Shell\n- Canon Rule #96 — SSR Requires Explicit Provider Tree\n\n---\n\n## Version History\n\n- **1.0.0** (2025-01-15): Initial rule based on Workbench migration E0282 debugging"},{"number":95,"slug":"ssr-requires-complete-html-shell","title":"SSR Requires Complete HTML Shell","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["ssr","hydration","html","leptos"],"language":"EN","version":"1.0.0","date":"2025-01-15","intro":"Leptos SSR requires a full HTML document structure to support meta injection and hydration. Missing html head or body tags causes runtime panics and empty responses.","problem":"missing html shell prevents SSR rendering and hydration","solution":"provide full html head and body structure through shell function","signals":["ERR_EMPTY_RESPONSE","leptos_meta panic","missing head tag"],"search_intent":"how to fix leptos ssr empty response","keywords":["leptos ssr html shell","leptos_meta head tag error","hydration scripts placement leptos","ssr empty response fix"],"body":"## Principle\n\n**Leptos SSR applications MUST provide a complete HTML document shell with explicit `<html>`, `<head>`, and `<body>` tags.**\n\nWithout this shell, leptos_meta components panic at runtime, SSR fails silently, and hydration cannot occur. The shell is structural, not cosmetic.\n\n---\n\n## The Problem\n\nWhen SSR components use `leptos_meta` (Title, Meta, etc.) without a complete HTML shell:\n\n- Runtime panic: \"you are using leptos_meta without a </head> tag\"\n- Server returns empty responses (ERR_EMPTY_RESPONSE)\n- No error message in build, only at first request\n- Hydration scripts have nowhere to attach\n\n### Real Symptoms\n```\nthread 'tokio-runtime-worker' panicked at leptos_meta/src/lib.rs:250:18:\nyou are using leptos_meta without a </head> tag\n```\n\nBrowser shows:\n```\nERR_EMPTY_RESPONSE\n```\n\n---\n\n## Anti Pattern\n```rust\n// ❌ FORBIDDEN: No HTML shell\n#[component]\npub fn App() -> impl IntoView {\n    provide_meta_context();\n    view! {\n        <Title text=\"My App\"/>\n        <Router>\n            <Routes>...</Routes>\n        </Router>\n    }\n}\n```\n\nThis fails because:\n- leptos_meta expects `<head>` to exist\n- SSR has no document structure to render into\n- Hydration scripts cannot be injected\n\n---\n\n## Canonical Pattern\n```rust\n// ✅ REQUIRED: Complete HTML shell\n#[cfg(feature = \"ssr\")]\npub fn shell(options: leptos::config::LeptosOptions) -> impl IntoView {\n    view! {\n        <!DOCTYPE html>\n        <html lang=\"en\">\n            <head>\n                <meta charset=\"utf-8\"/>\n                <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n                <AutoReload options=options.clone()/>\n                <HydrationScripts options/>\n                <MetaTags/>\n            </head>\n            <body>\n                <App/>\n            </body>\n        </html>\n    }\n}\n\n#[component]\npub fn App() -> impl IntoView {\n    provide_meta_context();\n    view! {\n        <Title text=\"My App\"/>\n        <Router>...</Router>\n    }\n}\n```\n\nServer uses `shell()` instead of `App()`:\n```rust\nlet app = Router::new()\n    .leptos_routes(&leptos_options, routes, {\n        let leptos_options = leptos_options.clone();\n        move || shell(leptos_options.clone())\n    })\n    .with_state(leptos_options);\n```\n\n---\n\n## Required Shell Components\n\n### Minimum Required\n```html\n<!DOCTYPE html>\n<html>\n    <head>\n        <AutoReload options/>\n        <HydrationScripts options/>\n        <MetaTags/>\n    </head>\n    <body>\n        <App/>\n    </body>\n</html>\n```\n\n### Recommended Production\n```html\n<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"light\">\n    <head>\n        <meta charset=\"utf-8\"/>\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n        <link rel=\"stylesheet\" href=\"/app.css\"/>\n        <AutoReload options/>\n        <HydrationScripts options/>\n        <MetaTags/>\n    </head>\n    <body>\n        <App/>\n        <script src=\"/runtime.js\"></script>\n    </body>\n</html>\n```\n\n---\n\n## Why This Is Required\n\nSSR operates in two phases:\n\n1. **Server render**: Generates HTML string\n   - Needs `<head>` to inject meta tags\n   - Needs `<body>` to place app content\n   - Needs `<!DOCTYPE>` for valid HTML5\n\n2. **Client hydration**: Attaches JS to existing DOM\n   - HydrationScripts must be in `<head>`\n   - App component must be in `<body>`\n   - DOM structure must match server output\n\nWithout the shell:\n- Phase 1 has no target to render into\n- Phase 2 has no structure to hydrate\n- leptos_meta has no `<head>` to populate\n\n---\n\n## Diagnostic Checklist\n\nIf you see \"leptos_meta without </head> tag\":\n```bash\n# 1. Verify shell() exists with #[cfg(feature = \"ssr\")]\ngrep -A 5 \"pub fn shell\" src/lib.rs\n\n# 2. Check server uses shell(), not App()\ngrep \"shell(\" src/main.rs\n\n# 3. Verify shell has complete structure\n# Must have: <!DOCTYPE html>, <html>, <head>, <body>\n\n# 4. Check MetaTags placement\n# Must be inside <head>\n```\n\n---\n\n## Enforcement\n\n- All Leptos SSR apps MUST implement `shell()` function\n- `shell()` MUST include `<!DOCTYPE html><html><head><body>`\n- Server routes MUST call `shell()`, not `App()` directly\n- `MetaTags` MUST be inside `<head>`\n- `App` component MUST be inside `<body>`\n\n---\n\n## Canonical Justification\n\n> **SSR is HTML document generation, not component mounting.**  \n> The server produces a complete HTML document.  \n> Partial DOM injection is a CSR pattern and does not apply to SSR.\n\nThis rule exists to:\n- Make SSR's document-centric nature explicit\n- Prevent \"empty response\" debugging cycles\n- Align with browser expectations for valid HTML\n- Enable proper hydration anchoring\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #94 — Leptos Workspace Features Must Be Explicit\n- Canon Rule #96 — SSR Requires Explicit Provider Tree\n- Canon Rule #90 — Hydration Is DOM Replacement\n\n---\n\n## Version History\n\n- **1.0.0** (2025-01-15): Initial rule based on Workbench migration SSR shell requirement"},{"number":96,"slug":"ssr-requires-explicit-provider-tree","title":"SSR Requires Explicit Provider Tree","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["providers","context","ssr","leptos"],"language":"EN","version":"1.0.0","date":"2025-01-15","intro":"SSR in Leptos does not auto-inject context providers, causing runtime panics when contexts are missing. All required providers must be explicitly declared in the component tree.","problem":"missing providers cause context not found panics during SSR","solution":"wrap app with all required providers in correct dependency order","signals":["context not found","server panic","empty response"],"search_intent":"how to fix leptos context not found","keywords":["leptos provider tree ssr","context not found leptos fix","leptos theme provider missing","ssr provider dependency order"],"body":"## Principle\n\n**Leptos SSR applications MUST explicitly wrap the component tree with all required Providers in the App component.**\n\nSSR cannot infer or auto-inject context providers. Every provider that child components depend on must be explicitly present in the tree, or the server will panic with \"Context not found\" errors.\n\n---\n\n## The Problem\n\nWhen components use contexts (Theme, Density, etc.) but the App component doesn't provide them:\n\n- Server panics: \"ThemeContext not found. Make sure ThemeProvider wraps your app.\"\n- Error occurs at first render, not at build time\n- Browser shows ERR_EMPTY_RESPONSE\n- No indication which provider is missing until you read panic message\n\n### Real Symptoms\n```\nthread 'tokio-runtime-worker' panicked at rs-design/src/providers/theme_types.rs:25:10:\nThemeContext not found. Make sure ThemeProvider wraps your app.\n```\n\nOr:\n```\nthread 'tokio-runtime-worker' panicked at rs-design/src/providers/density_types.rs:50:10:\nDensityContext not found. Make sure DensityProvider is in the component tree.\n```\n\n---\n\n## Anti Pattern\n```rust\n// ❌ FORBIDDEN: No providers\n#[component]\npub fn App() -> impl IntoView {\n    provide_meta_context();\n    view! {\n        <Title text=\"My App\"/>\n        <Router>\n            <Routes>\n                <Route path=\"\" view=HomePage/>\n            </Routes>\n        </Router>\n    }\n}\n\n// HomePage uses ThemeContext but it's not provided\n#[component]\nfn HomePage() -> impl IntoView {\n    let theme = use_theme(); // ← PANIC: ThemeContext not found\n    view! { <div class=theme.class>\"Hello\"</div> }\n}\n```\n\nThis fails because:\n- SSR renders top-down\n- Contexts must exist before children access them\n- No auto-wiring of providers in SSR\n\n---\n\n## Canonical Pattern\n```rust\n// ✅ REQUIRED: Explicit provider tree\nuse rs_design::providers::{ThemeProvider, DensityProvider};\n\n#[component]\npub fn App() -> impl IntoView {\n    provide_meta_context();\n\n    view! {\n        <Title text=\"My App\"/>\n        <ThemeProvider>\n            <DensityProvider>\n                <Router>\n                    <Routes>\n                        <Route path=\"\" view=HomePage/>\n                    </Routes>\n                </Router>\n            </DensityProvider>\n        </ThemeProvider>\n    }\n}\n\n// Now HomePage can safely use contexts\n#[component]\nfn HomePage() -> impl IntoView {\n    let theme = use_theme(); // ✅ Works\n    let density = use_density(); // ✅ Works\n    view! { <div>\"Hello\"</div> }\n}\n```\n\n---\n\n## Provider Ordering Rules\n\nProviders MUST be nested in dependency order:\n```rust\n// ✅ CORRECT: Dependencies inside dependents\n<ThemeProvider>           // ← Outer: no dependencies\n    <DensityProvider>     // ← Uses theme\n        <Router>          // ← Uses both\n            <App/>\n        </Router>\n    </DensityProvider>\n</ThemeProvider>\n\n// ❌ WRONG: Dependent outside dependency\n<DensityProvider>         // ← Tries to use theme\n    <ThemeProvider>       // ← Not available yet\n        <Router>\n```\n\nCommon provider order for CanonRS apps:\n\n1. `ThemeProvider` (outermost - no dependencies)\n2. `DensityProvider` (depends on theme)\n3. Domain providers (SelectionProvider, DragDropProvider, etc.)\n4. `Router` (innermost - uses all contexts)\n\n---\n\n## Why SSR Cannot Auto Provide\n\nCSR differences that don't apply to SSR:\n\n| CSR Behavior | SSR Behavior |\n|--------------|--------------|\n| Can mount providers lazily | Must exist before render |\n| Can inject via root mount | No injection point |\n| Runtime provider discovery | Compile-time tree only |\n\nSSR is static tree generation. There is no \"mount point\" where providers can be injected after the fact.\n\n---\n\n## Common Providers In CanonRS\n```rust\n// Standard CanonRS provider stack\nuse rs_design::providers::{\n    ThemeProvider,\n    DensityProvider,\n};\nuse rs_design::ui::selection::SelectionProvider;\nuse rs_design::ui::drag_drop::{DragDropProvider, DragDropCallbacksProvider};\nuse rs_design::SidebarProvider;\n\n#[component]\npub fn App() -> impl IntoView {\n    provide_meta_context();\n\n    view! {\n        <ThemeProvider>\n            <DensityProvider>\n                <DragDropProvider>\n                    <DragDropCallbacksProvider>\n                        <SelectionProvider<String> mode=SelectionMode::Multiple>\n                            <SidebarProvider>\n                                <Router>\n                                    <AppRoutes/>\n                                </Router>\n                            </SidebarProvider>\n                        </SelectionProvider<String>>\n                    </DragDropCallbacksProvider>\n                </DragDropProvider>\n            </DensityProvider>\n        </ThemeProvider>\n    }\n}\n```\n\n---\n\n## Diagnostic Checklist\n\nIf you see \"Context not found\" panic:\n```bash\n# 1. Identify missing provider from panic message\n# Example: \"ThemeContext not found\" → need ThemeProvider\n\n# 2. Check App component structure\ngrep -A 20 \"pub fn App\" src/lib.rs\n\n# 3. Verify provider is imported\ngrep \"ThemeProvider\" src/lib.rs\n\n# 4. Check provider wraps Router\n# Provider must be OUTSIDE Router, not inside\n\n# 5. Verify provider order (dependencies first)\n```\n\n---\n\n## Enforcement\n\n- All SSR apps MUST wrap Router with required providers\n- Providers MUST be ordered by dependency (no deps first)\n- New providers MUST be documented with their dependencies\n- CI MUST test SSR render to catch missing providers\n\n---\n\n## Canonical Justification\n\n> **SSR is static tree generation with no runtime injection.**  \n> Contexts are compile-time tree structure, not runtime services.  \n> Providers must be explicit because SSR has no mounting phase.\n\nThis rule exists to:\n- Make provider dependencies explicit and auditable\n- Prevent \"works in dev, breaks in prod\" SSR issues\n- Encode SSR's static nature as governance\n- Eliminate context discovery debugging cycles\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #94 — Leptos Workspace Features Must Be Explicit\n- Canon Rule #95 — SSR Requires Complete HTML Shell\n- Canon Rule #99 — CSR Islands Must Not Own Routing or Providers\n\n---\n\n## Version History\n\n- **1.0.0** (2025-01-15): Initial rule based on Workbench migration provider panic debugging"},{"number":97,"slug":"leptos-08-requires-floating-nightly-toolchain","title":"Leptos 0.8 Requires Floating Nightly Toolchain","status":"ENFORCED","severity":"MEDIUM","category":"build-tooling","tags":["rust","nightly","toolchain","leptos"],"language":"EN","version":"1.0.0","date":"2025-01-15","intro":"Pinned nightly toolchains or minor versions break compatibility with Leptos 0.8 due to evolving unstable features. Floating versions are required to maintain compatibility.","problem":"pinned toolchain or versions cause build incompatibility errors","solution":"use floating nightly toolchain and unpinned minor versions in dependencies","signals":["rustc not supported","edition2024 error","dependency conflict"],"search_intent":"why leptos build fails with rustc version","keywords":["leptos nightly toolchain required","rustc version mismatch leptos","floating version cargo leptos","leptos edition2024 error"],"body":"## Principle\n\n**Leptos 0.8 applications MUST use unpinned `nightly` toolchain and unpinned minor versions in Cargo.toml.**\n\nLeptos 0.8 depends on unstable Rust features that evolve rapidly. Pinning toolchain dates or minor versions creates incompatibilities that manifest as cryptic build errors.\n\n---\n\n## The Problem\n\nWhen using pinned nightly or pinned Leptos minor versions:\n\n- Build fails with \"rustc X.Y is not supported by leptos\"\n- Or: \"feature `edition2024` is required but not stabilized\"\n- Or: Dependency resolution conflicts with newer crates\n- Error messages don't clearly indicate toolchain mismatch\n\n### Real Symptoms\n```\nerror: rustc 1.86.0-nightly is not supported by the following packages:\n  leptos@0.8.15 requires rustc 1.88\n```\n\nOr:\n```\nerror: feature `edition2024` is required\nThe package requires the Cargo feature called `edition2024`,\nbut that feature is not stabilized in this version of Cargo.\n```\n\nOr:\n```\nerror: failed to select a version for the requirement `leptos_axum = \"^0.8.15\"`\ncandidate versions found which didn't match: 0.8.7, 0.8.6, 0.8.5, ...\n```\n\n---\n\n## Anti Pattern\n```toml\n# ❌ FORBIDDEN: Pinned nightly date\n# rust-toolchain.toml\n[toolchain]\nchannel = \"nightly-2024-11-01\"  # ← Breaks with new Leptos releases\n\n# ❌ FORBIDDEN: Pinned minor version\n[workspace.dependencies]\nleptos = { version = \"0.8.15\", features = [\"nightly\"] }  # ← Breaks\nleptos_axum = \"0.8.15\"  # ← May not exist or be incompatible\n```\n\nThis fails because:\n- Leptos 0.8.x releases require progressively newer nightly features\n- Minor version `0.8.15` may require rustc 1.88 (doesn't exist yet)\n- Older nightlies lack required unstable features\n- Cargo can't resolve exact minor + floating nightly\n\n---\n\n## Canonical Pattern\n```toml\n# ✅ REQUIRED: Floating nightly\n# rust-toolchain.toml\n[toolchain]\nchannel = \"nightly\"  # ← Always latest\ncomponents = [\"rustfmt\", \"clippy\"]\n\n# ✅ REQUIRED: Floating minor version\n[workspace.dependencies]\nleptos = { version = \"0.8\", features = [\"nightly\"] }  # ← No minor\nleptos_router = { version = \"0.8\", features = [\"nightly\"] }\nleptos_meta = { version = \"0.8\" }\nleptos_axum = { version = \"0.8\" }\n```\n\nLet Cargo resolve to latest compatible minor version automatically.\n\n---\n\n## Why This Is Required\n\nLeptos 0.8 development model:\n\n1. Uses unstable Rust features (edition2024, type system extensions)\n2. Tracks latest nightly closely\n3. Releases minor versions frequently (0.8.x)\n4. Requires specific rustc versions per release\n\nPinning creates incompatibility matrix:\n\n| Pinned Nightly | Pinned Leptos | Result |\n|----------------|---------------|---------|\n| nightly-2024-11-01 | 0.8.15 | ❌ rustc too old |\n| nightly-2025-01-10 | 0.8.7 | ❌ leptos too old |\n| nightly | 0.8 | ✅ Cargo resolves |\n\nOnly floating both allows Cargo to find compatible versions.\n\n---\n\n## Acceptable Pinning Scenarios\n\nOnly pin when:\n\n1. **Reproducing a specific bug**\n```toml\n   # Temporary for bug report\n   channel = \"nightly-2025-01-10\"\n   leptos = \"=0.8.12\"\n```\n\n2. **Production releases** (pin in CI, not in source)\n```yaml\n   # .github/workflows/deploy.yml\n   - run: rustup override set nightly-2025-01-10\n```\n\n3. **Archival projects** (not actively developed)\n```toml\n   # Last-known-good for legacy app\n   channel = \"nightly-2024-12-01\"\n```\n\nNever pin in active development or in committed source for libraries.\n\n---\n\n## Diagnostic Checklist\n\nIf you see rustc version errors:\n```bash\n# 1. Check rust-toolchain.toml\ncat rust-toolchain.toml\n# Should be: channel = \"nightly\" (no date)\n\n# 2. Check Leptos versions\ngrep \"leptos.*=\" Cargo.toml\n# Should be: version = \"0.8\" (no minor)\n\n# 3. Update toolchain\nrustup update nightly\nrustup default nightly\n\n# 4. Clean and rebuild\ncargo clean\ncargo build\n\n# 5. Check Cargo.lock for resolved versions\ngrep \"name = \\\"leptos\\\"\" Cargo.lock -A 2\n```\n\n---\n\n## Enforcement\n\n- All Leptos 0.8 projects MUST use `channel = \"nightly\"` without date\n- Cargo.toml MUST use `version = \"0.8\"` without minor for Leptos crates\n- CI MUST use `rustup update` before builds\n- Production deploys MAY pin in CI scripts, not in source\n- Pinned versions MUST be documented with expiration date\n\n---\n\n## Canonical Justification\n\n> **Leptos 0.8 is pre-1.0 and tracks nightly closely.**  \n> Stability comes from semantic versioning (0.8 API contract),  \n> not from toolchain pinning (which creates brittleness).\n\nThis rule exists to:\n- Align with Leptos 0.8's development model\n- Prevent \"works on my machine\" due to toolchain drift\n- Let Cargo's version resolution work as designed\n- Reduce CI/CD complexity (no toolchain matrix)\n\n---\n\n## Migration Path For Pinned Projects\n```bash\n# 1. Remove date from rust-toolchain.toml\nsed -i 's/nightly-[0-9-]*/nightly/' rust-toolchain.toml\n\n# 2. Remove minor versions from Cargo.toml\nsed -i 's/leptos.*=.*\"0\\.8\\.[0-9]*\"/leptos = { version = \"0.8\"/' Cargo.toml\n\n# 3. Update and clean\nrustup update nightly\ncargo clean\nrm Cargo.lock\n\n# 4. Rebuild\ncargo build\n```\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #94 — Leptos Workspace Features Must Be Explicit\n- Canon Rule #93 — Leptos WASM Dev Builds Must Use Release Mode\n- Canon Rule #100 — Build Orchestrators Must Be Workspace-Scoped\n\n---\n\n## Version History\n\n- **1.0.0** (2025-01-15): Initial rule based on Workbench migration toolchain incompatibility debugging"},{"number":98,"slug":"axum-leptos-ssr-closures-must-own-state","title":"Axum + Leptos SSR Closures Must Own State","status":"ENFORCED","severity":"MEDIUM","category":"component-architecture","tags":["axum","ownership","closures","rust"],"language":"EN","version":"1.0.0","date":"2025-01-15","intro":"Sharing state across Axum router closures causes borrow checker errors due to move semantics. Each closure must own its own cloned state to satisfy lifetime requirements.","problem":"shared state across closures causes borrow of moved value errors","solution":"clone state before each closure and move ownership into closures","signals":["E0382 error","borrow of moved value","closure ownership issue"],"search_intent":"how to fix borrow of moved value axum","keywords":["axum closure ownership rust","borrow of moved value fix","leptos options clone closure","rust move closure pattern"],"body":"## Principle\n\n**When configuring Axum Router with Leptos SSR, each closure MUST own its copy of `LeptosOptions` via explicit `clone()` before the `move` keyword.**\n\nAxum's router builder requires multiple closures that outlive the builder scope. Without explicit clones, the borrow checker prevents compilation with \"borrow of moved value\" errors.\n\n---\n\n## The Problem\n\nWhen trying to share `LeptosOptions` across multiple router closures:\n\n- Compiler error: \"borrow of moved value: `leptos_options`\"\n- Error points to `.with_state()` or second closure\n- First closure moves the value, subsequent code cannot borrow\n- Not immediately obvious that the solution is clone-before-move\n\n### Real Symptoms\n```rust\nlet leptos_options = conf.leptos_options.clone();\n\nlet app = Router::new()\n    .leptos_routes(&leptos_options, routes, {\n        move || shell(leptos_options.clone())  // ← Moves leptos_options\n    })\n    .fallback(leptos_axum::file_and_error_handler(move |_| {\n        shell(leptos_options.clone())  // ← ERROR: leptos_options moved\n    }))\n    .with_state(leptos_options.clone());  // ← ERROR: value borrowed here\n```\n\nError:\n```\nerror[E0382]: borrow of moved value: `leptos_options`\n  --> src/main.rs:22:21\n   |\n11 |     let leptos_options = conf.leptos_options.clone();\n   |         -------------- move occurs because `leptos_options` has type `LeptosOptions`\n19 |         move || shell(leptos_options.clone())\n   |         -------- value moved into closure here\n22 |         .with_state(leptos_options.clone());\n   |                     ^^^^^^^^^^^^^^ value borrowed here after move\n```\n\n---\n\n## Anti Pattern\n```rust\n// ❌ FORBIDDEN: Shared reference across closures\nlet leptos_options = conf.leptos_options.clone();\n\nlet app = Router::new()\n    .leptos_routes(&leptos_options, routes, {\n        move || shell(leptos_options.clone())  // ← First move\n    })\n    .fallback(leptos_axum::file_and_error_handler(move |_| {\n        shell(leptos_options.clone())  // ← Second move: ERROR\n    }))\n    .with_state(leptos_options);  // ← Borrow after move: ERROR\n```\n\nThis fails because:\n- First `move` closure takes ownership of `leptos_options`\n- Second `move` closure cannot access moved value\n- `with_state()` cannot borrow after move\n\n---\n\n## Canonical Pattern\n```rust\n// ✅ REQUIRED: Clone before each closure\nlet conf = get_configuration(None).unwrap();\nlet leptos_options = conf.leptos_options.clone();\nlet routes = generate_route_list(App);\n\nlet app = Router::new()\n    .leptos_routes(&leptos_options, routes, {\n        let leptos_options = leptos_options.clone();  // ← Clone 1\n        move || shell(leptos_options.clone())\n    })\n    .fallback(leptos_axum::file_and_error_handler({\n        let leptos_options = leptos_options.clone();  // ← Clone 2\n        move |_| shell(leptos_options.clone())\n    }))\n    .with_state(leptos_options);  // ← Original still owned\n```\n\nEach closure gets its own clone before the `move` keyword.\n\n---\n\n## Why This Is Required\n\nAxum Router ownership model:\n\n1. **Builder pattern**: Router methods consume `self`\n2. **Closure requirements**: Each closure needs `'static` lifetime\n3. **No shared references**: Cannot use `&LeptosOptions` in `move` closures\n\nThe only way to satisfy all three constraints:\n- Clone before each closure\n- Move the clone into the closure\n- Original value remains owned for next operation\n\n---\n\n## Pattern Variations\n\n### Multiple Clones In Sequence\n```rust\nlet opts = leptos_options.clone();\n\nlet app = Router::new()\n    .leptos_routes(&leptos_options, routes, {\n        let opts = opts.clone();  // ← Clone from previous clone\n        move || shell(opts.clone())\n    })\n    .fallback(leptos_axum::file_and_error_handler({\n        let opts = opts.clone();  // ← Another clone\n        move |_| shell(opts.clone())\n    }))\n    .with_state(leptos_options);\n```\n\n### Arc For True Sharing\n```rust\nuse std::sync::Arc;\n\nlet leptos_options = Arc::new(conf.leptos_options.clone());\n\nlet app = Router::new()\n    .leptos_routes(&leptos_options, routes, {\n        let opts = Arc::clone(&leptos_options);\n        move || shell((*opts).clone())\n    })\n    .fallback(leptos_axum::file_and_error_handler({\n        let opts = Arc::clone(&leptos_options);\n        move |_| shell((*opts).clone())\n    }))\n    .with_state((*leptos_options).clone());\n```\n\nNote: `Arc` is rarely needed; simple `clone()` is sufficient for `LeptosOptions`.\n\n---\n\n## Performance Considerations\n\n**Q: Doesn't cloning hurt performance?**\n\nA: No, for these reasons:\n\n1. `LeptosOptions` is small (~200 bytes)\n2. Clones happen once at server startup (not per request)\n3. Cost is negligible compared to server initialization\n4. Alternative (Arc) has runtime overhead for every access\n\nBenchmark:\n```\nClone LeptosOptions:  ~50ns\nArc::clone:          ~10ns (pointer copy)\nArc deref per use:   ~5ns (adds up over requests)\n```\n\nFor startup code, simple `clone()` is the right choice.\n\n---\n\n## Diagnostic Checklist\n\nIf you see \"borrow of moved value\" in router setup:\n```bash\n# 1. Identify which closure moved the value first\n# Look for first `move ||` that uses the variable\n\n# 2. Add clone before EACH closure that needs it\nlet var = var.clone();\n\n# 3. Verify pattern:\n# - Clone BEFORE move keyword\n# - Inside closure block, AFTER opening brace\n\n# 4. Check that original variable is still owned\n# for final .with_state() call\n```\n\n---\n\n## Enforcement\n\n- All Leptos SSR router setup MUST use clone-before-move pattern\n- Each `move` closure MUST have its own clone\n- Code review MUST check for shared borrows in router closures\n- CI MUST compile without ownership errors\n\n---\n\n## Canonical Justification\n\n> **Axum Router requires `'static` closures.**  \n> `'static` + `move` semantics = must own the data.  \n> Cloning is the only correct solution.\n\nThis rule exists to:\n- Encode Rust ownership rules as explicit pattern\n- Prevent hours debugging borrow checker errors\n- Make lifetime requirements visible in code structure\n- Align with Axum's API design constraints\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #95 — SSR Requires Complete HTML Shell\n- Canon Rule #96 — SSR Requires Explicit Provider Tree\n- Canon Rule #100 — Build Orchestrators Must Be Workspace-Scoped\n\n---\n\n## Version History\n\n- **1.0.0** (2025-01-15): Initial rule based on Workbench migration Axum router borrow checker debugging"},{"number":99,"slug":"csr-islands-must-not-own-routing-or-providers","title":"CSR Islands Must Not Own Routing or Providers","status":"ENFORCED","severity":"HIGH","category":"core-runtime","tags":["csr","islands","routing","providers"],"language":"EN","version":"1.0.0","date":"2025-01-15","intro":"CSR islands in SSR architectures must remain isolated UI units without owning routing or global providers. Violations cause hydration mismatches and context conflicts.","problem":"islands create routing or providers causing conflicts with SSR host","solution":"restrict islands to local rendering and delegate routing and providers to host","signals":["hydration mismatch","router conflict","context already exists"],"search_intent":"why csr islands break routing in ssr","keywords":["csr islands architecture ssr","leptos islands routing conflict","provider duplication issue","hydration mismatch islands"],"body":"## Principle\n\n**WASM islands in an SSR-first application MUST NOT create their own Router or provide global Providers.**\n\nIslands are guests in the SSR host's DOM. They mount at specific nodes, render local UI, and unmount cleanly. Routing and providers are the host's responsibility.\n\n---\n\n## The Problem\n\nWhen WASM islands try to own routing or providers:\n\n- Router conflicts with SSR's router (two routers fighting)\n- Provider contexts leak between islands and host\n- Hydration mismatches (SSR expects one tree, CSR creates another)\n- Islands cannot be lazy-loaded independently\n- Memory leaks on unmount (providers not cleaned up)\n\n### Real Symptoms\n```\n✗ Hydration mismatch: expected <Router>, found <div>\n✗ Context \"Theme\" already exists in tree\n✗ Cannot unmount island: router still active\n✗ Navigation broken after island mount\n```\n\n---\n\n## Anti Pattern\n```rust\n// ❌ FORBIDDEN: Island creates router\n#[wasm_bindgen]\npub fn mount_markdown(selector: &str) {\n    mount_to(\n        selector,\n        || view! {\n            <Router>  // ← WRONG: Island creating router\n                <Routes>\n                    <Route path=\"/docs\" view=MarkdownViewer/>\n                </Routes>\n            </Router>\n        }\n    );\n}\n\n// ❌ FORBIDDEN: Island provides global context\n#[wasm_bindgen]\npub fn mount_editor(selector: &str) {\n    mount_to(\n        selector,\n        || view! {\n            <ThemeProvider>  // ← WRONG: Island providing context\n                <CodeEditor/>\n            </ThemeProvider>\n        }\n    );\n}\n```\n\nThis fails because:\n- SSR already has a Router managing navigation\n- SSR already has ThemeProvider in component tree\n- Island's router/provider conflicts with host\n- Unmounting the island leaves orphaned contexts\n\n---\n\n## Canonical Pattern\n```rust\n// ✅ REQUIRED: Island is just a component\n#[wasm_bindgen]\npub fn mount_markdown(selector: &str) {\n    mount_to(\n        selector,\n        || view! {\n            <MarkdownViewer content=initial_content />\n        }\n    );\n}\n\n// ✅ REQUIRED: Island uses host's context\n#[wasm_bindgen]\npub fn mount_editor(selector: &str) {\n    mount_to(\n        selector,\n        || view! {\n            <CodeEditor/>  // Uses theme from host's ThemeProvider\n        }\n    );\n}\n\n// ✅ Host provides router and contexts\n#[component]\npub fn App() -> impl IntoView {\n    view! {\n        <ThemeProvider>\n            <Router>\n                <Routes>\n                    <Route path=\"/docs\" view=DocsPage/>\n                </Routes>\n            </Router>\n        </ThemeProvider>\n    }\n}\n\n#[component]\nfn DocsPage() -> impl IntoView {\n    view! {\n        <div>\n            <h1>\"Documentation\"</h1>\n            <div id=\"markdown-mount\"></div>  // ← Island mounts here\n        </div>\n    }\n}\n```\n\nHost manages routing, providers, and island mount points.\n\n---\n\n## Island Responsibilities Allowed\n\nIslands MAY:\n\n- Render local UI components\n- Manage local state (useState, RwSignal)\n- Use contexts provided by host (use_theme, use_density)\n- Handle local events (onClick, onChange)\n- Make API calls\n- Update their own DOM subtree\n\nIslands MUST NOT:\n\n- Create Router\n- Provide ThemeProvider, DensityProvider, or other global contexts\n- Modify DOM outside their mount node\n- Listen to global route changes (host handles this)\n- Persist state in global storage (use host's state management)\n\n---\n\n## Host Responsibilities Required\n\nSSR host MUST:\n\n- Provide single Router for entire app\n- Provide all global contexts (Theme, Density, etc.)\n- Create mount points for islands (`<div id=\"island-mount\">`)\n- Lazy-load island WASM on demand\n- Unmount islands when route changes\n- Pass initial props to islands via data attributes\n\n---\n\n## Communication Patterns\n\n### Host To Island Props\n```rust\n// Host\nview! {\n    <div\n        id=\"editor-mount\"\n        data-theme=\"dark\"\n        data-lang=\"rust\"\n    ></div>\n}\n\n// Island\n#[wasm_bindgen]\npub fn mount_editor(selector: &str) {\n    let el = document().query_selector(selector).unwrap().unwrap();\n    let theme = el.get_attribute(\"data-theme\").unwrap();\n    let lang = el.get_attribute(\"data-lang\").unwrap();\n\n    mount_to(el, move || view! {\n        <CodeEditor theme=theme lang=lang/>\n    });\n}\n```\n\n### Island To Host Events\n```rust\n// Island dispatches custom event\n#[wasm_bindgen]\npub fn mount_editor(selector: &str) {\n    mount_to(selector, || view! {\n        <CodeEditor on_save=move |code| {\n            let event = CustomEvent::new(\"editor:save\").unwrap();\n            event.detail().set(\"code\", code);\n            window().dispatch_event(&event);\n        }/>\n    });\n}\n\n// Host listens for event\nwindow.addEventListener(\"editor:save\", (e) => {\n    console.log(\"Code saved:\", e.detail.code);\n});\n```\n\n---\n\n## Lazy Loading Islands\n```rust\n// Host lazy-loads island\n#[component]\nfn DocsPage() -> impl IntoView {\n    let load_island = create_action(|_| async {\n        // Dynamic import\n        let module = js_sys::eval(\n            r#\"import(\"/islands/markdown/markdown.js\")\"#\n        ).await;\n        module.mount_markdown(\"#markdown-mount\");\n    });\n\n    view! {\n        <div>\n            <button on:click=move |_| load_island.dispatch(())>\n                \"Load Markdown Viewer\"\n            </button>\n            <div id=\"markdown-mount\"></div>\n        </div>\n    }\n}\n```\n\n---\n\n## Diagnostic Checklist\n\nIf islands cause hydration or routing issues:\n```bash\n# 1. Check island code for Router\ngrep -r \"Router\" islands/*/src/\n\n# 2. Check island code for Providers\ngrep -r \"Provider\" islands/*/src/\n\n# 3. Verify island only uses mount_to\ngrep -r \"mount_to\" islands/*/src/\n\n# 4. Check host provides contexts\ngrep -r \"ThemeProvider\\|DensityProvider\" shell/src/lib.rs\n\n# 5. Verify islands don't modify global state\ngrep -r \"window\\.\" islands/*/src/\n```\n\n---\n\n## Enforcement\n\n- All WASM islands MUST use `mount_to()` only\n- Islands MUST NOT create Router\n- Islands MUST NOT provide global contexts\n- Islands MUST NOT access DOM outside mount node\n- Host MUST provide all routing and contexts\n- Code review MUST verify island isolation\n\n---\n\n## Canonical Justification\n\n> **Islands are components, not applications.**  \n> They render at specific DOM nodes managed by the host.  \n> Router and providers are architectural concerns, not component concerns.\n\nThis rule exists to:\n- Maintain SSR's architectural authority\n- Enable true lazy loading of islands\n- Prevent context/router conflicts\n- Allow islands to mount/unmount cleanly\n- Keep islands small and focused\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #95 — SSR Requires Complete HTML Shell\n- Canon Rule #96 — SSR Requires Explicit Provider Tree\n- Canon Rule #90 — Hydration Is DOM Replacement\n\n---\n\n## Version History\n\n- **1.0.0** (2025-01-15): Initial rule based on Workbench migration SSR-first + islands architecture"},{"number":100,"slug":"build-orchestrators-must-be-workspace-scoped","title":"Build Orchestrators Must Be Workspace-Scoped","status":"ENFORCED","severity":"MEDIUM","category":"build-tooling","tags":["workspace","cargo-leptos","build","configuration"],"language":"EN","version":"1.0.0","date":"2025-01-15","intro":"Build orchestrator configuration in Leptos fails when defined at crate level instead of workspace root. Tools like cargo-leptos require centralized metadata to resolve paths, assets, and hot reload correctly.","problem":"build configuration in member crates is ignored or conflicts in workspace builds","solution":"define all build orchestrator metadata in workspace root cargo.toml","signals":["metadata not found","hot reload broken","asset 404","config conflict"],"search_intent":"how to fix cargo leptos metadata","keywords":["cargo leptos workspace config","leptos build metadata error","workspace metadata leptos","cargo leptos configuration root"],"body":"## Principle\n\n**Build orchestration configuration (cargo-leptos, trunk, etc.) MUST be declared in the workspace root `Cargo.toml`, not in individual member crates.**\n\nBuild orchestrators operate at the workspace level and require centralized configuration. Member-level configuration is ignored or causes conflicts.\n\n---\n\n## The Problem\n\nWhen build orchestrator configuration exists only in member crates:\n\n- cargo-leptos cannot find configuration\n- Build fails with \"metadata not found\" errors\n- Hot reload doesn't work (watches wrong directories)\n- Asset paths are incorrect\n- Multiple members create conflicting configurations\n\n### Real Symptoms\n```\nError: `cargo metadata` exited with an error:\n  could not find `leptos` in workspace metadata\n```\n\nOr:\n```\nwarning: no [[workspace.metadata.leptos]] found in workspace root\nfalling back to defaults (probably incorrect)\n```\n\nOr hot reload that doesn't trigger, assets that 404, etc.\n\n---\n\n## Anti-Pattern (FORBIDDEN)\n```toml\n# ❌ FORBIDDEN: Configuration in member crate only\n# shell/Cargo.toml\n[package.metadata.leptos]\noutput-name = \"my-app\"\nsite-root = \"target/site\"\nsite-addr = \"127.0.0.1:3000\"\n# ... etc\n```\n\nThis fails because:\n- cargo-leptos reads `workspace.metadata.leptos`, not `package.metadata.leptos`\n- Workspace tools cannot discover member-level config\n- Multiple members would conflict\n- CI/CD cannot find configuration\n\n---\n\n## Canonical Pattern (REQUIRED)\n```toml\n# ✅ REQUIRED: Configuration in workspace root\n# Cargo.toml (workspace root)\n[workspace]\nmembers = [\"shell\", \"islands/markdown\"]\n\n[[workspace.metadata.leptos]]\nname = \"my-app-shell\"\nbin-package = \"my-app-shell\"\nlib-package = \"my-app-shell\"\nsite-root = \"target/site\"\nsite-pkg-dir = \"pkg\"\nsite-addr = \"127.0.0.1:3003\"\nreload-port = 3004\nassets-dir = \"shell/public\"\nstyle-file = \"shell/style/main.css\"\nenv = \"DEV\"\nbin-features = [\"ssr\"]\nlib-features = [\"hydrate\"]\nwatch-additional-files = [\"packages-rust/rs-design/src\"]\n\n# Member crate has NO metadata\n# shell/Cargo.toml\n[package]\nname = \"my-app-shell\"\n# ... dependencies only\n```\n\n---\n\n## Multi-Package Workspaces\n\nFor workspaces with multiple Leptos applications:\n```toml\n# Workspace root with multiple apps\n[[workspace.metadata.leptos]]\nname = \"admin-panel\"\nbin-package = \"admin-panel\"\nlib-package = \"admin-panel\"\nsite-root = \"target/admin\"\nsite-addr = \"127.0.0.1:3001\"\n\n[[workspace.metadata.leptos]]\nname = \"customer-portal\"\nbin-package = \"customer-portal\"\nlib-package = \"customer-portal\"\nsite-root = \"target/customer\"\nsite-addr = \"127.0.0.1:3002\"\n\n# Build specific app\ncargo leptos watch --bin-package admin-panel\n```\n\n---\n\n## Required Configuration Fields\n\n### Minimum Required\n```toml\n[[workspace.metadata.leptos]]\nbin-package = \"my-app\"       # Must match binary crate name\nlib-package = \"my-app\"       # Must match library crate name\nsite-root = \"target/site\"    # Where final HTML/assets go\n```\n\n### Recommended Production\n```toml\n[[workspace.metadata.leptos]]\nname = \"my-app\"                           # Human-readable name\nbin-package = \"my-app\"                    # Binary crate name\nlib-package = \"my-app\"                    # Library crate name\nsite-root = \"target/site\"                 # Build output directory\nsite-pkg-dir = \"pkg\"                      # WASM package directory\nsite-addr = \"127.0.0.1:3000\"             # Dev server address\nreload-port = 3001                        # Hot reload WebSocket port\nassets-dir = \"shell/public\"               # Static assets source\nstyle-file = \"shell/style/main.css\"       # Tailwind/CSS entry point\nenv = \"DEV\"                               # Environment\nbin-features = [\"ssr\"]                    # Server features\nlib-features = [\"hydrate\"]                # Client features\nwatch-additional-files = [                # Extra watch paths\n    \"packages-rust/rs-design/src\",\n    \"config/*.toml\"\n]\n```\n\n---\n\n## Watch Additional Files\n\nCritical for monorepos:\n```toml\n[[workspace.metadata.leptos]]\n# ...\nwatch-additional-files = [\n    \"/opt/docker/monorepo/packages-rust/rs-design/src\",  # Absolute path OK\n    \"../shared-lib/src\",                                  # Relative to workspace\n    \"config/**/*.toml\",                                   # Glob patterns\n]\n```\n\nWithout this:\n- Changes in dependency crates don't trigger rebuild\n- Hot reload misses external changes\n- Development workflow breaks\n\n---\n\n## Why Workspace-Scoped\n\nBuild orchestrators need workspace-level view:\n\n1. **Dependency resolution**: Must see all workspace members\n2. **Path resolution**: Relative paths are workspace-relative\n3. **Watch configuration**: Monitors workspace root\n4. **Output coordination**: Prevents multiple apps from conflicting\n\nMember-level config cannot provide this global view.\n\n---\n\n## Other Build Tools\n\nSame principle applies to other orchestrators:\n\n### Trunk\n```toml\n# ✅ Workspace root\n[workspace.metadata.trunk]\npublic-url = \"/app/\"\ndist = \"target/trunk\"\n```\n\n### wasm-pack\n```toml\n# ✅ Workspace root\n[workspace.metadata.wasm-pack]\nprofile-dev = \"dev\"\nprofile-release = \"release\"\n```\n\n### Custom Build Scripts\n```toml\n# ✅ Workspace root\n[workspace.metadata.my-builder]\nconfig-file = \"build-config.toml\"\noutput-dir = \"dist\"\n```\n\n---\n\n## Diagnostic Checklist\n\nIf cargo-leptos cannot find config:\n```bash\n# 1. Check workspace root for metadata\ngrep -A 10 \"workspace.metadata.leptos\" Cargo.toml\n\n# 2. Verify bin-package matches actual crate name\ngrep \"name.*=\" shell/Cargo.toml\n\n# 3. Check paths are workspace-relative\n# assets-dir should be relative to workspace root\n\n# 4. Verify cargo-leptos sees config\ncargo leptos --help\n# Should show your app in available targets\n\n# 5. Check for conflicting member-level metadata\ngrep -r \"package.metadata.leptos\" */Cargo.toml\n```\n\n---\n\n## Migration from Member Config\n```bash\n# 1. Copy metadata from member to workspace root\n# From shell/Cargo.toml [package.metadata.leptos]\n# To Cargo.toml [[workspace.metadata.leptos]]\n\n# 2. Remove member-level metadata\nsed -i '/\\[package\\.metadata\\.leptos\\]/,/^$/d' shell/Cargo.toml\n\n# 3. Fix relative paths (now workspace-relative)\n# Change: assets-dir = \"public\"\n# To:     assets-dir = \"shell/public\"\n\n# 4. Test\ncargo leptos watch\n```\n\n---\n\n## Enforcement\n\n- All cargo-leptos projects MUST use `[[workspace.metadata.leptos]]`\n- Member crates MUST NOT have `[package.metadata.leptos]`\n- Paths in metadata MUST be workspace-relative\n- CI MUST verify metadata exists in workspace root\n- New projects MUST use workspace-scoped configuration from start\n\n---\n\n## Canonical Justification\n\n> **Build orchestration is a workspace concern, not a package concern.**  \n> Orchestrators coordinate multiple crates, resolve paths globally,  \n> and manage output directories that span the workspace.\n\nThis rule exists to:\n- Centralize build configuration for discoverability\n- Prevent conflicting configurations across members\n- Enable proper dependency tracking and hot reload\n- Align with Cargo's workspace-first design\n- Simplify CI/CD (single source of truth)\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #94 — Leptos Workspace Features Must Be Explicit\n- Canon Rule #97 — Leptos 0.8 Requires Floating Nightly Toolchain\n- Canon Rule #93 — Leptos WASM Dev Builds Must Use Release Mode\n\n---\n\n## Version History\n\n- **1.0.0** (2025-01-15): Initial rule based on Workbench migration cargo-leptos configuration requirements"},{"number":101,"slug":"workbench-assets-must-be-product-scoped","title":"Workbench Assets Must Be Product-Scoped","status":"ENFORCED","severity":"MEDIUM","category":"governance","tags":["assets","design-system","ownership","monorepo"],"language":"EN","version":"1.0.0","date":"2026-01-15","intro":"Placing illustrative assets inside the design system creates coupling and violates separation of concerns. Design systems must remain focused on primitives and behavior, not product-specific visuals.","problem":"illustrative assets stored in design system create coupling and misuse of scope","solution":"keep illustrative assets inside product scope and restrict design system to primitives","signals":["asset coupling","design drift","wrong ownership"],"search_intent":"prevent design system asset coupling","keywords":["design system asset scope","product scoped assets","monorepo asset ownership","ui illustration separation"],"body":"## Principle\n\n**Illustrative assets belong to the product, never to the design system.**\n\nThe design system defines rules and UI primitives.  \nProducts define narrative, illustration, and branding.\n\n---\n\n## Rule\n\n- `rs-design` MAY contain:\n  - UI icons\n  - Control glyphs\n  - Interaction symbols\n\n- `rs-design` MUST NOT contain:\n  - Illustrations\n  - Pillars\n  - Marketing or storytelling visuals\n\n- Products (Workbench, apps) MUST own their own illustrative SVGs.\n\n---\n\n## Forbidden\n```\npackages-rust/rs-design/assets/symbols/pillars/*.svg\n```\n\n---\n\n## Required\n```\nproducts/<product-name>/shell/public/assets/\n```\n\n---\n\n## Canonical Justification\n\n> A design system encodes behavior, not narrative.\n\n---"},{"number":102,"slug":"runtime-js-is-shell-infrastructure","title":"Runtime JS Is Shell Infrastructure","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["runtime","js","events","shell"],"language":"EN","version":"1.0.0","date":"2026-01-15","intro":"Embedding runtime JavaScript logic inside components breaks separation of concerns and leads to inconsistent behavior. Interaction logic like clipboard and event handling must be centralized.","problem":"components directly execute runtime js causing coupling and inconsistent behavior","solution":"move all runtime javascript to shell layer and keep components declarative","signals":["event duplication","dom coupling","inconsistent behavior"],"search_intent":"how to separate runtime js leptos","keywords":["leptos runtime js separation","shell infrastructure js","component event handling architecture","js behavior separation ui"],"body":"## Principle\n\n**Components declare intent. The shell executes effects.**\n\nRuntime JavaScript (clipboard, drag, shortcuts) is infrastructure,  \nnot component logic.\n\n---\n\n## Rule\n\n- Rust components MAY:\n  - Render `data-*` attributes\n  - Declare interaction intent\n\n- Rust components MUST NOT:\n  - Call clipboard APIs\n  - Touch DOM imperatively\n  - Attach JS listeners\n\n- All runtime JS MUST live in the shell.\n\n---\n\n## Forbidden\n```rust\non:click=move |_| navigator.clipboard.write_text(...)\n```\n\n---\n\n## Required\n```js\ndocument.addEventListener(\"click\", e => {\n  const btn = e.target.closest(\"[data-copy-button]\");\n  if (!btn) return;\n  navigator.clipboard.writeText(btn.dataset.copyText);\n});\n```\n\n---\n\n## Canonical Justification\n\n> Runtime behavior is a deployment concern, not a component concern.\n\n---"},{"number":103,"slug":"critical-runtime-js-must-be-inline-in-ssr","title":"Critical Runtime JS Must Be Inline in SSR","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["ssr","hydration","scripts","runtime"],"language":"EN","version":"1.1.0","date":"2026-01-15","intro":"External runtime scripts in Leptos SSR environments execute unpredictably due to script ordering issues. This leads to silent failures where listeners do not attach correctly after hydration.","problem":"external scripts execute after hydration causing event listeners to fail","solution":"inline critical runtime javascript before hydration scripts in ssr head","signals":["silent failure","listeners not working","race condition"],"search_intent":"how to fix ssr script ordering","keywords":["leptos ssr script order","hydration script race condition","inline runtime js ssr","leptos event listener fail"],"body":"## Principle\n\n**Critical runtime JS must execute before hydration.**\n\nIn SSR environments, script ordering is not stable.  \nInline scripts are the only deterministic mechanism.\n\n---\n\n## Problem Statement\n\nWhen using Leptos SSR with `cargo-leptos`, external script tags can be:\n- Reordered by `AutoReload` hot-reload injection\n- Executed after WASM hydration starts\n- Subject to race conditions with dynamic DOM updates\n\n**Real-world symptom:**\n```\n✅ <script src=\"/runtime.js\"> appears in HTML\n✅ HTTP 200 - file loads successfully\n✅ Console shows script executed\n❌ Event listeners don't work\n❌ Clipboard API fails silently\n❌ No error messages\n```\n\n**Root cause:** Script executes AFTER hydration attaches React-like event system,  \ncausing listener registration to be overwritten or ignored.\n\n---\n\n## Rule\n\n- Critical runtime JS MUST be:\n  - **Inline** (not external file)\n  - Placed in `<head>` before `<HydrationScripts/>`\n  - Executed synchronously (no `defer`, no `async`)\n\n- External scripts MUST NOT be used for:\n  - Clipboard API (`navigator.clipboard`, `execCommand`)\n  - Drag & Drop (`dragstart`, `drop` listeners)\n  - Keyboard shortcuts (`keydown`, `keyup` global listeners)\n  - Theme bootstrap (anti-flash scripts)\n  - Any event delegation on `document` or `window`\n\n---\n\n## Forbidden\n```rust\n// ❌ External script in SSR shell\nview! {\n    <head>\n        <script src=\"/runtime.js\"></script>\n        <HydrationScripts options/>\n    </head>\n}\n```\n```rust\n// ❌ Using leptos_meta::Script (still subject to reordering)\nuse leptos_meta::Script;\nview! {\n    <Script src=\"/runtime.js\"/>\n}\n```\n\n---\n\n## Required\n```rust\n// ✅ Inline critical runtime\n#[cfg(feature = \"ssr\")]\npub fn shell(options: LeptosOptions) -> impl IntoView {\n    view! {\n        <!DOCTYPE html>\n        <html>\n            <head>\n                <script>\n                    // Runtime executes immediately, before any hydration\n                    console.log(\"🟢 Canon runtime loaded\");\n                    document.addEventListener(\"click\", e => {{\n                        const btn = e.target.closest(\"[data-copy-button]\");\n                        if (!btn) return;\n                        const text = btn.getAttribute(\"data-copy-text\");\n                        navigator.clipboard.writeText(text);\n                        btn.textContent = \"Copied!\";\n                        setTimeout(() => btn.textContent = \"Copy\", 1500);\n                    }}, true);\n                </script>\n                \n                <HydrationScripts options/>\n            </head>\n            <body>\n                <App/>\n            </body>\n        </html>\n    }\n}\n```\n\n---\n\n## Detection\n\nHow to identify if your code violates this rule:\n```bash\n# 1. Check if runtime script is external\ngrep -r '<script src.*runtime' shell/src/lib.rs\n\n# 2. Verify script appears in HTML but doesn't work\ncurl -s http://localhost:3003/ | grep -o '<script.*runtime.*'\n\n# 3. Test: Click feature → no effect, no error\n# 4. Console: script loaded ✅ but listeners silent ❌\n```\n\n**Diagnostic pattern:**\n- Browser DevTools → Network: script loads (200 OK)\n- Browser DevTools → Console: no errors\n- Feature doesn't work intermittently or consistently\n- Hard refresh sometimes fixes it temporarily\n\n---\n\n## Migration Path\n\n**From external to inline:**\n```bash\n# 1. Read current external script\ncat static/runtime.js\n\n# 2. Minify to single line (optional)\n# Remove newlines, reduce whitespace\n\n# 3. Embed in shell() lib.rs\n# Wrap in <script>...</script> inside <head>\n\n# 4. Remove external file reference\n# Delete <script src=\"...\"> line\n\n# 5. Test: feature works immediately after page load\n```\n\n---\n\n## Trade-offs\n\n| Aspect | External Script | Inline Script |\n|--------|----------------|---------------|\n| **Browser cache** | ✅ Cached across pages | ❌ Downloaded with HTML |\n| **Execution order** | ❌ Non-deterministic | ✅ Guaranteed |\n| **Hot-reload safety** | ❌ Reordered by tooling | ✅ Immune to reordering |\n| **File size** | ✅ Separate asset | ❌ Increases HTML size |\n| **Dev experience** | ✅ Easy to edit | ⚠️ Edit in Rust file |\n\n**Canon decision:**  \nFor critical runtime (< 5KB), determinism > cache benefits.\n\n---\n\n## Canonical Justification\n\n> Inline scripts are the only race-free execution model in SSR.  \n> Runtime correctness is not negotiable.  \n> Cache optimization is premature for infrastructure code.\n\n---\n\n## References\n\n- Leptos SSR hydration model: hydration attaches after initial render\n- `AutoReload` behavior: injects WebSocket + reload logic dynamically\n- Event delegation pattern: requires listeners before first interaction\n\n---"},{"number":104,"slug":"autoreload-breaks-script-order-guarantees","title":"AutoReload Breaks Script Order Guarantees","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["autoreload","ssr","scripts","hot reload"],"language":"EN","version":"1.1.0","date":"2026-01-15","intro":"Leptos AutoReload injects scripts dynamically, breaking execution order guarantees. This causes runtime scripts to execute in unexpected phases, leading to intermittent failures.","problem":"autoreload injects scripts unpredictably breaking execution order","solution":"avoid relying on external script order and inline critical runtime logic","signals":["intermittent failure","hot reload bug","timing issue"],"search_intent":"why leptos autoreload breaks scripts","keywords":["leptos autoreload script order","ssr hot reload issue","script execution race leptos","autoreload injection problem"],"body":"## Principle\n\n**AutoReload may reorder or inject scripts unpredictably.**\n\nHot-reload tooling alters DOM execution order and must not be trusted  \nfor runtime-critical logic.\n\n---\n\n## Problem Statement\n\nLeptos SSR's `<AutoReload/>` component provides hot-reload during development.  \nIt works by:\n1. Injecting a WebSocket connection to the dev server\n2. Listening for file changes\n3. Dynamically inserting/updating script tags in `<head>`\n\n**The hidden cost:**  \nEven if you write scripts in a specific order in `shell()`, AutoReload  \ninjects wrapper code that can execute BETWEEN your intended script order.\n\n**Real scenario that cost 18 hours:**\n```rust\n// Developer writes this order:\n<script src=\"/runtime.js\"></script>\n<HydrationScripts options/>\n\n// Browser receives this order:\n<script>/* AutoReload WebSocket */</script>\n<script src=\"/runtime.js\"></script>\n<script>/* Hydration starts */</script>\n<script>/* AutoReload wrapper continues */</script>\n```\n\nResult: Runtime script loads but listeners register in wrong lifecycle phase.\n\n---\n\n## Rule\n\n- `AutoReload` MUST be treated as non-deterministic.\n- Script tag order in `shell()` source ≠ execution order in browser.\n- Runtime-critical JS MUST NOT rely on external script ordering when `AutoReload` is active.\n- Production builds MUST NOT include `AutoReload`.\n\n---\n\n## Symptoms of Violation\n\n**Pattern A: Intermittent Failures**\n- Feature works after hard refresh (Ctrl+Shift+R)\n- Feature breaks after hot-reload triggers\n- Works in production, fails in dev\n\n**Pattern B: Silent Failures**\n```\n✅ Script tag appears in HTML source\n✅ HTTP 200 - file downloaded\n✅ No console errors\n✅ No network errors\n❌ Event listeners don't fire\n❌ Clipboard API does nothing\n❌ Drag & drop fails\n```\n\n**Pattern C: Timing-Dependent**\n- Adding `setTimeout` \"fixes\" it\n- Adding `console.log` changes behavior\n- Works on slow connections, fails on fast\n\n---\n\n## Detection\n```bash\n# 1. Check if AutoReload is active\ngrep -n \"AutoReload\" shell/src/lib.rs\n\n# 2. Inspect actual HTML order in browser\ncurl -s http://localhost:3003/ | grep -n '<script'\n\n# 3. Compare source order vs browser order\n# Source:  runtime.js (line 38) → HydrationScripts (line 39)\n# Browser: AutoReload (injected) → runtime.js → more AutoReload\n\n# 4. Test behavior difference\n# Dev:  make dev  → feature broken\n# Prod: cargo leptos build --release → feature works\n```\n\n**Diagnostic questions:**\n- Does removing `<AutoReload/>` fix it? → This rule applies\n- Does inline script fix it? → Rule #103 applies\n- Does it work in production? → AutoReload is the culprit\n\n---\n\n## Required Mitigation\n\n### Option A: Inline Critical Scripts (Recommended)\n```rust\n// Remove AutoReload dependency entirely for critical runtime\nview! {\n    <head>\n        <script>\n            // Inline = immune to AutoReload reordering\n            document.addEventListener(\"click\", /* ... */);\n        </script>\n        <HydrationScripts options/>\n    </head>\n}\n```\n\n### Option B: Disable AutoReload in Dev (Not Recommended)\n```rust\n#[cfg(feature = \"ssr\")]\npub fn shell(options: LeptosOptions) -> impl IntoView {\n    view! {\n        <head>\n            // ⚠️ Loses hot-reload benefits\n            // <AutoReload options=options.clone()/>\n            <script src=\"/runtime.js\"></script>\n            <HydrationScripts options/>\n        </head>\n    }\n}\n```\n\n### Option C: Conditional AutoReload (Compromise)\n```rust\nview! {\n    <head>\n        <script>/* inline critical runtime */</script>\n        \n        #[cfg(debug_assertions)]\n        <AutoReload options=options.clone()/>\n        \n        <HydrationScripts options/>\n    </head>\n}\n```\n\n---\n\n## Migration Path\n\n**Identifying affected code:**\n```bash\n# Find external scripts that might be reordered\ngrep -rn '<script src' shell/src/lib.rs\n\n# Find AutoReload usage\ngrep -rn 'AutoReload' shell/src/\n\n# Check if runtime depends on execution order\ngrep -rn 'addEventListener.*document\\|window' static/\n```\n\n**Step-by-step fix:**\n1. Identify critical runtime scripts (clipboard, drag, shortcuts)\n2. Inline them before `<HydrationScripts/>`\n3. Keep non-critical scripts external (analytics, etc.)\n4. Test in dev with AutoReload active\n5. Verify production build still works\n\n---\n\n## Trade-offs\n\n| Approach | Dev Experience | Runtime Safety | Bundle Size |\n|----------|---------------|----------------|-------------|\n| **External + AutoReload** | ✅ Fast hot-reload | ❌ Race conditions | ✅ Cached |\n| **Inline + AutoReload** | ✅ Fast hot-reload | ✅ Deterministic | ⚠️ +2-5KB HTML |\n| **External - AutoReload** | ❌ Manual refresh | ✅ Deterministic | ✅ Cached |\n\n**Canon decision:**  \nRuntime safety > dev convenience for production-critical features.\n\n---\n\n## Why This Happens (Technical Deep Dive)\n\nAutoReload implementation:\n```rust\n// Simplified internal behavior\npub fn AutoReload() -> impl IntoView {\n    view! {\n        <script>\n            // WebSocket connection injected HERE\n            const ws = new WebSocket(\"ws://localhost:3001\");\n            ws.onmessage = () => location.reload();\n        </script>\n    }\n}\n```\n\nThe `view!` macro expands this into multiple script injections,  \nand Leptos SSR doesn't guarantee atomic ordering of sibling elements  \nduring hydration preparation phase.\n\n**Not a bug, but a design trade-off:**  \nAutoReload prioritizes developer experience over execution determinism.\n\n---\n\n## Canonical Justification\n\n> Development tooling is allowed to be unstable.  \n> Runtime correctness is not.\n\nHot-reload is a productivity tool, not infrastructure.  \nCritical runtime must be independent of development tooling.\n\n---\n\n## References\n\n- Leptos AutoReload source: https://github.com/leptos-rs/leptos\n- SSR script injection order: non-deterministic by design\n- Production builds: AutoReload compiled out via feature flags\n\n---\n\n## See Also\n\n- **Rule #103:** Critical Runtime JS Must Be Inline in SSR\n- **Rule #102:** Runtime JS Is Shell Infrastructure\n\n---"},{"number":105,"slug":"visual-indicators-must-have-a-single-owner","title":"canon-rule-105-visual-indicators-must-have-a-single-owner","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["ui","ownership","visual","architecture"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Splitting visual indicators across multiple components creates ambiguity and fragile styling. This leads to duplicated visuals and patch-based fixes.","problem":"multiple layers render the same visual indicator causing duplication","solution":"assign a single owner component for each visual indicator","signals":["duplicate visuals","css hacks","misalignment"],"search_intent":"how to fix duplicate ui indicators","keywords":["ui visual ownership pattern","component indicator duplication","ui architecture ownership","css visual conflict"],"body":"## Principle\n\n**A visual indicator MUST have exactly one canonical owner in the component hierarchy.**\n\nIndicators such as:\n- underline\n- highlight\n- focus ring\n- active border\n- selection marker\n\nCANNOT be shared, duplicated, or split across multiple layers.\n\n---\n\n## The Problem\n\nWhen two layers render parts of the same visual signal:\n\n- The model becomes ambiguous\n- Styling degenerates into patching\n- Bugs are \"fixed\" with offsets or overrides\n- Architecture silently rots\n\nThis is not a CSS issue — it is a **semantic ownership violation**.\n\n---\n\n## Forbidden Patterns\n\n- Container draws baseline, child draws active line\n- Parent draws inactive state, child draws active delta\n- Two elements visually completing the same signal\n\n**If two elements draw the same idea, the architecture is wrong.**\n\n---\n\n## Canonical Pattern\n\nChoose ONE owner:\n\n- Either the *item* owns the indicator  \n- Or the *container* owns the indicator  \n\nNever both.\n\nAll styling and tokens for that indicator MUST live in the owning layer's CSS.\n\n---\n\n## Real-World Trigger\n\nThis rule was formalized after a Tabs component rendered:\n\n- a baseline on the list\n- an active underline on the trigger\n\nResulting in duplicated lines and invalid fixes.\n\n---\n\n## Enforcement\n\n- Visual duplication is a rule violation\n- Fix requires redesign, not CSS tweaks\n- Reviewers MUST reject patches that \"align\" or \"mask\" the issue\n\n---\n\n## Canonical Justification\n\n> **Visual semantics are part of architecture.**  \n> If ownership is unclear, implementation will decay.\n\n---\n\n## Version History\n\n- **1.0.0** — Extracted from Tabs architecture failure"},{"number":106,"slug":"ui-neutralizes-hostile-css-not-primitives","title":"canon-rule-106-ui-neutralizes-hostile-css-not-primitives","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["primitives","css","ui","layering"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Applying CSS fixes inside primitives breaks architectural layering and contaminates core abstractions. Global CSS issues must be handled at the UI layer.","problem":"css fixes applied in primitives break layer separation","solution":"handle css neutralization only in ui layer while keeping primitives pure","signals":["css leakage","primitive contamination","layer violation"],"search_intent":"how to isolate css in ui layer","keywords":["ui css neutralization pattern","primitive purity css","layer separation ui css","design system layering css"],"body":"## Principle\n\n**CSS neutralization is a UI concern, never a primitive concern.**\n\nPrimitives MUST remain pure:\n- semantic HTML\n- data-attributes\n- zero styling logic\n- zero browser behavior\n\nIf external or global CSS interferes, the fix belongs to the UI layer.\n\n---\n\n## The Problem\n\nGlobal resets and base styles may affect primitives unintentionally.\n\nIncorrect responses include:\n- adding CSS to primitives\n- adding wrapper hacks\n- modifying primitive semantics\n\nThese actions violate CanonRS layering.\n\n---\n\n## Canonical Response\n\n- Primitives stay untouched\n- UI applies controlled neutralization\n- CSS selectors target `button[data-*]`, not generic elements\n- Techniques like `all: revert` are allowed **only in UI CSS**\n\n---\n\n## Forbidden Patterns\n\n- CSS inside primitives\n- Primitive-specific overrides\n- Runtime fixes to compensate for styling leaks\n\n---\n\n## Canonical Pattern\n```\nPrimitive → HTML + data-attributes  \nUI        → ergonomics + resilience  \nCSS       → token-driven interpretation\n```\n\nLayer boundaries must never collapse.\n\n---\n\n## Canonical Justification\n\n> **If primitives know about CSS, the system is already broken.**\n\n---\n\n## Version History\n\n- **1.0.0** — Extracted from Tabs / button reset incident"},{"number":107,"slug":"token-architecture-theme-specificity","title":"Token Architecture & Theme Specificity","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","css","themes","specificity"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Incorrect token placement in CSS creates specificity conflicts and prevents themes from overriding values. Mixing structural and color tokens leads to unpredictable behavior.","problem":"color tokens in root prevent themes from overriding values","solution":"separate structural tokens in root and color tokens in theme selectors","signals":["theme override fail","color mismatch","specificity conflict"],"search_intent":"how to fix css token specificity","keywords":["css token architecture","theme specificity css","design system tokens separation","css variable override issue"],"body":"## The Principle\n\n**:root defines structure. Themes define colors.**\n\nCSS token architecture MUST separate structural tokens (spacing, typography, shadows) from thematic tokens (colors). This prevents specificity conflicts and ensures themes can override only what they should.\n\n---\n\n## The Problem\n\n### ❌ Wrong Pattern (Conflicting Specificity)\n```css\n/* tokens.css */\n:root {\n  --color-primary-bg: hsl(38 92% 50%);  /* Hardcoded default */\n  --color-bg-surface: hsl(0 0% 100%);\n  --space-lg: 1rem;  /* OK - structural */\n}\n\n[data-theme=\"clean-slate\"] {\n  --color-primary-bg: hsl(238 83% 66%);  /* 🚫 CANNOT override :root */\n}\n```\n\n**Why this breaks:**\n- `:root` has equal specificity to `[data-theme]`\n- Source order determines winner (unpredictable)\n- Themes cannot reliably override colors\n- **Result:** Clean Slate shows Amber colors\n\n---\n\n## The Solution\n\n### ✅ Correct Pattern (Separation of Concerns)\n```css\n/* tokens.css - STRUCTURAL ONLY */\n:root {\n  /* ━━━ STRUCTURAL (never change with theme) ━━━ */\n  --space-xs: 0.25rem;\n  --space-sm: 0.5rem;\n  --space-lg: 1rem;\n  --space-xl: 1.5rem;\n  \n  --font-size-sm: 0.875rem;\n  --font-size-md: 1rem;\n  --font-size-lg: 1.125rem;\n  \n  --radius-sm: 0.25rem;\n  --radius-md: 0.375rem;\n  --radius-lg: 0.5rem;\n  \n  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);\n  \n  --z-base: 0;\n  --z-popover: 1000;\n  --z-modal: 3000;\n  \n  /* ━━━ NO COLOR TOKENS HERE ━━━ */\n}\n\n/* theme-presets.css - COLORS ONLY */\nhtml[data-theme=\"clean-slate\"] {\n  --background: 210 40% 98%;\n  --foreground: 217 33% 17%;\n  --primary: 238 83% 66%;\n  --primary-foreground: 0 0% 100%;\n  \n  /* Canon semantic tokens */\n  --color-bg-surface: hsl(210 40% 98%);\n  --color-bg-elevated: hsl(0 0% 100%);\n  --color-primary-bg: hsl(238 83% 66%);\n  --color-primary-fg: hsl(0 0% 100%);\n}\n\nhtml[data-theme=\"clean-slate\"].dark {\n  --background: 222 47% 11%;\n  --foreground: 214 32% 91%;\n  --primary: 234 89% 73%;\n  \n  --color-bg-surface: hsl(222 47% 11%);\n  --color-bg-elevated: hsl(217 33% 17%);\n  --color-primary-bg: hsl(234 89% 73%);\n}\n```\n\n**Key differences:**\n1. `:root` contains ZERO color tokens\n2. `html[data-theme]` uses element selector for higher specificity\n3. Themes control ALL color-related tokens\n4. Structural tokens never conflict with themes\n\n---\n\n## Token Classification\n\n### Structural Tokens (in :root)\n**Never theme-specific, always system-wide**\n\n| Category | Tokens | Rationale |\n|----------|--------|-----------|\n| **Spacing** | `--space-*`, `--space-control-*` | Mathematical scale |\n| **Typography** | `--font-*`, `--line-height-*` | Brand identity |\n| **Radius** | `--radius-*` | Personality system-wide |\n| **Shadow** | `--shadow-*` | Elevation hierarchy |\n| **Z-Index** | `--z-*` | Stacking context |\n| **Motion** | `--motion-*` | UX consistency |\n| **Border Width** | `--border-width-*` | Visual weight |\n\n### Thematic Tokens (in [data-theme])\n**Theme-specific, override per preset**\n\n| Category | Tokens | Rationale |\n|----------|--------|-----------|\n| **Background** | `--color-bg-*` | Surface colors |\n| **Foreground** | `--color-fg-*` | Text colors |\n| **Primary** | `--color-primary-*` | Brand accent |\n| **Semantic** | `--color-danger-*`, `--color-success-*` | Status colors |\n| **Border** | `--color-border-*` | Structural outlines |\n\n---\n\n## Specificity Rules\n\n### CSS Specificity Hierarchy\n```css\n/* SPECIFICITY: 0-0-0 (lowest) */\n:root {\n  --space-lg: 1rem;  /* ✅ OK - structural */\n}\n\n/* SPECIFICITY: 0-1-0 */\n[data-theme=\"amber\"] {\n  --color-primary: ...;  /* ⚠️ Equal to :root (source order wins) */\n}\n\n/* SPECIFICITY: 0-1-1 (CORRECT) */\nhtml[data-theme=\"amber\"] {\n  --color-primary: ...;  /* ✅ ALWAYS wins over :root */\n}\n\n/* SPECIFICITY: 0-2-1 (HIGHER) */\nhtml[data-theme=\"amber\"].dark {\n  --color-primary: ...;  /* ✅ Wins over light mode */\n}\n```\n\n### Golden Rule\n**Always use `html[data-theme=\"...\"]` for themes**\n\nThis ensures theme tokens ALWAYS override `:root` defaults, regardless of source order.\n\n---\n\n## File Structure\n\n### Canonical Layout\n```\npackages-rust/rs-design/style/\n├── tokens.css              # Structural tokens only (:root)\n├── theme-presets.css       # Color tokens only (html[data-theme])\n└── components/\n    └── *.css              # Use var(--color-*), var(--space-*)\n```\n\n### Import Order (CRITICAL)\n```css\n/* application.css */\n@import \"./tokens.css\";         /* 1. Structure first */\n@import \"./theme-presets.css\";  /* 2. Themes override */\n@import \"./components/*.css\";   /* 3. Components use tokens */\n```\n\n**Why order matters:**\n- Themes must load AFTER structural tokens\n- Components must load AFTER themes\n- Wrong order = themes don't apply\n\n---\n\n## Real-World Example\n\n### Before (Broken)\n```css\n/* tokens.css */\n:root {\n  --space-lg: 1rem;                    /* ✅ OK */\n  --color-primary-bg: hsl(38 92% 50%); /* ❌ WRONG - color in :root */\n}\n\n[data-theme=\"clean-slate\"] {\n  --color-primary-bg: hsl(238 83% 66%); /* 🚫 Cannot override */\n}\n```\n\n**Symptom:** Selecting \"Clean Slate\" shows Amber colors.\n\n**Console test:**\n```javascript\ngetComputedStyle(document.documentElement)\n  .getPropertyValue('--color-primary-bg')\n// Returns: \"hsl(38 92% 50%)\" (WRONG - should be Clean Slate blue)\n```\n\n### After (Fixed)\n```css\n/* tokens.css */\n:root {\n  --space-lg: 1rem;  /* ✅ Structural only */\n  /* NO COLOR TOKENS */\n}\n\n/* theme-presets.css */\nhtml[data-theme=\"clean-slate\"] {\n  --color-primary-bg: hsl(238 83% 66%);  /* ✅ Always wins */\n}\n```\n\n**Console test:**\n```javascript\ngetComputedStyle(document.documentElement)\n  .getPropertyValue('--color-primary-bg')\n// Returns: \"hsl(238 83% 66%)\" (CORRECT - Clean Slate blue)\n```\n\n---\n\n## Validation Checklist\n\n### Token Audit\n```bash\n# Check for color tokens in :root (FORBIDDEN)\ngrep -A 100 \"^:root {\" tokens.css | grep \"color-\"\n# Should return EMPTY\n\n# Check for structural tokens in themes (DISCOURAGED)\ngrep -A 50 \"\\[data-theme=\" theme-presets.css | grep \"space-\\|font-\\|radius-\"\n# Should return MINIMAL or NONE\n```\n\n### Browser DevTools Test\n```javascript\n// 1. Check theme applied\ndocument.documentElement.getAttribute('data-theme')\n// Should return: \"clean-slate\" or \"amber-minimal\"\n\n// 2. Check class applied\ndocument.documentElement.className\n// Should return: \"light\" or \"dark\"\n\n// 3. Verify color override\ngetComputedStyle(document.documentElement).getPropertyValue('--color-primary-bg')\n// Should return theme-specific HSL value\n\n// 4. Verify structural token\ngetComputedStyle(document.documentElement).getPropertyValue('--space-lg')\n// Should return: \"1rem\" (same for all themes)\n```\n\n---\n\n## Anti-Patterns\n\n### ❌ Colors in :root\n```css\n:root {\n  --color-bg-surface: hsl(0 0% 100%);  /* 🚫 FORBIDDEN */\n}\n```\n\n**Fix:** Move to `html[data-theme=\"default\"]`\n\n### ❌ [data-theme] without html\n```css\n[data-theme=\"amber\"] {  /* ⚠️ Low specificity */\n  --color-primary: ...;\n}\n```\n\n**Fix:** Use `html[data-theme=\"amber\"]`\n\n### ❌ Structural tokens in themes\n```css\nhtml[data-theme=\"amber\"] {\n  --space-lg: 2rem;  /* 🚫 DISCOURAGED - breaks consistency */\n}\n```\n\n**Fix:** Keep structural tokens in `:root` only\n\n### ❌ !important in themes\n```css\nhtml[data-theme=\"amber\"] {\n  --color-primary: ... !important;  /* 🚫 NEVER - specificity is enough */\n}\n```\n\n**Fix:** Remove `!important`, rely on specificity\n\n---\n\n## Benefits\n\n### ✅ Predictable Theme Switching\n- Theme tokens ALWAYS override defaults\n- No source-order dependency\n- Dark mode works consistently\n\n### ✅ Maintainable Architecture\n```\n:root             → Infrastructure (never changes)\nhtml[data-theme]  → Aesthetics (changes per theme)\n```\n\n### ✅ Portable Themes\n- Themes are self-contained\n- No hidden dependencies on :root colors\n- Easy to add new themes\n\n### ✅ Debuggable\n```javascript\n// Always shows active theme's value\ngetComputedStyle(html).getPropertyValue('--color-primary-bg')\n```\n\n---\n\n## Related Rules\n\n- **Rule #25:** Theme Presets Contract (what themes can define)\n- **Rule #56:** Monorepo CSS Build Pipeline (how CSS artifacts are built)\n- **Rule #50:** Provider Singleton Pattern (how themes are applied via React context)\n\n---\n\n## Normative Requirements\n\n**MUST:**\n- Structural tokens MUST live in `:root` only\n- Color tokens MUST live in `html[data-theme]` selectors\n- Theme selectors MUST include `html` element for specificity\n- Import order MUST be: tokens → themes → components\n\n**MUST NOT:**\n- Color tokens MUST NOT appear in `:root`\n- Structural tokens MUST NOT be overridden by themes\n- Use `!important` in theme definitions\n- Use bare `[data-theme]` without `html` element\n\n**SHOULD:**\n- Validate token separation in CI\n- Test theme switching in browser DevTools\n- Document theme-specific color meanings\n\n---\n\n**Author:** Canon Working Group  \n**Supersedes:** Implicit token patterns  \n**Related:** Canon Rule #25 (Theme Presets Contract)"},{"number":108,"slug":"visual-surfaces-contract","title":"Visual Surfaces Contract","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["surfaces","tokens","css","components"],"language":"EN","version":"1.0.0","date":"2025-01-16","intro":"Inconsistent visual hierarchy arises when components define their own styling instead of using semantic surfaces. This leads to design drift and complex dark mode handling.","problem":"components define appearance instead of using semantic surface tokens","solution":"enforce surface types and token-based styling for all components","signals":["design drift","inconsistent ui","dark mode break"],"search_intent":"how to enforce design surfaces","keywords":["design system surfaces","css surface tokens","semantic ui surfaces","token based styling components"],"body":"## The Principle\n\n**Components don't choose appearance. They obey surfaces.**\n\nVisual hierarchy in Canon is achieved through **semantic surface types**, not ad-hoc styling. This ensures consistency, prevents design drift, and makes dark mode trivial.\n\n---\n\n## Surface Types (MANDATORY)\n\n### 1. surface-base\n**Usage:** Application background, primary content area  \n**Token:** `--color-bg-surface`\n\n**Where to use:**\n- ✅ Page backgrounds\n- ✅ Layout root\n- ✅ Areas without emphasis\n\n**Where NOT to use:**\n- ❌ Cards or highlighted containers\n- ❌ Navigation components\n- ❌ Component previews\n```css\n.page-root {\n  background: var(--color-bg-surface);\n}\n```\n\n### 2. surface-muted\n**Usage:** Auxiliary areas, navigation, grouping  \n**Token:** `--color-bg-muted`\n\n**Where to use:**\n- ✅ Sidebar\n- ✅ Secondary navigation\n- ✅ Rail containers\n- ✅ Subtle grouping areas\n\n**Where NOT to use:**\n- ❌ Primary content\n- ❌ Component showcases\n- ❌ Documentation blocks\n```css\n.sidebar {\n  background: var(--color-bg-muted);\n}\n```\n\n### 3. surface-elevated\n**Usage:** Content in focus, highlighted information  \n**Token:** `--color-bg-elevated`\n\n**Where to use:**\n- ✅ Cards\n- ✅ Preview blocks\n- ✅ Tables\n- ✅ API documentation blocks\n- ✅ Comparison tables\n\n**ALWAYS accompanied by:**\n- Border (`--color-border-muted`)\n- Border radius (`--radius-lg` or `--radius-xl`)\n- Shadow (`--shadow-sm` or `--shadow-md`)\n```css\n.card {\n  background: var(--color-bg-elevated);\n  border: var(--border-width-hairline) solid var(--color-border-muted);\n  border-radius: var(--radius-lg);\n  box-shadow: var(--shadow-sm);\n}\n```\n\n### 4. surface-hero\n**Usage:** Component headers, documentation intros  \n**Tokens:** Uses base surface + spacing + typography scale\n\n**Key rule:**\nHero creates hierarchy through **scale and space**, NOT color contrast.\n\n**Where to use:**\n- ✅ Component headers\n- ✅ Documentation hero sections\n- ✅ Section introductions\n\n**Characteristics:**\n- Larger typography (`--font-size-2xl`, `--font-size-3xl`)\n- Generous spacing (`--space-3xl` top padding)\n- NO custom background color\n```css\n.hero {\n  padding: var(--space-3xl) 0 var(--space-2xl);\n  background: var(--color-bg-surface); /* Same as base */\n}\n\n.hero-title {\n  font-size: var(--font-size-3xl);\n  font-weight: var(--font-weight-bold);\n  line-height: var(--line-height-tight);\n}\n```\n\n---\n\n## Shadow Contract\n\n| Use Case | Token |\n|----------|-------|\n| Light cards | `--shadow-sm` |\n| Important containers | `--shadow-md` |\n| Overlays | `--shadow-lg` |\n| Modals | `--shadow-xl` |\n\n### Rules\n- ❌ NEVER create custom shadow values\n- ❌ NEVER use Tailwind arbitrary shadows (`shadow-[0_4px_8px_...]`)\n- ✅ ALWAYS use semantic shadow tokens\n```css\n/* ✅ CORRECT */\n.preview-block {\n  box-shadow: var(--shadow-md);\n}\n\n/* ❌ WRONG */\n.preview-block {\n  box-shadow: 0 4px 8px rgba(0,0,0,0.1);\n}\n```\n\n---\n\n## Border Contract\n\n| Situation | Token |\n|-----------|-------|\n| Default borders | `--color-border-default` |\n| Subtle borders | `--color-border-muted` |\n\n### Rules\n- ✅ Border ALWAYS before shadow (layering order)\n- ❌ Border is NEVER semantic color (danger/success)\n- ✅ Border width uses `--border-width-hairline` or `--border-width-thin`\n```css\n/* ✅ CORRECT - Neutral border */\n.elevated-card {\n  border: var(--border-width-hairline) solid var(--color-border-muted);\n  box-shadow: var(--shadow-sm);\n}\n\n/* ❌ WRONG - Semantic color as border */\n.elevated-card {\n  border: 1px solid var(--color-primary-bg); /* FORBIDDEN */\n}\n```\n\n---\n\n## Spacing Contract\n\n| Context | Minimum Token |\n|---------|---------------|\n| Inside cards | `--space-lg` |\n| Between sections | `--space-2xl` |\n| Hero top padding | `--space-3xl` |\n\n### Rules\n- ❌ NO arbitrary padding values (`px` units)\n- ❌ NO Tailwind utility padding (`p-4`, `py-8`)\n- ✅ ALWAYS use semantic spacing tokens\n```css\n/* ✅ CORRECT */\n.preview-surface {\n  padding: var(--space-3xl);\n  gap: var(--space-2xl);\n}\n\n/* ❌ WRONG */\n.preview-surface {\n  padding: 48px; /* Hardcoded */\n}\n```\n\n---\n\n## Dark Mode Law\n\n**Dark mode does NOT redefine components. It redefines tokens.**\n\n### ✅ Allowed\n```css\nhtml[data-theme=\"clean-slate\"].dark {\n  --color-bg-surface: hsl(222 47% 11%);\n  --color-bg-elevated: hsl(217 33% 17%);\n  --color-border-muted: hsl(215 14% 30%);\n}\n```\n\n### ❌ Forbidden\n```css\n.dark .card {\n  background: #1a1a1a; /* FORBIDDEN - component-level override */\n}\n```\n\n**Why:**\n- Component styles reference tokens\n- Tokens change with theme\n- Components automatically adapt\n- No duplicated dark mode styles\n\n---\n\n## Component Mapping (CLOSED)\n\n| Component | Surface Type |\n|-----------|-------------|\n| ComponentHeader | `hero` |\n| PreviewBlock | `elevated` |\n| ApiBlock | `elevated` |\n| ComparisonBlock | `elevated` |\n| RulesBlock | `elevated` |\n| UsageBlock | `elevated` |\n| Sidebar | `muted` |\n| Page | `base` |\n\n**This mapping is ENFORCED and CLOSED.**  \nNew components MUST fit into existing surface types.\n\n---\n\n## Real-World Examples\n\n### ComponentHeader (Hero)\n```css\n.canon-component-hero {\n  position: relative;\n  padding: var(--space-3xl) 0 var(--space-2xl);\n  margin-bottom: var(--space-3xl);\n  background: var(--color-bg-surface); /* Base surface */\n}\n\n.canon-component-title {\n  font-size: var(--font-size-3xl);\n  font-weight: var(--font-weight-bold);\n  line-height: var(--line-height-tight);\n  color: var(--color-fg-default);\n}\n```\n\n### PreviewBlock (Elevated)\n```css\n.canon-preview-surface {\n  display: flex;\n  flex-direction: column;\n  gap: var(--space-3xl);\n  \n  padding: var(--space-3xl);\n  \n  background: linear-gradient(\n    180deg,\n    var(--color-bg-elevated),\n    var(--color-bg-surface)\n  );\n  \n  border: var(--border-width-hairline) solid var(--color-border-muted);\n  border-radius: var(--radius-2xl);\n  box-shadow: var(--shadow-xl);\n}\n```\n\n### ButtonGrid Groups (Elevated Cards)\n```css\n.canon-button-preview-group {\n  display: flex;\n  flex-direction: column;\n  gap: var(--space-lg);\n  \n  padding: var(--space-xl);\n  \n  background: var(--color-bg-surface);\n  border-radius: var(--radius-xl);\n  border: 1px solid var(--color-border-muted);\n}\n```\n\n### Sidebar (Muted)\n```css\n.sidebar-root {\n  background: var(--color-bg-muted);\n  border-right: var(--border-width-hairline) solid var(--color-border-default);\n}\n```\n\n---\n\n## Anti-Patterns (FORBIDDEN)\n\n### ❌ Creating New Colors for \"Emphasis\"\n```css\n/* WRONG */\n.special-card {\n  background: #f0f9ff; /* Custom blue tint - FORBIDDEN */\n}\n```\n\n**Fix:** Use `surface-elevated` with proper shadow.\n\n### ❌ Gradient as Surface\n```css\n/* WRONG */\n.card {\n  background: linear-gradient(to bottom, #fff, #f5f5f5); /* FORBIDDEN */\n}\n```\n\n**Fix:** Use `var(--color-bg-elevated)` with shadow for depth.\n\n### ❌ Tailwind Utilities in Blocks\n```css\n/* WRONG - component HTML */\n<div class=\"bg-white dark:bg-gray-900 p-8 rounded-lg shadow-md\">\n```\n\n**Fix:** Create semantic component with token-based CSS.\n\n### ❌ Shadow Without Border\n```css\n/* WRONG */\n.card {\n  background: var(--color-bg-elevated);\n  box-shadow: var(--shadow-md); /* Missing border */\n}\n```\n\n**Fix:** Add border BEFORE shadow.\n\n### ❌ Changing Visual Without Token\n```css\n/* WRONG */\n.button-primary {\n  background: #3b82f6; /* Hardcoded */\n}\n```\n\n**Fix:** Use `var(--color-primary-bg)`.\n\n---\n\n## Golden Rule (UNBREAKABLE)\n\n> **Components don't choose appearance. They obey surfaces.**\n\nThis means:\n1. Components reference tokens, not values\n2. Tokens change with theme/mode\n3. Components automatically adapt\n4. No component-specific dark mode CSS\n\n---\n\n## Validation Checklist\n\n### Token Usage Audit\n```bash\n# Check for hardcoded colors\ngrep -r \"background: #\\|background: rgb\\|background: hsl(\" src/components/\n\n# Check for hardcoded spacing\ngrep -r \"padding: [0-9].*px\\|margin: [0-9].*px\" src/components/\n\n# Check for Tailwind utilities in component CSS\ngrep -r \"class=.*bg-\\|class=.*p-\\|class=.*shadow-\" src/components/\n```\n\n### Dark Mode Test\n```javascript\n// 1. Switch to dark mode\ndocument.documentElement.classList.add('dark')\n\n// 2. Verify surfaces update\ngetComputedStyle(document.querySelector('.canon-preview-surface'))\n  .backgroundColor\n// Should change automatically\n\n// 3. NO component-specific dark styles should exist\ndocument.querySelectorAll('[class*=\"dark:\"]').length\n// Should return 0 in component CSS\n```\n\n### Surface Type Audit\n```bash\n# Every elevated surface must have border + shadow\ngrep -A 5 \"color-bg-elevated\" src/components/ | grep -E \"border|box-shadow\"\n```\n\n---\n\n## Benefits\n\n### ✅ Consistent Visual Hierarchy\n- All cards look consistent\n- Depth is predictable\n- Users develop spatial memory\n\n### ✅ Zero Dark Mode Duplication\n```css\n/* Before: 100 lines duplicated */\n.card { background: white; }\n.dark .card { background: #1a1a1a; }\n\n/* After: 1 line, automatic */\n.card { background: var(--color-bg-elevated); }\n```\n\n### ✅ Theme Portability\n- New theme = change tokens\n- Components adapt automatically\n- No component refactoring\n\n### ✅ Prevents Design Drift\n```css\n/* IMPOSSIBLE to do this */\n.special-card { background: #custom; } /* Linter error */\n```\n\n---\n\n## System Status After Rule #82\n\n| Aspect | Status |\n|--------|--------|\n| Tokens | 🟢 CLOSED |\n| Surfaces | 🟢 CONTRACT DEFINED |\n| Dark Mode | 🟢 CONSISTENT |\n| Design Drift | 🟢 IMPOSSIBLE |\n| Scalability | 🟢 SAFE |\n\n---\n\n## Related Rules\n\n- **Rule #81:** Token Architecture & Theme Specificity\n- **Rule #25:** Theme Presets Contract\n- **Rule #56:** Monorepo CSS Build Pipeline\n\n---\n\n## Normative Requirements\n\n**MUST:**\n- Components MUST use semantic surface tokens\n- Elevated surfaces MUST include border + shadow\n- Spacing MUST use semantic spacing tokens\n- Dark mode MUST redefine tokens, not components\n\n**MUST NOT:**\n- Hardcode color values in components\n- Create custom shadows\n- Use Tailwind utilities for semantic components\n- Add component-specific dark mode styles\n\n**SHOULD:**\n- Document surface type for each component\n- Validate token usage in CI\n- Test dark mode automatically\n\n---\n\n**Author:** Canon Working Group  \n**Supersedes:** Implicit surface patterns  \n**Related:** Canon Rule #81 (Token Architecture)"},{"number":111,"slug":"model-first-css-second","title":"Model First, CSS Second","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["architecture","css","model","responsibility"],"language":"EN","version":"1.0.0","date":"2026-01-17","intro":"Using CSS to compensate for architectural flaws leads to fragile systems and hidden technical debt. Structural issues must be resolved at the model level, not visually masked.","problem":"css is used to fix structural and ownership issues in component architecture","solution":"fix the underlying model and component hierarchy instead of applying css workarounds","signals":["css hacks","important usage","layout overlap"],"search_intent":"how to fix css architecture issues","keywords":["css architecture anti pattern","model vs css responsibility","ui structure vs styling","css workaround problem"],"body":"## Principle\n\n**CSS never fixes an architecturally invalid model.**\n\nIf the solution requires \"clever\" CSS, the model is wrong.\n\n---\n\n## Definition\n\nThe *model* defines:\n- Who owns the state\n- Who renders each visual signal\n- Where responsibilities live\n\nCSS **only visually expresses** that model.\nIt does not redefine, correct, or compensate for architecture.\n\n---\n\n## Forbidden: Using CSS to\n\n❌ Fix structural overlap\n❌ Hide responsibility conflicts\n❌ \"Adjust\" incorrect visual authority\n\nForbidden examples:\n- `!important`\n- `margin: -1px`\n- compensatory padding\n- specificity hacks\n\n---\n\n## Required\n\n✅ Fix the HTML/hierarchy\n✅ Redefine who owns the visual signal\n✅ Remove the wrong element\n\n---\n\n## Canonical Diagnosis\n\nIf the solution requires:\n- workarounds\n- \"temporary\" exceptions\n- defensive explanatory comments\n\n👉 the model is incorrect.\n\n---\n\n## Justification\n\nCSS is declarative, not corrective.\nUsing it to compensate for architecture creates silent technical debt.\n\n---\n\n## Final Rule\n\n> **When CSS becomes a workaround, the error is in the model — always.**\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #107 — Token Architecture Theme Specificity\n- Canon Rule #109 — Single Visual Authority\n- Canon Rule #106 — UI Neutralizes Hostile CSS, Not Primitives"},{"number":112,"slug":"ui-owns-visual-style","title":"UI Owns Visual Style","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["ui","primitives","css","layering"],"language":"EN","version":"1.0.0","date":"2026-01-17","intro":"Mixing visual styling into primitives breaks separation of concerns and reduces reusability. UI layer must exclusively control appearance while primitives remain structural.","problem":"visual styling is applied in primitives instead of ui layer","solution":"restrict styling to ui layer and keep primitives purely structural","signals":["style leakage","duplicate css","primitive coupling"],"search_intent":"how to separate ui and primitives","keywords":["ui layer styling pattern","primitive vs ui separation","design system layering","css ownership ui layer"],"body":"## Principle\n\n**Only the UI layer can apply visual style.**\n\nPrimitives **never** own visual responsibility.\nCSS **never** lives in primitives.\nPrimitives only expose structure and state.\n\n---\n\n## Definition\n\nIn CanonRS, the separation is absolute:\n\n**Primitive:**\n- Semantic HTML\n- `data-*` attributes\n- Declarative state (`data-state`)\n- ZERO CSS\n- ZERO tokens\n- ZERO appearance\n\n**UI:**\n- Applies styling\n- Consumes canonical tokens\n- Defines visual affordances\n- Translates state → visual\n\n---\n\n## Correct Examples\n\n✅ `button[data-tabs-trigger]` styled in `style/ui/tabs.css`\n✅ Primitive renders only `<button data-tabs-trigger>`\n✅ Visual state comes from `data-state=\"active\"`\n\n---\n\n## Anti-patterns (FORBIDDEN)\n\n❌ CSS in `src/primitives/*`\n❌ Primitive with `class=\"\"`\n❌ Primitive \"knowing\" how it looks\n❌ Primitive importing tokens\n\n---\n\n## Violation Symptoms\n\n- Duplicated CSS between UI and primitive\n- Primitive difficult to reuse\n- Style \"leaking\" to other components\n- Visual refactors breaking structure\n\n---\n\n## Justification\n\nPrimitives are reusable structural blocks.\nUI is where visual decisions live.\nMixing the two breaks scalability and governance.\n\n---\n\n## Final Rule\n\n> **If there's appearance, there's UI.\n> If there's structure, there's primitive.**\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #111 — Model First, CSS Second\n- Canon Rule #108 — Visual Surfaces Contract\n- Canon Rule #106 — UI Neutralizes Hostile CSS, Not Primitives"},{"number":113,"slug":"states-are-data-not-style","title":"States Are Data, Not Style","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["state","css","data-attributes","components"],"language":"EN","version":"1.0.0","date":"2026-01-17","intro":"Encoding state as visual style creates tight coupling and limits flexibility. State must remain semantic and independent from appearance to support scalable theming.","problem":"state is encoded as visual style instead of semantic data","solution":"express state via data attributes and map to visuals only in css","signals":["style coupling","theme break","implicit state"],"search_intent":"how to separate state from style","keywords":["state vs style css","data attributes state pattern","semantic state ui","css state mapping"],"body":"## Principle\n\n**State is data, not appearance.**\n\nState declares **what it is**.\nCSS decides **how it looks**.\n\n---\n\n## Definition\n\nStates in CanonRS:\n\n- Are expressed via `data-*`\n- Are semantic (`active`, `inactive`, `disabled`)\n- Carry no embedded visual meaning\n\nExample:\n```html\n<button data-state=\"active\">\n```\n\nThis state **does NOT say**:\n- color\n- underline\n- background\n- animation\n\nIt only declares: **active**.\n\n---\n\n## Required\n\n✅ Use `data-state` for states\n✅ Map state → visual only in CSS\n✅ Keep state independent of theme\n\n---\n\n## Anti-patterns (FORBIDDEN)\n\n❌ States encoded as visual classes (`.is-blue`)\n❌ Style logic in Rust component\n❌ Implicit states via DOM (e.g., `:first-child`)\n\n---\n\n## Direct Benefits\n\n- Swappable themes without touching component\n- Consistent accessibility\n- Predictable SSR\n- UI scalable across multiple products\n\n---\n\n## Justification\n\nWhen state becomes style, you lose semantics.\nWhen state is data, the system gains flexibility.\n\n---\n\n## Final Rule\n\n> **State describes truth.\n> Style describes appearance.**\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #108 — Visual Surfaces Contract\n- Canon Rule #112 — UI Owns Visual Style\n- Canon Rule #107 — Token Architecture Theme Specificity"},{"number":114,"slug":"single-visual-authority","title":"Single Visual Authority","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["visual","ownership","css","components"],"language":"EN","version":"1.0.0","date":"2026-01-17","intro":"Multiple layers rendering the same visual signal cause conflicts and unpredictable UI behavior. Visual responsibility must be uniquely assigned to a single component.","problem":"multiple elements render the same visual signal causing conflicts","solution":"assign a single component as the visual authority for each signal","signals":["duplicate borders","misaligned visuals","css conflicts"],"search_intent":"how to fix duplicate visual signals","keywords":["visual authority ui pattern","duplicate ui signals","css ownership conflict","component visual ownership"],"body":"## Principle\n\n**A component has a single visual authority.**\n\nNever more than one layer should draw the same visual affordance.\n\n---\n\n## Definition\n\nIn CanonRS, **each visual signal has exactly one owner**:\n\n- Active line\n- Hover background\n- Focus ring\n- Indicator border\n- Elevation shadow\n\nIf two elements draw the same signal → **architectural BUG**.\n\n---\n\n## Correct Examples\n\n✅ Selection line drawn **only** in `TabsTrigger`\n✅ `TabsList` defines only layout, not active indicator\n✅ Focus ring applied only to the real interactive element\n\n---\n\n## Anti-patterns (FORBIDDEN)\n\n❌ List with `border-bottom` + trigger with `border-bottom`\n❌ Wrapper drawing visual state\n❌ \"Compensations\" with negative margin\n❌ Two selectors competing for the same affordance\n\n---\n\n## Violation Symptoms\n\n- Duplicated lines\n- \"Fighting\" borders\n- Corrections with `-1px`\n- Style depends on DOM order\n\n---\n\n## Justification\n\nSingle visual authority guarantees:\n- predictability\n- readability\n- safe refactoring\n- absence of workarounds\n\n---\n\n## Final Rule\n\n> **If two elements draw the same thing,\n> the architecture is wrong.**\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #105 — Visual Indicators Must Have a Single Owner\n- Canon Rule #111 — Model First, CSS Second\n- Canon Rule #112 — UI Owns Visual Style"},{"number":115,"slug":"reset-awareness-and-boundaries","title":"Reset Awareness & CSS Boundaries","status":"ENFORCED","severity":"MEDIUM","category":"styling-css","tags":["css","reset","ui","boundaries"],"language":"EN","version":"1.0.0","date":"2026-01-17","intro":"Ignoring global CSS resets leads to inconsistent component behavior across environments. UI components must explicitly handle reset effects without breaking architectural layers.","problem":"components do not account for global css reset causing inconsistent behavior","solution":"handle reset effects in ui layer while keeping primitives untouched","signals":["style inconsistency","reset conflict","layout break"],"search_intent":"how to handle css reset ui","keywords":["css reset handling ui","global css normalization issue","ui reset boundary pattern","css reset component conflict"],"body":"## Principle\n\n**UI components must assume the existence of global resets.**\n\nGlobal reset is not a bug.\nIgnoring it is.\n\n---\n\n## Definition\n\nIn enterprise systems:\n\n- CSS reset exists\n- Global normalization exists\n- Inevitable inheritance exists\n\nThe role of UI component is to:\n- neutralize reset **explicitly**\n- without violating primitives\n- without depending on fragile order\n\n---\n\n## Allowed\n\n✅ `all: revert` in UI\n✅ Re-establish `font`, `display`, `box-sizing`\n✅ Increase specificity with `button[data-*]`\n\n---\n\n## Forbidden\n\n❌ Fix reset inside primitive\n❌ Use `!important` to \"win\"\n❌ Create wrappers just to fight CSS\n❌ Rely on browser defaults\n\n---\n\n## Violation Symptoms\n\n- Component appearance changes between apps\n- Works isolated, breaks integrated\n- Cascading local corrections\n- Dependency on CSS load order\n\n---\n\n## Justification\n\nReset is infrastructure.\nUI must be resilient to it.\nPrimitive must be ignorant of it.\n\n---\n\n## Final Rule\n\n> **Reset is not fought.\n> Reset is bounded.**\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #106 — UI Neutralizes Hostile CSS, Not Primitives\n- Canon Rule #112 — UI Owns Visual Style\n- Canon Rule #110 — Reset Awareness and Boundaries (duplicate number, needs cleanup)"},{"number":116,"slug":"wasm-externref-table-limits","title":"WASM Externref Table Limits","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["wasm","externref","callbacks","runtime"],"language":"EN","version":"1.0.0","date":"2026-01-17","intro":"Excessive JS↔WASM references exhaust browser externref tables, causing runtime crashes. Each callback or handler adds entries, leading to resource exhaustion at scale.","problem":"unbounded creation of externrefs causes wasm runtime crashes","solution":"avoid per-component callbacks and delegate behavior to global shell handlers","signals":["table grow error","runtime crash","externref overflow"],"search_intent":"how to fix wasm externref error","keywords":["wasm externref table limit","leptos callback memory issue","js wasm reference overflow","externref table grow error"],"body":"## Principle\n\n**Components must not create unbounded JS↔WASM references.**\n\nThe externref table is a finite browser resource.\nDesign systems that violate this will crash at runtime.\n\n---\n\n## The Problem\n\nWhen using WASM with JS interop:\n- Each `#[wasm_bindgen]` export creates table entries\n- Each `Callback`, `on:*` handler, `web_sys` API call consumes refs\n- Providers that register global handlers multiply this\n- Browser refuses to grow table beyond limits\n\n### Real Error\n```\nRangeError: WebAssembly.Table.grow(): failed to grow table by 4\nat __wbindgen_init_externref_table\n```\n\nThis is NOT:\n- A build error\n- A memory limit\n- A WASM size issue\n- Fixable with compiler flags\n\nThis IS:\n- Runtime resource exhaustion\n- Architectural boundary violation\n- Direct consequence of too many JS bindings\n\n---\n\n## Anti-Pattern (FORBIDDEN)\n```rust\n// ❌ FORBIDDEN - creates externref per component\n#[component]\npub fn DraggableItem() -> impl IntoView {\n    let on_drag = move |ev: web_sys::DragEvent| { ... };\n    view! { <div on:dragstart=on_drag /> }\n}\n\n// ❌ FORBIDDEN - providers with closures\n#[component]\npub fn DragDropProvider(children: Children) -> impl IntoView {\n    let ctx = DragContext::new();\n    ctx.set_handler(move |event| { ... }); // externref!\n    children()\n}\n```\n\nEach instance = new externref.\n100 items = 100 refs.\nBrowser says NO.\n\n---\n\n## Canonical Pattern (REQUIRED)\n```rust\n// ✅ REQUIRED - zero externrefs\n#[component]\npub fn DraggableItem(id: String) -> impl IntoView {\n    view! { <div data-drag-id=id draggable=\"true\" /> }\n}\n```\n```javascript\n// ✅ Shell runtime (ONE handler globally)\ndocument.addEventListener(\"dragstart\", e => {\n  const el = e.target.closest(\"[data-drag-id]\");\n  if (!el) return;\n  // handle via data attributes\n});\n```\n\n---\n\n## Enforcement\n\nUI Components and Blocks MUST:\n- Render data-* attributes only\n- Use Signal/RwSignal for state\n- Delegate runtime behavior to shell\n\nUI Components MUST NOT:\n- Use `on:*` handlers with closures\n- Call `web_sys` APIs directly\n- Register callbacks per instance\n- Create `Callback::new` in render\n\n---\n\n## Scope\n\nThis applies to:\n- UI components (Button, Input, etc.)\n- Blocks (DataTable, Tree, Drag&Drop)\n- Any repeating element\n\nThis does NOT apply to:\n- Shell/App boundary (one-time setup OK)\n- Primitives (limited, controlled use)\n- Server-side only code\n\n---\n\n## Canonical Justification\n\n> Externrefs are a finite resource. Components that consume them unbounded will always fail at scale.\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #102 — Runtime JS Is Shell Infrastructure\n- Canon Rule #103 — Critical Runtime JS Must Be Inline\n- Canon Rule #93 — WASM Dev Builds Must Use Release Mode\n\n---"},{"number":117,"slug":"design-system-callbacks-are-props-not-handlers","title":"Design System Callbacks Are Props, Not Handlers","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["callbacks","design-system","wasm","events"],"language":"EN","version":"1.0.0","date":"2026-01-17","intro":"Creating inline event handlers in UI components increases externref usage and leads to runtime crashes. Callback handling must be decoupled from component rendering.","problem":"inline handlers create externrefs leading to wasm runtime limits","solution":"pass callbacks as props and delegate execution to shell via data attributes","signals":["externref overflow","runtime crash","event handler issue"],"search_intent":"how to avoid wasm callback limits","keywords":["leptos callback props pattern","wasm event handler externref","ui callback architecture","design system event delegation"],"body":"## Principle\n\n**UI components receive callbacks as props. They DO NOT create handlers inline.**\n\nEvery `on:*=move |_| { }` creates an externref.\nDesign systems with hundreds of components = thousands of refs = runtime crash.\n\n---\n\n## The Problem\n\nCurrent pattern (FORBIDDEN):\n```rust\n#[component]\npub fn Button(on_click: Callback<()>) -> impl IntoView {\n    view! {\n        <button on:click=move |_| on_click.run(())>  // ❌ externref!\n    }\n}\n```\n\nEach Button instance = 1 externref\n100 buttons = 100 refs = Table.grow() crash\n\n---\n\n## Canonical Pattern (REQUIRED)\n```rust\n#[component]\npub fn Button(\n    on_click: Callback<()>,\n    #[prop(into)] id: String,\n) -> impl IntoView {\n    view! {\n        <button data-action=\"click\" data-action-id=id>  // ✅ zero externref\n    }\n}\n```\n\nShell handles clicks:\n```javascript\ndocument.addEventListener(\"click\", e => {\n    const btn = e.target.closest(\"[data-action='click']\");\n    if (!btn) return;\n    window.dispatchEvent(new CustomEvent(\"leptos:action\", {\n        detail: { id: btn.dataset.actionId }\n    }));\n});\n```\n\n---\n\n## Enforcement\n\nUI Components MUST:\n- Accept callbacks as props\n- Render data-* attributes\n- Never use `on:*=move`\n\nUI Components MUST NOT:\n- Create inline handlers\n- Call `Callback::new` in render\n- Use closures with captured state\n\n---\n\n## Related Canon Rules\n\n- Canon Rule #116 — WASM Externref Table Limits\n- Canon Rule #102 — Runtime JS Is Shell Infrastructure\n\n---"},{"number":118,"slug":"view-type-boundary","title":"View<\\_> Type Boundary Prohibition","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["rust","types","view","inference"],"language":"EN","version":"1.0.0","date":"2025-01-21","intro":"Using View<_> outside component boundaries causes type inference explosions and compilation failures. Open inference propagates across the codebase, breaking type resolution.","problem":"open view type inference causes variance cycles and compilation errors","solution":"restrict view<_> to components and use impl into view or anyview elsewhere","signals":["variance cycle","e0391 error","type mismatch"],"search_intent":"how to fix rust view type errors","keywords":["rust view type inference","leptos anyview pattern","view generic error rust","type boundary leptos"],"body":"## Critical Rule\n\n**`View<_>` IS NOT A BOUNDARY TYPE**\n\n`View<_>` with open inference MUST ONLY exist as direct return of `#[component]`.\n\nViolation = Variance cycles / Inference explosions / Refactoring brittleness\n\n---\n\n## The Problem (Real Production Case)\n\n### Symptom\n\n```rust\n// Change ONE line in workflow/demo.rs\nfn render_step_buttons<F>(...) -> View<_> { ... }\n\n// Result: 197 compilation errors across ENTIRE codebase\nerror[E0391]: cycle detected when computing variances\n```\n\n### Root Cause\n\n`View<_>` with generic `_` creates **open type inference graph**:\n\n- TypedBuilder propagates variance\n- Generic functions propagate constraints\n- Cross-file type unification fails\n- Single change = cascading failure\n\n---\n\n## Prohibited Patterns\n\n### ❌ FORBIDDEN #1: Helper Returns View<\\_>\n\n```rust\n// ❌ WRONG - Open inference boundary\nfn render_step_buttons<F>(status: RwSignal<StepStatus>, on_change: F) -> View<_>\nwhere\n    F: Fn(StepStatus) + 'static + Copy,\n{\n    let content: View<_> = match status.get() {\n        Status::A => view! { <Button>A</Button> },\n        Status::B => view! { <Button>B</Button> },\n    };\n    content\n}\n```\n\n**Why it breaks:**\n\n- `View<_>` forces global inference\n- `F` generic propagates to TypedBuilder\n- ButtonPropsBuilder → HeaderPropsBuilder → cycle\n\n---\n\n### ❌ FORBIDDEN #2: View<\\_> in Intermediates\n\n```rust\n// ❌ WRONG - Type leaks across match arms\nlet content: View<_> = match x {\n    A => view! { <div/> },\n    B => view! { <span/> },\n};\n```\n\n**Why it breaks:**\n\n- `View<Div>` ≠ `View<Span>`\n- Compiler cannot unify\n- Error: mismatched types\n\n---\n\n### Forbidden #3: Vec View\n\n```rust\n// ❌ WRONG - Incompatible collection type\nlet mut result: Vec<View> = Vec::new();\nresult.push(view! { <div/> }.into_view());\n```\n\n**Why it breaks:**\n\n- `View<View<HtmlElement<Div>>>` ≠ `View`\n- Double wrapping\n- Expected `View`, found `View<View<...>>`\n\n---\n\n### ❌ FORBIDDEN #4: View<\\_> in Closures\n\n```rust\n// ❌ WRONG - Closure return type leaks\nlet renderer = move || -> View<_> {\n    view! { <div/> }\n};\n```\n\n**Why it breaks:**\n\n- Closure inference conflicts with component inference\n- Type parameter escapes local scope\n\n---\n\n## Correct Patterns\n\n### ✅ CORRECT #1: Component Returns View<\\_>\n\n```rust\n// ✅ ALLOWED - Component is inference boundary\n#[component]\npub fn MyComponent() -> impl IntoView {\n    view! { <div>\"Hello\"</div> }\n}\n```\n\n**Why it works:**\n\n- `#[component]` macro closes inference\n- `impl IntoView` is opaque boundary\n- No propagation beyond component\n\n---\n\n### Correct #2: Helper Returns impl IntoView\n\n```rust\n// ✅ CORRECT - Closed boundary\nfn render_step_buttons(\n    status: RwSignal<StepStatus>,\n    on_change: Callback<StepStatus>,\n) -> impl IntoView {\n    move || match status.get() {\n        Status::A => view! { <Button>A</Button> }.into_any(),\n        Status::B => view! { <Button>B</Button> }.into_any(),\n    }\n}\n```\n\n**Why it works:**\n\n- `impl IntoView` is opaque\n- `move ||` captures without generics\n- `.into_any()` unifies types\n- No TypedBuilder propagation\n\n---\n\n### Correct #3: Match with AnyView\n\n```rust\n// ✅ CORRECT - Unified type\nmove || match x {\n    A => view! { <div/> }.into_any(),\n    B => view! { <span/> }.into_any(),\n}\n```\n\n**Why it works:**\n\n- `AnyView` is unified type\n- All arms return same concrete type\n- No inference needed\n\n---\n\n### Correct #4: Vec AnyView\n\n```rust\n// ✅ CORRECT - Homogeneous collection\nlet mut result: Vec<AnyView> = Vec::new();\nresult.push(view! { <div inner_html={h}></div> }.into_any());\n```\n\n**Why it works:**\n\n- `AnyView` is concrete type\n- `.into_any()` converts explicitly\n- No double wrapping\n\n---\n\n### Correct #5: Closure Returns AnyView\n\n```rust\n// ✅ CORRECT - Closed return type\nlet renderer = move || -> AnyView {\n    view! { <div/> }.into_any()\n};\n```\n\n**Why it works:**\n\n- `AnyView` is explicit\n- No inference leakage\n\n---\n\n## Decision Matrix\n\n| Context                 | Allowed Return Type     | Pattern                              |\n| ----------------------- | ----------------------- | ------------------------------------ |\n| `#[component]` function | `impl IntoView`         | Direct `view! {}`                    |\n| Helper function         | `impl IntoView`         | `move \\|\\| match { ... }.into_any()` |\n| Match arm               | `AnyView`               | `.into_any()` each arm               |\n| Vec collection          | `Vec<AnyView>`          | `.into_any()` before push            |\n| Closure                 | `AnyView` or `impl ...` | Explicit type annotation             |\n| Intermediate variable   | ❌ NEVER `View<_>`      | Use closure or `AnyView`             |\n\n---\n\n## Real Production Error (Your Case)\n\n### Error\n\n```\nerror[E0391]: cycle detected when computing the variances for items in this crate\nnote: ...which requires computing function signature of `render_step_buttons`\nnote: ...which requires computing the variances of `ButtonPropsBuilder`\nnote: ...which again requires computing the variances for items in this crate\n```\n\n### Broken Code\n\n```rust\n// workflow/demo.rs:69\nfn render_step_buttons<F>(status: RwSignal<StepStatus>, on_change: F) -> View<_>\nwhere\n    F: Fn(StepStatus) + 'static + Copy,\n{\n    let content: View<_> = match status.get() {\n        // ...\n    };\n    content\n}\n```\n\n### Fix\n\n```rust\nfn render_step_buttons(\n    status: RwSignal<StepStatus>,\n    on_change: Callback<StepStatus>,\n) -> impl IntoView {\n    move || match status.get() {\n        StepStatus::Completed => view! {\n            <Button on_click=move |_| { on_change.run(StepStatus::Pending); }>\n                \"Reset\"\n            </Button>\n        }.into_any(),\n        StepStatus::Pending => view! {\n            <Button on_click=move |_| { on_change.run(StepStatus::Active); }>\n                \"Start\"\n            </Button>\n        }.into_any(),\n        // ... other arms with .into_any()\n    }\n}\n```\n\n### Why Fix Works\n\n1. ❌ Removed generic `F` → ✅ concrete `Callback<StepStatus>`\n2. ❌ Removed `View<_>` return → ✅ `impl IntoView`\n3. ❌ Removed intermediate `let content: View<_>` → ✅ `move ||`\n4. ❌ Removed implicit inference → ✅ explicit `.into_any()`\n\nResult: **197 errors → 0 errors**\n\n---\n\n## Validation Checklist\n\n### Static Analysis\n\n```bash\n# Find violations\nrg \"-> View<_>\" src/ --type rust | grep -v \"#\\[component\\]\"\nrg \"let.*: View<_>\" src/ --type rust\nrg \"Vec<View>\" src/ --type rust\n```\n\n### CI Check (Recommended)\n\n```bash\n#!/bin/bash\n# .github/workflows/canon-rule-116.sh\n\nviolations=$(rg \"-> View<_>\" src/ --type rust | grep -v \"#\\[component\\]\" | wc -l)\n\nif [ $violations -gt 0 ]; then\n    echo \"❌ Canon Rule #116 violated: View<_> used outside component\"\n    exit 1\nfi\n```\n\n---\n\n## Migration Strategy\n\n### Phase 1: Audit\n\n```bash\n# Find all View<_> usage\nrg \"View<_>\" src/ --type rust -l > view_violations.txt\n```\n\n### Phase 2: Isolate\n\n```rust\n// Temporary feature flag\n#[cfg(not(feature = \"strict_view_boundary\"))]\nmod legacy {\n    // Old code with View<_>\n}\n```\n\n### Phase 3: Refactor\n\nPriority order:\n\n1. **Helpers** → `impl IntoView`\n2. **Match/If** → `AnyView`\n3. **Collections** → `Vec<AnyView>`\n4. **Closures** → explicit `AnyView`\n\n### Phase 4: Enforce\n\n```toml\n# Cargo.toml\n[features]\ndefault = [\"strict_view_boundary\"]\nstrict_view_boundary = []\n```\n\n---\n\n## Related Rules\n\nThis rule **closes the gap** between:\n\n- **#54 Render Must Be Total** - prevents runtime panics → this prevents compile-time explosions\n- **#63 Leptos Reactivity Closures** - defines when to access signals → this defines what to return\n- **#86 Children/ChildrenFn Contract** - defines ownership boundaries → this defines type boundaries\n- **#13 Specialization vs Substitution** - prevents over-generics → this prevents type leakage\n- **#43/44 Domain Components/Orchestrators** - separates concerns → this enforces separation structurally\n\n---\n\n## Quick Reference Card\n\n```rust\n// ✅ ALWAYS SAFE\n#[component]\npub fn Foo() -> impl IntoView { view! {} }\n\nfn helper() -> impl IntoView {\n    move || view! {}.into_any()\n}\n\nlet items: Vec<AnyView> = vec![\n    view! {}.into_any(),\n];\n\n// ❌ NEVER ALLOWED\nfn helper() -> View<_> { ... }           // ❌ Open inference\nlet x: View<_> = match { ... };          // ❌ Type leakage\nlet items: Vec<View> = vec![];           // ❌ Incompatible collection\nlet f = || -> View<_> { ... };           // ❌ Closure escape\n```\n\n---\n\n**Enforcement Level:** CRITICAL\n**Last Updated:** 2025-01-21\n**Status:** Active immediately\n\nThis rule is **non-negotiable** for production stability."},{"number":119,"slug":"no-optional-into-ui-layer","title":"No #[prop(optional, into)] in UI Layer","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["props","ui","leptos","types"],"language":"EN","version":"1.1.0","date":"2025-01-22","intro":"Using #[prop(optional, into)] in UI components causes type ambiguity and widespread compiler errors. It breaks ergonomics and increases complexity at call sites.","problem":"optional into props create type ambiguity and compiler errors","solution":"use explicit option types in ui components and avoid implicit conversions","signals":["e0308 error","type mismatch","ambiguous props"],"search_intent":"how to fix leptos prop optional","keywords":["leptos prop optional into error","ui component option types","leptos type inference props","rust option prop pattern"],"body":"## Principle\n\n**UI layer components MUST NOT use `#[prop(optional, into)]` — use explicit `Option<T>` types instead.**\n\n---\n\n## The Problem\n\nThe combination `#[prop(optional, into)]` creates catastrophic type ambiguity in Leptos:\n\n1. **Compiler cannot infer** whether you're passing `T` or `Option<T>`\n2. **Multiplies E0308 errors** across every call site\n3. **Forces users to add `.into()` or `Some()` everywhere**\n4. **Breaks intuitive API design** — users expect `Option<T>` to mean \"optional\"\n\n**Real symptoms from production:**\n```rust\nerror[E0308]: mismatched types\n  --> src/ui/component.rs:42:31\n   |\n42 |         <Component class=\"foo\" />\n   |                           ^^^^^ expected `Option<String>`, found `&str`\n```\n\nThis error appears **hundreds of times** when `#[prop(optional, into)]` is used in UI components.\n\n---\n\n## Forbidden Patterns\n\n### Forbidden: Using prop optional into in UI Layer\n```rust\n// UI Component (FORBIDDEN)\n#[component]\npub fn Button(\n    children: Children,\n    #[prop(optional, into)] class: Option<String>,  // ❌ NEVER DO THIS\n    #[prop(optional, into)] id: Option<String>,     // ❌ NEVER DO THIS\n) -> impl IntoView {\n    view! {\n        <ButtonPrimitive class={class.unwrap_or_default()}>\n            {children()}\n        </ButtonPrimitive>\n    }\n}\n\n// User code — FAILS with E0308\nview! {\n    <Button class=\"my-class\" />  // Error: expected Option<String>, found &str\n}\n```\n\n**Why this fails:**\n- Leptos cannot infer whether `\"my-class\"` should be `Some(\"my-class\".into())` or `\"my-class\".into()`\n- User must write `class=Some(\"my-class\".to_string())` everywhere\n- Completely breaks ergonomics\n\n---\n\n## Canonical Pattern\n\n### Canonical: Explicit Option in UI\n```rust\n// UI Component (CORRECT)\n#[component]\npub fn Button(\n    children: Children,\n    #[prop(optional)] class: Option<String>,  // ✅ Just Option<String>\n    #[prop(optional)] id: Option<String>,     // ✅ Just Option<String>\n) -> impl IntoView {\n    view! {\n        <ButtonPrimitive\n            class={class}\n            id={id}\n        >\n            {children()}\n        </ButtonPrimitive>\n    }\n}\n\n// User code — WORKS naturally\nview! {\n    <Button class=\"my-class\".to_string() />  // ✅ Clean, explicit\n    <Button class=Some(\"my-class\".to_string()) />  // ✅ Also works\n    <Button />  // ✅ None is implicit\n}\n```\n\n**Why this works:**\n- User explicitly converts `&str → String` when needed\n- No type ambiguity at call site\n- Component passes `Option` directly to Primitive\n- Clean separation of concerns\n\n---\n\n## Rationale\n\n### Architectural Reasons\n\n1. **Type inference boundary:**\n   - UI components are called by application code (uncontrolled context)\n   - Application code should not pay type inference tax\n   - Primitives handle pass-through without conversion (see Canon Rule #124)\n\n2. **Ergonomics vs Safety:**\n   - `#[prop(optional, into)]` trades call-site clarity for implementation convenience\n   - In UI layer, this trade-off is backwards\n   - Better: call site stays clear, Primitive handles pass-through\n\n3. **Error multiplication:**\n   - One `#[prop(optional, into)]` → 10-50 E0308 errors in application code\n   - Each call site must add `.into()` or `Some()`\n   - Violates locality principle\n\n### What This Rule Protects\n\n- **Call-site ergonomics** — users write natural code\n- **Type clarity** — no ambiguous conversions\n- **Compiler performance** — fewer inference attempts\n- **Maintenance** — errors stay localized to UI layer\n\n---\n\n## Enforcement\n\n### Static Analysis (Recommended)\n```rust\n// Validator pseudocode\nfor component in ui_layer_components {\n    for prop in component.props {\n        if prop.has_attribute(\"optional\") && prop.has_attribute(\"into\") {\n            emit_error!(\n                \"Canon Rule #119: UI components cannot use #[prop(optional, into)]\"\n            );\n        }\n    }\n}\n```\n\n### CI Check\n```bash\n# Grep for forbidden pattern in UI layer\nrg '#\\[prop\\(optional.*into\\)' packages-rust/rs-design/src/ui/ && exit 1\n```\n\n### Code Review Checklist\n\n- [ ] No `#[prop(optional, into)]` in `src/ui/` directory\n- [ ] All optional props use plain `Option<T>`\n- [ ] Components pass `Option` directly to Primitives\n\n---\n\n## Exceptions\n\n**No exceptions. This rule applies to ALL components in UI layer.**\n\n**Note:** Primitives also CANNOT use `#[prop(into)]` per Canon Rule #124. This creates a consistent, zero-conversion architecture across all layers.\n\n---\n\n## Related Rules\n\n- **Canon Rule #75:** Primitives have zero styling logic\n- **Canon Rule #89:** Primitives are SSR-safe\n- **Canon Rule #117:** Event handler boundaries\n- **Canon Rule #124:** Primitive Contract Types (Primitives CANNOT use `#[prop(into)]`)\n\n---\n\n## Version History\n\n- **1.1.0** — Removed Primitive exception, aligned with Canon Rule #124 (2025-01-22)\n- **1.0.0** — Initial version with Primitive exception (2025-01-22)"},{"number":120,"slug":"dom-events-vs-semantic-callbacks","title":"DOM Events vs Semantic Callbacks Boundary","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["events","callbacks","ui","primitives"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Mixing DOM events with semantic callbacks breaks abstraction boundaries and creates type confusion in Leptos components. UI APIs become coupled to browser details, leading to errors and inconsistent usage patterns.","problem":"dom events are exposed in ui layer and callbacks are incorrectly wrapped or typed","solution":"restrict dom events to primitives and expose only semantic callbacks in ui components","signals":["e0308 type mismatch","some callback wrapping","on:click in ui api"],"search_intent":"how to separate dom events from","keywords":["leptos dom vs semantic events","callback type mismatch rust leptos","ui event abstraction pattern","on_click vs on:click leptos"],"body":"## Principle\n\n**Primitives use `on:event` (DOM events). UI components use `on_event` (semantic callbacks). Never wrap callbacks in `Some()` at UI layer.**\n\n---\n\n## The Problem\n\nMixing DOM event syntax with callback props creates three structural failures:\n\n1. **Type confusion:** `on:click` vs `on_click` vs `onclick` — unclear boundaries\n2. **Unnecessary wrapping:** `Some(Callback::new(...))` patterns proliferate\n3. **Primitive leakage:** DOM events appear in UI layer public APIs\n\n**Real symptoms from production:**\n```rust\nerror[E0308]: mismatched types\n  --> src/ui/button.rs:15:21\n   |\n15 |         on_click=Some(Callback::new(move |_| handle_click()))\n   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n   |                 expected `Callback<MouseEvent>`, found `Option<Callback<_>>`\n```\n\nThis error appears when developers confuse primitive-level optional props with UI-level semantic handlers.\n\n---\n\n## Forbidden Patterns\n\n### ❌ Forbidden: DOM events in UI layer public API\n```rust\n// UI Component (WRONG)\n#[component]\npub fn Button(\n    children: Children,\n    on:click: Callback<MouseEvent>,  // ❌ DOM event in UI API\n) -> impl IntoView {\n    view! {\n        <ButtonPrimitive on:click=on:click>\n            {children()}\n        </ButtonPrimitive>\n    }\n}\n```\n\n**Why this fails:**\n- Exposes DOM implementation detail\n- Forces users to think about `MouseEvent`\n- Breaks abstraction boundary\n\n### ❌ Forbidden: Wrapping callbacks in Some()\n```rust\n// UI Component (WRONG)\n#[component]\npub fn TreeNode(\n    on_select: Callback<String>,\n) -> impl IntoView {\n    view! {\n        <div on:click=Some(Callback::new(move |_| {  // ❌ NEVER DO THIS\n            on_select.run(\"id\".to_string());\n        }))>\n            \"Node\"\n        </div>\n    }\n}\n```\n\n**Why this fails:**\n- `Some()` wrapper is redundant\n- Should pass callback directly or use `on:click` syntax\n- Creates type confusion\n\n### ❌ Forbidden: Optional callbacks as Option<Callback<T>>\n```rust\n// UI Component (WRONG)\n#[component]\npub fn CommandItem(\n    #[prop(optional)] on_click: Option<Callback<MouseEvent>>,  // ❌ WRONG TYPE\n) -> impl IntoView {\n    view! {\n        <button on:click=move |ev| {\n            if let Some(ref handler) = on_click {  // ❌ Awkward pattern\n                handler.run(ev);\n            }\n        }>\n            \"Item\"\n        </button>\n    }\n}\n```\n\n**Why this fails:**\n- Makes optional callbacks awkward to use\n- Should use `#[prop(optional)]` with non-Option type\n- Or provide no-op default\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical: Primitives use on:event\n```rust\n// Primitive (CORRECT)\n#[component]\npub fn ButtonPrimitive(\n    children: ChildrenFn,\n    #[prop(optional)] class: Option<String>,\n) -> impl IntoView {\n    view! {\n        <button \n            data-button=\"\"\n            class={class}\n            // DOM events available implicitly via Leptos\n        >\n            {children()}\n        </button>\n    }\n}\n```\n\n**Usage in UI layer:**\n```rust\nview! {\n    <ButtonPrimitive on:click=move |_| handle_click()>  // ✅ Direct DOM event\n        \"Click me\"\n    </ButtonPrimitive>\n}\n```\n\n### ✅ Canonical: UI components use semantic callbacks\n```rust\n// UI Component (CORRECT)\n#[component]\npub fn Button(\n    children: Children,\n    #[prop(optional)] on_click: Option<Callback<()>>,  // ✅ Semantic, unit type\n    #[prop(default = false)] disabled: bool,\n) -> impl IntoView {\n    let on_click_handler = on_click.unwrap_or_else(|| Callback::new(|_| {}));\n    \n    view! {\n        <ButtonPrimitive on:click=move |_| {\n            if !disabled {\n                on_click_handler.run(());  // ✅ Semantic unit event\n            }\n        }>\n            {children()}\n        </ButtonPrimitive>\n    }\n}\n```\n\n**Usage in application:**\n```rust\nview! {\n    <Button on_click=Callback::new(|_| save_form())>  // ✅ Clean semantic API\n        \"Save\"\n    </Button>\n}\n```\n\n### ✅ Canonical: Optional semantic callbacks with default\n```rust\n// UI Component (CORRECT)\n#[component]\npub fn TreeNode(\n    node_id: String,\n    #[prop(default = Callback::new(|_| {}))] on_select: Callback<String>,  // ✅ Default no-op\n) -> impl IntoView {\n    view! {\n        <div on:click=move |_| on_select.run(node_id.clone())>\n            \"Node\"\n        </div>\n    }\n}\n```\n\n**Why this works:**\n- No `Option<>` wrapping needed\n- No `if let Some` patterns\n- Clean call site: `<TreeNode on_select=my_handler />`\n\n---\n\n## Rationale\n\n### Architectural Clarity\n\n1. **Primitives = DOM boundary**\n   - Only primitives touch raw HTML elements\n   - Only primitives use `on:event` syntax\n   - Primitives are SSR-safe, stateless\n\n2. **UI = Semantic boundary**\n   - UI components expose business logic events\n   - Use `on_event` (underscore) for semantic callbacks\n   - Type: `Callback<T>` where `T` is domain-specific\n\n3. **Separation of concerns**\n   - DOM events (`MouseEvent`, `KeyboardEvent`) stay in primitives\n   - Semantic events (`String`, `()`, domain types) in UI layer\n   - Application code never imports `web_sys` types\n\n### What This Rule Protects\n\n- **API clarity:** Users know what layer they're in by syntax\n- **Type safety:** No `Option<Callback<T>>` awkwardness\n- **SSR compatibility:** DOM events never leak to server-side\n- **Maintenance:** Clear refactoring boundaries\n\n---\n\n## Enforcement\n\n### Pattern Matching (Linter)\n```rust\n// Check 1: No on:event in UI component props\nif in_ui_layer(component) {\n    for prop in component.props {\n        if prop.name.starts_with(\"on:\") {\n            emit_error!(\"Canon Rule #120: Use on_event, not on:event\");\n        }\n    }\n}\n\n// Check 2: No Some(Callback::new(...))\nif code.contains(\"Some(Callback::new(\") {\n    emit_warning!(\"Canon Rule #120: Pass callback directly, remove Some()\");\n}\n```\n\n### CI Check\n```bash\n# Grep for forbidden patterns\nrg 'on:.* = .*Callback' packages-rust/rs-design/src/ui/ && exit 1\nrg 'Some\\(Callback::new' packages-rust/rs-design/src/ui/ && exit 1\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a UI component needs DOM-level control, it's misclassified — it should be a primitive.\n\n---\n\n## Examples Summary\n\n| Layer | Event Type | Syntax | Example |\n|-------|-----------|---------|---------|\n| **Primitive** | DOM | `on:click` | `<button on:click=handler>` |\n| **UI** | Semantic | `on_click` | `<Button on_click=handler>` |\n| **Application** | Business | `on_save` | `<Form on_save=save_fn>` |\n\n---\n\n## Related Rules\n\n- **Canon Rule #75:** Primitives have zero styling\n- **Canon Rule #89:** Primitives are SSR-safe\n- **Canon Rule #119:** No `#[prop(optional, into)]` in UI layer\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":121,"slug":"storedvalue-for-noncopy-in-view","title":"StoredValue for Non-Copy Values in view! Closures","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["leptos","closures","ownership","ssr"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Leptos view closures require Fn semantics for reactivity, but moving non-Copy values forces FnOnce and breaks compilation. This commonly occurs when String, Vec, or structs are captured directly inside view! closures.","problem":"non-copy values in view closures cause FnOnce errors and break reactivity","solution":"wrap all non-copy values in StoredValue before view and access via get_value or with_value","signals":["E0525 error","expected Fn found FnOnce","closure moves value","re-render failure"],"search_intent":"how to fix FnOnce closure leptos","keywords":["leptos storedvalue usage","fnonce vs fn leptos","closure move error leptos","leptos non copy closure"],"body":"## Principle\n\n**Non-Copy values captured by closures inside `view!` MUST be wrapped in `StoredValue` — never moved directly.**\n\n---\n\n## The Problem\n\nLeptos `view!` macro generates closures that must be `Fn` (callable multiple times). When non-Copy values are moved into these closures, Rust enforces `FnOnce` semantics, causing compile failures.\n\n**The root cause chain:**\n\n1. `view!` needs `Fn` for reactivity (re-render on signal changes)\n2. Moving non-Copy values → closure becomes `FnOnce`\n3. `FnOnce` cannot be called multiple times → compile error\n4. `Clone` doesn't help — still consumes on first call\n\n**Real symptoms from production:**\n```rust\nerror[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`\n  --> src/ui/component.rs:15:5\n   |\n15 | /     view! {\n16 | |         <div>\n17 | |             {registry.search()}  // ← moves `registry`\n   | |              -------- closure is `FnOnce` because it moves the variable out\n18 | |         </div>\n19 | |     }\n   | |_____^ this closure implements `FnOnce`, not `Fn`\n```\n\n**Additional symptoms:**\n- `NonNull<()> cannot be shared between threads safely` (Fragment not Send+Sync)\n- Closures capturing `String`, `Vec`, custom structs fail\n- Works once, fails on re-render\n\n---\n\n## Forbidden Patterns\n\n### Forbidden: Moving String into view closure\n```rust\n#[component]\npub fn Component(title: String) -> impl IntoView {\n    view! {\n        <div>{title}</div>  // ❌ Moves `title` — FnOnce error\n    }\n}\n```\n\n**Compile error:**\n```\nerror[E0525]: expected closure implementing `Fn`, found `FnOnce`\n  because it moves the variable `title` out of its environment\n```\n\n### Forbidden: Moving registry context into closure\n```rust\n#[component]\npub fn CommandPalette(registry: CommandRegistry) -> impl IntoView {\n    view! {\n        {move || {\n            let results = registry.search(\"query\");  // ❌ Moves registry\n            results.into_iter().map(|r| view! { <div>{r}</div> }).collect_view()\n        }}\n    }\n}\n```\n\n**Why this fails:**\n- `registry` is not `Copy`\n- Closure moves `registry` on first call\n- Second render attempts to move again → already moved\n- `FnOnce` violation\n\n### Forbidden: Clone inside closure still FnOnce\n```rust\n#[component]\npub fn Component(data: Vec<String>) -> impl IntoView {\n    view! {\n        {move || {\n            data.clone()  // ❌ Still moves `data` into closure environment\n                .into_iter()\n                .map(|s| view! { <div>{s}</div> })\n                .collect_view()\n        }}\n    }\n}\n```\n\n**Why this fails:**\n- `.clone()` happens *inside* closure\n- Closure still captures `data` by move\n- `FnOnce` violation remains\n\n### Forbidden: Invoking Children inside view\n```rust\n#[component]\npub fn Wrapper(children: Children) -> impl IntoView {\n    view! {\n        <div>\n            {children()}  // ❌ Moves `children` (Fragment)\n        </div>\n    }\n}\n```\n\n**Why this fails:**\n- `children()` returns `Fragment`\n- `Fragment` is not `Send + Sync` (contains `NonNull<()>`)\n- Cannot be captured in reactive closure\n\n---\n\n## Canonical Pattern\n\n### Canonical: Wrap non Copy values in StoredValue\n```rust\n#[component]\npub fn Component(title: String) -> impl IntoView {\n    let title = StoredValue::new(title);  // ✅ Store before view!\n    \n    view! {\n        <div>{move || title.get_value()}</div>  // ✅ Access via closure\n    }\n}\n```\n\n**Why this works:**\n- `StoredValue` is `Copy`\n- Closure captures copy of `StoredValue`, not the `String`\n- `get_value()` retrieves value on each render\n- Closure remains `Fn`\n\n### Canonical: Registry in StoredValue\n```rust\n#[component]\npub fn CommandPalette(registry: CommandRegistry) -> impl IntoView {\n    let registry = StoredValue::new(registry);  // ✅ Store once\n    \n    view! {\n        {move || {\n            let results = registry.with_value(|r| r.search(\"query\"));\n            results.into_iter().map(|r| view! { <div>{r}</div> }).collect_view()\n        }}\n    }\n}\n```\n\n**Why this works:**\n- `StoredValue<CommandRegistry>` is `Copy`\n- `with_value()` provides safe access to inner value\n- Closure can be called multiple times\n\n### Canonical: Children with ChildrenFn StoredValue\n```rust\n#[component]\npub fn Wrapper(children: ChildrenFn) -> impl IntoView {\n    let children = StoredValue::new(children);  // ✅ Store ChildrenFn\n    \n    view! {\n        <div>\n            {move || children.with_value(|c| c())}  // ✅ Invoke via closure\n        </div>\n    }\n}\n```\n\n**Why this works:**\n- `ChildrenFn` is `Arc<dyn Fn() -> View>`, not `Copy`\n- `StoredValue` makes it capturable\n- Closure invokes function on each render\n\n### Canonical: Multiple non Copy values\n```rust\n#[component]\npub fn Complex(\n    title: String,\n    items: Vec<Item>,\n    config: AppConfig,\n) -> impl IntoView {\n    let title = StoredValue::new(title);\n    let items = StoredValue::new(items);\n    let config = StoredValue::new(config);\n    \n    view! {\n        <div>\n            <h1>{move || title.get_value()}</h1>\n            {move || items.with_value(|i| {\n                i.iter().map(|item| view! { <div>{item.name}</div> }).collect_view()\n            })}\n        </div>\n    }\n}\n```\n\n---\n\n## Rationale\n\n### Why This Rule Exists\n\n1. **Leptos reactivity model:**\n   - `view!` generates `Fn` closures for re-rendering\n   - Non-Copy values violate `Fn` contract\n   - `StoredValue` is the official escape hatch\n\n2. **SSR + Hydration safety:**\n   - `StoredValue` works in both SSR and client contexts\n   - Provides stable identity across renders\n   - Thread-safe (when inner value is `Send + Sync`)\n\n3. **Architectural boundary:**\n   - Signals → reactive primitives (Copy)\n   - StoredValue → non-reactive storage (Copy wrapper)\n   - Raw values → must be wrapped or cloned outside `view!`\n\n### What This Rule Protects\n\n- **Closure trait correctness** — `Fn` vs `FnOnce` invariants\n- **Re-render safety** — closures callable multiple times\n- **SSR compatibility** — no client-only assumptions\n- **Type system consistency** — explicit ownership semantics\n\n---\n\n## Enforcement\n\n### Static Analysis\n```rust\n// Linter pseudocode\nfor component in all_components {\n    for value in captured_values_in_view_macro(component) {\n        if !value.is_copy() && !value.is_signal() && !value.is_stored_value() {\n            emit_error!(\n                \"Canon Rule #121: Non-Copy value '{}' must be wrapped in StoredValue\",\n                value.name\n            );\n        }\n    }\n}\n```\n\n### CI Check\n```bash\n# Check for common FnOnce errors\ncargo check 2>&1 | grep \"expected.*Fn.*found.*FnOnce\" && \\\n    echo \"❌ Canon Rule #121: Use StoredValue for non-Copy values\" && exit 1\n```\n\n### Code Review Checklist\n\n- [ ] All non-Copy props wrapped in `StoredValue` before `view!`\n- [ ] No `String`, `Vec`, custom structs captured directly\n- [ ] `ChildrenFn` always in `StoredValue` if passed to child components\n- [ ] No `Fragment` captured in reactive closures\n\n---\n\n## Exceptions\n\n### Exception 1 Values cloned outside view\n```rust\n#[component]\npub fn Component(title: String) -> impl IntoView {\n    let title_clone = title.clone();  // ✅ Clone outside\n    \n    view! {\n        <div>{title_clone}</div>  // ✅ Moved once, not re-captured\n    }\n}\n```\n\n**When allowed:**\n- Clone happens before `view!`\n- Value used exactly once in template\n- No reactive closure needed\n\n### Exception 2 Copy types never need StoredValue\n```rust\n#[component]\npub fn Component(count: i32, enabled: bool) -> impl IntoView {\n    view! {\n        <div>\n            {move || count}  // ✅ i32 is Copy\n            {move || enabled}  // ✅ bool is Copy\n        </div>\n    }\n}\n```\n\n### Exception 3 Signals already reactive\n```rust\n#[component]\npub fn Component(count: Signal<i32>) -> impl IntoView {\n    view! {\n        <div>{move || count.get()}</div>  // ✅ Signal is Copy\n    }\n}\n```\n\n**All other cases require `StoredValue`.**\n\n---\n\n## Quick Reference\n\n| Type | Copy? | Needs StoredValue? |\n|------|-------|-------------------|\n| `i32`, `bool`, `f64` | ✅ | ❌ No |\n| `Signal<T>` | ✅ | ❌ No |\n| `String`, `Vec<T>` | ❌ | ✅ Yes |\n| `ChildrenFn` | ❌ | ✅ Yes |\n| `Fragment` | ❌ | ✅ Yes (or invoke before `view!`) |\n| Custom structs | ❌ | ✅ Yes (unless #[derive(Copy)]) |\n\n---\n\n## Related Rules\n\n- **Canon Rule #63:** Leptos Reactivity Closures (Fn vs FnOnce)\n- **Canon Rule #86:** Children vs ChildrenFn Contract\n- **Canon Rule #98:** Axum-Leptos SSR Closures Must Own State\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":122,"slug":"no-conditional-rendering-with-closures","title":"No Conditional Rendering with .then() Closures","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["leptos","rendering","ssr","closures"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Using bool.then closures for conditional rendering introduces FnOnce semantics, type inference failures, and hydration mismatches. This creates unstable DOM structures and breaks reactive rendering.","problem":"conditional rendering with then closures creates FnOnce errors and hydration issues","solution":"use Show component for all conditional rendering instead of then closures","signals":["FnOnce error","hydration mismatch","type inference failure","expected element found nothing"],"search_intent":"how to fix leptos conditional rendering","keywords":["leptos show component","leptos conditional rendering fix","then closure leptos error","leptos hydration mismatch"],"body":"## Principle\n\n**Conditional rendering MUST use `<Show>` component — never `condition.then(|| view!)`.**\n\n---\n\n## The Problem\n\nUsing `bool.then(|| view!)` for conditional rendering causes three structural failures:\n\n1. **FnOnce violations** — closure captures values by move\n2. **Type inference failures** — return type ambiguity\n3. **SSR hydration mismatches** — inconsistent DOM structure\n\n**Real symptoms from production:**\n```rust\nerror[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`\n  --> src/components/modal.rs:23:9\n   |\n23 |         {open.then(|| view! {\n   |          ^^^^ this closure implements `FnOnce`, not `Fn`\n   |               because it moves the variable `content` out\n```\n\n**Additional symptoms:**\n- Hydration warnings: \"expected element, found nothing\"\n- Closures that work once, fail on re-render\n- Type inference loops in reactive contexts\n\n---\n\n## Forbidden Patterns\n\n### Forbidden: Using then for conditional views\n```rust\n#[component]\npub fn Modal(open: Signal<bool>, content: String) -> impl IntoView {\n    view! {\n        {open.get().then(|| view! {  // ❌ FORBIDDEN\n            <div class=\"modal\">\n                {content}  // Moves `content` — FnOnce error\n            </div>\n        })}\n    }\n}\n```\n\n**Why this fails:**\n- `.then()` returns `Option<T>`\n- Closure captures `content` by move\n- Closure is `FnOnce`, not `Fn`\n- Cannot re-render when `open` changes\n\n### Forbidden: Nested then conditions\n```rust\n#[component]\npub fn Dashboard(user: User) -> impl IntoView {\n    view! {\n        {user.is_admin.then(|| view! {  // ❌ First .then()\n            {user.has_access.then(|| view! {  // ❌ Nested .then()\n                <AdminPanel />\n            })}\n        })}\n    }\n}\n```\n\n**Why this fails:**\n- Double FnOnce violation\n- Type inference explosion\n- Hydration structure unpredictable\n\n### Forbidden: then with complex children\n```rust\n#[component]\npub fn List(items: Vec<Item>, show_details: bool) -> impl IntoView {\n    view! {\n        <ul>\n            {items.into_iter().map(|item| {\n                view! {\n                    <li>\n                        {item.name}\n                        {show_details.then(|| view! {  // ❌ Inside iterator\n                            <div>{item.description}</div>\n                        })}\n                    </li>\n                }\n            }).collect_view()}\n        </ul>\n    }\n}\n```\n\n**Why this fails:**\n- Closure per list item — each can fail independently\n- `show_details` captured by move per item\n- Becomes unmaintainable quickly\n\n---\n\n## Canonical Pattern\n\n### Canonical: Use Show component\n```rust\n#[component]\npub fn Modal(open: Signal<bool>, content: String) -> impl IntoView {\n    view! {\n        <Show when=move || open.get()>  // ✅ CORRECT\n            <div class=\"modal\">\n                {content}\n            </div>\n        </Show>\n    }\n}\n```\n\n**Why this works:**\n- `<Show>` handles closure lifecycle correctly\n- Content evaluated lazily when condition true\n- No FnOnce issues\n- SSR-safe hydration\n\n### Canonical: Nested conditions with Show\n```rust\n#[component]\npub fn Dashboard(user: User) -> impl IntoView {\n    let is_admin = user.is_admin;\n    let has_access = user.has_access;\n    \n    view! {\n        <Show when=move || is_admin>  // ✅ First condition\n            <Show when=move || has_access>  // ✅ Nested condition\n                <AdminPanel />\n            </Show>\n        </Show>\n    }\n}\n```\n\n**Why this works:**\n- Each `<Show>` manages its own reactivity\n- Clear nesting structure\n- Predictable hydration\n\n### Canonical: Conditional with fallback\n```rust\n#[component]\npub fn Content(user: Option<User>) -> impl IntoView {\n    view! {\n        <Show\n            when=move || user.is_some()\n            fallback=|| view! { <div>\"Please log in\"</div> }\n        >\n            <UserProfile user=user.unwrap() />\n        </Show>\n    }\n}\n```\n\n**Why this works:**\n- `fallback` provides else-branch semantically\n- No manual `if/else` needed\n- SSR renders correct branch\n\n### Canonical: Multiple conditions\n```rust\n#[component]\npub fn StatusBadge(status: Signal<Status>) -> impl IntoView {\n    view! {\n        <Show when=move || matches!(status.get(), Status::Success)>\n            <span class=\"badge-success\">\"Success\"</span>\n        </Show>\n        <Show when=move || matches!(status.get(), Status::Error)>\n            <span class=\"badge-error\">\"Error\"</span>\n        </Show>\n        <Show when=move || matches!(status.get(), Status::Pending)>\n            <span class=\"badge-pending\">\"Pending\"</span>\n        </Show>\n    }\n}\n```\n\n**Alternative using single <Show> with match:**\n```rust\nview! {\n    {move || match status.get() {\n        Status::Success => view! { <span class=\"badge-success\">\"Success\"</span> }.into_any(),\n        Status::Error => view! { <span class=\"badge-error\">\"Error\"</span> }.into_any(),\n        Status::Pending => view! { <span class=\"badge-pending\">\"Pending\"</span> }.into_any(),\n    }}\n}\n```\n\n---\n\n## Rationale\n\n### Architectural Reasons\n\n1. **Closure trait requirements:**\n   - Leptos needs `Fn` for re-rendering\n   - `.then()` creates `FnOnce` closures naturally\n   - `<Show>` component designed for `Fn` semantics\n\n2. **SSR + Hydration safety:**\n   - `<Show>` generates consistent DOM markers\n   - Client knows where to hydrate conditional content\n   - `.then(|| view!)` has unpredictable structure\n\n3. **Type system clarity:**\n   - `<Show>` has explicit type signature\n   - `.then()` relies on inference (often fails)\n   - Compiler errors are clearer with `<Show>`\n\n### What This Rule Protects\n\n- **Closure correctness** — Fn vs FnOnce invariants\n- **Hydration stability** — predictable DOM structure\n- **Code maintainability** — explicit conditional semantics\n- **Type inference** — reduces compiler workload\n\n---\n\n## Enforcement\n\n### Static Analysis\n```rust\n// Linter pseudocode\nfor expr in view_macro_expressions {\n    if expr.is_method_call(\"then\") && expr.arg_is_view_macro() {\n        emit_error!(\n            \"Canon Rule #122: Use <Show> for conditional rendering, not .then()\"\n        );\n    }\n}\n```\n\n### CI Check\n```bash\n# Grep for forbidden pattern\nrg '\\.then\\(\\|\\|.*view!' packages-rust/rs-design/src/ && \\\n    echo \"❌ Canon Rule #122: Replace .then() with <Show>\" && exit 1\n```\n\n### Code Review Checklist\n\n- [ ] No `.then(|| view! {...})` patterns\n- [ ] All conditional rendering uses `<Show>`\n- [ ] Nested conditions use nested `<Show>` or match expressions\n- [ ] Fallback branches use `fallback=|| view! {...}`\n\n---\n\n## Exceptions\n\n### Exception 1 Non view conditionals\n```rust\n// Conditional string (NOT a view) — ALLOWED\nlet label = if count > 10 { \"Many\" } else { \"Few\" };\n\nview! {\n    <div>{label}</div>  // ✅ OK — not conditional rendering\n}\n```\n\n### Exception 2 Option map simple values\n```rust\n// Simple value mapping — ALLOWED\nview! {\n    <div>{user.map(|u| u.name)}</div>  // ✅ OK — maps to String, not view\n}\n```\n\n### Exception 3 Match expressions\n```rust\n// Match on enum — ALLOWED (but prefer <Show> when possible)\nview! {\n    {move || match status.get() {\n        Status::Loading => view! { <Spinner /> }.into_any(),\n        Status::Error(e) => view! { <Error msg=e /> }.into_any(),\n        Status::Success(data) => view! { <Content data=data /> }.into_any(),\n    }}\n}\n```\n\n**Prefer `<Show>` when possible, but match is acceptable for complex enums.**\n\n---\n\n## Migration Guide\n\n### Before (Forbidden)\n```rust\nview! {\n    {is_visible.then(|| view! {\n        <div>\"Content\"</div>\n    })}\n}\n```\n\n### After (Canonical)\n```rust\nview! {\n    <Show when=move || is_visible>\n        <div>\"Content\"</div>\n    </Show>\n}\n```\n\n### Before (Nested)\n```rust\nview! {\n    {user.is_some().then(|| view! {\n        {user.unwrap().is_admin.then(|| view! {\n            <AdminPanel />\n        })}\n    })}\n}\n```\n\n### After (Canonical)\n```rust\nview! {\n    <Show when=move || user.is_some()>\n        <Show when=move || user.unwrap().is_admin>\n            <AdminPanel />\n        </Show>\n    </Show>\n}\n```\n\n---\n\n## Quick Reference\n\n| Pattern | Allowed? | Use Instead |\n|---------|----------|-------------|\n| `condition.then(\\|\\| view!)` | ❌ | `<Show when=..>` |\n| `<Show when=..>` | ✅ | — |\n| `if x { y } else { z }` (values) | ✅ | — |\n| `match status { .. }` (views) | ⚠️ | Prefer `<Show>` |\n| `option.map(\\|x\\| x.field)` | ✅ | — |\n\n---\n\n## Related Rules\n\n- **Canon Rule #63:** Leptos Reactivity Closures (Fn vs FnOnce)\n- **Canon Rule #121:** StoredValue for Non-Copy in view!\n- **Canon Rule #90:** Hydration is DOM Replacement\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":123,"slug":"component-architecture-taxonomy","title":"Component Architecture Taxonomy and Contracts","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["architecture","layers","contracts","design-system"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Lack of strict component classification leads to boundary leakage, inconsistent responsibilities, and architectural decay. Without taxonomy, primitives, UI, and domain logic mix unpredictably.","problem":"components are not classified leading to boundary violations across layers","solution":"enforce strict taxonomy with five layers and explicit contracts per component type","signals":["primitive with state","ui with css tokens","component confusion"],"search_intent":"how to structure component architecture layers","keywords":["component architecture layers ui","design system taxonomy","primitive ui component separation","frontend architecture contracts"],"body":"## Principle\n\n**Every component MUST be classified into exactly one architectural type, and MUST respect the contracts of its layer.**\n\n---\n\n## The Problem\n\nWithout explicit architectural boundaries, codebases degrade into:\n\n1. **Classification chaos** — \"Is this a primitive or UI component?\"\n2. **Contract violations** — Primitives with business logic, UI with CSS tokens\n3. **Boundary leakage** — DOM events in application code, styling in primitives\n4. **Onboarding friction** — New developers don't know where to put code\n5. **Review ambiguity** — PRs lack objective validation criteria\n\n**Real symptoms from production:**\n\n- Primitives calling `use_context()` for state\n- UI components defining CSS variables\n- Application code using `on:click` instead of semantic callbacks\n- Blocks managing global z-index\n- Layouts with business logic\n\n**Without this rule:**\n- \"Where does this go?\" becomes a recurring question\n- Architecture erodes through small violations\n- Technical debt compounds invisibly\n- Code reviews become subjective debates\n\n---\n\n## Architectural Layers\n\nCanonRS defines **5 architectural layers**, each with distinct types and contracts:\n```\n┌─────────────────────────────────────────┐\n│          5. Layout Layer                │  ← Shell, Zones\n├─────────────────────────────────────────┤\n│          4. Block Layer                 │  ← Semantic, Interactive\n├─────────────────────────────────────────┤\n│          3. Component Layer (Domain)    │  ← Stateful, Orchestrator\n├─────────────────────────────────────────┤\n│          2. UI Layer                    │  ← Adapter, Controlled, Composite\n├─────────────────────────────────────────┤\n│          1. Primitive Layer             │  ← Pure, Interactive, Container\n└─────────────────────────────────────────┘\n```\n\nEach layer has:\n- **Allowed responsibilities** (CAN)\n- **Forbidden responsibilities** (CANNOT)\n- **Input/output contracts**\n- **Concrete types**\n\n---\n\n## Layer 1: Primitive Contracts\n\n**Purpose:** Render semantic HTML with zero logic, zero styling, zero state.\n\n### Critical Children Contract\n\n**ALL Primitives MUST use `ChildrenFn` if children are rendered inside `view!`.**\n\nThis is non-negotiable and prevents 90% of `FnOnce` errors.\n```rust\n// ✅ CORRECT\n#[component]\npub fn Primitive(children: ChildrenFn) -> impl IntoView {\n    view! {\n        <div>{children()}</div>\n    }\n}\n\n// ❌ WRONG\n#[component]\npub fn Primitive(children: Children) -> impl IntoView {\n    view! {\n        <div>{children()}</div>  // FnOnce error\n    }\n}\n```\n\n### Types\n\n#### 1.1 PurePrimitive\n**Definition:** Stateless HTML element with data attributes only.\n\n**Examples:** `ButtonPrimitive`, `InputPrimitive`, `LabelPrimitive`\n\n**CAN:**\n- Render semantic HTML (`<button>`, `<input>`, `<label>`)\n- Accept `ChildrenFn` (MANDATORY if rendering children)\n- Accept text content props (String)\n- Expose data attributes (`data-button`, `data-state`)\n- Accept `Option<String>` for optional props (NO `into`)\n- Pass through DOM events implicitly (Leptos handles this)\n\n**CANNOT:**\n- Have internal state (`RwSignal`, `StoredValue`)\n- Call `use_context()` or access global state\n- Define CSS (no classes, no inline styles, no tokens)\n- Convert types with `#[prop(into)]` — FORBIDDEN\n- Call `.unwrap_or_default()` — conversion is UI layer responsibility\n- Emit semantic callbacks (`on_click`, `on_select`)\n- Use `Children` type (MUST use `ChildrenFn`)\n```rust\n// ✅ CORRECT: PurePrimitive\n#[component]\npub fn ButtonPrimitive(\n    children: ChildrenFn,  // ✅ ChildrenFn, not Children\n    #[prop(optional)] class: Option<String>,  // ✅ NO into\n    #[prop(optional)] disabled: Option<bool>,  // ✅ NO into\n) -> impl IntoView {\n    view! {\n        <button \n            data-button=\"\" \n            class={class}  // ✅ Pass Option directly, no unwrap\n            disabled={disabled}\n        >\n            {children()}\n        </button>\n    }\n}\n```\n```rust\n// ❌ WRONG: Type conversion in Primitive\n#[component]\npub fn ButtonPrimitive(\n    children: ChildrenFn,\n    #[prop(optional, into)] class: Option<String>,  // ❌ NEVER use `into`\n) -> impl IntoView {\n    view! {\n        <button class={class.unwrap_or_default()}>  // ❌ NEVER unwrap\n            {children()}\n        </button>\n    }\n}\n```\n\n#### 1.2 InteractivePrimitive\n**Definition:** HTML form element that emits DOM events.\n\n**Examples:** `CheckboxPrimitive`, `SelectPrimitive`, `RadioPrimitive`\n\n**CAN:**\n- Everything from PurePrimitive\n- Accept `checked`, `value`, `disabled` props as `Option<T>` (NO `into`)\n- Emit DOM events: `on:change`, `on:input`, `on:focus`\n\n**CANNOT:**\n- Transform event data (e.g., `event.target.checked → bool`)\n- Manage validation state\n- Have semantic callbacks\n- Use `#[prop(into)]` for any prop\n```rust\n// ✅ CORRECT: InteractivePrimitive\n#[component]\npub fn CheckboxPrimitive(\n    #[prop(optional)] checked: Option<bool>,  // ✅ NO into\n    #[prop(optional)] class: Option<String>,  // ✅ NO into\n) -> impl IntoView {\n    view! {\n        <input \n            type=\"checkbox\" \n            checked={checked}\n            data-checkbox=\"\"\n            class={class}\n        />\n    }\n}\n```\n\n#### 1.3 ContainerPrimitive\n**Definition:** Structural wrapper with no visual semantics.\n\n**Examples:** `DialogPrimitive`, `PopoverPrimitive`, `CardPrimitive`\n\n**CAN:**\n- Everything from PurePrimitive\n- Accept multiple `ChildrenFn` slots (header, body, footer)\n- Provide structural data attributes (`data-dialog-content`, `data-popover-trigger`)\n\n**CANNOT:**\n- Manage open/close state\n- Handle keyboard navigation logic\n- Position itself (no `position: fixed` logic)\n- Use `#[prop(into)]`\n```rust\n// ✅ CORRECT: ContainerPrimitive\n#[component]\npub fn DialogPrimitive(\n    children: ChildrenFn,\n    #[prop(optional)] open: Option<bool>,  // ✅ NO into\n) -> impl IntoView {\n    view! {\n        <div data-dialog=\"\" data-state={if open.unwrap_or(false) { \"open\" } else { \"closed\" }}>\n            {children()}\n        </div>\n    }\n}\n```\n\n---\n\n## Layer 2: UI Component Contracts\n\n**Purpose:** Provide ergonomic APIs, normalize props, bridge Primitive → Application.\n\n### Critical Children Contract\n\n**UI Components rendering children inside `view!` MUST use `ChildrenFn` wrapped in `StoredValue`.**\n```rust\n// ✅ CORRECT\n#[component]\npub fn UIComponent(children: ChildrenFn) -> impl IntoView {\n    let children = StoredValue::new(children);\n    view! {\n        <div>{move || children.with_value(|c| c())}</div>\n    }\n}\n```\n\nSee **Canon Rule #121** for complete `StoredValue` guidance.\n\n### Types\n\n#### 2.1 AdapterUI\n**Definition:** Wraps a Primitive, adds ergonomics, normalizes types.\n\n**Examples:** `Button`, `Input`, `Label`\n\n**CAN:**\n- Call exactly one Primitive\n- Accept `Option<T>` WITHOUT `#[prop(into)]` (Rule #119)\n- Call `.unwrap_or_default()` on props before passing to Primitive\n- Add variant/size props (`ButtonVariant`, `ButtonSize`)\n- Set data attributes based on props (`data-variant=\"outline\"`)\n- Provide default values\n- Use `Children` or `ChildrenFn` (invoke before `view!` if using `Children`)\n\n**CANNOT:**\n- Define CSS tokens or variables\n- Call multiple Primitives (that's CompositeUI)\n- Have business logic state\n- Call `use_context()` for domain state\n- Use `#[prop(optional, into)]` (forbidden by Rule #119)\n```rust\n// ✅ CORRECT: AdapterUI\n#[component]\npub fn Button(\n    children: Children,\n    #[prop(default = ButtonVariant::Solid)] variant: ButtonVariant,\n    #[prop(optional)] class: Option<String>,  // ✅ NO into\n) -> impl IntoView {\n    view! {\n        <ButtonPrimitive \n            class=Some(format!(\"{} {}\", variant.as_class(), class.unwrap_or_default()))\n        >\n            {children}  // ✅ Children invoked implicitly by Leptos\n        </ButtonPrimitive>\n    }\n}\n```\n\n#### 2.2 ControlledUI\n**Definition:** Manages visual state (loading, disabled, expanded).\n\n**Examples:** `Accordion`, `Tabs`, `Select`\n\n**CAN:**\n- Have `RwSignal` for **visual state only** (not domain state)\n- Emit semantic callbacks: `on_change`, `on_select`, `on_toggle`\n- Type: `Callback<T>` where `T` is domain-specific (not `MouseEvent`)\n- Manage keyboard navigation\n- Sync state with props (controlled/uncontrolled pattern)\n- Accept `Option<T>` WITHOUT `#[prop(into)]`\n\n**CANNOT:**\n- Fetch data or call APIs\n- Access global application state\n- Define layout (flex, grid, positioning)\n- Use CSS classes for state (use data attributes)\n- Use `#[prop(optional, into)]`\n```rust\n// ✅ CORRECT: ControlledUI\n#[component]\npub fn Tabs(\n    children: ChildrenFn,\n    #[prop(default = 0)] default_tab: usize,\n    on_change: Callback<usize>,\n) -> impl IntoView {\n    let (active_tab, set_active_tab) = signal(default_tab);  // ✅ Visual state\n    \n    Effect::new(move |_| {\n        on_change.run(active_tab.get());  // ✅ Semantic callback\n    });\n    \n    view! {\n        <div data-tabs=\"\" data-active={active_tab.get()}>\n            {children()}\n        </div>\n    }\n}\n```\n\n#### 2.3 CompositeUI\n**Definition:** Composes multiple Primitives/UIs into a single component.\n\n**Examples:** `Card`, `Dialog`, `Popover`, `Form`\n\n**CAN:**\n- Compose 2+ Primitives or UI components\n- Provide slot-based APIs (header, body, footer)\n- Coordinate child component state\n- Accept `ChildrenFn` for flexible composition\n- Accept `Option<T>` WITHOUT `#[prop(into)]`\n\n**CANNOT:**\n- Have business logic (validation, API calls)\n- Know about domain models (`User`, `Order`, `Product`)\n- Define page-level layout\n- Use `#[prop(optional, into)]`\n```rust\n// ✅ CORRECT: CompositeUI\n#[component]\npub fn Card(\n    header: Option<ChildrenFn>,\n    children: ChildrenFn,\n    footer: Option<ChildrenFn>,\n) -> impl IntoView {\n    view! {\n        <CardPrimitive>\n            {header.map(|h| view! { <div data-card-header=\"\">{h()}</div> })}\n            <div data-card-body=\"\">{children()}</div>\n            {footer.map(|f| view! { <div data-card-footer=\"\">{f()}</div> })}\n        </CardPrimitive>\n    }\n}\n```\n\n---\n\n## Layer 3: Component Domain Contracts\n\n**Purpose:** Implement business logic, manage domain state, orchestrate workflows.\n\n### Types\n\n#### 3.1 StatefulComponent\n**Definition:** Has business logic and domain state.\n\n**Examples:** `LoginForm`, `ShoppingCart`, `UserProfile`\n\n**CAN:**\n- Use `RwSignal` for domain state\n- Call APIs, fetch data\n- Use `use_context()` for global state\n- Validate data\n- Emit domain events: `on_login`, `on_checkout`, `on_save`\n- Use UI components from Layer 2\n- Accept `Option<T>` props naturally\n\n**CANNOT:**\n- Use Primitives directly (must go through UI layer)\n- Define CSS or data attributes\n- Know about routing (use callbacks instead)\n```rust\n// ✅ CORRECT: StatefulComponent\n#[component]\npub fn LoginForm(on_login: Callback<User>) -> impl IntoView {\n    let (email, set_email) = signal(String::new());\n    let (password, set_password) = signal(String::new());\n    let (loading, set_loading) = signal(false);\n    \n    let handle_submit = move |_| {\n        set_loading.set(true);\n        // API call, validation, etc.\n        on_login.run(user);  // ✅ Domain callback\n    };\n    \n    view! {\n        <form on:submit=handle_submit>\n            <Input value=email on_input=move |v| set_email.set(v) />\n            <Button on_click=handle_submit loading=loading.get()>\n                \"Login\"\n            </Button>\n        </form>\n    }\n}\n```\n\n#### 3.2 OrchestratorComponent\n**Definition:** Coordinates multiple StatefulComponents or complex workflows.\n\n**Examples:** `CheckoutWizard`, `MultiStepForm`, `Dashboard`\n\n**CAN:**\n- Manage workflow state (step 1, 2, 3)\n- Coordinate child components\n- Handle cross-component communication\n- Use `provide_context()` for child components\n\n**CANNOT:**\n- Render Primitives directly\n- Define global layout (that's Layer 5)\n```rust\n// ✅ CORRECT: OrchestratorComponent\n#[component]\npub fn CheckoutWizard() -> impl IntoView {\n    let (step, set_step) = signal(1);\n    let cart = use_context::<CartContext>().unwrap();\n    \n    view! {\n        <div data-wizard=\"\">\n            <Show when=move || step.get() == 1>\n                <CartReview on_next=move |_| set_step.set(2) />\n            </Show>\n            <Show when=move || step.get() == 2>\n                <ShippingForm on_next=move |_| set_step.set(3) />\n            </Show>\n            <Show when=move || step.get() == 3>\n                <PaymentForm on_submit=move |_| complete_order() />\n            </Show>\n        </div>\n    }\n}\n```\n\n---\n\n## Layer 4: Block Contracts\n\n**Purpose:** Define semantic page sections, compose Components into features.\n\n### Types\n\n#### 4.1 SemanticBlock\n**Definition:** Structural page section with semantic meaning.\n\n**Examples:** `HeroBlock`, `FooterBlock`, `SidebarBlock`, `FeatureGridBlock`\n\n**CAN:**\n- Use `<section>`, `<header>`, `<footer>`, `<aside>`, `<nav>`\n- Compose UI components and domain components\n- Define block-level structure (not page-level)\n- Accept content via props\n\n**CANNOT:**\n- Manage application routing\n- Have global state (cart, user session)\n- Define z-index or position (that's Layout layer)\n```rust\n// ✅ CORRECT: SemanticBlock\n#[component]\npub fn HeroBlock(\n    title: String,\n    subtitle: String,\n    cta: ChildrenFn,\n) -> impl IntoView {\n    view! {\n        <section data-hero=\"\">\n            <h1>{title}</h1>\n            <p>{subtitle}</p>\n            <div data-hero-cta=\"\">\n                {cta()}\n            </div>\n        </section>\n    }\n}\n```\n\n#### 4.2 InteractiveBlock\n**Definition:** Feature-complete interactive section.\n\n**Examples:** `CommentSectionBlock`, `SearchBlock`, `FilterBarBlock`\n\n**CAN:**\n- Combine multiple domain components\n- Manage block-level interactions\n- Emit block-level events: `on_filter_change`, `on_comment_submit`\n\n**CANNOT:**\n- Know about page structure (header position, sidebar width)\n- Define page transitions\n```rust\n// ✅ CORRECT: InteractiveBlock\n#[component]\npub fn CommentSectionBlock(\n    post_id: String,\n    on_comment: Callback<Comment>,\n) -> impl IntoView {\n    let (comments, set_comments) = signal(vec![]);\n    \n    view! {\n        <section data-comments=\"\">\n            <h3>\"Comments\"</h3>\n            <CommentList comments=comments />\n            <CommentForm on_submit=move |c| {\n                set_comments.update(|cs| cs.push(c.clone()));\n                on_comment.run(c);\n            } />\n        </section>\n    }\n}\n```\n\n---\n\n## Layer 5: Layout Contracts\n\n**Purpose:** Define page structure, manage stacking context, control global positioning.\n\n### Types\n\n#### 5.1 ShellLayout\n**Definition:** Application-level layout (header, sidebar, main, footer).\n\n**Examples:** `AppShell`, `DashboardShell`, `MarketingShell`\n\n**CAN:**\n- Define top-level structure (`<header>`, `<aside>`, `<main>`, `<footer>`)\n- Manage global z-index layers\n- Control responsive breakpoints for layout\n- Provide context for child routes\n\n**CANNOT:**\n- Have business logic\n- Fetch data or call APIs\n- Render domain components directly (use `<Outlet />` or slots)\n```rust\n// ✅ CORRECT: ShellLayout\n#[component]\npub fn AppShell() -> impl IntoView {\n    view! {\n        <div data-shell=\"\">\n            <header data-shell-header=\"\">\n                <NavBar />\n            </header>\n            <aside data-shell-sidebar=\"\">\n                <Sidebar />\n            </aside>\n            <main data-shell-main=\"\">\n                <Outlet />  // ✅ Router content\n            </main>\n        </div>\n    }\n}\n```\n\n#### 5.2 ZoneLayout\n**Definition:** Page-level layout regions (grid, flexbox zones).\n\n**Examples:** `TwoColumnLayout`, `DashboardGridLayout`, `SplitLayout`\n\n**CAN:**\n- Define CSS Grid or Flexbox structure\n- Accept `ChildrenFn` for zones\n- Manage responsive layout changes\n\n**CANNOT:**\n- Know what components are in zones\n- Have state management\n```rust\n// ✅ CORRECT: ZoneLayout\n#[component]\npub fn TwoColumnLayout(\n    left: ChildrenFn,\n    right: ChildrenFn,\n) -> impl IntoView {\n    view! {\n        <div data-layout=\"two-column\">\n            <div data-zone=\"left\">{left()}</div>\n            <div data-zone=\"right\">{right()}</div>\n        </div>\n    }\n}\n```\n\n---\n\n## Forbidden Cross-Layer Violations\n\n### ❌ Primitive calling UI Component\n```rust\n// WRONG\n#[component]\npub fn ButtonPrimitive() -> impl IntoView {\n    view! {\n        <button>\n            <Icon name=\"check\" />  // ❌ UI component in Primitive\n        </button>\n    }\n}\n```\n\n### ❌ Primitive with type conversion\n```rust\n// WRONG\n#[component]\npub fn ButtonPrimitive(\n    #[prop(optional, into)] class: Option<String>,  // ❌ NEVER use `into`\n) -> impl IntoView {\n    view! {\n        <button class={class.unwrap_or_default()}>  // ❌ NEVER unwrap\n            \"Button\"\n        </button>\n    }\n}\n```\n\n### ❌ UI Component with CSS tokens\n```rust\n// WRONG\n#[component]\npub fn Button() -> impl IntoView {\n    view! {\n        <button style=\"background: var(--color-primary)\">  // ❌ CSS in UI\n            \"Click\"\n        </button>\n    }\n}\n```\n\n### ❌ Component using Primitive directly\n```rust\n// WRONG\n#[component]\npub fn LoginForm() -> impl IntoView {\n    view! {\n        <form>\n            <ButtonPrimitive>  // ❌ Should use Button (UI layer)\n                \"Submit\"\n            </ButtonPrimitive>\n        </form>\n    }\n}\n```\n\n### ❌ Block managing routing\n```rust\n// WRONG\n#[component]\npub fn HeroBlock() -> impl IntoView {\n    let navigate = use_navigate();  // ❌ Routing in Block\n    \n    view! {\n        <section>\n            <button on:click=move |_| navigate(\"/signup\")>\"Sign Up\"</button>\n        </section>\n    }\n}\n```\n\n---\n\n## Rationale\n\n### Why This Rule Exists\n\n1. **Objective classification:**\n   - \"Where does this go?\" has a definitive answer\n   - Code review becomes rule validation, not opinion\n\n2. **Boundary enforcement:**\n   - Prevents responsibility leakage\n   - Maintains separation of concerns\n   - Enables independent evolution of layers\n\n3. **Onboarding velocity:**\n   - New developers learn taxonomy once\n   - Code location becomes predictable\n   - PR structure follows architecture\n\n4. **Long-term maintainability:**\n   - Violations are detectable\n   - Refactoring has clear targets\n   - Technical debt is visible\n\n### What This Rule Protects\n\n- **Architectural integrity** — layers stay separated\n- **Code discoverability** — predictable file structure\n- **Refactoring safety** — clear contracts to maintain\n- **Team scalability** — shared mental model\n- **Type safety** — proper prop handling prevents E0308 cascades\n- **Reactivity correctness** — proper Children types prevent FnOnce errors\n\n---\n\n## Enforcement\n\n### Static Analysis\n```rust\n// Validator pseudocode\nfor component in all_components {\n    let layer = classify_by_path(component);\n    let violations = check_contracts(component, layer);\n    \n    if !violations.is_empty() {\n        emit_error!(\n            \"Canon Rule #123: Component '{}' in {} layer violates contracts: {:?}\",\n            component.name,\n            layer,\n            violations\n        );\n    }\n}\n```\n\n### File Structure Convention\n```\npackages-rust/rs-design/src/\n├── primitives/          # Layer 1\n│   ├── button.rs        # PurePrimitive\n│   ├── checkbox.rs      # InteractivePrimitive\n│   └── dialog.rs        # ContainerPrimitive\n├── ui/                  # Layer 2\n│   ├── button.rs        # AdapterUI\n│   ├── tabs.rs          # ControlledUI\n│   └── card.rs          # CompositeUI\n├── components/          # Layer 3 (domain)\n│   ├── login_form.rs    # StatefulComponent\n│   └── checkout.rs      # OrchestratorComponent\n├── blocks/              # Layer 4\n│   ├── hero.rs          # SemanticBlock\n│   └── comments.rs      # InteractiveBlock\n└── layouts/             # Layer 5\n    ├── app_shell.rs     # ShellLayout\n    └── two_column.rs    # ZoneLayout\n```\n\n### CI Checks\n```bash\n# Check 1: Primitives have no state\nrg 'RwSignal|use_context' packages-rust/rs-design/src/primitives/ && exit 1\n\n# Check 2: Primitives never use #[prop(into)]\nrg '#\\[prop\\(.*into.*\\)' packages-rust/rs-design/src/primitives/ && exit 1\n\n# Check 3: UI layer never uses #[prop(optional, into)]\nrg '#\\[prop\\(optional.*into\\)' packages-rust/rs-design/src/ui/ && exit 1\n\n# Check 4: Primitives use ChildrenFn, not Children\nrg 'children: Children[^F]' packages-rust/rs-design/src/primitives/ && exit 1\n\n# Check 5: Components don't use Primitives directly\nrg 'ButtonPrimitive|InputPrimitive' products/*/src/components/ && exit 1\n\n# Check 6: Blocks don't have routing\nrg 'use_navigate|use_location' packages-rust/rs-design/src/blocks/ && exit 1\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nEvery component must fit into exactly one type. If a component seems to span multiple types, it's misclassified and should be split.\n\n---\n\n## Quick Reference Table\n\n| Layer | Types | Children Type | `#[prop(into)]` | CAN | CANNOT |\n|-------|-------|---------------|-----------------|-----|--------|\n| **1. Primitive** | Pure, Interactive, Container | `ChildrenFn` ONLY | ❌ NEVER | Render HTML, data attrs | State, CSS, unwrap, conversion |\n| **2. UI** | Adapter, Controlled, Composite | `Children` or `ChildrenFn` | ❌ NEVER | Normalize props, unwrap, semantic events | CSS tokens, domain logic, `#[prop(into)]` |\n| **3. Component** | Stateful, Orchestrator | Any | ✅ Allowed | Domain state, API calls | Use Primitives directly |\n| **4. Block** | Semantic, Interactive | Any | ✅ Allowed | Compose components, section structure | Global state, routing |\n| **5. Layout** | Shell, Zone | Any | ✅ Allowed | Define structure, z-index | Business logic, data fetching |\n\n---\n\n## Related Rules\n\n- **Canon Rule #75:** Primitives have zero styling\n- **Canon Rule #89:** Primitives are SSR-safe\n- **Canon Rule #119:** No `#[prop(optional, into)]` in UI layer\n- **Canon Rule #120:** DOM Events vs Semantic Callbacks\n- **Canon Rule #121:** StoredValue for Non-Copy in view!\n- **Canon Rule #122:** No Conditional Rendering with .then()\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)\n  - Added explicit `ChildrenFn` requirement for Primitives\n  - Prohibited `#[prop(into)]` in Primitives completely\n  - Clarified type conversion responsibilities per layer"},{"number":124,"slug":"primitive-contract-types","title":"Primitive Contract Types","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["primitives","contracts","ssr","types"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Primitives without strict contracts accumulate state, styling, and logic, breaking SSR safety and type clarity. Hybrid primitives introduce coupling and unpredictable behavior.","problem":"primitives contain logic state or type conversion violating architectural boundaries","solution":"enforce pure interactive or container primitive types with strict no state no conversion rules","signals":["primitive with state","prop into usage","unwrap in primitive"],"search_intent":"how to define primitive components correctly","keywords":["primitive component rules","ssr safe primitives leptos","no prop into pattern rust","design system primitive types"],"body":"## Principle\n\n**Primitives render semantic HTML with zero logic, zero state, zero styling. Every Primitive MUST be classified as Pure, Interactive, or Container.**\n\n---\n\n## The Problem\n\nWithout formal primitive types, developers create \"hybrid primitives\" that:\n\n1. Mix DOM rendering with state management\n2. Apply CSS directly instead of data attributes\n3. Convert types (`.into()`, `unwrap_or_default()`) instead of passing through\n4. Emit semantic callbacks instead of raw DOM events\n\n**Real symptoms:**\n- Primitives calling `use_context()`\n- Primitives with `RwSignal`\n- Primitives using `#[prop(into)]` causing type cascades\n- Primitives with business logic\n\n**Without this rule:**\n- Primitive layer becomes polluted with UI concerns\n- SSR safety breaks (state, browser APIs)\n- Hydration mismatches emerge\n- Testing becomes dependent on environment\n\n---\n\n## Primitive Types\n\n### Type 1: PurePrimitive\n\n**Definition:** Stateless HTML element with pass-through props only.\n\n**Use when:** Rendering semantic HTML without interaction or state.\n\n**Examples:** `ButtonPrimitive`, `LabelPrimitive`, `DivPrimitive`, `SpanPrimitive`\n\n#### CAN:\n- Render one semantic HTML element (`<button>`, `<label>`, `<div>`)\n- Accept `ChildrenFn` (MANDATORY if children rendered)\n- Accept `Option<String>` for optional props (class, id, aria-*)\n- Expose data attributes (`data-button=\"\"`, `data-state=\"loading\"`)\n- Pass props directly to HTML without transformation\n\n#### CANNOT:\n- Have internal state (`RwSignal`, `create_signal`, `StoredValue`)\n- Call `use_context()` or any context provider\n- Use `#[prop(into)]` on ANY prop\n- Call `.unwrap_or_default()` or `.unwrap()`\n- Define CSS (inline styles, classes, tokens)\n- Emit semantic callbacks (`on_click: Callback<()>`)\n- Use `Children` type (MUST use `ChildrenFn`)\n\n#### Canonical Example:\n```rust\n// ✅ CORRECT: PurePrimitive\n#[component]\npub fn ButtonPrimitive(\n    children: ChildrenFn,  // ✅ ChildrenFn mandatory\n    #[prop(optional)] class: Option<String>,  // ✅ NO into\n    #[prop(optional)] disabled: Option<bool>,  // ✅ Pass through\n    #[prop(optional)] r#type: Option<String>,\n) -> impl IntoView {\n    view! {\n        <button\n            data-button=\"\"\n            class={class}  // ✅ Pass Option directly\n            disabled={disabled}\n            type={r#type}\n        >\n            {children()}\n        </button>\n    }\n}\n```\n\n#### Forbidden Example:\n```rust\n// ❌ WRONG: Type conversion in Primitive\n#[component]\npub fn ButtonPrimitive(\n    children: ChildrenFn,\n    #[prop(optional, into)] class: Option<String>,  // ❌ NEVER use into\n) -> impl IntoView {\n    view! {\n        <button class={class.unwrap_or_default()}>  // ❌ NEVER unwrap\n            {children()}\n        </button>\n    }\n}\n```\n\n---\n\n### Type 2: InteractivePrimitive\n\n**Definition:** HTML form element that emits raw DOM events.\n\n**Use when:** User input is captured (forms, selections, toggles).\n\n**Examples:** `InputPrimitive`, `CheckboxPrimitive`, `SelectPrimitive`, `TextareaPrimitive`\n\n#### CAN:\n- Everything from PurePrimitive\n- Render form elements (`<input>`, `<select>`, `<textarea>`)\n- Accept `value`, `checked`, `selected` as `Option<T>`\n- Emit raw DOM events implicitly (`on:change`, `on:input`, `on:focus`)\n- Accept `name`, `placeholder`, `autocomplete` props\n\n#### CANNOT:\n- Transform event values (`event.target.value → String`)\n- Manage validation state\n- Emit semantic callbacks (`on_change: Callback<String>`)\n- Call `.unwrap()` or `.into()` on any prop\n- Have focus management logic\n\n#### Canonical Example:\n```rust\n// ✅ CORRECT: InteractivePrimitive\n#[component]\npub fn InputPrimitive(\n    #[prop(optional)] value: Option<String>,  // ✅ NO into\n    #[prop(optional)] placeholder: Option<String>,\n    #[prop(optional)] disabled: Option<bool>,\n    #[prop(optional)] r#type: Option<String>,\n) -> impl IntoView {\n    view! {\n        <input\n            data-input=\"\"\n            value={value}\n            placeholder={placeholder}\n            disabled={disabled}\n            type={r#type}\n        />\n    }\n}\n```\n\n#### Forbidden Example:\n```rust\n// ❌ WRONG: Semantic callback in Primitive\n#[component]\npub fn InputPrimitive(\n    on_change: Callback<String>,  // ❌ Semantic callback forbidden\n) -> impl IntoView {\n    view! {\n        <input on:input=move |ev| {\n            on_change.run(event_target_value(&ev));  // ❌ Transformation\n        } />\n    }\n}\n```\n\n---\n\n### Type 3: ContainerPrimitive\n\n**Definition:** Structural wrapper providing slots without visual semantics.\n\n**Use when:** Grouping content with semantic structure (dialogs, cards, popovers).\n\n**Examples:** `DialogPrimitive`, `PopoverPrimitive`, `CardPrimitive`, `AccordionItemPrimitive`\n\n#### CAN:\n- Everything from PurePrimitive\n- Accept multiple `ChildrenFn` slots (header, body, footer)\n- Render container elements (`<div>`, `<section>`, `<article>`)\n- Provide structural data attributes (`data-dialog-content`, `data-card-header`)\n- Accept `open`, `expanded`, `collapsed` as `Option<bool>`\n\n#### CANNOT:\n- Manage open/close state internally\n- Handle keyboard navigation (that's UI layer)\n- Position itself (`position: fixed` logic)\n- Trap focus or manage z-index\n- Emit semantic events (`on_open`, `on_close`)\n\n#### Canonical Example:\n```rust\n// ✅ CORRECT: ContainerPrimitive\n#[component]\npub fn DialogPrimitive(\n    children: ChildrenFn,\n    #[prop(optional)] open: Option<bool>,\n) -> impl IntoView {\n    view! {\n        <div\n            data-dialog=\"\"\n            data-state={open.and_then(|v| if v { Some(\"open\") } else { Some(\"closed\") })}\n            role=\"dialog\"\n        >\n            {children()}\n        </div>\n    }\n}\n```\n\n#### Forbidden Example:\n```rust\n// ❌ WRONG: State management in Primitive\n#[component]\npub fn DialogPrimitive(\n    children: ChildrenFn,\n) -> impl IntoView {\n    let (open, set_open) = signal(false);  // ❌ State in Primitive\n    \n    view! {\n        <div data-dialog=\"\" data-state={if open.get() { \"open\" } else { \"closed\" }}>\n            {children()}\n        </div>\n    }\n}\n```\n\n---\n\n## Universal Primitive Contracts\n\n**ALL Primitives MUST follow these contracts regardless of type:**\n\n### 1. Children Contract\n**If children are rendered inside `view!`, MUST use `ChildrenFn`.**\n```rust\n// ✅ CORRECT\nchildren: ChildrenFn\n\n// ❌ WRONG\nchildren: Children  // Causes FnOnce errors\n```\n\n### 2. No Type Conversion\n**NEVER use `#[prop(into)]` on any prop.**\n```rust\n// ✅ CORRECT\n#[prop(optional)] class: Option<String>\n\n// ❌ WRONG\n#[prop(optional, into)] class: Option<String>\n```\n\n### 3. No Unwrapping\n**Pass `Option<T>` directly to HTML — no `.unwrap()` or `.unwrap_or_default()`.**\n```rust\n// ✅ CORRECT\n<button class={class}>\n\n// ❌ WRONG\n<button class={class.unwrap_or_default()}>\n```\n\n### 4. No State\n**Zero internal state — no `RwSignal`, `StoredValue`, `use_context()`.**\n\n### 5. No Styling\n**Zero CSS — no inline styles, no CSS classes (only pass-through), no tokens.**\n\n### 6. Data Attributes Only\n**Expose state via data attributes, not CSS classes.**\n```rust\n// ✅ CORRECT\ndata-state={disabled.and_then(|v| if v { Some(\"disabled\") } else { Some(\"enabled\") })}\n\n// ❌ WRONG\nclass={disabled.and_then(|v| if v { Some(\"btn-disabled\") } else { Some(\"btn-enabled\") })}\n```\n\n---\n\n## Rationale\n\n### Why Primitives Exist\n\n1. **SSR Safety:** No browser APIs, no client-only state\n2. **Hydration Predictability:** No conditional logic affecting structure\n3. **Zero Coupling:** Can be used in any context without dependencies\n4. **Testing Simplicity:** Pure functions, no mocking needed\n5. **Type Clarity:** No `.into()` ambiguity\n\n### What This Rule Protects\n\n- **SSR/Hydration contracts** — primitives render identically server/client\n- **Type inference** — no `.into()` cascades\n- **Architectural boundaries** — primitives never know about UI/domain\n- **Reactivity correctness** — `ChildrenFn` prevents `FnOnce` errors\n\n---\n\n## Enforcement\n\n### Static Analysis\n```rust\n// Check 1: No #[prop(into)]\nfor prop in primitive.props {\n    if prop.has_attribute(\"into\") {\n        emit_error!(\"Canon Rule #124: Primitives cannot use #[prop(into)]\");\n    }\n}\n\n// Check 2: ChildrenFn required\nfor prop in primitive.props {\n    if prop.name == \"children\" && prop.type != \"ChildrenFn\" {\n        emit_error!(\"Canon Rule #124: Primitives must use ChildrenFn, not Children\");\n    }\n}\n\n// Check 3: No state\nif primitive.contains(\"RwSignal\") || primitive.contains(\"use_context\") {\n    emit_error!(\"Canon Rule #124: Primitives cannot have state\");\n}\n```\n\n### CI Checks\n```bash\n# No #[prop(into)] in primitives\nrg '#\\[prop\\(.*into.*\\)' packages-rust/rs-design/src/primitives/ && exit 1\n\n# No state in primitives\nrg 'RwSignal|create_signal|use_context' packages-rust/rs-design/src/primitives/ && exit 1\n\n# ChildrenFn only (not Children)\nrg 'children: Children[^F]' packages-rust/rs-design/src/primitives/ && exit 1\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a primitive needs state, type conversion, or callbacks, it's misclassified — move it to UI layer.\n\n---\n\n## Quick Reference\n\n| Aspect | Pure | Interactive | Container |\n|--------|------|-------------|-----------|\n| HTML Element | `<button>`, `<div>` | `<input>`, `<select>` | `<div>` with slots |\n| Children | `ChildrenFn` | N/A | `ChildrenFn` (multiple) |\n| DOM Events | Implicit | `on:change`, `on:input` | Implicit |\n| State Props | ❌ None | `value`, `checked` | `open`, `expanded` |\n| Semantic Callbacks | ❌ Never | ❌ Never | ❌ Never |\n\n---\n\n## Related Rules\n\n- **Canon Rule #75:** Primitives have zero styling\n- **Canon Rule #89:** Primitives are SSR-safe\n- **Canon Rule #119:** No `#[prop(optional, into)]` in UI layer\n- **Canon Rule #121:** StoredValue for Non-Copy in view!\n- **Canon Rule #125:** UI Component Contracts (next rule)\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":125,"slug":"ui-component-contracts","title":"UI Component Contracts","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["ui","contracts","callbacks","state"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"UI components without strict contracts leak primitives, mix domain logic, and create type ambiguity. This results in poor ergonomics and inconsistent APIs across the design system.","problem":"ui components mix responsibilities and use ambiguous prop patterns","solution":"enforce adapter controlled and composite ui types with strict callback and prop rules","signals":["prop optional into","mouseevent in api","primitive leakage"],"search_intent":"how to design ui component contracts","keywords":["ui component patterns adapter controlled","leptos callback design","ui abstraction layer design","component api ergonomics"],"body":"## Principle\n\n**UI components bridge Primitives and Application code by normalizing props, managing visual state, and emitting semantic callbacks. Every UI component MUST be classified as Adapter, Controlled, or Composite.**\n\n---\n\n## The Problem\n\nWithout formal UI layer types, developers create components that:\n\n1. Use Primitives directly in domain code (skipping UI layer)\n2. Mix visual state with domain state\n3. Define CSS tokens instead of using design system\n4. Emit DOM events (`MouseEvent`) instead of semantic callbacks\n5. Use `#[prop(optional, into)]` causing type ambiguity\n\n**Real symptoms:**\n- E0308 errors cascading across call sites\n- FnOnce errors from improper `Children` usage\n- Primitive props leaking into application code\n- CSS defined in component instead of tokens\n\n**Without this rule:**\n- UI layer becomes ad-hoc collection of wrappers\n- No clear boundary between UI and domain\n- Code reviews debate \"is this UI or Component?\"\n\n---\n\n## UI Component Types\n\n### Type 1: AdapterUI\n\n**Definition:** Wraps exactly ONE Primitive, adds ergonomics, normalizes types.\n\n**Use when:** Making Primitive API more convenient without adding logic.\n\n**Examples:** `Button`, `Input`, `Label`, `Checkbox`\n\n#### CAN:\n- Wrap exactly one Primitive component\n- Accept `Option<T>` props WITHOUT `#[prop(into)]` (Rule #119)\n- Call `.unwrap_or_default()` on props before passing to Primitive\n- Add variant/size enums (`ButtonVariant::Solid`, `InputSize::Md`)\n- Set data attributes based on variants (`data-variant=\"outline\"`)\n- Use `Children` (invoke before `view!`) or `ChildrenFn` (with `StoredValue`)\n- Provide sensible defaults for optional props\n\n#### CANNOT:\n- Call multiple Primitives (that's CompositeUI)\n- Have visual state (`RwSignal` for loading, expanded, etc.)\n- Emit DOM events to users (`on:click` in public API)\n- Define CSS tokens or inline styles\n- Call `use_context()` for domain state\n- Use `#[prop(optional, into)]` (forbidden by Rule #119)\n\n#### Canonical Example:\n```rust\n// ✅ CORRECT: AdapterUI\n#[component]\npub fn Button(\n    children: Children,\n    #[prop(default = ButtonVariant::Solid)] variant: ButtonVariant,\n    #[prop(default = ButtonSize::Md)] size: ButtonSize,\n    #[prop(optional)] class: Option<String>,  // ✅ NO into\n    #[prop(optional)] disabled: Option<bool>,\n) -> impl IntoView {\n    let class_val = format!(\n        \"{} {}\",\n        class.unwrap_or_default(),\n        variant.as_class()\n    );\n    \n    view! {\n        <ButtonPrimitive\n            class=Some(class_val)\n            disabled=disabled\n        >\n            {children}\n        </ButtonPrimitive>\n    }\n}\n```\n\n#### Forbidden Example:\n```rust\n// ❌ WRONG: Multiple Primitives in AdapterUI\n#[component]\npub fn Button(children: Children) -> impl IntoView {\n    view! {\n        <ButtonPrimitive>  // ❌ Calling 2 primitives\n            <SpanPrimitive>{children}</SpanPrimitive>\n        </ButtonPrimitive>\n    }\n}\n```\n\n---\n\n### Type 2: ControlledUI\n\n**Definition:** Manages visual state and emits semantic callbacks.\n\n**Use when:** Component needs internal state for UI concerns (tabs, accordions, dropdowns).\n\n**Examples:** `Tabs`, `Accordion`, `Select`, `Combobox`\n\n#### CAN:\n- Have `RwSignal` for **visual state only** (active tab, expanded, open)\n- Emit semantic callbacks: `on_change`, `on_select`, `on_toggle`\n  - Type: `Callback<T>` where `T` is domain-specific (`String`, `usize`, `MyEnum`)\n  - NEVER `Callback<MouseEvent>` or other DOM types\n- Manage keyboard navigation (arrow keys, enter, escape)\n- Sync state with props (controlled/uncontrolled pattern)\n- Accept `Option<T>` props WITHOUT `#[prop(into)]`\n- Use `ChildrenFn` with `StoredValue` pattern (Rule #121)\n\n#### CANNOT:\n- Have domain state (user data, API responses, cart contents)\n- Fetch data or call APIs\n- Use `use_context()` for application state\n- Define page layout (flex, grid positioning)\n- Use CSS classes for state (use data attributes)\n- Use `#[prop(optional, into)]`\n- Emit DOM events in public API\n\n#### Canonical Example:\n```rust\n// ✅ CORRECT: ControlledUI\n#[component]\npub fn Tabs(\n    children: ChildrenFn,\n    #[prop(default = 0)] default_tab: usize,\n    on_change: Callback<usize>,  // ✅ Semantic callback\n) -> impl IntoView {\n    let (active_tab, set_active_tab) = signal(default_tab);  // ✅ Visual state\n    \n    Effect::new(move |_| {\n        on_change.run(active_tab.get());  // ✅ Emit semantic event\n    });\n    \n    view! {\n        <div data-tabs=\"\" data-active={active_tab.get()}>\n            {children()}\n        </div>\n    }\n}\n```\n\n#### Forbidden Example:\n```rust\n// ❌ WRONG: Domain state in ControlledUI\n#[component]\npub fn UserTabs() -> impl IntoView {\n    let user = use_context::<User>().unwrap();  // ❌ Domain state\n    let (active, set_active) = signal(0);\n    \n    view! {\n        <div data-tabs=\"\">\n            <button on:click=move |_| set_active.set(0)>\n                {user.name}  // ❌ Using domain data\n            </button>\n        </div>\n    }\n}\n```\n\n---\n\n### Type 3: CompositeUI\n\n**Definition:** Composes 2+ Primitives or UI components into cohesive unit.\n\n**Use when:** Multiple primitives form a single logical component (cards, forms, modals).\n\n**Examples:** `Card`, `Dialog`, `Popover`, `Form`, `Alert`\n\n#### CAN:\n- Compose 2+ Primitives OR UI components\n- Provide slot-based APIs (header, body, footer)\n- Accept multiple `ChildrenFn` for different slots\n- Coordinate child component state (which slot is active)\n- Accept `Option<T>` props WITHOUT `#[prop(into)]`\n- Use `ChildrenFn` for all slots\n\n#### CANNOT:\n- Have business logic (validation, API calls, domain rules)\n- Know about domain models (`User`, `Product`, `Order`)\n- Manage routing or navigation\n- Fetch data\n- Use `#[prop(optional, into)]`\n\n#### Canonical Example:\n```rust\n// ✅ CORRECT: CompositeUI\n#[component]\npub fn Card(\n    header: Option<ChildrenFn>,\n    children: ChildrenFn,\n    footer: Option<ChildrenFn>,\n    #[prop(optional)] class: Option<String>,  // ✅ NO into\n) -> impl IntoView {\n    view! {\n        <CardPrimitive class=class>\n            {header.map(|h| view! {\n                <div data-card-header=\"\">{h()}</div>\n            })}\n            <div data-card-body=\"\">{children()}</div>\n            {footer.map(|f| view! {\n                <div data-card-footer=\"\">{f()}</div>\n            })}\n        </CardPrimitive>\n    }\n}\n```\n\n#### Forbidden Example:\n```rust\n// ❌ WRONG: Domain logic in CompositeUI\n#[component]\npub fn UserCard(user_id: String) -> impl IntoView {\n    let user = fetch_user(user_id);  // ❌ API call in UI layer\n    \n    view! {\n        <CardPrimitive>\n            <div>{user.name}</div>  // ❌ Domain data\n        </CardPrimitive>\n    }\n}\n```\n\n---\n\n## Universal UI Contracts\n\n**ALL UI components MUST follow these contracts:**\n\n### 1. No `#[prop(optional, into)]`\n**NEVER combine `optional` and `into` (Rule #119).**\n```rust\n// ✅ CORRECT\n#[prop(optional)] class: Option<String>\n\n// ❌ WRONG\n#[prop(optional, into)] class: Option<String>\n```\n\n### 2. Semantic Callbacks Only\n**Emit `Callback<T>` where `T` is domain type — NEVER `MouseEvent`.**\n```rust\n// ✅ CORRECT\non_click: Callback<()>\non_select: Callback<String>\non_change: Callback<usize>\n\n// ❌ WRONG\non_click: Callback<MouseEvent>  // DOM event in UI layer\n```\n\n### 3. Children Handling\n**Use `Children` (invoke before `view!`) or `ChildrenFn` (with `StoredValue`).**\n```rust\n// ✅ Option 1: Children (simple)\n#[component]\npub fn Button(children: Children) -> impl IntoView {\n    view! {\n        <ButtonPrimitive>{children}</ButtonPrimitive>\n    }\n}\n\n// ✅ Option 2: ChildrenFn (reactive)\n#[component]\npub fn Tabs(children: ChildrenFn) -> impl IntoView {\n    let children = StoredValue::new(children);\n    view! {\n        <div>{move || children.with_value(|c| c())}</div>\n    }\n}\n```\n\n### 4. Visual State Only\n**`RwSignal` for UI concerns only — never domain state.**\n```rust\n// ✅ CORRECT: Visual state\nlet (active_tab, set_active_tab) = signal(0);\nlet (loading, set_loading) = signal(false);\n\n// ❌ WRONG: Domain state\nlet (user, set_user) = signal(User::default());\nlet (cart_items, set_cart_items) = signal(vec![]);\n```\n\n### 5. No Direct Primitive Usage in Application\n**Application code uses UI layer — never Primitives directly.**\n```rust\n// ✅ CORRECT: Application uses UI component\nview! {\n    <Button on_click=save>\"Save\"</Button>\n}\n\n// ❌ WRONG: Application uses Primitive\nview! {\n    <ButtonPrimitive on:click=save>\"Save\"</ButtonPrimitive>\n}\n```\n\n---\n\n## Rationale\n\n### Why UI Layer Exists\n\n1. **Abstraction boundary** — hides Primitive implementation details\n2. **Ergonomics** — provides defaults, variants, convenience\n3. **Type safety** — normalizes props, eliminates `.into()` ambiguity\n4. **Semantic events** — converts DOM → business intent\n5. **Visual state management** — keeps UI concerns separate from domain\n\n### What This Rule Protects\n\n- **Call-site ergonomics** — users write natural code\n- **Type clarity** — no ambiguous conversions\n- **Separation of concerns** — UI ≠ domain ≠ primitives\n- **Reactivity correctness** — proper Children types prevent errors\n\n---\n\n## Enforcement\n\n### Static Analysis\n```rust\n// Check 1: No #[prop(optional, into)]\nfor prop in ui_component.props {\n    if prop.has_attributes(&[\"optional\", \"into\"]) {\n        emit_error!(\"Canon Rule #125: UI cannot use #[prop(optional, into)]\");\n    }\n}\n\n// Check 2: No DOM events in callbacks\nfor prop in ui_component.props {\n    if prop.type.contains(\"Callback<MouseEvent>\") {\n        emit_error!(\"Canon Rule #125: Use Callback<()>, not Callback<MouseEvent>\");\n    }\n}\n```\n\n### CI Checks\n```bash\n# No #[prop(optional, into)] in UI layer\nrg '#\\[prop\\(optional.*into\\)' packages-rust/rs-design/src/ui/ && exit 1\n\n# No domain state patterns\nrg 'use_context::<User>|fetch_|api::' packages-rust/rs-design/src/ui/ && exit 1\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a UI component needs domain logic or API calls, it's misclassified — move to Component layer (Rule #126).\n\n---\n\n## Quick Reference\n\n| Type | Wraps | State | Callbacks | Example |\n|------|-------|-------|-----------|---------|\n| **Adapter** | 1 Primitive | ❌ None | ❌ None | `Button`, `Input` |\n| **Controlled** | 1+ Primitives | ✅ Visual only | ✅ Semantic | `Tabs`, `Select` |\n| **Composite** | 2+ Primitives/UI | ❌ None | ❌ None | `Card`, `Dialog` |\n\n---\n\n## Related Rules\n\n- **Canon Rule #119:** No `#[prop(optional, into)]` in UI layer\n- **Canon Rule #120:** DOM Events vs Semantic Callbacks\n- **Canon Rule #121:** StoredValue for Non-Copy in view!\n- **Canon Rule #124:** Primitive Contract Types (previous rule)\n- **Canon Rule #126:** Component Domain Contracts (next rule)\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":126,"slug":"component-domain-contracts","title":"Component Domain Contracts","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["domain","state","business-logic","components"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Mixing domain logic with UI concerns creates untestable and tightly coupled components. Without clear contracts, business rules leak across layers and become inconsistent.","problem":"components mix domain logic with ui or primitive concerns","solution":"separate domain logic into stateful and orchestrator components with strict boundaries","signals":["api calls in ui","primitive usage in component","routing inside component"],"search_intent":"how to separate domain logic from ui components","keywords":["domain component architecture","stateful vs orchestrator component","frontend business logic separation","reactive state management patterns"],"body":"## Principle\n\n**Components own domain state, implement business logic, and orchestrate workflows. Every Component MUST be classified as Stateful or Orchestrator.**\n\n---\n\n## The Problem\n\nWithout formal component layer types, developers create components that:\n\n1. Mix domain logic with visual concerns (validation + tabs state in one component)\n2. Use Primitives directly instead of UI layer\n3. Manage global state locally (duplicating cart, user session)\n4. Emit DOM events instead of domain events\n5. Know about routing/navigation instead of using callbacks\n\n**Real symptoms:**\n- Business logic scattered across UI components\n- Validation rules duplicated in multiple places\n- API calls in UI layer components\n- State management chaos (local vs global unclear)\n- Components tightly coupled to routing\n\n**Without this rule:**\n- Domain logic leaks into UI layer\n- Components become untestable (mixed concerns)\n- State boundaries blur\n- Refactoring becomes impossible\n\n---\n\n## Component Types\n\n### Type 1: StatefulComponent\n\n**Definition:** Owns domain state and implements business logic for a single feature.\n\n**Use when:** Managing user input, validation, API calls, or feature-specific state.\n\n**Examples:** `LoginForm`, `ProductSearch`, `CommentForm`, `UserProfile`, `ShoppingCartSummary`\n\n#### CAN:\n- Use `RwSignal` for domain state (form values, loading, errors)\n- Call APIs and fetch data\n- Use `use_context()` to access global state\n- Validate data according to business rules\n- Emit domain events: `on_login`, `on_submit`, `on_save`, `on_delete`\n  - Type: `Callback<DomainType>` (e.g., `Callback<User>`, `Callback<Order>`)\n- Use UI components from Layer 2 (Button, Input, Select)\n- Accept domain props: `user: User`, `product_id: String`\n- Manage local error state and success messages\n\n#### CANNOT:\n- Use Primitives directly (MUST go through UI layer)\n- Define CSS or data attributes (use UI components)\n- Know about routing (use callbacks instead: `on_success: Callback<()>`)\n- Manage global state locally (cart, user session — use context)\n- Coordinate multiple features (that's OrchestratorComponent)\n\n#### Canonical Example:\n```rust\n// ✅ CORRECT: StatefulComponent\n#[component]\npub fn LoginForm(\n    on_login: Callback<User>,  // ✅ Domain callback\n    on_cancel: Callback<()>,\n) -> impl IntoView {\n    let (email, set_email) = signal(String::new());\n    let (password, set_password) = signal(String::new());\n    let (loading, set_loading) = signal(false);\n    let (error, set_error) = signal(None::<String>);\n    \n    let handle_submit = move |_| {\n        set_loading.set(true);\n        set_error.set(None);\n        \n        // Validation (business logic)\n        if email.get().is_empty() {\n            set_error.set(Some(\"Email required\".to_string()));\n            set_loading.set(false);\n            return;\n        }\n        \n        // API call (domain logic)\n        spawn_local(async move {\n            match api::login(email.get(), password.get()).await {\n                Ok(user) => on_login.run(user),  // ✅ Emit domain event\n                Err(e) => set_error.set(Some(e.to_string())),\n            }\n            set_loading.set(false);\n        });\n    };\n    \n    view! {\n        <form on:submit=handle_submit>\n            <Input\n                value=email.get()\n                on_input=move |v| set_email.set(v)\n                placeholder=\"Email\"\n            />\n            <Input\n                value=password.get()\n                on_input=move |v| set_password.set(v)\n                placeholder=\"Password\"\n                input_type=\"password\"\n            />\n            \n            <Show when=move || error.get().is_some()>\n                <div class=\"error\">{error.get()}</div>\n            </Show>\n            \n            <Button on_click=handle_submit loading=loading.get()>\n                \"Login\"\n            </Button>\n            <Button variant=ButtonVariant::Ghost on_click=move |_| on_cancel.run(())>\n                \"Cancel\"\n            </Button>\n        </form>\n    }\n}\n```\n\n#### Forbidden Example 1: Using Primitive directly\n```rust\n// ❌ WRONG: Primitive in Component layer\n#[component]\npub fn LoginForm() -> impl IntoView {\n    view! {\n        <form>\n            <InputPrimitive />  // ❌ Should use Input (UI layer)\n            <ButtonPrimitive>\"Login\"</ButtonPrimitive>  // ❌ Should use Button\n        </form>\n    }\n}\n```\n\n#### Forbidden Example 2: Routing in Component\n```rust\n// ❌ WRONG: Navigation in Component\n#[component]\npub fn LoginForm() -> impl IntoView {\n    let navigate = use_navigate();  // ❌ Routing knowledge\n    \n    let handle_login = move |user: User| {\n        navigate(\"/dashboard\");  // ❌ Component knows routes\n    };\n    \n    view! { /* ... */ }\n}\n```\n\n#### Forbidden Example 3: Managing global state locally\n```rust\n// ❌ WRONG: Cart state in local component\n#[component]\npub fn ProductCard() -> impl IntoView {\n    let (cart, set_cart) = signal(vec![]);  // ❌ Cart is global, not local\n    \n    let add_to_cart = move |product: Product| {\n        set_cart.update(|c| c.push(product));\n    };\n    \n    view! { /* ... */ }\n}\n```\n\n---\n\n### Type 2: OrchestratorComponent\n\n**Definition:** Coordinates multiple StatefulComponents or manages complex workflows.\n\n**Use when:** Multi-step processes, dashboards, or cross-feature coordination needed.\n\n**Examples:** `CheckoutWizard`, `MultiStepForm`, `Dashboard`, `OnboardingFlow`, `ReportBuilder`\n\n#### CAN:\n- Manage workflow state (current step, progress, completion)\n- Coordinate multiple StatefulComponents\n- Use `provide_context()` to share state with children\n- Handle cross-component communication\n- Emit workflow events: `on_complete`, `on_step_change`, `on_cancel`\n- Use `RwSignal` for orchestration state (step index, validation status)\n- Use UI components and StatefulComponents\n\n#### CANNOT:\n- Render Primitives directly\n- Define global layout (that's Layout layer, Rule #128)\n- Manage application-wide state (user session, theme — use app-level context)\n- Know about routing (use callbacks)\n\n#### Canonical Example:\n```rust\n// ✅ CORRECT: OrchestratorComponent\n#[component]\npub fn CheckoutWizard(\n    on_complete: Callback<Order>,\n    on_cancel: Callback<()>,\n) -> impl IntoView {\n    let (step, set_step) = signal(1);  // ✅ Workflow state\n    let (cart_validated, set_cart_validated) = signal(false);\n    let (shipping_info, set_shipping_info) = signal(None::<ShippingInfo>);\n    let (payment_info, set_payment_info) = signal(None::<PaymentInfo>);\n    \n    // Provide context for child components\n    provide_context(CheckoutContext {\n        step: step.read_only(),\n        set_step,\n    });\n    \n    let handle_cart_review = move |valid: bool| {\n        set_cart_validated.set(valid);\n        if valid {\n            set_step.set(2);\n        }\n    };\n    \n    let handle_shipping = move |info: ShippingInfo| {\n        set_shipping_info.set(Some(info));\n        set_step.set(3);\n    };\n    \n    let handle_payment = move |info: PaymentInfo| {\n        set_payment_info.set(Some(info));\n        \n        // Create order (orchestration logic)\n        let order = Order {\n            shipping: shipping_info.get().unwrap(),\n            payment: info,\n        };\n        \n        on_complete.run(order);  // ✅ Emit workflow completion\n    };\n    \n    view! {\n        <div data-wizard=\"\">\n            <ProgressBar current=step.get() total=3 />\n            \n            <Show when=move || step.get() == 1>\n                <CartReview on_continue=handle_cart_review />\n            </Show>\n            \n            <Show when=move || step.get() == 2>\n                <ShippingForm on_submit=handle_shipping />\n            </Show>\n            \n            <Show when=move || step.get() == 3>\n                <PaymentForm on_submit=handle_payment />\n            </Show>\n            \n            <Button\n                variant=ButtonVariant::Ghost\n                on_click=move |_| on_cancel.run(())\n            >\n                \"Cancel\"\n            </Button>\n        </div>\n    }\n}\n```\n\n#### Forbidden Example 1: Layout concerns in Orchestrator\n```rust\n// ❌ WRONG: Layout in Orchestrator\n#[component]\npub fn Dashboard() -> impl IntoView {\n    view! {\n        <div style=\"display: grid; grid-template-columns: 200px 1fr;\">  // ❌ Layout\n            <Sidebar />\n            <MainContent />\n        </div>\n    }\n}\n```\n\n#### Forbidden Example 2: Global state management\n```rust\n// ❌ WRONG: Application state in Orchestrator\n#[component]\npub fn Dashboard() -> impl IntoView {\n    let (user, set_user) = signal(None);  // ❌ User is app-level, not orchestrator\n    let (theme, set_theme) = signal(Theme::Light);  // ❌ Theme is app-level\n    \n    view! { /* ... */ }\n}\n```\n\n---\n\n## Universal Component Contracts\n\n**ALL Components MUST follow these contracts:**\n\n### 1. Never Use Primitives Directly\n**MUST use UI layer components — never Primitives.**\n```rust\n// ✅ CORRECT\n<Button on_click=save>\"Save\"</Button>\n<Input value=name on_input=set_name />\n\n// ❌ WRONG\n<ButtonPrimitive on:click=save>\"Save\"</ButtonPrimitive>\n<InputPrimitive value=name />\n```\n\n### 2. Emit Domain Callbacks\n**Use `Callback<DomainType>` — NEVER `Callback<MouseEvent>`.**\n```rust\n// ✅ CORRECT\non_submit: Callback<FormData>\non_login: Callback<User>\non_save: Callback<Product>\non_delete: Callback<String>  // ID\n\n// ❌ WRONG\non_click: Callback<MouseEvent>\non_change: Callback<Event>\n```\n\n### 3. No Routing Knowledge\n**Use callbacks for navigation — don't call `use_navigate()`.**\n```rust\n// ✅ CORRECT\n#[component]\npub fn LoginForm(\n    on_success: Callback<User>,  // Parent handles navigation\n) -> impl IntoView { /* ... */ }\n\n// ❌ WRONG\n#[component]\npub fn LoginForm() -> impl IntoView {\n    let navigate = use_navigate();\n    let handle_login = move |user| navigate(\"/dashboard\");  // ❌\n    // ...\n}\n```\n\n### 4. Domain State vs Visual State\n**Distinguish clearly: domain state (data, validation) vs visual state (loading, errors).**\n```rust\n// ✅ CORRECT\nlet (email, set_email) = signal(String::new());  // Domain state\nlet (loading, set_loading) = signal(false);  // Visual state\nlet (error, set_error) = signal(None::<String>);  // Visual state\n\n// ❌ WRONG (mixing concerns)\nlet (tab_index, set_tab_index) = signal(0);  // Visual state in domain component\n```\n\n### 5. Global vs Local State\n**Use context for global state — don't duplicate in components.**\n```rust\n// ✅ CORRECT: Use global context\nlet cart = use_context::<CartContext>().unwrap();\n\n// ❌ WRONG: Local cart state\nlet (cart, set_cart) = signal(CartState::default());\n```\n\n---\n\n## Rationale\n\n### Why Component Layer Exists\n\n1. **Business logic centralization** — one place for domain rules\n2. **Testability** — components can be tested with mock callbacks\n3. **State ownership** — clear boundaries for state management\n4. **Separation from UI** — domain logic independent of visual presentation\n5. **Reusability** — components work with any UI theme/style\n\n### What This Rule Protects\n\n- **Domain integrity** — business logic stays pure\n- **Testing simplicity** — no DOM dependencies in tests\n- **Architectural clarity** — layer boundaries enforced\n- **Refactoring safety** — domain logic changes don't break UI\n\n---\n\n## Enforcement\n\n### Static Analysis\n```rust\n// Check 1: No Primitives in Component layer\nfor import in component.imports {\n    if import.path.contains(\"primitives/\") {\n        emit_error!(\"Canon Rule #126: Components cannot use Primitives directly\");\n    }\n}\n\n// Check 2: No routing\nif component.contains(\"use_navigate\") || component.contains(\"use_location\") {\n    emit_error!(\"Canon Rule #126: Components cannot use routing directly\");\n}\n\n// Check 3: Domain callbacks only\nfor prop in component.props {\n    if prop.type.contains(\"Callback<MouseEvent>\") {\n        emit_error!(\"Canon Rule #126: Use domain callbacks, not DOM events\");\n    }\n}\n```\n\n### CI Checks\n```bash\n# No Primitive usage in components\nrg 'ButtonPrimitive|InputPrimitive|SelectPrimitive' products/*/src/components/ && exit 1\n\n# No routing in components\nrg 'use_navigate|use_location' products/*/src/components/ && exit 1\n\n# No layout in components\nrg 'display: grid|display: flex' products/*/src/components/ && exit 1\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a component needs to use Primitives or routing, it's misclassified or the architecture needs reconsideration.\n\n---\n\n## Quick Reference\n\n| Type | State | API Calls | Coordination | Example |\n|------|-------|-----------|--------------|---------|\n| **Stateful** | ✅ Domain + Visual | ✅ Yes | ❌ No | `LoginForm`, `ProductSearch` |\n| **Orchestrator** | ✅ Workflow only | ⚠️ Rare | ✅ Yes | `CheckoutWizard`, `Dashboard` |\n\n---\n\n## Related Rules\n\n- **Canon Rule #119:** No `#[prop(optional, into)]` in UI layer\n- **Canon Rule #120:** DOM Events vs Semantic Callbacks\n- **Canon Rule #124:** Primitive Contract Types\n- **Canon Rule #125:** UI Component Contracts (previous rule)\n- **Canon Rule #127:** Block Composition Contracts (next rule)\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":127,"slug":"block-composition-contracts","title":"Block Composition Contracts","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["blocks","composition","layout","structure"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Blocks without strict boundaries accumulate domain logic, routing, and global state, becoming unmanageable. This breaks separation between structure and behavior.","problem":"blocks contain business logic state or routing instead of pure composition","solution":"restrict blocks to semantic or interactive composition without domain responsibilities","signals":["api call in block","routing in block","global state usage"],"search_intent":"how to structure block components in frontend architecture","keywords":["block component architecture","semantic vs interactive blocks","frontend composition patterns","design system block layer"],"body":"## Principle\n\n**Blocks compose Components into semantic page sections without domain state or routing. Every Block MUST be classified as Semantic or Interactive.**\n\n---\n\n## The Problem\n\nWithout formal block types, developers create blocks that:\n\n1. Manage global state (user session, cart) locally\n2. Contain routing logic (navigation, URL handling)\n3. Define page-level layout (header positioning, z-index)\n4. Implement business logic instead of composing components\n5. Blur boundary between Block and Component layers\n\n**Real symptoms:**\n- Blocks with API calls\n- Blocks managing authentication state\n- Blocks defining CSS Grid layouts for entire pages\n- Blocks with complex validation logic\n- Blocks that are untestable (too many responsibilities)\n\n**Without this rule:**\n- Block layer becomes dumping ground for \"big components\"\n- No clear distinction from Component layer\n- Code reviews struggle to classify\n- Refactoring becomes guesswork\n\n---\n\n## Block Types\n\n### Type 1: SemanticBlock\n\n**Definition:** Structural page section with semantic HTML and passive composition.\n\n**Use when:** Defining page regions like heroes, footers, feature grids, pricing tables.\n\n**Examples:** `HeroBlock`, `FooterBlock`, `FeatureGridBlock`, `PricingTableBlock`, `TestimonialBlock`\n\n#### CAN:\n- Use semantic HTML: `<section>`, `<header>`, `<footer>`, `<aside>`, `<nav>`, `<article>`\n- Compose UI components and StatefulComponents\n- Accept content via props (strings, enums, simple data)\n- Accept `ChildrenFn` for flexible slots\n- Define block-level structure (grid of cards, list of features)\n- Use data attributes for styling hooks (`data-hero`, `data-feature-grid`)\n\n#### CANNOT:\n- Have domain state (`RwSignal` for user data, cart, etc.)\n- Call APIs or fetch data\n- Use `use_context()` for application state\n- Manage routing or navigation\n- Define z-index or position (that's Layout layer)\n- Implement business logic (validation, calculation)\n- Know about authentication or authorization\n\n#### Canonical Example:\n```rust\n// ✅ CORRECT: SemanticBlock\n#[component]\npub fn HeroBlock(\n    title: String,\n    subtitle: String,\n    image_url: String,\n    cta: ChildrenFn,  // CTA button provided by caller\n) -> impl IntoView {\n    view! {\n        <section data-hero=\"\" class=\"hero-block\">\n            <div data-hero-content=\"\">\n                <h1>{title}</h1>\n                <p>{subtitle}</p>\n                <div data-hero-cta=\"\">\n                    {cta()}\n                </div>\n            </div>\n            <div data-hero-image=\"\">\n                <img src={image_url} alt=\"Hero\" />\n            </div>\n        </section>\n    }\n}\n```\n\n#### Forbidden Example 1: State in SemanticBlock\n```rust\n// ❌ WRONG: Domain state in Semantic Block\n#[component]\npub fn HeroBlock() -> impl IntoView {\n    let user = use_context::<User>().unwrap();  // ❌ State\n    \n    view! {\n        <section data-hero=\"\">\n            <h1>\"Welcome, \"{user.name}</h1>  // ❌ Domain data\n        </section>\n    }\n}\n```\n\n#### Forbidden Example 2: Routing in SemanticBlock\n```rust\n// ❌ WRONG: Navigation in Semantic Block\n#[component]\npub fn HeroBlock() -> impl IntoView {\n    let navigate = use_navigate();  // ❌ Routing\n    \n    view! {\n        <section data-hero=\"\">\n            <Button on_click=move |_| navigate(\"/signup\")>  // ❌\n                \"Get Started\"\n            </Button>\n        </section>\n    }\n}\n```\n\n---\n\n### Type 2: InteractiveBlock\n\n**Definition:** Feature-complete section with local interactions but no global state.\n\n**Use when:** Sections need interaction but don't manage application state (comments, search, filters).\n\n**Examples:** `CommentSectionBlock`, `SearchBlock`, `FilterBarBlock`, `ChatWidgetBlock`, `FeedbackFormBlock`\n\n#### CAN:\n- Compose multiple StatefulComponents\n- Manage block-level interactions (expand/collapse section)\n- Emit block-level events: `on_filter_change`, `on_comment_submit`, `on_search`\n  - Type: `Callback<BlockData>` where `BlockData` is block-specific\n- Have local visual state (section expanded, filter panel open)\n- Use `RwSignal` for **block UI state only** (not domain state)\n\n#### CANNOT:\n- Manage global application state (user, cart, session)\n- Call authentication APIs\n- Know about page structure (header height, sidebar width)\n- Define page transitions or routing\n- Implement complex business logic (that's Component layer)\n- Fetch global data (user preferences, app config)\n\n#### Canonical Example:\n```rust\n// ✅ CORRECT: InteractiveBlock\n#[component]\npub fn CommentSectionBlock(\n    post_id: String,\n    on_comment_submit: Callback<Comment>,  // ✅ Block-level event\n) -> impl IntoView {\n    let (expanded, set_expanded) = signal(true);  // ✅ Block UI state\n    \n    view! {\n        <section data-comments=\"\" data-expanded={expanded.get()}>\n            <header data-comments-header=\"\">\n                <h3>\"Comments\"</h3>\n                <Button\n                    variant=ButtonVariant::Ghost\n                    on_click=move |_| set_expanded.update(|e| *e = !*e)\n                >\n                    {move || if expanded.get() { \"Collapse\" } else { \"Expand\" }}\n                </Button>\n            </header>\n            \n            <Show when=move || expanded.get()>\n                <CommentList post_id=post_id.clone() />\n                <CommentForm\n                    post_id=post_id.clone()\n                    on_submit=move |comment| {\n                        on_comment_submit.run(comment);\n                    }\n                />\n            </Show>\n        </section>\n    }\n}\n```\n\n#### Forbidden Example 1: Global state in InteractiveBlock\n```rust\n// ❌ WRONG: Global state in Block\n#[component]\npub fn SearchBlock() -> impl IntoView {\n    let (search_history, set_search_history) = signal(vec![]);  // ❌ Global data\n    \n    provide_context(SearchContext {  // ❌ Global context\n        history: search_history,\n    });\n    \n    view! { /* ... */ }\n}\n```\n\n#### Forbidden Example 2: Business logic in InteractiveBlock\n```rust\n// ❌ WRONG: Validation logic in Block\n#[component]\npub fn FilterBarBlock() -> impl IntoView {\n    let validate_price = |min: f64, max: f64| -> Result<(), String> {  // ❌\n        if min > max {\n            return Err(\"Min must be less than max\".to_string());\n        }\n        // Complex validation...\n        Ok(())\n    };\n    \n    view! { /* ... */ }\n}\n```\n\n---\n\n## Universal Block Contracts\n\n**ALL Blocks MUST follow these contracts:**\n\n### 1. No Global State Management\n**Never use `provide_context()` for application-wide state.**\n```rust\n// ✅ CORRECT: Emit events, let parent handle state\non_submit: Callback<FormData>\n\n// ❌ WRONG: Provide global context\nprovide_context(UserContext { user });\n```\n\n### 2. No Routing\n**Use callbacks for navigation — never call `use_navigate()`.**\n```rust\n// ✅ CORRECT: Callback for navigation\non_cta_click: Callback<()>\n\n// ❌ WRONG: Direct navigation\nlet navigate = use_navigate();\nnavigate(\"/signup\");\n```\n\n### 3. Semantic HTML Required\n**Use appropriate semantic elements for structure.**\n```rust\n// ✅ CORRECT\n<section data-hero=\"\">\n<footer data-footer=\"\">\n<aside data-sidebar=\"\">\n\n// ❌ WRONG\n<div data-hero=\"\">  // Should be <section>\n<div data-footer=\"\">  // Should be <footer>\n```\n\n### 4. No Layout Concerns\n**Don't define page-level positioning or z-index.**\n```rust\n// ✅ CORRECT: Block-level structure\n<section data-comments=\"\" class=\"comment-section\">\n\n// ❌ WRONG: Page layout\n<section style=\"position: fixed; top: 0; z-index: 9999;\">\n```\n\n### 5. Composition Over Implementation\n**Compose Components — don't reimplement their logic.**\n```rust\n// ✅ CORRECT: Compose existing components\n<CommentList post_id=id />\n<CommentForm on_submit=handler />\n\n// ❌ WRONG: Reimplement form logic\nlet (text, set_text) = signal(String::new());\n// ... validation, API call, etc.\n```\n\n---\n\n## Rationale\n\n### Why Block Layer Exists\n\n1. **Semantic structure** — groups related components into meaningful sections\n2. **Reusability** — blocks can be reused across pages\n3. **Composition boundary** — clear separation from Component and Layout layers\n4. **Content modeling** — maps to CMS content blocks\n5. **Testing** — blocks testable independently of page context\n\n### What This Rule Protects\n\n- **Layer boundaries** — blocks don't become \"mega components\"\n- **State locality** — prevents global state leaking into blocks\n- **Semantic HTML** — enforces proper document structure\n- **Composition clarity** — blocks compose, don't implement\n\n---\n\n## Enforcement\n\n### Static Analysis\n```rust\n// Check 1: No global state\nif block.contains(\"provide_context\") {\n    emit_error!(\"Canon Rule #127: Blocks cannot provide global context\");\n}\n\n// Check 2: No routing\nif block.contains(\"use_navigate\") || block.contains(\"use_location\") {\n    emit_error!(\"Canon Rule #127: Blocks cannot use routing\");\n}\n\n// Check 3: Semantic HTML\nif block.contains(\"<div data-hero=\") {\n    emit_warning!(\"Canon Rule #127: Use <section> instead of <div> for semantic blocks\");\n}\n```\n\n### CI Checks\n```bash\n# No routing in blocks\nrg 'use_navigate|use_location' packages-rust/rs-design/src/blocks/ && exit 1\n\n# No provide_context in blocks\nrg 'provide_context' packages-rust/rs-design/src/blocks/ && exit 1\n\n# No API calls in blocks\nrg 'api::|fetch_|async fn' packages-rust/rs-design/src/blocks/ && exit 1\n```\n\n---\n\n## Exceptions\n\n### Exception 1: Block UI State (Allowed)\n```rust\n// ✅ ALLOWED: Block-level UI state\nlet (section_expanded, set_section_expanded) = signal(true);\nlet (filter_open, set_filter_open) = signal(false);\n```\n\n**When allowed:**\n- State controls block visibility/expansion only\n- State does not represent domain data\n- State is not shared across blocks\n\n### Exception 2: Data Props (Allowed)\n```rust\n// ✅ ALLOWED: Accepting data props\n#[component]\npub fn PricingTableBlock(\n    plans: Vec<PricingPlan>,  // Data passed in, not fetched\n    on_select: Callback<String>,\n) -> impl IntoView { /* ... */ }\n```\n\n**When allowed:**\n- Data passed as props, not fetched internally\n- Block displays data, doesn't manage it\n\n**No other exceptions exist.**\n\n---\n\n## Quick Reference\n\n| Type | State | Composition | Events | Example |\n|------|-------|-------------|--------|---------|\n| **Semantic** | ❌ None | ✅ Passive | ❌ None | `HeroBlock`, `FooterBlock` |\n| **Interactive** | ✅ UI only | ✅ Active | ✅ Block-level | `CommentSection`, `SearchBlock` |\n\n---\n\n## Related Rules\n\n- **Canon Rule #124:** Primitive Contract Types\n- **Canon Rule #125:** UI Component Contracts\n- **Canon Rule #126:** Component Domain Contracts (previous rule)\n- **Canon Rule #128:** Layout Shell and Zone Contracts (next rule)\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":128,"slug":"layout-shell-and-zone-contracts","title":"Layout Shell and Zone Contracts","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["layout","shell","zone","architecture"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Layouts without strict classification mix structure with business logic, creating tightly coupled and untestable systems. This breaks reuse and introduces hidden dependencies.","problem":"layouts mix structure with logic causing tight coupling and poor reuse","solution":"enforce shell and zone layout types with strict structural-only responsibilities","signals":["layout with api calls","auth logic in layout","state in layout","non reusable layout"],"search_intent":"how to separate layout and logic architecture","keywords":["layout shell zone pattern","leptos layout architecture","separation layout business logic","structural layout contracts"],"body":"## Principle\n\n**Layouts define structural boundaries and positioning without business logic. Every Layout MUST be classified as Shell or Zone.**\n\n---\n\n## The Problem\n\nWithout formal layout types, developers create layouts that:\n\n1. Contain business logic (authentication, data fetching)\n2. Manage domain state (user preferences, cart data)\n3. Render domain components directly (forms, dashboards)\n4. Mix structural concerns with feature concerns\n5. Create untestable, tightly-coupled page structures\n\n**Real symptoms:**\n- Layouts fetching user data\n- Layouts with conditional rendering based on auth\n- Layouts managing sidebar state globally\n- Layouts that can't be reused across routes\n- Page structures that break when refactored\n\n**Without this rule:**\n- Layout becomes dumping ground for \"page-level stuff\"\n- Structural changes break feature code\n- Testing requires full app context\n- Reusability impossible\n\n---\n\n## Layout Types\n\n### Type 1 ShellLayout\n\n**Definition:** Application-level structure defining top-level regions (header, sidebar, main, footer).\n\n**Use when:** Defining app-wide structure that persists across routes.\n\n**Examples:** `AppShell`, `DashboardShell`, `AuthShell`, `MarketingShell`, `AdminShell`\n\n#### CAN\n- Define top-level structure: `<header>`, `<aside>`, `<main>`, `<footer>`\n- Manage global z-index stacking (modals above content, tooltips above modals)\n- Control responsive layout breakpoints (sidebar collapse at mobile)\n- Accept `ChildrenFn` slots for each region\n- Use `<Outlet />` for router content in main area\n- Provide layout-level context (sidebar collapsed state)\n- Define CSS Grid or Flexbox for shell structure\n- Accept layout configuration props (sidebar position: left/right)\n\n#### CANNOT\n\n**ZoneLayouts are pure structural functions and MUST be signal-free. Any state implies misclassification as Shell or UI component.**\n- Have business logic (validation, calculations, domain rules)\n- Fetch data or call APIs\n- Render domain components directly (forms, dashboards) — use slots/outlets\n- Know about authentication or authorization state\n- Manage user session or preferences\n- Emit domain callbacks (`on_save`, `on_submit`)\n- Know about routing paths (\"/dashboard\", \"/settings\")\n\n#### Canonical Example\n```rust\n// ✅ CORRECT: ShellLayout\n#[component]\npub fn AppShell(\n    #[prop(optional)] sidebar_content: Option<ChildrenFn>,\n    #[prop(default = false)] sidebar_collapsed: bool,\n) -> impl IntoView {\n    let (collapsed, set_collapsed) = signal(sidebar_collapsed);\n    \n    // Provide layout context for children\n    provide_context(LayoutContext {\n        sidebar_collapsed: collapsed.read_only(),\n        toggle_sidebar: Callback::new(move |_| {\n            set_collapsed.update(|c| *c = !*c);\n        }),\n    });\n    \n    view! {\n        <div data-shell=\"\" data-sidebar-collapsed={collapsed.get()}>\n            <header data-shell-header=\"\">\n                <slot name=\"header\" />\n            </header>\n            \n            {sidebar_content.map(|content| view! {\n                <aside data-shell-sidebar=\"\" data-collapsed={collapsed.get()}>\n                    {content()}\n                </aside>\n            })}\n            \n            <main data-shell-main=\"\">\n                <Outlet />  // ✅ Router handles content\n            </main>\n            \n            <footer data-shell-footer=\"\">\n                <slot name=\"footer\" />\n            </footer>\n        </div>\n    }\n}\n```\n\n#### Forbidden Example Business Logic In Shell\n```rust\n// ❌ WRONG: Authentication logic in Shell\n#[component]\npub fn AppShell() -> impl IntoView {\n    let user = use_context::<User>().unwrap();  // ❌ Domain state\n    \n    let logout = move |_| {  // ❌ Business logic\n        api::logout().await;\n        navigate(\"/login\");\n    };\n    \n    view! {\n        <div data-shell=\"\">\n            <header>\n                <span>\"Welcome, \"{user.name}</span>  // ❌\n                <Button on_click=logout>\"Logout\"</Button>\n            </header>\n            <main><Outlet /></main>\n        </div>\n    }\n}\n```\n\n#### Forbidden Example Direct Component Rendering\n```rust\n// ❌ WRONG: Rendering domain components in Shell\n#[component]\npub fn AppShell() -> impl IntoView {\n    view! {\n        <div data-shell=\"\">\n            <header>\n                <UserMenu />  // ❌ Domain component in shell\n            </header>\n            <aside>\n                <NavigationMenu />  // ❌ Should be passed as slot\n            </aside>\n            <main><Outlet /></main>\n        </div>\n    }\n}\n```\n\n---\n\n### Type 2 ZoneLayout\n\n**Definition:** Page-level layout defining content zones without knowing what fills them.\n\n**Use when:** Structuring page content areas (two-column, grid, split-view).\n\n**Examples:** `TwoColumnLayout`, `SidebarLayout`, `GridLayout`, `SplitLayout`, `MasterDetailLayout`\n\n**ZoneLayouts are pure structural functions and MUST be signal-free. Any state implies misclassification as Shell or UI component.**\n\n#### CAN\n- Define CSS Grid or Flexbox structure for page zones\n- Accept multiple `ChildrenFn` for different zones (left, right, main, aside)\n- Provide responsive breakpoints (stack on mobile, side-by-side on desktop)\n- Define zone-level spacing and gaps\n- Use data attributes for zone identification (`data-zone=\"sidebar\"`)\n- Accept layout configuration (ratios, alignments)\n\n#### CANNOT\n\n**ZoneLayouts are pure structural functions and MUST be signal-free. Any state implies misclassification as Shell or UI component.**\n- Know what components are in zones (content-agnostic)\n- Have state management (no `RwSignal`)\n- Fetch data or call APIs\n- Emit callbacks or events\n- Manage z-index or stacking context (that's Shell's job)\n- Render specific components (pass everything as children)\n\n#### Canonical Example\n```rust\n// ✅ CORRECT: ZoneLayout\n#[component]\npub fn TwoColumnLayout(\n    left: ChildrenFn,\n    right: ChildrenFn,\n    #[prop(default = \"1fr 2fr\")] columns: &'static str,\n) -> impl IntoView {\n    view! {\n        <div\n            data-layout=\"two-column\"\n            style:grid-template-columns={columns}\n        >\n            <div data-zone=\"left\">\n                {left()}\n            </div>\n            <div data-zone=\"right\">\n                {right()}\n            </div>\n        </div>\n    }\n}\n```\n\n#### Forbidden Example State In ZoneLayout\n```rust\n// ❌ WRONG: State management in Zone\n#[component]\npub fn TwoColumnLayout(left: ChildrenFn, right: ChildrenFn) -> impl IntoView {\n    let (expanded, set_expanded) = signal(false);  // ❌ State\n    \n    view! {\n        <div data-layout=\"two-column\">\n            <div data-zone=\"left\" data-expanded={expanded.get()}>\n                {left()}\n                <Button on_click=move |_| set_expanded.update(|e| *e = !*e)>  // ❌\n                    \"Toggle\"\n                </Button>\n            </div>\n            <div data-zone=\"right\">{right()}</div>\n        </div>\n    }\n}\n```\n\n#### Forbidden Example Knowing Content\n```rust\n// ❌ WRONG: Layout knows about content\n#[component]\npub fn DashboardLayout() -> impl IntoView {\n    view! {\n        <div data-layout=\"dashboard\">\n            <div data-zone=\"sidebar\">\n                <DashboardNav />  // ❌ Should be passed as ChildrenFn\n            </div>\n            <div data-zone=\"main\">\n                <DashboardContent />  // ❌\n            </div>\n        </div>\n    }\n}\n```\n\n---\n\n## Universal Layout Contracts\n\n**ALL Layouts MUST follow these contracts:**\n\n### Structure Only\n**Define regions and positioning — nothing else.**\n```rust\n// ✅ CORRECT: Pure structure\n<div data-shell=\"\">\n    <header data-shell-header=\"\">\n        {header_content()}\n    </header>\n    <main data-shell-main=\"\">\n        <Outlet />\n    </main>\n</div>\n\n// ❌ WRONG: Mixed with logic\n<div data-shell=\"\">\n    <header>\n        {if user.is_admin() { /* admin header */ }}  // ❌\n    </header>\n</div>\n```\n\n### Content Agnostic\n**Accept content via slots — never render specific components.**\n```rust\n// ✅ CORRECT: Generic slots\n#[component]\npub fn Layout(\n    header: ChildrenFn,\n    main: ChildrenFn,\n) -> impl IntoView { /* ... */ }\n\n// ❌ WRONG: Specific components\n#[component]\npub fn Layout() -> impl IntoView {\n    view! {\n        <header><NavBar /></header>  // ❌ Knows about NavBar\n        <main><Dashboard /></main>   // ❌ Knows about Dashboard\n    }\n}\n```\n\n### No Business Logic\n**Zero domain logic, validation, or API calls.**\n```rust\n// ✅ CORRECT: Pure layout\nprovide_context(LayoutContext {\n    sidebar_collapsed: collapsed.read_only(),\n});\n\n// ❌ WRONG: Business logic\nlet user = fetch_user().await;  // ❌\nif user.is_authenticated() { /* ... */ }  // ❌\n```\n\n### No Routing Knowledge\n**Use `<Outlet />` for router content — don't know about paths.**\n```rust\n// ✅ CORRECT: Generic outlet\n<main><Outlet /></main>\n\n// ❌ WRONG: Path knowledge\n<main>\n    {if location == \"/dashboard\" { /* ... */ }}  // ❌\n</main>\n```\n\n### Responsive Structure\n**Handle responsive layout changes structurally, not logically.**\n```rust\n// ✅ CORRECT: CSS-based responsive\n<div data-shell=\"\" class=\"shell-responsive\">\n    // CSS handles breakpoints\n</div>\n\n// ❌ WRONG: Logic-based responsive\n{if is_mobile.get() {\n    view! { <MobileLayout /> }\n} else {\n    view! { <DesktopLayout /> }\n}}\n```\n\n---\n\n## Rationale\n\n### Why Layout Layer Exists\n\n1. **Separation of structure and content** — layout changes don't break features\n2. **Reusability** — same layout across multiple routes/pages\n3. **Testing simplicity** — layouts testable without domain context\n4. **Responsive design** — centralized breakpoint management\n5. **Z-index management** — global stacking context control\n\n### What This Rule Protects\n\n- **Structural stability** — layout changes isolated from business logic\n- **Feature independence** — features work in any layout\n- **Maintainability** — clear responsibility boundaries\n- **Testability** — layouts testable in isolation\n\n---\n\n## Enforcement\n\n### Static Analysis\n```rust\n// Check 1: No business logic\nif layout.contains(\"api::\") || layout.contains(\"fetch_\") {\n    emit_error!(\"Canon Rule #128: Layouts cannot call APIs\");\n}\n\n// Check 2: No domain state\nif layout.contains(\"use_context::<User>\") || layout.contains(\"use_context::<Cart>\") {\n    emit_error!(\"Canon Rule #128: Layouts cannot use domain context\");\n}\n\n// Check 3: No specific component imports\nfor import in layout.imports {\n    if import.path.contains(\"components/\") {\n        emit_error!(\"Canon Rule #128: Layouts cannot import domain components\");\n    }\n}\n```\n\n### CI Checks\n```bash\n# No API calls in layouts\nrg 'api::|fetch_|async fn' packages-rust/rs-design/src/layouts/ && exit 1\n\n# No domain context in layouts\nrg 'use_context::<User>|use_context::<Cart>' packages-rust/rs-design/src/layouts/ && exit 1\n\n# No routing paths in layouts\nrg '\"/dashboard\"|\"/settings\"|\"/profile\"' packages-rust/rs-design/src/layouts/ && exit 1\n```\n\n---\n\n## Exceptions\n\n### Exception 1: Layout-Specific Context (Allowed)\n```rust\n// ✅ ALLOWED: Layout state context\nprovide_context(LayoutContext {\n    sidebar_collapsed: collapsed.read_only(),\n    toggle_sidebar: callback,\n});\n```\n\n**When allowed:**\n- Context is purely structural (collapsed, expanded, visible)\n- Context does not contain domain data\n- Context controls layout behavior only\n\n### Exception 2: Router Outlet (Required)\n```rust\n// ✅ REQUIRED: Router integration\n<main data-shell-main=\"\">\n    <Outlet />  // Router outlet is mandatory in Shells\n</main>\n```\n\n**No other exceptions exist.**\n\n---\n\n## Quick Reference\n\n| Type | Structure | Content | State | Routing | Example |\n|------|-----------|---------|-------|---------|---------|\n| **Shell** | App-level | Slots + `<Outlet />` | ✅ Layout only | `<Outlet />` | `AppShell`, `DashboardShell` |\n| **Zone** | Page-level | `ChildrenFn` slots | ❌ None | ❌ None | `TwoColumnLayout`, `GridLayout` |\n\n---\n\n## Common Patterns\n\n### Pattern 1 Shell With Optional Regions\n```rust\n#[component]\npub fn AppShell(\n    #[prop(optional)] header: Option<ChildrenFn>,\n    #[prop(optional)] sidebar: Option<ChildrenFn>,\n    #[prop(optional)] footer: Option<ChildrenFn>,\n) -> impl IntoView {\n    view! {\n        <div data-shell=\"\">\n            {header.map(|h| view! { <header data-shell-header=\"\">{h()}</header> })}\n            {sidebar.map(|s| view! { <aside data-shell-sidebar=\"\">{s()}</aside> })}\n            <main data-shell-main=\"\"><Outlet /></main>\n            {footer.map(|f| view! { <footer data-shell-footer=\"\">{f()}</footer> })}\n        </div>\n    }\n}\n```\n\n### Pattern 2 Responsive Zone\n```rust\n#[component]\npub fn ResponsiveLayout(\n    sidebar: ChildrenFn,\n    main: ChildrenFn,\n) -> impl IntoView {\n    view! {\n        <div\n            data-layout=\"responsive\"\n            class=\"layout-responsive\"  // CSS handles breakpoints\n        >\n            <div data-zone=\"sidebar\">{sidebar()}</div>\n            <div data-zone=\"main\">{main()}</div>\n        </div>\n    }\n}\n```\n\n---\n\n## Related Rules\n\n- **Canon Rule #124:** Primitive Contract Types\n- **Canon Rule #125:** UI Component Contracts\n- **Canon Rule #126:** Component Domain Contracts\n- **Canon Rule #127:** Block Composition Contracts (previous rule)\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":129,"slug":"ssr-event-safety","title":"SSR Event Safety","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["ssr","hydration","events","render"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Event handlers attached to dynamically generated nodes during SSR cause hydration mismatches due to non-deterministic listener reconstruction. This leads to runtime panics.","problem":"dynamic event handlers in ssr lists break hydration determinism","solution":"avoid event handlers in iterators and use delegation or csr only patterns","signals":["unreachable panic","hydration error","event binding failure"],"search_intent":"why ssr events break in lists","keywords":["leptos ssr event handler issue","hydration mismatch dynamic list","event handler iterator problem","ssr closure mismatch leptos"],"body":"## Principle\n\n**Blocks MUST be treated as SSR-static structures. Interactivity in blocks MUST be delegated or CSR-only.**\n\n**Event handlers `on:*`) MUST NOT be attached to nodes generated dynamically by `map()`, `<For>`, or any iterator within SSR-rendered components.**\n\n---\n\n## The Problem\n\nWhen event handlers are attached to dynamically generated nodes during SSR:\n\n1. The server renders static HTML with no event listeners\n2. During hydration, the client tries to attach listeners to DOM nodes\n3. The DOM walking algorithm expects identical structure between SSR and CSR\n4. Dynamic closures create non-deterministic listener IDs\n5. Hydration fails with `RuntimeError: unreachable` panic\n\n**Observable symptoms:**\n- `RuntimeError: unreachable` in browser console\n- Panic occurs only after page load (during hydration)\n- Removing `on:click` makes the error disappear\n- Error happens even with empty handler: `on:click=|_| {}`\n\n---\n\n## Forbidden Patterns\n\n### Forbidden\n```rust\n// Event handler in map()\n{pillars.into_iter().map(|pillar| {\n    view! {\n        <button on:click=move |_| {\n            log!(\"Clicked: {}\", pillar.label);\n        }>\n            {pillar.label}\n        </button>\n    }\n}).collect_view()}\n```\n\n### Forbidden\n```rust\n// Event handler in <For>\n<For\n    each=move || items.get()\n    key=|item| item.id\n    children=|item| {\n        view! {\n            <button on:click=move |_| handle(item)>\n                \"Click\"\n            </button>\n        }\n    }\n/>\n```\n\n### Forbidden\n```rust\n// Event handler in component instantiated by iterator\n{cards.map(|card| {\n    view! {\n        <Card on_click=move |_| select(card.id) />\n    }\n})}\n```\n\n---\n\n## Canonical Pattern\n\n### Canonical Data Attributes For Event Delegation\n```rust\n// Use data-* attributes for identification\n{pillars.into_iter().map(|pillar| {\n    view! {\n        <button \n            class=\"pillar-item\"\n            attr:data-pillar-id={pillar.id.clone()}\n            attr:data-action=\"select\"\n        >\n            {pillar.label}\n        </button>\n    }\n}).collect_view()}\n\n// Event delegation handled externally (JavaScript or CSR-only wrapper)\n```\n\n### Canonical Child Component Pattern\n```rust\n// Move handler to stable child component\n{pillars.into_iter().map(|pillar| {\n    view! {\n        <PillarItem pillar=pillar />\n    }\n}).collect_view()}\n\n// Child component with CSR-only interactivity\n#[component]\nfn PillarItem(pillar: Pillar) -> impl IntoView {\n    #[cfg(not(feature = \"ssr\"))]\n    let handler = move |_| {\n        log!(\"Clicked: {}\", pillar.label);\n    };\n    \n    view! {\n        <button attr:data-pillar-id={pillar.id}>\n            {pillar.label}\n        </button>\n    }\n}\n```\n\n### Canonical CSR Only Wrapper\n```rust\n// Render static in SSR, add handlers in CSR\n#[cfg(not(feature = \"ssr\"))]\nfn attach_handlers() {\n    // JavaScript or WASM event delegation\n}\n```\n\n---\n\n## Rationale\n\nLeptos 0.7+ uses deterministic DOM walking for hydration. During hydration:\n\n1. The framework walks the DOM tree from SSR\n2. For each node, it expects a specific event listener at a specific index\n3. Dynamic closures in iterators generate non-deterministic listener IDs\n4. The mismatch causes an assertion failure: `unreachable`\n\nThis is a known limitation of Leptos SSR hydration and DOM walking. The server cannot serialize JavaScript closures. The client must reconstruct them identically.\n\n**Why this is CRITICAL:**\n- Breaks user interactivity silently\n- Only appears in production (SSR mode)\n- No helpful error message\n- Affects all list-based UIs\n\n---\n\n## Enforcement\n\n**Static Analysis:**\n- Lint rule: detect `on:*` inside `map()`, `<For>`, or iterators\n- Pattern match: `view! { ... on:click ... }` within iterator context\n\n**Runtime:**\n- Hydration panic in browser console\n- `console_error_panic_hook` shows no Rust stack trace (WASM assertion)\n\n**Code Review:**\n- Check for `on:*` in any dynamic list rendering\n- Verify CSR-only annotation or data-attributes pattern\n\n---\n\n## Exceptions\n\n**Exception 1: CSR-Only Components**\n```rust\n#[cfg(not(feature = \"ssr\"))]\n{items.map(|item| {\n    view! {\n        <button on:click=move |_| handle(item)>\"Click\"</button>\n    }\n})}\n```\nThis is acceptable because no SSR hydration occurs.\n\n**Exception 2: Single Static Instance**\nIf the component is NOT rendered by an iterator, handlers are safe:\n```rust\nview! {\n    <button on:click=move |_| handle()>\"Single button\"</button>\n}\n```\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":130,"slug":"controlled-ui-contract","title":"Controlled UI Contract","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["signals","state","reactivity","ui"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Passing plain values instead of signals to UI components breaks reactive updates and causes stale UI state. Components lose synchronization with application state.","problem":"ui components receive plain values instead of signals breaking reactivity","solution":"require ui components to accept signal or rwsignal for reactive state props","signals":["ui not updating","stale state","manual rerender"],"search_intent":"how to fix reactivity issues with","keywords":["leptos signal ui pattern","reactive state ui components","signal vs value leptos","frontend reactivity architecture"],"body":"## Principle\n\n**UI components that control reactive state MUST accept `Signal<T>` or `RwSignal<T>`, never plain `T` for stateful props.**\n\n---\n\n## The Problem\n\nWhen UI components accept plain types for reactive state:\n\n1. The component cannot react to state changes\n2. Parent components must call `.get()` to \"adapt\" the signal\n3. This breaks reactivity—updates don't propagate\n4. State becomes stale and UI diverges from reality\n\n**Observable symptoms:**\n- UI doesn't update when state changes\n- Dialog doesn't open/close when signal changes\n- Tabs don't switch when active tab signal changes\n- Components require manual re-rendering\n\n---\n\n## Forbidden Patterns\n\n### ❌ Forbidden\n```rust\n// UI accepts bool instead of Signal<bool>\n#[component]\npub fn Dialog(\n    children: ChildrenFn,\n    open: bool,  // ❌ Wrong!\n) -> impl IntoView {\n    view! {\n        <DialogPrimitive open={open}>\n            {children()}\n        </DialogPrimitive>\n    }\n}\n\n// Usage forces .get() and breaks reactivity\n<Dialog open=is_open.get()>  // ❌ Only reads once!\n    \"Content\"\n</Dialog>\n```\n\n### ❌ Forbidden\n```rust\n// Tabs accepts String instead of Signal<String>\n#[component]\npub fn Tabs(\n    active: String,  // ❌ Wrong!\n) -> impl IntoView {\n    // Cannot react to changes\n}\n```\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical — Signal-Based UI\n```rust\n// UI accepts Signal<bool>\n#[component]\npub fn Dialog(\n    children: ChildrenFn,\n    open: Signal<bool>,  // ✅ Correct!\n) -> impl IntoView {\n    view! {\n        <DialogPrimitive open=move || open.get()>\n            {children()}\n        </DialogPrimitive>\n    }\n}\n\n// Usage preserves reactivity\nlet (is_open, set_is_open) = signal(false);\n\nview! {\n    <Dialog open=is_open.into()>  // ✅ Reactive!\n        \"Content\"\n    </Dialog>\n}\n```\n\n### ✅ Canonical — Contract Flow\n```\nBlock/Page\n    ↓ Signal<T>\nUI Component\n    ↓ T (via .get())\nPrimitive\n```\n\n**Key insight:** The UI layer is responsible for calling `.get()` to snapshot the signal for the Primitive. The Block never calls `.get()`.\n\n---\n\n## Rationale\n\n**Separation of concerns:**\n- **Blocks/Pages**: Manage state (`Signal<T>`)\n- **UI Components**: Handle reactivity (`.get()` at render time)\n- **Primitives**: Render snapshots (plain `T`)\n\n**Why this matters:**\n1. **Reactivity**: UI components must respond to state changes\n2. **Single Responsibility**: Each layer has one job\n3. **Predictability**: State flows one direction\n4. **SSR Safety**: Signals work in both SSR and CSR\n\n**Anti-pattern consequence:**\nWhen UI accepts plain `T`, parent components call `.get()` too early. The value is \"frozen\" and updates are lost. This creates bugs that only appear at runtime.\n\n---\n\n## Enforcement\n\n**Type System:**\n- UI component signatures must use `Signal<T>` for reactive state\n- Compiler enforces this automatically\n\n**Code Review:**\n- Check UI component props\n- Verify no `.get()` calls in parent components passing to UI\n\n**Linter:**\n- Detect `#[component]` with state-like props (e.g., `open`, `active`, `selected`) that aren't `Signal<T>`\n\n---\n\n## Exceptions\n\n**Exception 1: Static Props**\nNon-reactive props like `variant`, `size`, `class` can be plain types:\n```rust\n#[component]\npub fn Button(\n    variant: ButtonVariant,  // ✅ OK - not reactive state\n    children: Children,\n) -> impl IntoView { ... }\n```\n\n**Exception 2: Primitives**\nPrimitives always accept plain types (they render snapshots):\n```rust\n#[component]\npub fn DialogPrimitive(\n    open: bool,  // ✅ OK - Primitive layer\n) -> impl IntoView { ... }\n```\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":131,"slug":"reactive-boundary-ownership","title":"Reactive Boundary Ownership","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["closure","ownership","storedvalue","reactivity"],"language":"EN","version":"1.0.0","date":"2025-01-22","intro":"Reactive boundaries re-execute closures multiple times, and moving non-Copy values into them causes FnOnce violations. This leads to compilation failures and unstable reactive behavior.","problem":"non copy values moved into reactive boundaries cause fnonce errors","solution":"clone simple values or use storedvalue for complex or stateful values","signals":["fnonce error","closure move error","ownership violation"],"search_intent":"how to fix fnonce errors in","keywords":["leptos reactive boundary ownership","storedvalue vs clone rust","closure ownership leptos","reactive closure fnonce error"],"body":"## Principle\n\n**Non-`Copy` values captured by reactive boundaries (`<Show>`, `<For>`, `<Suspense>`, `<Transition>`) MUST either:**\n- **be explicitly cloned, OR**\n- **use `StoredValue` when cloning is not appropriate**\n\n**Sub-principle:** `StoredValue` is NOT for pass-through props. It is for expensive or stateful values that need stable identity across reactive executions.\n\n---\n\n## The Problem\n\nReactive boundaries create closures that may execute multiple times. When non-`Copy` values are moved into these closures:\n\n1. Rust's ownership rules require `Fn` trait (callable multiple times)\n2. Moving non-`Copy` values makes the closure `FnOnce` (callable once)\n3. Leptos expects `Fn`, causing compilation failure\n4. Error: `expected Fn, found FnOnce`\n\n**Observable symptoms:**\n- `error[E0525]: expected a closure that implements the Fn trait, but this closure only implements FnOnce`\n- `closure is FnOnce because it moves the variable X out of its environment`\n- Compilation failure, not runtime error\n\n---\n\n## Forbidden Patterns\n\n### ❌ Forbidden — Moving String into Show\n```rust\n#[component]\npub fn TabsContent(\n    children: ChildrenFn,\n    class: Option<String>,\n) -> impl IntoView {\n    let class = class.unwrap_or_default();\n    \n    view! {\n        <Show when=is_active>\n            <TabsContentPrimitive class=class>  // ❌ Moves class\n                {children()}  // ❌ Also moves children\n            </TabsContentPrimitive>\n        </Show>\n    }\n}\n// Error: closure is FnOnce because it moves `class` and `children`\n```\n\n### ❌ Forbidden — Using StoredValue for Pass-Through Props\n```rust\n// ❌ ANTI-PATTERN! StoredValue is not for simple props\nlet class = StoredValue::new(class.unwrap_or_default());\nlet id = StoredValue::new(id.unwrap_or_default());\n\nview! {\n    <Show when=is_active>\n        <Primitive \n            class=class.get_value()  // ❌ Wrong use of StoredValue\n            id=id.get_value()        // ❌ Just clone instead!\n        />\n    </Show>\n}\n```\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical — Clone for Strings\n```rust\n#[component]\npub fn TabsContent(\n    children: ChildrenFn,\n    class: Option<String>,\n) -> impl IntoView {\n    let class = class.unwrap_or_default();\n    \n    view! {\n        <Show when=is_active>\n            <TabsContentPrimitive class=class.clone()>  // ✅ Explicit clone\n                {children()}\n            </TabsContentPrimitive>\n        </Show>\n    }\n}\n```\n\n### ✅ Canonical — StoredValue for ChildrenFn (Correct Use)\n```rust\n#[component]\npub fn Dialog(\n    children: ChildrenFn,\n    open: Signal<bool>,\n) -> impl IntoView {\n    let children = StoredValue::new(children);  // ✅ ChildrenFn needs StoredValue\n    \n    view! {\n        <Show when=open>\n            <DialogPrimitive>\n                {children.get_value()()}  // ✅ Call stored function\n            </DialogPrimitive>\n        </Show>\n    }\n}\n```\n\n### ✅ Canonical — Copy Types (No Extra Work)\n```rust\n#[component]\npub fn Tabs(\n    active_tab: Signal<usize>,  // ✅ usize is Copy\n) -> impl IntoView {\n    view! {\n        <Show when=move || active_tab.get() > 0>\n            // ✅ No clone needed - usize is Copy\n        </Show>\n    }\n}\n```\n\n---\n\n## Rationale\n\n**The problem:**\nReactive boundaries (`<Show>`, `<For>`, etc.) create closures that Leptos may call multiple times. Rust requires these closures to be `Fn`, which means they cannot move non-`Copy` values out of their environment.\n\n**Why clone is the default solution:**\n- Strings are cheap to clone (Rc-based internally)\n- Explicit cloning documents intent clearly\n- Preserves ownership semantics\n- No runtime overhead for reactive tracking\n\n**When StoredValue is justified:**\n`StoredValue` should be used ONLY when:\n1. The value is expensive to clone (e.g., large `Vec`, complex structs)\n2. The value needs stable identity across reactive executions\n3. The value is captured by multiple independent closures\n4. The value is a closure itself (e.g., `ChildrenFn`, `Callback`)\n\n**When StoredValue is WRONG:**\n- ❌ Pass-through props (`class`, `id`, `aria-*`)\n- ❌ Simple strings or primitives\n- ❌ As a \"quick fix\" for ownership errors\n- ❌ To avoid thinking about ownership\n\n**Critical insight:**\nUsing `StoredValue` for simple props creates false complexity and obscures data flow. It makes code harder to understand and maintain. Always prefer `clone()` unless you have a specific, documented reason to use `StoredValue`.\n\n---\n\n## Enforcement\n\n**Compiler:**\n- Rust's type system automatically catches violations\n- Error messages explicitly state \"`FnOnce` vs `Fn`\"\n\n**Code Review:**\n- **CRITICAL**: Challenge every `StoredValue` usage—demand justification\n- Ensure `clone()` is used for strings in reactive boundaries\n- Verify `ChildrenFn` and expensive types justify `StoredValue`\n\n**Linter:**\n- Detect unnecessary `StoredValue` for `String`, `Option<String>`, primitives\n- Flag missing `clone()` for `String` in reactive boundaries\n- Warn on `StoredValue` without accompanying comment explaining why\n\n---\n\n## Exceptions\n\n**Exception 1: Copy Types**\nNo special handling needed for `Copy` types:\n```rust\nlet count: i32 = 42;\nview! {\n    <Show when=is_active>\n        <div>{count}</div>  // ✅ Copy type - no clone needed\n    </Show>\n}\n```\n\n**Exception 2: Owned Values Consumed Once**\nIf the value is only used once and not in a reactive boundary:\n```rust\nview! {\n    <Primitive class=class>  // ✅ Direct move - no boundary\n        \"Content\"\n    </Primitive>\n}\n```\n\n**Exception 3: Closures and Functions**\n`ChildrenFn`, `Callback`, and other function types MUST use `StoredValue`:\n```rust\nlet children = StoredValue::new(children);  // ✅ Function type - justified\n```\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version (2025-01-22)"},{"number":132,"slug":"layout-composition-over-abstraction","title":"Layout Composition Over Abstraction","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["layout","composition","children","ownership"],"language":"EN","version":"1.0.0","date":"2025-01-23","intro":"Layouts that orchestrate composition or manage slots introduce ownership issues and hydration mismatches. This leads to unpredictable rendering and tight coupling.","problem":"layouts handle composition logic causing ownership and hydration issues","solution":"restrict layouts to structural wrappers and move composition to application layer","signals":["children move error","hydration mismatch","slot complexity"],"search_intent":"how to structure layouts without composition logic leptos","keywords":["layout composition pattern frontend","children ownership leptos","layout abstraction anti pattern","ssr layout issues leptos"],"body":"## Principle\n\n**Layouts MUST be structural wrappers and MUST NOT orchestrate composition logic or slot abstraction.**\n\n---\n\n## The Problem\n\nWhen layouts attempt to manage slots, render props, or complex composition:\n\n- Ownership errors with `Children` and `ViewFn`\n- SSR hydration mismatches\n- Non-deterministic render order\n- Tight coupling between layout and app-specific structure\n- Hard-to-debug move semantics failures\n\nThese issues surfaced directly in Leptos 0.8 during multi-slot layout design.\n\n---\n\n## Forbidden Patterns\n\n### ❌ Forbidden\n```rust\n#[component]\npub fn DashboardLayout(\n    sidebar: Children,\n    header: Children,\n    children: Children,\n) -> impl IntoView {\n    view! {\n        <Sidebar>{sidebar()}</Sidebar>\n        <Header>{header()}</Header>\n        <main>{children()}</main>\n    }\n}\n```\n\nLayouts MUST NOT orchestrate content wiring.\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n```rust\n#[component]\npub fn DashboardLayout(children: Children) -> impl IntoView {\n    view! {\n        <div class=\"layout-dashboard\" data-layout=\"dashboard\">\n            {children()}\n        </div>\n    }\n}\n```\n\nComposition is handled by the consuming application.\n\n---\n\n## Rationale\n\nLayouts define **structure and zones**, not **content relationships**.\n\nThis rule:\n- Preserves ownership clarity\n- Prevents SSR/CSR divergence\n- Keeps layouts reusable and stable\n\nThis is an architectural invariant.\n\n---\n\n## Enforcement\n\n- Code review rejection of slot-based layouts\n- Static detection of multiple `Children` in layouts\n- CI rule forbidding composition logic in layouts\n\n---\n\n## Exceptions\n\nNo exceptions. This rule is absolute.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":133,"slug":"children-consumption-locality","title":"Children Consumption Locality","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["children","ownership","closure","leptos"],"language":"EN","version":"1.0.0","date":"2025-01-23","intro":"Passing Children across layers causes move errors and closure violations because Children is FnOnce. This breaks SSR rendering and introduces ownership complexity.","problem":"children is forwarded across layers causing ownership and fnonce errors","solution":"consume children immediately at the closest render point","signals":["move error","fnonce closure","render failure"],"search_intent":"how to fix children ownership issues in leptos","keywords":["leptos children fnonce issue","children ownership rust ui","view macro children error","reactive rendering children leptos"],"body":"## Principle\n\n**`Children` MUST be consumed in the closest possible component to their final render location.**\n\n---\n\n## The Problem\n\n`Children` in Leptos is `FnOnce`. When passed across component layers:\n\n- Move errors occur\n- Closures created by `view!` cannot safely capture it\n- SSR rendering panics or fails\n- Workarounds introduce unnecessary indirection\n\nThis caused repeated architectural failures.\n\n---\n\n## Forbidden Patterns\n\n### ❌ Forbidden\n```rust\nfn Wrapper(children: Children) -> impl IntoView {\n    view! {\n        <Provider>\n            {children()}\n        </Provider>\n    }\n}\n```\n\nThis moves `Children` into a closure boundary.\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n```rust\nfn Wrapper(children: Children) -> impl IntoView {\n    let content = children();\n    view! {\n        <Provider>\n            {content}\n        </Provider>\n    }\n}\n```\n\nOr consume `Children` only in the final composing component.\n\n---\n\n## Rationale\n\nThis rule enforces:\n- Ownership determinism\n- Predictable SSR behavior\n- Clear reactive boundaries\n\nIt is a direct enforcement of reactive safety.\n\n---\n\n## Enforcement\n\n- Static analysis: detect `children()` inside `view!` closures\n- Code review rejection of forwarded `Children`\n- CI lint rule: `Children` must be consumed exactly once\n\n---\n\n## Exceptions\n\nNo exceptions. This rule is absolute.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":134,"slug":"layouts-are-css-and-semantics-only","title":"Layouts Are CSS and Semantics Only","status":"ENFORCED","severity":"HIGH","category":"styling-css","tags":["layout","css","semantics","structure"],"language":"EN","version":"1.0.0","date":"2025-01-23","intro":"Embedding behavior or state inside layouts breaks structural purity and introduces coupling with UI logic. Layouts must remain deterministic and CSS-driven.","problem":"layouts include behavior or state breaking structural determinism","solution":"restrict layouts to semantic containers and css structure only","signals":["state in layout","event in layout","layout coupling"],"search_intent":"why layouts must be stateless","keywords":["layout css semantics only","stateless layout architecture","leptos layout purity","structure vs behavior separation"],"body":"## Principle\n\n**Layouts MUST provide only semantic containers and CSS contracts, never behavioral or reactive logic.**\n\n---\n\n## The Problem\n\nWhen layouts include logic beyond structure:\n\n- Layouts become stateful implicitly\n- UI behavior leaks into structure\n- CSS zones lose determinism\n- SSR/CSR boundaries become unclear\n- Layout reuse across apps breaks\n\nThis leads to fragile layouts that cannot be reasoned about statically.\n\n---\n\n## Forbidden Patterns\n\n### Forbidden\n```rust\n#[component]\npub fn Layout(children: Children) -> impl IntoView {\n    let open = create_rw_signal(true);\n\n    view! {\n        <div>\n            <button on:click=move |_| open.set(false) />\n            {children()}\n        </div>\n    }\n}\n```\n\nLayouts MUST NOT manage state or events.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```rust\n#[component]\npub fn Layout(children: Children) -> impl IntoView {\n    view! {\n        <div class=\"layout\" data-layout=\"app\">\n            {children()}\n        </div>\n    }\n}\n```\n\nAll behavior is delegated to UI or Controllers.\n\n---\n\n## Rationale\n\nLayouts define **where things live**, not **how they behave**.\n\nThis rule:\n- Preserves layout purity\n- Guarantees CSS zone correctness\n- Enables static reasoning\n- Aligns with Canon Layout Zones Contract\n\n---\n\n## Enforcement\n\n- Lint: forbid signals and event handlers in layouts\n- Code review: layouts must be stateless\n- CI: detect reactive primitives in `layouts/`\n\n---\n\n## Exceptions\n\nNo exceptions. This rule is absolute.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":135,"slug":"ui-vs-layout-responsibility-boundary","title":"UI vs Layout Responsibility Boundary","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["ui","layout","architecture","separation"],"language":"EN","version":"1.0.0","date":"2025-01-23","intro":"Blurring responsibilities between UI and layouts creates coupling and breaks system evolution. Each layer must own a distinct concern.","problem":"ui and layout responsibilities mix causing architectural coupling","solution":"enforce strict boundary where layout handles zones and ui handles behavior","signals":["provider in layout","behavior in layout","coupled layers"],"search_intent":"how to separate ui and layout","keywords":["ui layout separation pattern","architecture boundary ui layout","leptos layout responsibility","component layering rules"],"body":"## Principle\n\n**Layouts define spatial zones; UI components define interaction and behavior.**\n\n---\n\n## The Problem\n\nWhen UI behavior leaks into layouts, or layouts leak into UI:\n\n- Responsibilities blur\n- Controllers attach to the wrong layer\n- CSS zones become coupled to behavior\n- Refactors break unrelated areas\n\nThis was observed in sidebar and header orchestration attempts.\n\n---\n\n## Forbidden Patterns\n\n### Forbidden\n```rust\n// Layout controlling UI behavior\n#[component]\npub fn DashboardLayout(children: Children) -> impl IntoView {\n    view! {\n        <SidebarProvider>\n            {children()}\n        </SidebarProvider>\n    }\n}\n```\n\nProviders and behavior belong to UI or Controllers, not layouts.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```rust\n// Layout = zones only\n#[component]\npub fn DashboardLayout(children: Children) -> impl IntoView {\n    view! {\n        <div data-layout=\"dashboard\" class=\"layout-dashboard\">\n            {children()}\n        </div>\n    }\n}\n```\n\n```rust\n// UI controls behavior\n<SidebarProvider>\n    <Sidebar />\n</SidebarProvider>\n```\n\n---\n\n## Rationale\n\nThis boundary ensures:\n- Predictable ownership\n- Clear mental model\n- Independent evolution of layout and UI\n- Enforcement of Canon Zone Contracts\n\nIt is foundational for large systems.\n\n---\n\n## Enforcement\n\n- CI rule: Layouts must not import Providers\n- Static scan: forbid `ui::*` behavior imports in `layouts/`\n- Code review boundary checks\n\n---\n\n## Exceptions\n\nNo exceptions. This rule is absolute.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":136,"slug":"controllers-are-csr-only","title":"Controllers Are CSR-Only","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["csr","ssr","controllers","wasm"],"language":"EN","version":"1.0.0","date":"2025-01-23","intro":"Executing controllers during SSR introduces hydration mismatches and runtime errors due to browser-only APIs. Controllers must run exclusively in CSR.","problem":"controllers executed during ssr cause hydration mismatch and runtime errors","solution":"gate controllers with wasm target and execute only in csr","signals":["hydration mismatch","window undefined","double execution"],"search_intent":"why controllers must be csr only","keywords":["csr only controller pattern","ssr controller error leptos","wasm controller gating","hydration mismatch controller"],"body":"## Principle\n\n**Controllers MUST exist and execute exclusively in CSR (wasm32), never during SSR.**\n\n---\n\n## The Problem\n\nWhen Controllers run during SSR:\n\n- Hydration mismatches occur\n- Event handlers bind to non-existent DOM\n- Side effects execute twice (SSR + CSR)\n- Panics happen due to browser-only APIs\n- Debugging becomes non-deterministic\n\nThis directly violates SSR safety guarantees.\n\n---\n\n## Forbidden Patterns\n\n### Forbidden\n```rust\n#[component]\npub fn SidebarController() -> impl IntoView {\n    // Runs during SSR — forbidden\n    create_effect(move |_| {\n        web_sys::window().unwrap();\n    });\n\n    view! { <></> }\n}\n```\n\nControllers MUST NOT exist without CSR gating.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```rust\n#[cfg(target_arch = \"wasm32\")]\n#[component]\npub fn SidebarController() -> impl IntoView {\n    create_effect(move |_| {\n        // Browser-only logic\n    });\n\n    view! { <></> }\n}\n```\n\n```rust\n// Usage\n{#[cfg(target_arch = \"wasm32\")]\n { view! { <SidebarController /> } }}\n```\n\n---\n\n## Rationale\n\nControllers orchestrate **behavior**, not structure.\n\nSSR renders structure only.\nCSR attaches behavior afterward.\n\nThis rule:\n- Guarantees hydration correctness\n- Prevents double execution\n- Enforces clean SSR/CSR boundary\n- Aligns with Canon Rule #129 (SSR Event Safety)\n\n---\n\n## Enforcement\n\n- CI: forbid Controllers without `cfg(target_arch = \"wasm32\")`\n- Static scan: detect browser APIs in non-CSR code\n- Code review checklist\n\n---\n\n## Exceptions\n\nNo exceptions. This rule is absolute.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":137,"slug":"providers-must-have-single-owner","title":"Providers Must Have a Single Owner","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["providers","state","ownership","context"],"language":"EN","version":"1.0.0","date":"2025-01-23","intro":"Multiple provider instantiations create duplicated state and desynchronized UI. A single ownership model is required for consistent state flow.","problem":"multiple providers create duplicated state and inconsistent ui","solution":"ensure each provider has a single owning component controlling lifecycle","signals":["duplicate provider","state desync","multiple contexts"],"search_intent":"why provider must have single owner","keywords":["provider single owner pattern","state duplication context issue","leptos provider ownership","context lifecycle architecture"],"body":"## Principle\n\n**Every Provider MUST have exactly one owning component responsible for its lifecycle.**\n\n---\n\n## The Problem\n\nWhen Providers are instantiated in multiple places:\n\n- State becomes duplicated\n- Signals diverge silently\n- UI desynchronizes\n- Debugging becomes impossible\n- Ownership is unclear\n\nThis was observed when Providers were placed in both Layouts and Blocks.\n\n---\n\n## Forbidden Patterns\n\n### Forbidden\n```rust\n// Provider inside a Block\n#[component]\npub fn SidebarBlock(children: Children) -> impl IntoView {\n    view! {\n        <SidebarProvider>\n            {children()}\n        </SidebarProvider>\n    }\n}\n```\n\nBlocks MUST NOT own Providers.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```rust\n// Provider owned by App-level composition\n<SidebarProvider>\n    <Sidebar />\n    <SidebarInset />\n</SidebarProvider>\n```\n\n```rust\n// Blocks consume context only\n#[component]\npub fn SidebarBlock(children: Children) -> impl IntoView {\n    view! {\n        <Sidebar>{children()}</Sidebar>\n    }\n}\n```\n\n---\n\n## Rationale\n\nProviders define **state boundaries**.\n\nA single owner ensures:\n- Predictable state flow\n- Deterministic reactivity\n- Clear architectural layering\n- Compatibility with SSR/CSR split\n\nThis is a core invariant of Canon UI architecture.\n\n---\n\n## Enforcement\n\n- CI: forbid Provider instantiation in Blocks\n- Static analysis: detect duplicated Providers\n- Code review ownership checks\n\n---\n\n## Exceptions\n\nNo exceptions. This rule is absolute.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":138,"slug":"children-must-be-consumed-immediately","title":"Children Must Be Consumed Immediately","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["children","leptos","ownership","render"],"language":"EN","version":"1.0.0","date":"2025-01-23","intro":"Forwarding or storing Children breaks ownership semantics in Leptos due to FnOnce behavior. This leads to compilation errors and unstable rendering.","problem":"children forwarding breaks ownership and causes render instability","solution":"consume children immediately in the receiving component","signals":["move error","ownership violation","render failure"],"search_intent":"why children must be consumed leptos","keywords":["leptos children ownership","fnonce children issue","children forwarding error","component render ownership"],"body":"## Principle\n\n**`Children` MUST be consumed immediately in the component that receives them and MUST NOT be forwarded across abstraction layers.**\n\n---\n\n## The Problem\n\nIn Leptos, `Children` is implemented as `FnOnce`.\n\nWhen `Children` is:\n- stored,\n- forwarded,\n- captured by closures,\n- or passed through multiple components,\n\nthe result is:\n\n- Ownership violations\n- Move errors during compilation\n- Non-deterministic render behavior\n- Incompatibility with `view!` macro closures\n- SSR/CSR boundary breakage\n\nThis issue surfaced repeatedly when Layouts attempted to forward `Children` to Blocks or UI components.\n\n---\n\n## Forbidden Patterns\n\n### Forbidden\n```rust\n#[component]\npub fn DashboardLayout(children: Children) -> impl IntoView {\n    view! {\n        <SidebarProvider>\n            {children()} // ❌ consumed inside closure, not immediately\n        </SidebarProvider>\n    }\n}\n```\n\n### Forbidden\n```rust\n#[component]\npub fn LayoutWrapper(children: Children) -> impl IntoView {\n    let slot = children; // ❌ storing Children\n    view! { <div /> }\n}\n```\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```rust\n#[component]\npub fn DashboardLayout(children: Children) -> impl IntoView {\n    let content = children(); // ✅ consume immediately\n\n    view! {\n        <div data-layout=\"dashboard\">\n            {content}\n        </div>\n    }\n}\n```\n\n### Canonical (Preferred)\n```rust\n// Layouts act as wrappers only\n#[component]\npub fn DashboardLayout(children: Children) -> impl IntoView {\n    view! {\n        <div data-layout=\"dashboard\">\n            {children()}\n        </div>\n    }\n}\n```\n\nComposition MUST happen at the application level.\n\n---\n\n## Rationale\n\nThis rule protects:\n\n- Rust ownership invariants\n- Leptos rendering model\n- SSR determinism\n- Architectural clarity\n\nLayouts are **structural**, not compositional routers.\n\nPassing `Children` across layers introduces hidden ownership coupling and violates Leptos execution semantics.\n\n---\n\n## Enforcement\n\n- Static analysis: detect forwarding of `Children`\n- CI: forbid storing `Children` in structs or closures\n- Code review checklist\n\n---\n\n## Exceptions\n\nNo exceptions. This rule is absolute.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":139,"slug":"slots-are-ui-level-only","title":"Slots Are UI-Level Only","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["slots","ui","layout","ownership"],"language":"EN","version":"1.0.0","date":"2025-01-23","intro":"Using slots in layouts introduces ownership complexity and breaks SSR safety due to Fn and lifetime boundaries. This leads to brittle composition and logic-heavy layouts.","problem":"slots are defined in layouts causing ownership and composition issues","solution":"restrict slots to ui components and keep layouts purely compositional","signals":["ownership explosion","fnonce issues","layout complexity"],"search_intent":"how to use slots correctly in ui components","keywords":["ui slots pattern","leptos slot ownership","layout vs ui slots","component slot architecture"],"body":"## Principle\n\n**Component slots MUST be defined and consumed exclusively at the UI component level, never in Layouts or Shells.**\n\n---\n\n## The Problem\n\nSlots introduce:\n\n- Typed child ownership\n- Complex lifetime semantics\n- Fn/FnOnce boundaries\n- Render-time branching\n\nWhen used in Layouts:\n\n- Ownership explodes across layers\n- SSR execution becomes unsafe\n- Layouts become logic-heavy\n- Composition becomes brittle\n\nThis directly contradicts Canon layout philosophy.\n\n---\n\n## Forbidden Patterns\n\n### ❌ Forbidden\n```rust\n#[component]\npub fn DashboardLayout(\n    sidebar: SidebarSlot,\n    header: HeaderSlot,\n) -> impl IntoView {\n    view! { ... }\n}\n```\n\nLayouts MUST NOT define slots.\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n```rust\n// UI-level component\n#[slot]\npub struct HeaderActions {\n    children: ChildrenFn,\n}\n\n#[component]\npub fn Header(actions: HeaderActions) -> impl IntoView {\n    view! {\n        <header>\n            {(actions.children)()}\n        </header>\n    }\n}\n```\n\n```rust\n// Layout uses composition only\n#[component]\npub fn DashboardLayout(children: Children) -> impl IntoView {\n    view! {\n        <div data-layout=\"dashboard\">\n            {children()}\n        </div>\n    }\n}\n```\n\n---\n\n## Rationale\n\nLayouts define **where things go**, not **what they are**.\n\nSlots define **what goes inside a component**.\n\nMixing the two:\n- Violates separation of concerns\n- Breaks ownership guarantees\n- Makes layouts untestable\n- Prevents reuse across apps\n\n---\n\n## Enforcement\n\n- CI: forbid `#[slot]` usage in layouts\n- Static scan: slot structs under `/layouts`\n- Code review validation\n\n---\n\n## Exceptions\n\nNo exceptions. This rule is absolute.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":140,"slug":"layouts-are-structural-not-behavioral","title":"Layouts Are Structural, Not Behavioral","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["layout","structure","architecture","ssr"],"language":"EN","version":"1.0.0","date":"2025-01-23","intro":"Layouts containing behavior or state introduce coupling and break SSR determinism. They must remain purely structural.","problem":"behavior in layouts breaks ssr determinism and reuse","solution":"keep layouts strictly structural with no state or logic","signals":["state in layout","event handler in layout","logic in layout"],"search_intent":"why layouts must be structural only","keywords":["layout structural only rule","leptos layout behavior issue","ssr deterministic layout","layout architecture separation"],"body":"## Principle\n\n**Layouts MUST define structure and zones only and MUST NOT contain behavior, state, or logic.**\n\n---\n\n## The Problem\n\nWhen layouts contain behavior:\n\n- State leaks across routes\n- Ownership rules are violated\n- SSR execution becomes non-deterministic\n- Layouts become tightly coupled to app logic\n- Reuse across products becomes impossible\n\nThis manifested as:\n- Signals inside layouts\n- Providers with implicit logic\n- Event handlers embedded in layout components\n- Conditional rendering based on business state\n\n---\n\n## Forbidden Patterns\n\n### Forbidden\n```rust\n#[component]\npub fn DashboardLayout(children: Children) -> impl IntoView {\n    let open = create_rw_signal(true); // ❌ state in layout\n\n    view! {\n        <div>\n            <button on:click=move |_| open.update(|v| *v = !*v) />\n            {children()}\n        </div>\n    }\n}\n```\n\n### Forbidden\n```rust\n#[component]\npub fn LayoutWithLogic(children: Children) -> impl IntoView {\n    <Show when=some_condition>\n        {children()}\n    </Show>\n}\n```\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```rust\n#[component]\npub fn DashboardLayout(children: Children) -> impl IntoView {\n    view! {\n        <div data-layout=\"dashboard\" class=\"layout-dashboard\">\n            {children()}\n        </div>\n    }\n}\n```\n\nBehavior and state MUST live in:\n- Controllers (CSR-only)\n- Application composition layer\n- Dedicated UI components\n\n---\n\n## Rationale\n\nLayouts are **spatial contracts**, not interactive entities.\n\nTheir role is to:\n- Define zones\n- Apply CSS structure\n- Provide semantic wrappers\n\nBy keeping layouts behavior-free, we guarantee:\n- SSR safety\n- Ownership simplicity\n- Compositional freedom\n- Predictable rendering\n\n---\n\n## Enforcement\n\n- Static analysis: forbid signals in layouts\n- CI: block event handlers in `/layouts`\n- Code review checklist\n\n---\n\n## Exceptions\n\nNo exceptions. This rule is absolute.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":141,"slug":"csr-composition-belongs-to-app-layer","title":"CSR Composition Belongs to the App Layer","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["csr","architecture","composition","ssr"],"language":"EN","version":"1.0.0","date":"2025-01-23","intro":"Placing CSR behavior outside the application layer introduces SSR crashes, hydration issues, and runtime leakage. Design systems and layouts become environment-aware and lose portability.","problem":"csr logic outside app layer breaks ssr boundaries and architecture separation","solution":"centralize all csr composition and orchestration in the application layer only","signals":["hydration mismatch","cfg wasm leakage","ssr crash","logic duplication"],"search_intent":"where to place csr logic in architecture","keywords":["csr composition app layer","leptos csr architecture","ssr csr separation pattern","runtime logic layering ui"],"body":"## Principle\n\n**All CSR-only composition, event wiring, and behavioral orchestration MUST occur in the application layer, never in layouts or design-system components.**\n\n---\n\n## The Problem\n\nWhen CSR behavior is embedded outside the app layer:\n\n- SSR crashes or hydrates incorrectly\n- `cfg(target_arch = \"wasm32\")` leaks everywhere\n- Design System becomes environment-aware\n- Logic duplication across products\n- Impossible-to-test side effects\n\nThis occurred when:\n- Controllers were placed inside layouts\n- UI components embedded CSR logic\n- Layouts attempted to orchestrate events\n\n---\n\n## Forbidden Patterns\n\n### ❌ Forbidden\n```rust\n#[component]\npub fn DashboardLayout(children: Children) -> impl IntoView {\n    view! {\n        <div>\n            {children()}\n            <SidebarController /> // ❌ CSR logic in layout\n        </div>\n    }\n}\n```\n\n### ❌ Forbidden\n```rust\n#[component]\npub fn Sidebar() -> impl IntoView {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        // ❌ UI component aware of runtime\n        document().add_event_listener(...)\n    }\n}\n```\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n```rust\n#[component]\npub fn AppLayout() -> impl IntoView {\n    view! {\n        <DashboardLayout>\n            <SidebarProvider>\n                <Sidebar />\n                <SidebarInset />\n            </SidebarProvider>\n\n            {#[cfg(target_arch = \"wasm32\")]\n            { view! { <SidebarController /> } }}\n        </DashboardLayout>\n    }\n}\n```\n\n- CSR logic is colocated with routing\n- Layouts remain pure\n- Design System remains SSR-safe\n\n---\n\n## Rationale\n\nThe application layer is the **only layer aware of runtime context**.\n\nThis separation guarantees:\n- Clean SSR boundaries\n- Portable Design System\n- Zero conditional compilation leakage\n- Clear ownership of side effects\n\nCSR orchestration is **not reusable UI** — it is app-specific glue.\n\n---\n\n## Enforcement\n\n- CI: forbid controllers in `/layouts` and `/packages-rust`\n- Lint: detect `cfg(target_arch)` outside app layer\n- Code review enforcement\n\n---\n\n## Exceptions\n\nNo exceptions. This rule is absolute.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":142,"slug":"children-must-always-be-optional","title":"canon-rule-142-children-must-always-be-optional","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["components","api","children","ui"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Requiring children as mandatory props creates brittle APIs and breaks composability. Components must support absence of children without structural failure.","problem":"mandatory children props create rigid and breaking component apis","solution":"declare children as optional and safely render when present","signals":["component requires children","unwrap children panic","composition break"],"search_intent":"how to make children optional leptos","keywords":["leptos optional children prop","component api flexibility","children option pattern ui","safe children rendering leptos"],"body":"## Context\nPublic UI components frequently evolve. Requiring `children` as a mandatory prop creates brittle APIs, breaks composability, and forces artificial wrappers.\n\nIn CanonRS, **absence of children must never break a component**.\n\n## Rule\nAll public components (UI, Components, Blocks) **MUST declare `children` as optional**.\n\n## Mandatory Pattern\n```rust\n#[component]\npub fn Component(\n    #[prop(optional)] children: Option<Children>,\n) -> impl IntoView {\n    view! {\n        <div>\n            {children.map(|c| c())}\n        </div>\n    }\n}\n```\n\n## Forbidden Patterns\n- `children: Children` without `#[prop(optional)]`\n- Calling `{children()}` without checking `Option`\n- Enforcing structural children in UI-level components\n\n## Rationale\n- Prevents breaking changes\n- Enables progressive composition\n- Preserves backward compatibility\n- Eliminates artificial layout wrappers\n\n## Scope\n- UI\n- Components\n- Blocks\n\n## Exception\nNone.\n\nChildren are optional by design."},{"number":143,"slug":"ui-must-be-hydration-deterministic","title":"canon-rule-143-ui-must-be-hydration-deterministic","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","ssr","ui","determinism"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"SSR hydration requires identical HTML between server and client. Reactive logic in rendered output creates mismatches and causes runtime failures.","problem":"non deterministic ui output breaks hydration and causes mismatches","solution":"render static deterministic html during ssr and defer behavior to events","signals":["hydration panic","dom mismatch","runtime error"],"search_intent":"why hydration mismatch happens in leptos","keywords":["leptos hydration deterministic ui","ssr csr mismatch html","hydration error leptos","deterministic rendering ssr"],"body":"## Context\nSSR + Hydration requires **bit-for-bit identical HTML** between server and client at hydration time.\n\nReactive closures or runtime-dependent logic inside rendered markup cause fatal hydration mismatches.\n\n## Rule\nUI components **MUST render deterministic, static HTML during SSR**.\n\nReactive logic **MUST NOT** influence rendered structure or text during hydration.\n\n## Forbidden Patterns\n```rust\n{move || signal.get()}\n{match context.get() { ... }}\n{if runtime_condition { ... }}\n```\n\n## Allowed Patterns\n- Static literals (`\"🌓\"`)\n- CSS-driven state via `data-*` attributes\n- Event handlers mutating state **after hydration**\n\n## Correct Pattern\n```rust\n<Button on_click=toggle>\n    \"🌓\"\n</Button>\n```\n\n## Rationale\n- Prevents hydration panics\n- Ensures SSR safety\n- Keeps UI predictable\n- Separates rendering from behavior\n\n## Scope\n- UI components\n- Blocks\n- Layouts\n\n## Exception\nNone.\n\nHydration determinism is non-negotiable."},{"number":144,"slug":"providers-expose-state-apps-own-interaction","title":"canon-rule-144-providers-expose-state-apps-own-interaction","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["providers","state","architecture","ui"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Embedding interaction logic inside providers creates coupling and breaks ownership boundaries. Providers must expose state only, leaving interaction decisions to the application layer.","problem":"providers include interaction logic causing coupling and ownership violations","solution":"limit providers to state exposure and mutation apis only","signals":["provider renders ui","implicit interaction","state coupling"],"search_intent":"how to separate provider state and ui logic","keywords":["provider state separation pattern","leptos provider architecture","state vs interaction boundary","context provider design rules"],"body":"## Context\nProviders are architectural primitives responsible for **state exposure**, not user interaction.\nEmbedding toggles, buttons, or UI logic inside Providers breaks ownership boundaries and causes coupling.\n\n## Rule\nProviders **MUST ONLY expose state and mutation APIs**.\nThey **MUST NOT** render interaction UI (toggles, buttons, menus).\n\n## Mandatory Pattern\n```rust\npub struct ThemeContext {\n    pub mode: RwSignal<ThemeMode>,\n}\n\nimpl ThemeContext {\n    pub fn set_mode(&self, mode: ThemeMode) {\n        self.mode.set(mode);\n    }\n}\n```\n\nUI decides *how* and *when* to call mutations.\n\n## Forbidden Patterns\n- `ThemeToggle` inside `ThemeProvider`\n- Rendering buttons inside Providers\n- Providers reacting to DOM events\n\n## Rationale\n- Enforces single ownership of behavior\n- Keeps Providers SSR-safe\n- Prevents implicit UX decisions\n- Enables multiple UIs over the same state\n\n## Scope\n- Providers\n- Context types\n\n## Exception\nNone.\n\nProviders expose state. Applications decide interaction."},{"number":145,"slug":"ui-must-not-mutate-global-state-implicitly","title":"canon-rule-145-ui-must-not-mutate-global-state-implicitly","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["ui","state","callbacks","architecture"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Implicit global state mutations hide side effects and reduce predictability. UI components must expose behavior via explicit callbacks instead of mutating shared state directly.","problem":"ui components mutate global state implicitly causing hidden side effects","solution":"enforce explicit callbacks for all state mutations from ui components","signals":["hidden state change","implicit side effect","context mutation in ui"],"search_intent":"how to avoid implicit state mutation ui","keywords":["explicit callbacks ui pattern","avoid implicit state mutation","leptos ui state handling","callback driven state architecture"],"body":"## Context\nImplicit global state mutation hides side effects and breaks predictability.\nUI components must be transparent: behavior is explicit via callbacks.\n\n## Rule\nUI components **MUST NOT mutate global state directly**.\nAll state changes **MUST occur via explicit callbacks or signals passed by the consumer**.\n\n## Mandatory Pattern\n```rust\n#[component]\npub fn Button(\n    #[prop(default = Callback::new(|_| {}))] on_click: Callback<()>,\n) -> impl IntoView {\n    view! {\n        <button on:click=move |_| on_click(())>\n            \"Click\"\n        </button>\n    }\n}\n```\n\n## Forbidden Patterns\n- `use_theme().set_mode(...)` inside UI\n- Accessing Provider context internally to mutate state\n- Side effects hidden inside render logic\n\n## Rationale\n- Makes data flow explicit\n- Simplifies reasoning and debugging\n- Enables reuse in different contexts\n- Prevents architectural leakage\n\n## Scope\n- UI components\n- Blocks\n\n## Exception\nControllers may coordinate state explicitly, never implicitly.\n\nUI renders. Callbacks act."},{"number":146,"slug":"ui-content-must-be-ssr-stable","title":"canon-rule-146-ui-content-must-be-ssr-stable","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["ssr","hydration","ui","content"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Dynamic content rendered during SSR creates mismatches during hydration due to DOM differences. UI must produce stable content identical across server and client.","problem":"dynamic content in ssr causes hydration mismatch and runtime errors","solution":"ensure all rendered content is static during ssr and defer changes to events","signals":["hydration panic","unreachable error","dom mismatch"],"search_intent":"how to fix ssr content mismatch","keywords":["ssr stable content leptos","hydration mismatch content","static rendering ssr ui","leptos ssr content rules"],"body":"## Context\nLeptos SSR performs DOM walking during hydration.\nAny difference between server-rendered HTML and initial client HTML causes fatal hydration errors.\n\nDynamic UI content rendered via reactive closures inside structural nodes is a primary source of mismatch.\n\n## Rule\nUI components **MUST render SSR-stable content**.\nReactive closures **MUST NOT** be used to generate structural or textual content that differs between SSR and CSR.\n\n## Mandatory Pattern\n```rust\n<Button>\n    \"🌓\"\n</Button>\n```\n\nState changes are handled via callbacks, not conditional rendering.\n\n## Forbidden Patterns\n```rust\n<Button>\n    {move || if theme.get() == Dark { \"🌙\" } else { \"☀️\" }}\n</Button>\n```\n\n## Rationale\n- Guarantees hydration safety\n- Prevents unreachable panic\n- Makes SSR deterministic\n- Separates state mutation from render shape\n\n## Scope\n- UI components\n- Blocks\n- Layout shells\n\n## Exception\nNone.\n\nIf content can differ, it must not be reactive."},{"number":147,"slug":"reactive-closures-are-data-not-structure","title":"canon-rule-147-reactive-closures-are-data-not-structure","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["closures","reactivity","ssr","ui"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Reactive closures evaluated differently in SSR and CSR cause structural instability when used for rendering decisions. They must be limited to data binding only.","problem":"reactive closures used for structure cause hydration mismatch and instability","solution":"restrict reactive closures to data binding and use Show for structure","signals":["hydration mismatch","unstable dom","render inconsistency"],"search_intent":"how to use reactive closures correctly leptos","keywords":["reactive closures leptos rules","data binding vs structure","leptos show conditional rendering","hydration safe closures"],"body":"## Context\nReactive closures in Leptos are evaluated at different times in SSR and CSR.\nUsing them to control DOM structure or textual nodes creates instability.\n\n## Rule\nReactive closures **MUST ONLY be used for data binding**, never for:\n- Conditional rendering\n- Structural decisions\n- Text node substitution\n\n## Allowed Usage\n```rust\n<input value={move || value.get()} />\n```\n\n## Forbidden Usage\n```rust\n<div>\n    {move || if open.get() { view! { <Panel/> } } }\n</div>\n```\n\n## Correct Pattern\n```rust\n<Show when=move || open.get()>\n    <Panel />\n</Show>\n```\n\n## Rationale\n- Preserves DOM shape\n- Aligns SSR and CSR execution\n- Makes intent explicit\n- Prevents hydration panics\n\n## Scope\n- UI\n- Components\n- Blocks\n\n## Exception\nNone.\n\nReactive closures bind values, not structure."},{"number":148,"slug":"ui-must-never-infer-intent-from-state","title":"canon-rule-148-ui-must-never-infer-intent-from-state","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["state","intent","ui","callbacks"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Inferring intent from state transitions introduces implicit behavior and duplicated logic in UI components. This leads to unpredictable flows and hidden side effects.","problem":"ui infers intent from state changes causing implicit behavior","solution":"express intent explicitly via callbacks or commands and update state after","signals":["implicit logic","duplicated behavior","side effects"],"search_intent":"how to separate intent from state in ui","keywords":["intent vs state ui","explicit callbacks pattern","state transition logic frontend","ui event architecture"],"body":"## Context\nState represents **what is**, not **why it changed**.\nInferring intent from state leads to implicit behavior, duplicated logic and unpredictable flows.\n\nThis mistake appears when UI tries to \"guess\" actions based on state transitions.\n\n## Rule\nUI components **MUST NOT infer intent from state changes**.\nIntent **MUST be explicit**, expressed via callbacks or commands.\n\n## Forbidden Pattern\n```rust\nif open.get() {\n    on_opened.emit(())\n}\n```\n\n## Correct Pattern\n```rust\non_toggle.emit(ToggleIntent::Open)\n```\n\nState updates happen **after** intent emission.\n\n## Rationale\n- Separates cause from effect\n- Prevents hidden side-effects\n- Enables deterministic flows\n- Makes audit and testing trivial\n\n## Scope\n- UI\n- Components\n- Controllers\n\n## Exception\nNone.\n\nState is data. Intent is an event."},{"number":149,"slug":"controllers-must-be-temporal-not-structural","title":"canon-rule-149-controllers-must-be-temporal-not-structural","status":"ENFORCED","severity":"HIGH","category":"behavior","tags":["controllers","architecture","async","coordination"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Controllers mixing structural rendering with temporal logic break composability and reuse. Controllers must focus exclusively on timing and coordination concerns.","problem":"controllers define structure instead of managing temporal logic","solution":"limit controllers to async flows and coordination and move rendering to ui","signals":["controller renders ui","structure in controller","coupling ui logic"],"search_intent":"what should controllers handle in architecture","keywords":["controller temporal logic pattern","async coordination architecture","separation controller ui","leptos controller design"],"body":"## Context\nControllers exist to manage **time**, **async**, and **coordination**.\nThey are not layout managers and must not define DOM structure.\n\nStructural logic inside controllers breaks composability and reuse.\n\n## Rule\nControllers **MUST manage temporal logic only**:\n- async flows\n- timers\n- transitions\n- coordination between signals\n\nControllers **MUST NOT**:\n- render elements\n- decide layout\n- own visual structure\n\n## Forbidden Pattern\n```rust\nif loading {\n    view! { <Spinner/> }\n}\n```\n\n## Correct Pattern\n```rust\ncontroller.on_load.emit(())\n```\n\nUI decides how loading is represented.\n\n## Rationale\n- Keeps controllers reusable\n- Preserves UI purity\n- Prevents architectural leakage\n- Aligns with Canon layer hierarchy\n\n## Scope\n- Controllers\n- Components\n\n## Exception\nNone.\n\nControllers manage *when*, never *what*."},{"number":150,"slug":"ui-must-be-deterministic-under-ssr-and-hydration","title":"canon-rule-150-ui-must-be-deterministic-under-ssr-and-hydration","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["ssr","hydration","ui","determinism"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"SSR and hydration require identical HTML output between server and client. Any runtime-dependent rendering breaks this contract and leads to failures.","problem":"non deterministic rendering breaks ssr hydration contract","solution":"ensure identical rendering output during ssr and initial hydration","signals":["hydration failure","dom mismatch","runtime divergence"],"search_intent":"why ssr hydration fails leptos","keywords":["ssr deterministic rendering","hydration mismatch leptos","ui rendering contract ssr","deterministic html output"],"body":"## Context\nSSR and hydration require **byte-for-byte compatible HTML**.\nAny divergence between server render and client initial render results in hydration failure.\n\nDynamic logic inside UI render paths breaks this contract.\n\n## Rule\nUI rendering **MUST be fully deterministic** during:\n- SSR render\n- Initial hydration\n\nUI **MUST NOT** depend on:\n- runtime-only values\n- reactive closures in render output\n- client-only signals during SSR\n\n## Forbidden Pattern\n```rust\nview! {\n    <Button>{move || theme.get_icon()}</Button>\n}\n```\n\n## Correct Pattern\n```rust\nview! {\n    <Button>\"🌓\"</Button>\n}\n```\n\nDynamic behavior must be handled **after hydration**, via explicit events.\n\n## Rationale\n- Prevents hydration panic\n- Guarantees SSR correctness\n- Makes rendering predictable\n- Enforces Canon SSR discipline\n\n## Scope\n- UI\n- Components\n- Layouts\n\n## Exception\nNone.\n\nIf it renders, it must render identically everywhere."},{"number":151,"slug":"visual-feedback-must-never-encode-application-state","title":"canon-rule-151-visual-feedback-must-never-encode-application-state","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["ui","state","feedback","architecture"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Using visual indicators to drive application logic creates hidden coupling and breaks state authority. Visual feedback must reflect state, not define it.","problem":"visual feedback encodes application logic causing hidden coupling","solution":"treat visuals as projection of state and never as decision source","signals":["logic based on icon","state inferred from ui","implicit coupling"],"search_intent":"how to separate ui feedback from state logic","keywords":["ui feedback state separation","visual state anti pattern","data first architecture ui","avoid ui driven logic"],"body":"## Context\nVisual indicators exist to reflect state, not define it.\nEncoding application logic into visual feedback creates hidden coupling.\n\nExamples include icons, colors, spinners or badges driving logic decisions.\n\n## Rule\nVisual feedback **MUST be a pure projection** of state.\nIt **MUST NOT**:\n- encode decisions\n- influence application flow\n- act as state source\n\n## Forbidden Pattern\n```rust\nif icon == \"error\" {\n    retry()\n}\n```\n\n## Correct Pattern\n```rust\nmatch state {\n    State::Error => show_error(),\n    _ => {}\n}\n```\n\nVisuals subscribe to state; they never drive it.\n\n## Rationale\n- Prevents implicit logic\n- Keeps state authoritative\n- Improves testability\n- Aligns with data-first architecture\n\n## Scope\n- UI\n- Components\n- Blocks\n\n## Exception\nNone.\n\nVisuals describe reality. They do not decide it."},{"number":152,"slug":"provider-callback-hydration-ownership","title":"canon-rule-152-provider-callback-hydration-ownership","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","callbacks","providers","wasm"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Heap-owned callbacks capturing provider context can be dropped before hydration reattachment, causing runtime panics. This breaks deterministic hydration in SSR environments.","problem":"callbacks capturing provider context are dropped before hydration causing runtime errors","solution":"use inline event handlers or hydration safe callback mechanisms","signals":["callback removed","unreachable panic","hydration failure"],"search_intent":"how to fix provider callback hydration errors","keywords":["leptos provider callback error","hydration ownership issue","callback dropped before attach","wasm hydration panic"],"body":"## Status\n\n**Mandatory**\n\n## Context\n\nIn SSR + Hydration environments (Leptos/Tachys), callbacks attached to DOM events\nare **reattached** during hydration. If a callback is **heap-owned** (`Callback::new`)\nand captures a **Provider context**, it may be **dropped before reattachment**,\ncausing runtime panics such as:\n\ncallback removed before attaching\nunreachable\n\npgsql\nCopiar código\n\nThis is not a visual mismatch issue.\nIt is a **lifetime + ownership violation** during hydration.\n\n## Rule\n\n**Callbacks that capture Provider context MUST NOT be created via `Callback::new`\ninside SSR execution paths.**\n\nInstead, they must be attached using **inline `on:event` handlers**\nor created via hydration-safe mechanisms.\n\n## Allowed Patterns\n\n### App / Layout Layer (preferred)\n\n```rust\n<Button\n    on:click=move |_| {\n        let current = theme_ctx.mode.get();\n        theme_ctx.set_mode(next_mode(current));\n    }\n>\n    \"🌓\"\n</Button>\non:event handlers are hydration-safe\n\nRuntime owns attachment lifecycle\n\nNo heap-owned callback is dropped prematurely\n\nStable Callback (advanced)\nrust\nCopiar código\nlet toggle = use_callback(move |_| {\n    theme_ctx.set_mode(...);\n});\nAllowed when abstraction is required\n\nMust be explicitly hydration-stable\n\nForbidden Patterns ❌\nrust\nCopiar código\nlet toggle = Callback::new(move |_| {\n    theme_ctx.set_mode(...);\n});\nWhen:\n\nUsed in App/Layout/Shell code\n\nCaptures Provider context\n\nRuns in SSR + hydrate environments\n\nThis WILL cause hydration panics.\n\nScope\nApplies to:\n\nThemeProvider\n\nDensityProvider\n\nLanguageProvider\n\nLayoutProvider\n\nAny CanonRS Provider\n\nRationale\nProviders are resolved during hydration\n\nHeap-owned callbacks may be dropped before DOM reattachment\n\non:event keeps ownership inside the runtime\n\nPrevents non-deterministic hydration crashes\n\nCanonical Guidance\nApp-layer: use on:event\n\nUI Components: may expose Callback props\n\nProviders: must never be mutated by unstable callbacks\n\nPrimitives: never own callbacks\n\nOutcome\nFollowing this rule guarantees:\n\nDeterministic hydration\n\nNo Tachys runtime panics\n\nCorrect Provider ownership semantics\n\nThis rule is non-negotiable for SSR-safe CanonRS applications.\n```"},{"number":153,"slug":"layouts-must-be-event-free-during-hydration","title":"canon-rule-153-layouts-must-be-event-free-during-hydration","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","layout","ssr","events"],"language":"EN","version":"1.0.0","date":"2026-01-16","intro":"Attaching DOM event listeners inside layouts during SSR hydration causes non-deterministic runtime failures due to unstable DOM nodes. This leads to panics and inconsistent behavior.","problem":"event listeners in layouts during hydration cause runtime mismatch and failures","solution":"remove all event bindings from layouts and delegate to csr controllers or ui","signals":["unreachable panic","callback removed","hydration failure","dom instability"],"search_intent":"why events break during hydration leptos","keywords":["leptos hydration event issue","layout event listener ssr","hydration callback error","ssr event timing problem"],"body":"## Status\n**Mandatory**\n\n## Context\n\nDuring SSR + Hydration, the DOM generated on the server **must be structurally and behaviorally identical** to the DOM expected by the client at hydration time.\n\nAny DOM event listener (`on:click`, `on:input`, etc.) attached inside **Layouts or Shells** during hydration introduces a high risk of runtime failure, including:\n\n- `callback removed before attaching`\n- `unreachable` panics during hydrate\n- Silent DOM replacement by the runtime\n\nThis issue is **not related to Providers, UI components, or business logic**, but to **event attachment timing** during hydration.\n\n## Problem\n\nLayouts (AppLayout, AppShell, PageLayout, etc.) are rendered:\n\n- On the server (SSR)\n- Immediately re-walked by the client during hydration\n\nIf an event listener exists at this level, the runtime may attempt to attach a callback to a node that:\n\n- Was moved\n- Was replaced\n- Was optimized away\n- Has not yet stabilized in the hydration phase\n\nThis causes **non-deterministic hydration failures**.\n\n## Rule\n\n**Layouts and Shells MUST NOT contain DOM event listeners.**\n\nThis includes, but is not limited to:\n\n- `on:click`\n- `on:input`\n- `on:change`\n- Any interactive callback bound directly in a Layout or Shell\n\nLayouts are **structural only**.\n\n## Allowed\n\n✅ Structural HTML  \n✅ Static content  \n✅ Slots (`Children`, `Option<Children>`)  \n✅ Pure UI composition  \n✅ Providers (context only, no events)  \n✅ CSS classes and `data-*` attributes  \n\n## Forbidden\n\n❌ Any `on:*` DOM event in Layouts  \n❌ Business logic inside Layouts  \n❌ State mutation in Layouts  \n❌ Controllers instantiated in Layouts  \n❌ Conditional rendering based on reactive state during hydration  \n\n## Correct Architecture\n\n### ❌ Invalid\n\n```rust\n#[component]\npub fn AppLayout() -> impl IntoView {\n    view! {\n        <Button on:click=toggle_theme>\n            \"🌓\"\n        </Button>\n    }\n}\n```\n\n### ✅ Valid\n\n```rust\n#[component]\npub fn AppLayout() -> impl IntoView {\n    view! {\n        <ThemeToggleSlot />\n    }\n}\n```\n\n```rust\n#[cfg(not(feature = \"ssr\"))]\n#[component]\npub fn ThemeToggle() -> impl IntoView {\n    let events = use_app_controller();\n\n    view! {\n        <Button on:click=move |_| events.toggle_theme()>\n            \"🌓\"\n        </Button>\n    }\n}\n```\n\n## Responsibility Split\n\n- **Layout**: structure only (SSR-safe)\n- **UI**: rendering only\n- **Controller / App layer**: event handling (CSR-only)\n- **Provider**: state ownership, no UI\n\n## Rationale\n\nThis rule guarantees:\n\n- Deterministic hydration\n- Zero runtime panics\n- Clear architectural boundaries\n- Predictable SSR/CSR behavior\n\nViolating this rule **will eventually cause hydration failure**, even if it appears to work temporarily.\n\n## Enforcement\n\nAny Layout or Shell containing DOM events is considered **architecturally invalid** and must be refactored.\n\n---\n\n**Summary**  \n> Layouts define structure.  \n> UI renders visuals.  \n> Controllers handle behavior.  \n> Hydration must be silent."},{"number":154,"slug":"css-layout-deterministic","title":"Deterministic Layout via Canonical CSS (SSR-Safe)","status":"ENFORCED","severity":"CRITICAL","category":"styling-css","tags":["css","layout","ssr","hydration"],"language":"EN","version":"1.0.0","date":"2026-01-26","intro":"Runtime layout manipulation causes DOM divergence between SSR and client hydration, leading to errors. Layout must be resolved deterministically via CSS.","problem":"layout is controlled by runtime logic instead of css causing hydration mismatch","solution":"define all layout positioning in css and use data attributes for state","signals":["hydration mismatch","layout drift","runtime positioning"],"search_intent":"how to fix layout hydration mismatch using css","keywords":["css deterministic layout","ssr layout mismatch","data attribute css pattern","layout rendering consistency"],"body":"## Principle\n\n**All layout positioning MUST be resolved exclusively by canonical CSS, never by runtime DOM manipulation or utility-based composition.**\n\n---\n\n## The Problem\n\nWithout this rule, SSR applications suffer from **hydration mismatch panics** caused by DOM divergence between server render and client hydration.\n\nObservable symptoms include:\n\n- Hydration errors when toggling layout elements\n- Runtime panics in Leptos / React SSR\n- Inconsistent sidebar positioning between SSR and CSR\n- Hidden coupling between JS logic and layout\n- Impossible-to-debug UI drift\n\nThese failures commonly arise when layout decisions are deferred to runtime utilities or conditional wrappers.\n\n---\n\n## Forbidden Patterns\n\n### ❌ Forbidden\n\n```rust\n// Conditional wrapper changes DOM structure after hydration\n{is_collapsed && view! { <div class=\"ml-auto\">...</div> }}\n```\n\n```html\n<!-- Runtime utility-based positioning -->\n<button class=\"flex justify-end ml-auto\">Toggle</button>\n```\n\n```css\n/* Inline or dynamic layout styles */\nstyle=\"margin-left: auto;\"\n```\n\nThese patterns violate the rule by:\n\n- Changing DOM structure between SSR and CSR\n- Encoding layout logic in runtime composition\n- Making layout non-deterministic\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n\n```html\n<button class=\"sidebar-toggle\" data-sidebar-toggle></button>\n```\n\n```css\n/* Canonical layout positioning */\n.sidebar-toggle {\n  position: absolute;\n  top: var(--spacing-md);\n  right: var(--spacing-md);\n}\n```\n\n```html\n<html data-sidebar=\"collapsed\"></html>\n```\n\n```css\n/* State-driven layout via data attributes */\nhtml[data-sidebar=\"collapsed\"] .sidebar {\n  width: var(--sidebar-width-collapsed);\n}\n```\n\nThis respects the rule because:\n\n- DOM structure is static and identical in SSR and CSR\n- Layout is fully resolved in CSS\n- State is expressed declaratively via `data-*`\n- JavaScript only triggers state changes, not layout\n\n---\n\n## Rationale\n\nThis rule enforces **deterministic rendering**, a core invariant of SSR systems.\n\nIt guarantees:\n\n- Identical DOM trees between server and client\n- Zero hydration mismatch\n- Clear separation of concerns:\n  - CSS defines layout\n  - Data attributes define state\n  - JS triggers actions only\n\nThis is not stylistic preference.  \nIt is a **hard architectural boundary** required for correctness, stability, and enterprise-scale maintainability.\n\n---\n\n## Enforcement\n\nThis rule MUST be enforced via:\n\n- Code review (reject runtime layout composition)\n- Static analysis (detect utility-based layout in critical zones)\n- CI checks on forbidden class usage in layouts\n- Architectural audits for SSR paths\n\nAny violation is a build-blocking defect.\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial canonical version"},{"number":155,"slug":"css-token-contract-is-architecture","title":"CSS Token Contract Is Architecture","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["css","tokens","theming","design-system"],"language":"EN","version":"1.0.0","date":"2026-01-26","intro":"Mixing CSS token formats causes runtime failures, theming inconsistencies, and unpredictable behavior. Tokens must follow a single deterministic format to ensure safe consumption and transformations.","problem":"mixed css token formats break theming and runtime css evaluation","solution":"enforce numeric hsl format for all tokens and apply functions at consumption","signals":["css not applied","invalid hsl value","theme broken","rgba fallback"],"search_intent":"why css tokens break with hsl var","keywords":["css token numeric hsl","design system token format","hsl var double wrap error","theming token consistency"],"body":"## Principle\n\n**CSS design tokens MUST use a single, deterministic format (numeric HSL) and NEVER mix formats within a design system.**\n\n- Tokens define values only, not CSS functions\n- Consumption applies functions at use site\n- Format contract is immutable across all tokens\n\n---\n\n## Problem\n\nWithout uniform token format:\n\n- **Runtime CSS failures**: `background: hsl(var(--color-primary))` breaks if token contains `hsl()` already\n- **Theming impossible**: Cannot interpolate or transform wrapped values\n- **Silent bugs**: Browser falls back to defaults, appearing as \"CSS not loading\"\n- **Maintenance chaos**: Developers cannot predict token format, leading to defensive code\n\n**Real symptom**: Button renders with `rgba(0,0,0,0)` background because `hsl(var(--primary))` receives `hsl(221 83% 53%)` instead of `221 83% 53%`.\n\n---\n\n## Forbidden Pattern\n\n### ❌ Forbidden\n```css\n/* tokens.css - MIXED FORMATS */\n--color-primary: hsl(221 83% 53%);        /* ❌ wrapped */\n--color-secondary: 180 50% 60%;           /* ✅ numeric */\n--color-accent: #3b82f6;                  /* ❌ hex */\n\n/* consumption */\nbackground: hsl(var(--color-primary));    /* BREAKS - double hsl() */\nbackground: var(--color-accent);          /* INCONSISTENT with others */\n```\n\n**Why forbidden:**\n- No contract - each token requires format inspection\n- `hsl(var(--color-primary))` produces invalid `hsl(hsl(...))`\n- Dark mode themes cannot transform hex values\n- Developers write defensive code checking format per token\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n```css\n/* tokens.css - UNIFORM FORMAT */\n:root {\n  --color-primary: 221 83% 53%;\n  --color-secondary: 180 50% 60%;\n  --color-accent: 217 91% 60%;\n}\n\n/* Semantic layer applies function */\nhtml[data-theme] {\n  --primary: var(--color-primary);\n}\n\n/* Consumption - always predictable */\n.button {\n  background: hsl(var(--primary));\n  color: hsl(var(--primary-foreground));\n}\n```\n\n**Why canonical:**\n- Single format = predictable consumption\n- Themes can transform numeric HSL (`calc()`, opacity)\n- No format inspection needed\n- Build tools can validate contract\n\n---\n\n## Rationale\n\n**Architectural invariants:**\n\n1. **Theming requires raw values**: `hsl()` wrapper prevents opacity manipulation (`hsl(var(--primary) / 0.5)` requires numeric input)\n2. **Build-time validation**: Uniform format enables static analysis\n3. **Developer ergonomics**: One mental model, zero format guessing\n4. **Framework agnostic**: Numeric HSL works in CSS, Tailwind, CSS-in-JS\n\n**This is not preference** - it's the only format that supports:\n- Alpha channel manipulation\n- Color space transformations\n- Static type checking in TypeScript token generators\n\n---\n\n## Enforcement\n\n**Build-time:**\n```bash\n# Validate all tokens are numeric HSL\ngrep -r \"^[[:space:]]*--color-\" style/tokens.css | \\\n  grep -v \"^[[:space:]]*--color-[^:]*: [0-9.]* [0-9.]*% [0-9.]*%;\" && \\\n  echo \"❌ Token format violation\" && exit 1\n```\n\n**Linting:**\n```javascript\n// stylelint rule\n\"custom-property-pattern\": {\n  \"^color-\": \"^[0-9.]+ [0-9.]+% [0-9.]+%$\"\n}\n```\n\n**CI check:**\n- All theme presets must pass format validation\n- `build:design` fails on mixed formats\n- No `hsl()`, `rgb()`, or hex in token definitions\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nToken format is architectural contract. Violating it breaks theming, SSR color resolution, and creates silent runtime failures.\n\nIf a value cannot be expressed in numeric HSL, it is not a color token.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-26)"},{"number":156,"slug":"css-variable-scope-is-non-negotiable","title":"CSS Variable Scope Is Non-Negotiable","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["css","variables","scope","theming"],"language":"EN","version":"1.0.0","date":"2026-01-26","intro":"Incorrect CSS variable scope prevents variables from resolving at runtime, leading to silent failures and broken themes. Scope alignment between definition and mapping is mandatory.","problem":"css variables defined in wrong scope resolve to empty values","solution":"define variable mappings in the same selector scope as source variables","signals":["empty css variable","theme not applied","silent css failure"],"search_intent":"why css var returns empty value","keywords":["css variable scope issue","css var undefined root","theming variable scope","css cascade variable resolution"],"body":"## Principle\n\n**CSS variable mappings MUST be defined in the same selector scope as the source variables they reference.**\n\n- `:root` does NOT inherit from `html[data-theme]`\n- Mappings in wrong scope resolve to undefined\n- Scope must match theme definition exactly\n\n---\n\n## Problem\n\nWithout scope alignment:\n\n- **Variables resolve to empty**: `var(--primary)` returns `\"\"` in browser\n- **Themes don't apply**: Button stays default browser color despite theme being set\n- **Silent failures**: CSS loads, HTML correct, but values never populate\n- **Debugging nightmare**: Everything \"looks right\" but doesn't work\n\n**Real symptom**: `getComputedStyle(document.documentElement).getPropertyValue('--primary')` returns empty string despite `--color-primary` being defined in theme.\n\n---\n\n## Forbidden Pattern\n\n### ❌ Forbidden\n```css\n/* generated-themes.css */\nhtml[data-theme=\"amber\"] {\n  --color-primary: 37.69 92% 50%;\n  --color-secondary: 221 83% 53%;\n}\n\n/* tokens.css - WRONG SCOPE */\n:root {\n  --primary: var(--color-primary);        /* ❌ UNDEFINED */\n  --secondary: var(--color-secondary);    /* ❌ UNDEFINED */\n}\n\n/* Result: var(--primary) is empty at runtime */\n```\n\n**Why forbidden:**\n- CSS variables do NOT traverse up selector specificity\n- `:root` executes before `html[data-theme]` in cascade\n- `var(--color-primary)` in `:root` has no value to reference\n- Creates illusion of working code that fails silently\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n```css\n/* generated-themes.css */\nhtml[data-theme=\"amber\"] {\n  --color-primary: 37.69 92% 50%;\n  --color-secondary: 221 83% 53%;\n}\n\n/* tokens.css - CORRECT SCOPE */\nhtml[data-theme] {\n  --primary: var(--color-primary);        /* ✅ DEFINED */\n  --secondary: var(--color-secondary);    /* ✅ DEFINED */\n}\n\n/* Consumption works */\n.button {\n  background: hsl(var(--primary));        /* ✅ Resolves correctly */\n}\n```\n\n**Why canonical:**\n- Mapping exists in same scope as source\n- `html[data-theme]` matches theme selector specificity\n- Variables resolve deterministically\n- Works for all themes without scope conflicts\n\n---\n\n## Rationale\n\n**CSS cascade rules:**\n\n1. **Specificity matters**: `html[data-theme]` (0,1,1) beats `:root` (0,1,0)\n2. **Declaration order**: `:root` loads before themed selectors\n3. **No upward reference**: `var()` only resolves within current scope or inherited\n\n**This is CSS spec, not opinion:**\n```\nhtml[data-theme] defines --color-primary\n  ↓ (can reference here)\nhtml[data-theme] maps --primary: var(--color-primary) ✅\n  \n:root tries to map --primary: var(--color-primary) ❌\n  ↑ (cannot reference - wrong scope)\nhtml[data-theme] defines --color-primary\n```\n\n**Why it matters:**\n- Themes must be hot-swappable (change `data-theme` value)\n- SSR must match CSR scope resolution\n- Dark mode requires scope precision\n\n---\n\n## Enforcement\n\n**Runtime check:**\n```javascript\n// In browser console or E2E test\nconst primary = getComputedStyle(document.documentElement)\n  .getPropertyValue('--primary');\n\nif (primary === '') {\n  throw new Error('Variable scope violation: --primary undefined');\n}\n```\n\n**Build-time validation:**\n```bash\n# Ensure mappings are in html[data-theme], not :root\ngrep -A 5 \"^:root {$\" style/tokens.css | \\\n  grep \"var(--color-\" && \\\n  echo \"❌ Scope violation: theme mapping in :root\" && exit 1\n```\n\n**Linting:**\n- Theme variable references must be in `html[data-theme]` block\n- `:root` can only define base tokens, never map themed variables\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nCSS variable scope is W3C spec behavior. Violating it creates runtime failures that cannot be \"worked around\" - the variables simply don't resolve.\n\nIf a mapping is needed globally, the source variable must also be global (`:root`).\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-26)"},{"number":157,"slug":"design-system-css-build-time-flattened","title":"Design System CSS Must Be Build-Time Flattened","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["css","build","design-system","imports"],"language":"EN","version":"1.0.0","date":"2026-01-26","intro":"Serving CSS with runtime @import chains causes silent failures and missing styles in production. Browsers cannot resolve module paths reliably, leading to incomplete styling.","problem":"css is served with runtime imports causing missing styles and silent failures","solution":"flatten all css at build time into a single file with no @import statements","signals":["missing styles","silent import failure","css loaded but empty"],"search_intent":"how to fix css imports not","keywords":["css import runtime issue","tailwind build flatten css","postcss import problem","design system css bundling"],"body":"## Principle\n\n**Applications MUST consume design system CSS as a single, flattened file with zero runtime `@import` statements.**\n\n- Build tools (Tailwind, PostCSS) resolve imports at compile time\n- Browsers receive flat CSS, not import chains\n- Apps never serve \"source\" CSS with relative imports\n\n---\n\n## Problem\n\nWithout build-time flattening:\n\n- **Silent import failures**: Browser ignores `@import \"./ui/button.css\"` if path breaks\n- **Tokens never load**: Relative paths fail in served dist vs source\n- **CSS appears loaded**: File downloads but imports fail silently\n- **Components \"have no style\"**: HTML correct, classes exist, but rules missing\n\n**Real symptom**: Button renders with correct `data-button` attribute but `rgba(0,0,0,0)` background. DevTools shows CSS file loaded but button rules absent.\n\n---\n\n## Forbidden Pattern\n\n### ❌ Forbidden\n```css\n/* canonrs.css - served directly to browser */\n@import \"./tokens.css\";\n@import \"./ui/button.css\";\n@import \"./layouts/dashboard.css\";\n\n[data-button] {\n  background: hsl(var(--primary));\n}\n```\n```html\n<!-- App serves source CSS directly -->\n<link rel=\"stylesheet\" href=\"/node_modules/@canonrs/design/style/canonrs.css\" />\n```\n\n**Why forbidden:**\n- `@import \"./ui/button.css\"` path is relative to CSS file location\n- Browser resolves from request URL, not CSS file path\n- `http://localhost:3000/ui/button.css` → 404\n- Tokens defined in `tokens.css` never load\n- CSS file \"loads\" (200 OK) but imports fail silently\n- No browser error, no console warning\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n```css\n/* style/main.css - app entry point */\n@import \"@canonrs/design/tokens.css\";\n@import \"@canonrs/design/generated-themes.css\";\n@import \"@canonrs/design/canonrs.css\";\n```\n```bash\n# Build flattens all imports\nnpx tailwindcss -i style/main.css -o public/style/output.css\n```\n```css\n/* public/style/output.css - served to browser (flat) */\n:root {\n  --color-primary: 221 83% 53%;\n  /* ... all tokens inline */\n}\n\nhtml[data-theme=\"amber\"] {\n  /* ... all themes inline */\n}\n\n[data-button] {\n  /* ... all components inline */\n}\n/* Zero @import statements */\n```\n```html\n<!-- App serves flattened artifact -->\n<link rel=\"stylesheet\" href=\"/style/output.css\" />\n```\n\n**Why canonical:**\n- Single HTTP request, zero subsequent imports\n- All CSS in one file, correct cascade order\n- No path resolution issues\n- Works in any serving context (dev, prod, CDN)\n\n---\n\n## Rationale\n\n**Browser `@import` limitations:**\n\n1. **No module resolution**: `@import \"@canonrs/design/tokens.css\"` doesn't work (no node_modules)\n2. **Relative path hell**: `./tokens.css` breaks when CSS served from different base URL\n3. **Silent failures**: Invalid imports are ignored, not errored\n4. **Performance**: Each import is blocking HTTP request\n\n**Build-time flattening solves:**\n- Resolves `@canonrs/*` aliases via PostCSS/Tailwind\n- Concatenates all CSS in correct order\n- Eliminates network waterfalls\n- Enables tree-shaking and minification\n\n**This is industry standard:**\n- Tailwind, PostCSS, Vite, Webpack all flatten by default\n- No production app serves CSS with `@import` chains\n- SSR requires deterministic CSS (can't await imports)\n\n---\n\n## Enforcement\n\n**Build pipeline:**\n```json\n{\n  \"scripts\": {\n    \"build:css\": \"tailwindcss -i style/main.css -o public/style/output.css\"\n  }\n}\n```\n\n**Validation:**\n```bash\n# Verify output has no @import\ngrep -c \"@import\" public/style/output.css && \\\n  echo \"❌ Unresolved @import in output\" && exit 1\n\n# Verify output has actual rules\ngrep -c \"\\[data-button\\]\" public/style/output.css || \\\n  echo \"❌ Components missing from output\" && exit 1\n```\n\n**CI check:**\n- `npm run build:css` must produce single file\n- Output file must contain all component rules\n- No `@import` statements in served CSS\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nRuntime `@import` is fundamentally incompatible with:\n- Design system distribution\n- SSR CSS injection\n- Production performance requirements\n- Reliable path resolution\n\nIf CSS needs dynamic loading, use code-splitting via JS modules, not CSS imports.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-26)"},{"number":158,"slug":"design-system-packages-immutable-contracts","title":"Design System Packages Are Immutable Contracts (No Direct File Imports)","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["design-system","packages","versioning","architecture"],"language":"EN","version":"1.0.0","date":"2026-01-26","intro":"Direct file imports from design system internals create tight coupling and break versioning guarantees. Applications become fragile and fail across environments.","problem":"apps import design system files directly causing coupling and breakage","solution":"consume design system only through versioned package exports","signals":["path import","ci break","coupling"],"search_intent":"how to consume design system via","keywords":["design system package contract","no relative imports architecture","frontend package boundaries","versioned design system usage"],"body":"## Principle\n\n**Applications MUST consume design system artifacts only through versioned package exports, never through direct file system imports.**\n\n- Design systems are distributed packages, not shared folders\n- Apps depend on `@canonrs/design`, not `../../packages-rust/rs-design/style/`\n- File paths create coupling; package exports create contracts\n- Changes propagate via version bumps, not file edits\n\n---\n\n## Problem\n\nWithout package boundaries:\n\n- **Silent breakage**: App imports `../../rs-design/style/button.css`, design system refactors → app breaks\n- **No versioning**: Cannot track which app uses which design system version\n- **Circular dependencies**: Apps and design system become interdependent\n- **Build fragility**: File paths break across environments (dev, CI, prod)\n- **Untracked changes**: CSS updates don't trigger app rebuilds\n\n**Real symptom**: App works locally, breaks in CI because relative path resolves differently. Or: design system refactor breaks 5 apps silently because they imported internal files directly.\n\n---\n\n## Forbidden Pattern\n\n### ❌ Forbidden\n```css\n/* App: style/main.css - DIRECT FILE IMPORT */\n@import \"../../packages-rust/rs-design/style/tokens.css\";\n@import \"../../packages-rust/rs-design/style/ui/button.css\";\n```\n```javascript\n// App: package.json - FILE PATH DEPENDENCY\n{\n  \"dependencies\": {\n    \"design-system\": \"file:../../packages-rust/rs-design\"\n  }\n}\n```\n```rust\n// App: Cargo.toml - PATH DEPENDENCY IN PRODUCTION\n[dependencies]\nrs-design = { path = \"../../packages-rust/rs-design\" }\n```\n\n**Why forbidden:**\n- **Coupling**: App knows design system internal structure\n- **Fragility**: Path breaks if either package moves\n- **No contract**: Design system can't refactor without checking all consumers\n- **CI failures**: Paths resolve differently in different environments\n- **No versioning**: Cannot rollback or pin versions\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n\n**Design System: package.json**\n```json\n{\n  \"name\": \"@canonrs/design\",\n  \"version\": \"0.1.0\",\n  \"exports\": {\n    \"./tokens.css\": {\n      \"style\": \"./dist/tokens.css\",\n      \"default\": \"./dist/tokens.css\"\n    },\n    \"./themes.css\": {\n      \"style\": \"./dist/themes.css\",\n      \"default\": \"./dist/themes.css\"\n    },\n    \"./canonrs.css\": {\n      \"style\": \"./dist/canonrs.css\",\n      \"default\": \"./dist/canonrs.css\"\n    }\n  }\n}\n```\n\n**App: style/main.css**\n```css\n/* Package imports - STABLE API */\n@import \"@canonrs/design/tokens.css\";\n@import \"@canonrs/design/themes.css\";\n@import \"@canonrs/design/canonrs.css\";\n```\n\n**App: package.json**\n```json\n{\n  \"dependencies\": {\n    \"@canonrs/design\": \"^0.1.0\"\n  }\n}\n```\n\n**Monorepo: pnpm-workspace.yaml**\n```yaml\npackages:\n  - 'packages/*'\n  - 'apps/*'\n```\n\n**Why canonical:**\n- **Versioned contract**: Apps pin `^0.1.0`, not file paths\n- **Refactor safety**: Design system can reorganize internals freely\n- **Environment agnostic**: Works in dev, CI, prod, Docker\n- **Explicit API**: Only exported files are public\n- **Rollback support**: `npm install @canonrs/design@0.0.9`\n\n---\n\n## Rationale\n\n**Package boundaries are architectural:**\n\n1. **Versioning enables change**: Without versions, any change is breaking\n2. **Exports define API surface**: Only exported files are stable\n3. **Dependency graph is auditable**: `npm ls` shows what depends on what\n4. **CI reproducibility**: Package versions lock behavior across environments\n\n**This is not \"best practice\" — it's how package ecosystems work:**\n- npm, cargo, pip all use package boundaries, not file paths\n- Relative imports (`../`) are for same-package files only\n- Cross-package imports must go through package manager\n\n**Real-world parallel:**\n```javascript\n// ❌ No one does this\nimport { Button } from \"../../node_modules/react/src/Button.js\";\n\n// ✅ Everyone does this\nimport { Button } from \"react\";\n```\n\nDesign systems are packages. Treat them as such.\n\n---\n\n## Enforcement\n\n**Linting:**\n```bash\n# Detect direct file imports in apps\ngrep -r \"@import.*\\.\\./.*rs-design\" apps/*/style/ && \\\n  echo \"❌ Direct file import detected\" && exit 1\n```\n\n**Cargo.toml check:**\n```bash\n# In production, no path dependencies\ngrep \"path.*rs-design\" apps/*/Cargo.toml | grep -v \"# dev-only\" && \\\n  echo \"❌ Path dependency in production\" && exit 1\n```\n\n**CI validation:**\n```yaml\n# Ensure design system is consumed as package\n- name: Check design system imports\n  run: |\n    if grep -r \"file:.*rs-design\" apps/*/package.json; then\n      echo \"Apps must use published package, not file: paths\"\n      exit 1\n    fi\n```\n\n**Build-time:**\n- Apps must resolve `@canonrs/design` via workspace, not path\n- `npm run build` must work with only `node_modules/@canonrs/design` present\n- Design system internal files must not be accessible to apps\n\n---\n\n## Exceptions\n\n**Development-only exception:**\n```toml\n# Cargo.toml - workspace member can use path for hot reload\n[dependencies]\nrs-design = { path = \"../../packages-rust/rs-design\" }  # dev-only\n\n# But must also support:\n# rs-design = \"0.1.0\"  # production\n```\n\nThis is acceptable **only if**:\n- App is in same monorepo as design system\n- Production build uses published version\n- Path is documented as dev-only\n\n**Production deployment must always use versioned package.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-26)"},{"number":159,"slug":"css-ui-fail-safe-template","title":"UI CSS Must Be Fail-Safe, Token-Only, and Attribute-Scoped","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["css","tokens","ui","attributes"],"language":"EN","version":"1.0.0","date":"2026-01-26","intro":"UI styles break when primitives fail to emit expected structure or tokens are inconsistently applied. This leads to fragile styling and unpredictable rendering.","problem":"ui css depends on structure or tokens that may be missing causing fragile styles","solution":"enforce token-only css scoped by data attributes with fail-safe defaults","signals":["missing styles","fragile css","inconsistent rendering"],"search_intent":"how to make ui css resilient","keywords":["fail safe css ui pattern","data attribute css scoping","token based css architecture","resilient design system css"],"body":"## Principle\n\n**Every UI CSS file MUST be fail-safe by construction: token-only, data-attribute–scoped, and resilient to partial or missing UI/Primitive wiring.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWithout this rule, UI styling becomes fragile and unsafe.\n\nTypical failures observed:\n\n- CSS silently breaks when a Primitive forgets to emit a"},{"number":160,"slug":"css-bootstrap-no-build","title":"First App Must Not Require CSS Build","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["css","build","onboarding","architecture"],"language":"EN","version":"1.0.0","date":"2026-01-27","intro":"Requiring CSS build steps in initial setup increases friction and couples users to toolchains prematurely. This breaks onboarding simplicity and predictability.","problem":"first app requires css build step causing onboarding friction","solution":"provide prebuilt css so first app runs without any build step","signals":["build required","onboarding friction","toolchain dependency"],"search_intent":"how to avoid css build step","keywords":["css bootstrap no build","zero build frontend setup","onboarding css strategy","prebuilt css architecture"],"body":"## Principle\n\n**The first CanonRS application must run with prebuilt CSS and no CSS build step.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nRequiring CSS build tools in the first app breaks onboarding.\n\n- Immediate friction for new users\n- Toolchain coupling (Node, Tailwind, PostCSS)\n- False perception of framework complexity\n- Non-deterministic first experience\n\n---\n\n## Forbidden Pattern\n\n### ❌ Forbidden\n\n```bash\nnpm install\nnpm run build:css\n```\n\nWhy this violates the rule.\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n\n```html\n<link rel=\"stylesheet\" href=\"/canonrs-bootstrap.css\" />\n```\n\nWhy this complies with the rule.\n\n---\n\n## Rationale\n\nThe first app validates architecture, not tooling.\n\n- Protects onboarding invariants\n- Enforces zero-build bootstrap\n- Prevents early toolchain lock-in\n- Not an opinion: measurable by absence of build steps\n\n---\n\n## Enforcement\n\n- Documentation review\n- CI check for CSS build scripts in first-app guides\n- Manual validation\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":161,"slug":"css-load-order","title":"Canonical CSS Load Order Is Mandatory","status":"ENFORCED","severity":"CRITICAL","category":"styling-css","tags":["css","order","cascade","theming"],"language":"EN","version":"1.0.0","date":"2026-01-27","intro":"Incorrect CSS load order breaks cascade rules and causes inconsistent styling. Tokens and themes may be overridden unexpectedly.","problem":"css files are loaded in wrong order causing cascade issues","solution":"enforce strict canonical load order for all css layers","signals":["style override","theme broken","visual inconsistency"],"search_intent":"how to fix css load order issues","keywords":["css load order cascade","design system css order","theming override issue","frontend css layering"],"body":"## Principle\n\n**CanonRS CSS must always load in a strict, predefined order.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nIncorrect CSS order causes silent visual corruption.\n\n- Tokens overridden by components\n- Themes partially applied\n- Layout misalignment\n- Non-reproducible UI bugs\n\n---\n\n## Forbidden Pattern\n\n### ❌ Forbidden\n\n```html\n<link rel=\"stylesheet\" href=\"components.css\" />\n<link rel=\"stylesheet\" href=\"tokens.css\" />\n```\n\nWhy this violates the rule.\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n\n```html\n<link rel=\"stylesheet\" href=\"tokens.css\" />\n<link rel=\"stylesheet\" href=\"themes.css\" />\n<link rel=\"stylesheet\" href=\"variants.css\" />\n<link rel=\"stylesheet\" href=\"ui.css\" />\n<link rel=\"stylesheet\" href=\"layouts.css\" />\n```\n\nWhy this complies with the rule.\n\n---\n\n## Rationale\n\nCSS is a dependency graph, not a flat list.\n\n- Enforces deterministic styling\n- Preserves token invariants\n- Prevents cascade inversion\n- Architectural, not stylistic\n\n---\n\n## Enforcement\n\n- Static HTML inspection\n- CI snapshot comparison\n- Review checklist\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":162,"slug":"providers-not-ui","title":"Providers Are Infrastructure, Not UI","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["providers","architecture","state","ui"],"language":"EN","version":"1.0.0","date":"2026-01-27","intro":"Treating providers as UI components mixes state management with presentation and breaks architectural boundaries. This leads to coupling and poor composition.","problem":"providers are used as ui components causing architectural leakage","solution":"keep providers as infrastructure and let ui consume their context","signals":["provider as ui","coupling","state leakage"],"search_intent":"how to separate providers from ui components","keywords":["provider vs ui separation","context architecture frontend","state provider pattern","ui composition rules"],"body":"## Principle\n\n**Providers must never be implemented or exposed as UI components.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nTreating providers as UI causes architectural leakage.\n\n- State logic mixed with presentation\n- Broken composition\n- Tight coupling to layouts\n- Unclear responsibility boundaries\n\n---\n\n## Forbidden Pattern\n\n### ❌ Forbidden\n\n```rust\n<ThemeProvider>\n    <Button />\n</ThemeProvider>\n```\n\nUsed as a visual component.\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n\n```rust\n<ThemeProvider>\n    {children()}\n</ThemeProvider>\n```\n\nUI consumes context, never replaces provider.\n\n---\n\n## Rationale\n\nProviders define system state, not visuals.\n\n- Enforces separation of concerns\n- Preserves composability\n- Prevents UI-state coupling\n- Canonical infrastructure boundary\n\n---\n\n## Enforcement\n\n- Code review\n- Static analysis (provider usage)\n- Architecture audits\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":163,"slug":"hydration-effects-only","title":"DOM Effects Are Hydration-Only","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","ssr","effects","dom"],"language":"EN","version":"1.0.0","date":"2026-01-27","intro":"Executing DOM effects during SSR causes mismatches and runtime failures. Effects must run only in hydration context to preserve deterministic rendering.","problem":"dom effects run during ssr causing hydration mismatch","solution":"guard all dom effects with hydrate feature or runtime checks","signals":["hydration mismatch","runtime error","dom panic"],"search_intent":"how to prevent dom effects in ssr","keywords":["ssr dom effects issue","hydrate feature rust leptos","dom mutation ssr error","hydration safe effects"],"body":"## Principle\n\n**DOM mutations must only run under the `hydrate` feature.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nRunning DOM effects during SSR causes hydration mismatch.\n\n- Silent crashes\n- Inconsistent HTML\n- Impossible-to-debug runtime errors\n- Broken SSR guarantees\n\n---\n\n## Forbidden Pattern\n\n### ❌ Forbidden\n\n```rust\nEffect::new(|| {\n    document().body().unwrap();\n});\n```\n\nWithout hydration guard.\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n\n```rust\n#[cfg(feature = \"hydrate\")]\nEffect::new(|| {\n    document().document_element();\n});\n```\n\nWhy this complies with the rule.\n\n---\n\n## Rationale\n\nSSR and hydration are separate execution domains.\n\n- Protects SSR determinism\n- Enforces runtime boundaries\n- Prevents DOM walking during SSR\n- Architectural invariant\n\n---\n\n## Enforcement\n\n- Feature-gated compilation\n- CI SSR build checks\n- Manual audit\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":164,"slug":"pkg-served-by-ssr","title":"WASM and JS Assets Must Be Served by SSR","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["ssr","wasm","assets","routing"],"language":"EN","version":"1.0.0","date":"2026-01-27","intro":"Missing asset routing for WASM and JS breaks hydration and prevents client initialization. Applications render HTML but fail to become interactive.","problem":"wasm and js assets are not served causing hydration failure","solution":"explicitly serve pkg assets from ssr server routes","signals":["404 wasm","no hydration","static html only"],"search_intent":"how to fix wasm not loading in ssr","keywords":["wasm asset routing ssr","leptos pkg serve issue","hydration missing assets","ssr static asset config"],"body":"## Principle\n\n**All `/pkg` assets must be served explicitly by the SSR server.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nMissing `/pkg` routing breaks hydration.\n\n- 404 on `.wasm` and `.js`\n- App loads HTML but never hydrates\n- Silent runtime failure\n- False UI rendering success\n\n---\n\n## Forbidden Pattern\n\n### ❌ Forbidden\n\n```rust\nRouter::new().fallback_service(ServeDir::new(\"public\"))\n```\n\nWithout `/pkg` handling.\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n\n```rust\n.nest_service(\"/pkg\", ServeDir::new(\"{site_root}/pkg\"))\n```\n\nWhy this complies with the rule.\n\n---\n\n## Rationale\n\nWASM is not optional in CanonRS.\n\n- Enforces runtime completeness\n- Guarantees hydration integrity\n- Prevents partial renders\n- Deployment invariant\n\n---\n\n## Enforcement\n\n- Runtime smoke test\n- CI asset existence check\n- Manual verification\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":165,"slug":"workbench-is-canon","title":"CanonRS Workbench Is a Canonical Reference","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["architecture","reference","canon","design"],"language":"EN","version":"1.0.0","date":"2026-01-27","intro":"Ignoring working reference implementations leads to architectural drift and inconsistent decisions. Proven patterns must guide design decisions.","problem":"architecture ignores working reference leading to inconsistency","solution":"treat workbench implementation as canonical source of truth","signals":["reinventing patterns","inconsistency","architecture drift"],"search_intent":"how to use reference implementation as architecture guide","keywords":["reference implementation architecture","canonical design patterns","frontend architecture governance","workbench as source of truth"],"body":"## Principle\n\n**A working CanonRS Workbench implementation defines canonical behavior.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nIgnoring the workbench leads to theoretical architecture drift.\n\n- Duplicate decisions\n- Conflicting patterns\n- Reinvented solutions\n- Fragmented framework behavior\n\n---\n\n## Forbidden Pattern\n\n### ❌ Forbidden\n\n```text\n“This is better than the workbench, let’s change it.”\n```\n\nWithout evidence.\n\n---\n\n## Canonical Pattern\n\n### ✅ Canonical\n\n```text\n“If the workbench does it and works, it is canon.”\n```\n\nWhy this complies with the rule.\n\n---\n\n## Rationale\n\nCanonRS canon is empirical, not speculative.\n\n- Protects proven architecture\n- Reduces decision entropy\n- Anchors rules in reality\n- Enterprise-grade stability\n\n---\n\n## Enforcement\n\n- Architecture review\n- Reference comparison\n- Documentation alignment\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":166,"slug":"dist-is-readonly","title":"Dist Is Read-Only","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["build","dist","artifacts","css"],"language":"EN","version":"1.0.0","date":"2026-01-28","intro":"Editing build artifacts directly breaks reproducibility and hides underlying architectural issues. Changes become non-traceable and inconsistent across environments.","problem":"dist files are manually edited causing non reproducible builds","solution":"apply all fixes in source and regenerate dist via build process","signals":["manual edit","inconsistent build","hidden bug"],"search_intent":"why not edit dist folder build artifacts","keywords":["dist folder readonly","build artifact immutability","frontend reproducible builds","css build output integrity"],"body":"## Principle\n\n**The dist/ directory is a build artifact and must never be edited manually.**\n\n---\n\n## Problem\n\nEditing files in dist/ creates non-reproducible builds and hides architectural errors.\n\n---\n\n## Forbidden Pattern\n\n❌ Editing any file under dist/ directly.\n\n---\n\n## Canonical Pattern\n\n✅ All fixes must be applied in the source directory and propagated via build.\n\n---\n\n## Enforcement\n\n- Code review\n- CI validation\n- Build reproducibility check\n\n---\n\n## Exceptions\n\nNone."},{"number":167,"slug":"canonrs-css-single-entrypoint","title":"CanonRS CSS Has a Single Entrypoint","status":"ENFORCED","severity":"HIGH","category":"styling-css","tags":["css","entrypoint","imports","architecture"],"language":"EN","version":"1.0.0","date":"2026-01-28","intro":"Multiple CSS entrypoints cause inconsistent imports, ordering issues, and missing tokens. This leads to unpredictable styling behavior across applications.","problem":"multiple css entrypoints cause ordering bugs and missing styles","solution":"expose a single canonical css entrypoint for all applications","signals":["missing tokens","import drift","css inconsistency"],"search_intent":"how to enforce single css entrypoint design system","keywords":["css single entrypoint pattern","design system css import order","frontend css architecture","canonical css bundle"],"body":"## Principle\n\n**CanonRS must expose exactly one canonical CSS entrypoint.**\n\n---\n\n## Problem\n\nMultiple CSS entrypoints cause import drift, ordering bugs, and missing tokens.\n\n---\n\n## Forbidden Pattern\n\n❌ Importing individual UI CSS files in applications.\n\n---\n\n## Canonical Pattern\n\n✅ Import only canonrs.css, which defines the full order.\n\n---\n\n## Enforcement\n\n- Static analysis\n- CSS import validation\n\n---\n\n## Exceptions\n\nNone."},{"number":168,"slug":"ui-must-declare-required-tokens","title":"UI Must Declare Required Tokens","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","ui","css","validation"],"language":"EN","version":"1.0.0","date":"2026-01-28","intro":"Using undefined CSS variables results in invisible or broken UI elements. Token usage must be explicit and verifiable to ensure reliable rendering.","problem":"ui references undefined tokens causing broken or invisible styles","solution":"ensure all tokens are defined before being used by ui components","signals":["missing variable","invisible ui","broken style"],"search_intent":"how to validate css tokens in ui components","keywords":["css token validation ui","design system token usage","undefined css variable issue","ui token contract"],"body":"## Principle\n\n**Every UI component must rely only on tokens that are explicitly defined.**\n\n---\n\n## Problem\n\nUsing undefined CSS variables leads to invisible or broken UI.\n\n---\n\n## Forbidden Pattern\n\n❌ UI CSS referencing tokens that do not exist in base tokens.\n\n---\n\n## Canonical Pattern\n\n✅ Tokens are defined first, UI consumes only canonical tokens.\n\n---\n\n## Enforcement\n\n- Token audit\n- CSS variable validation\n\n---\n\n## Exceptions\n\nNone."},{"number":169,"slug":"token-order-is-architectural","title":"Token Import Order Is Architectural","status":"ENFORCED","severity":"HIGH","category":"styling-css","tags":["css","tokens","order","cascade"],"language":"EN","version":"1.0.0","date":"2026-01-28","intro":"Incorrect token import order causes partial styling and broken component rendering. The cascade must follow a strict architectural sequence.","problem":"tokens are imported in wrong order causing inconsistent styles","solution":"enforce canonical import order from core tokens to layouts and blocks","signals":["partial styles","broken components","order conflict"],"search_intent":"how to fix css token import order issues","keywords":["css token order cascade","design system token import order","frontend css layering tokens","token dependency order"],"body":"## Principle\n\n**Token import order defines system correctness.**\n\n---\n\n## Problem\n\nOut-of-order tokens cause partial styling and broken components.\n\n---\n\n## Forbidden Pattern\n\n❌ Importing UI CSS before core tokens.\n\n---\n\n## Canonical Pattern\n\n✅ Core → Tokens → Themes → Variants → UI → Layouts → Blocks.\n\n---\n\n## Enforcement\n\n- Build-time checks\n- Canonical entrypoint review\n\n---\n\n## Exceptions\n\nNone."},{"number":170,"slug":"html-css-contract-must-match","title":"HTML and CSS Must Share the Same Contract","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["html","css","contracts","ui"],"language":"EN","version":"1.0.0","date":"2026-01-28","intro":"Mismatch between rendered HTML attributes and CSS selectors results in missing styles. The contract between markup and styling must be consistent.","problem":"html attributes do not match css selectors causing broken styling","solution":"ensure html and css share identical contract using data attributes","signals":["no styles applied","selector mismatch","ui broken"],"search_intent":"how to fix css selector not","keywords":["html css contract mismatch","data attribute styling pattern","css selector issue frontend","ui contract consistency"],"body":"## Principle\n\n**Rendered HTML attributes must exactly match CSS selectors.**\n\n---\n\n## Problem\n\nCSS using data-* while HTML renders classes (or vice versa) breaks styling.\n\n---\n\n## Forbidden Pattern\n\n❌ CSS selectors that do not match rendered HTML.\n\n---\n\n## Canonical Pattern\n\n✅ Data-attribute driven contracts shared by Rust and CSS.\n\n---\n\n## Enforcement\n\n- Runtime inspection\n- Component contract review\n\n---\n\n## Exceptions\n\nNone."},{"number":171,"slug":"no-phantom-variables","title":"Phantom Variables Are Forbidden","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["css","variables","tokens","validation"],"language":"EN","version":"1.0.0","date":"2026-01-28","intro":"Referencing undefined variables creates silent UI failures and unpredictable styling. Every variable must be traceable to a defined token.","problem":"undefined css variables are used causing silent failures","solution":"ensure all variables are declared and traceable in token definitions","signals":["undefined variable","silent failure","broken ui"],"search_intent":"how to detect undefined css variables","keywords":["css phantom variables issue","design system variable validation","undefined css variable fix","token completeness check"],"body":"## Principle\n\n**Every referenced variable must be defined and traceable.**\n\n---\n\n## Problem\n\nPhantom variables create silent UI failures.\n\n---\n\n## Forbidden Pattern\n\n❌ Referencing variables not defined in any token file.\n\n---\n\n## Canonical Pattern\n\n✅ All variables originate from core or base tokens.\n\n---\n\n## Enforcement\n\n- CSS variable scan\n- Token completeness check\n\n---\n\n## Exceptions\n\nNone."},{"number":172,"slug":"site-does-not-own-design","title":"The Site Does Not Own Design","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["architecture","design-system","ownership","ui"],"language":"EN","version":"1.0.0","date":"2026-01-28","intro":"Allowing applications to define design rules leads to fragmentation and inconsistency across the system. Design must be centralized.","problem":"applications define design rules causing fragmentation","solution":"centralize all design definitions within the design system packages","signals":["design drift","inconsistency","fragmentation"],"search_intent":"how to enforce design system ownership architecture","keywords":["design system ownership pattern","ui centralization architecture","frontend design governance","component design separation"],"body":"## Principle\n\n**Applications consume design; they do not define it.**\n\n---\n\n## Problem\n\nAllowing sites to define UI rules causes framework fragmentation.\n\n---\n\n## Forbidden Pattern\n\n❌ Defining component-level design inside site projects.\n\n---\n\n## Canonical Pattern\n\n✅ All design lives in rs-design and canonrs-design.\n\n---\n\n## Enforcement\n\n- Repository boundaries\n- Architecture review\n\n---\n\n## Exceptions\n\nNone."},{"number":173,"slug":"css-is-a-first-class-system","title":"CSS Is a First-Class System","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["css","architecture","design-system","rules"],"language":"EN","version":"1.0.0","date":"2026-01-28","intro":"Treating CSS as secondary leads to unstructured styling and hard-to-debug issues. CSS must follow strict architectural rules like any other system.","problem":"css is treated as ad hoc leading to inconsistent and untraceable styling","solution":"treat css as a governed system with strict architectural rules","signals":["css hacks","inconsistent styles","debug difficulty"],"search_intent":"how to structure css as system not decoration","keywords":["css architecture system","design system css governance","frontend styling rules","css structured approach"],"body":"## Principle\n\n**CSS in CanonRS is a governed system, not decoration.**\n\n---\n\n## Problem\n\nTreating CSS as secondary leads to untraceable UI bugs.\n\n---\n\n## Forbidden Pattern\n\n❌ Ad-hoc CSS fixes without architectural grounding.\n\n---\n\n## Canonical Pattern\n\n✅ CSS follows the same rigor as Rust code.\n\n---\n\n## Enforcement\n\n- Rule compliance\n- Design audits\n\n---\n\n## Exceptions\n\nNone."},{"number":174,"slug":"tokens-are-compile-time-contracts","title":"canon-rule-174-tokens-are-compile-time-contracts","status":"ENFORCED","severity":"BLOCKING","category":"build-tooling","tags":["tokens","build","css","architecture"],"language":"EN","version":"1.0.0","date":"2026-01-29","intro":"Tokens defined without compile-time validation lead to drift, inconsistency, and runtime errors. Token usage must be enforced during build.","problem":"tokens are not validated at build time causing inconsistencies and errors","solution":"enforce token definitions and usage through compile time validation","signals":["token drift","invalid token","build failure"],"search_intent":"how to enforce design tokens at compile time","keywords":["compile time tokens validation","design system token contract","css token build enforcement","frontend token architecture"],"body":"_This rule makes token chaos architecturally impossible._"},{"number":175,"slug":"dark-light-css-concern-not-component","title":"Dark/Light Is a CSS Concern, Not a Component Concern","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["css","theming","tokens","components"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Embedding dark/light logic inside components creates duplication, inconsistency, and hydration mismatches. Theme resolution must be handled exclusively by CSS token layers.","problem":"components implement dark light logic causing duplication and coupling","solution":"keep components theme agnostic and delegate context resolution to css tokens","signals":["duplicate logic","theme inconsistency","hydration mismatch"],"search_intent":"how to separate theming from components css","keywords":["css theming tokens pattern","dark mode separation frontend","component theme agnostic design","design system theme architecture"],"body":"## Principle\n\n**Components must never reference `.dark` class or implement dark/light logic—theme system handles all context resolution.**\n\n- Components are theme-agnostic\n- Dark/light is CSS variable resolution, not selector logic\n- No component CSS contains `.dark` selectors\n\n---\n\n## Problem\n\nWhen components implement dark/light logic:\n\n- **Duplication** - every component reimplements theme switching\n- **Inconsistency** - components interpret \"dark\" differently\n- **Coupling** - components coupled to theme implementation\n- **Maintenance explosion** - changing theme strategy requires editing all components\n- **Hydration mismatches** - SSR and CSR disagree on theme state\n\nReal discovery: Button worked perfectly in dark AND light **without any component changes**—proof that theme system is complete.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```css\n/* button_ui.css */\n[data-button][data-ui-variant=\"solid\"] {\n  background: var(--button-primary-bg);\n}\n\n/* ❌ Component knows about dark mode */\n.dark [data-button][data-ui-variant=\"solid\"] {\n  background: hsl(38 92% 50%);\n}\n```\n```css\n/* ❌ Component has dark-specific selector */\n[data-button][data-ui-variant=\"solid\"].dark {\n  background: var(--button-primary-bg-dark);\n}\n```\n\n**Why forbidden:** Component is now aware of theme context. Violates separation of concerns. Creates maintenance burden.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```css\n/* button_ui.css - Component is theme-agnostic */\n[data-button][data-ui-variant=\"solid\"] {\n  background: var(--button-primary-bg);  /* ✅ No dark/light logic */\n  color: var(--button-primary-fg);\n}\n```\n```css\n/* themes/dark/ui.css - Theme resolves context */\n@layer theme {\n  [data-theme=\"canonrs\"].dark {\n    --semantic-action-primary-bg: hsl(var(--color-primary));\n  }\n}\n```\n```css\n/* themes/light/ui.css */\n@layer theme {\n  [data-theme=\"canonrs\"]:not(.dark) {\n    --semantic-action-primary-bg: hsl(var(--color-primary));\n  }\n}\n```\n\n**Why correct:** Component references stable token. Theme system resolves value based on context. Component code unchanged for dark/light.\n\n---\n\n## Rationale\n\n### Separation of Concerns\n```\nComponent:  \"I need a primary action background\"\nTheme:      \"In dark mode, primary action is hsl(38 92% 50%)\"\n```\n\nComponent doesn't know or care about dark/light. Theme injects the right value.\n\n### Architectural Invariants\n\n1. **Components consume contracts** - they reference `--button-*` tokens\n2. **Themes provide implementations** - they resolve semantic → values\n3. **No component logic for context** - context is CSS cascade, not selectors\n\n### Bugs Prevented\n\n- Component implements dark mode incorrectly\n- Inconsistent dark/light across components\n- SSR/CSR hydration mismatch (component logic differs)\n- Theme refactor requires editing 78 components\n\n### Why Not Opinion\n\nThis is **interface segregation**. Components depend only on the interface they need (`--button-primary-bg`), not implementation details (`.dark` class).\n\n---\n\n## Enforcement\n\n### Linter Rule\n```bash\n# No component CSS can reference .dark\ngrep -r \"\\.dark\" styles/ui/*.css && exit 1\ngrep -r \"\\.dark\" styles/blocks/*.css && exit 1\ngrep -r \"\\.dark\" styles/layouts/*.css && exit 1\n```\n\n### Static Analysis\n```yaml\n# stylelint\nselectors-no-theme-classes:\n  - error\n  - forbidden: [\".dark\", \".light\"]\n    scope: [\"ui/**\", \"blocks/**\", \"layouts/**\"]\n```\n\n### Review Checklist\n\n- [ ] Component CSS has no `.dark` or `.light` selectors\n- [ ] Component CSS has no `[data-theme]` selectors\n- [ ] Components only reference family tokens (`--button-*`, `--card-*`)\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a component needs context-aware styling, the token layer is incomplete. Create semantic tokens to express that context.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":176,"slug":"governance-is-the-single-source-of-truth","title":"Governance Is the Single Source of Truth","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["governance","architecture","contracts","design-system"],"language":"EN","version":"1.0.0","date":"2026-01-29","intro":"Decisions made outside governance create drift, inconsistency, and duplicated logic. The system loses determinism and becomes difficult to maintain.","problem":"multiple layers define rules instead of central governance causing inconsistency","solution":"route all contract resolution and validation exclusively through governance layer","signals":["logic duplication","inconsistency","drift"],"search_intent":"how to enforce single source of truth governance","keywords":["governance architecture pattern","single source of truth design system","frontend governance enforcement","contract resolution architecture"],"body":"## Principle\n\n**All authoritative decisions in CanonRS flow through governance.**\n\nGovernance is the **only layer** allowed to:\n- interpret contracts\n- resolve components\n- validate mappings\n- enforce invariants\n\nNo other layer may override or bypass it.\n\n---\n\n## Definition\n\nThe **governance layer** is defined as:\n\n```\npackages-rust/rs-design/src/design/governance/\n```\n\nThis layer represents the **canonical brain** of the design system.\n\n---\n\n## Responsibilities\n\nGovernance MUST:\n\n- Resolve which tokens a component may consume\n- Enforce family boundaries\n- Validate contract compliance\n- Detect inventory drift\n- Fail fast on architectural violations\n- Produce deterministic outcomes\n\nGovernance is **authoritative**, not advisory.\n\n---\n\n## Forbidden Patterns\n\n### Forbidden\n\n- UI resolving its own tokens\n- Blocks inferring behavior dynamically\n- Layouts bypassing governance\n- “Helper logic” that duplicates governance rules\n- Runtime decision-making that changes design contracts\n\n```rust\n// ❌ WRONG\nif component == \"alert\" {\n    use_token(\"alert-bg-error\");\n}\n```\n\n```rust\n// ❌ WRONG\ncomponent.resolve_tokens_locally();\n```\n\n---\n\n## Canonical Patterns\n\n### Canonical\n\n- Governance resolves → others consume\n- Governance validates → build enforces\n- Governance is queried, never copied\n\n```rust\n// ✅ CORRECT\nlet resolved = governance::resolve(component_id)?;\n```\n\n---\n\n## Change Policy\n\nAny change to governance logic is **BREAKING**.\n\nRequired steps:\n\n1. Explicit version bump\n2. Canon Rule update (if behavior changes)\n3. Governance test update\n4. Full CI validation\n\nSilent logic changes are forbidden.\n\n---\n\n## Enforcement\n\n### Build Time\n\n- Governance is compiled as a mandatory dependency\n- Token resolution flows through governance only\n- Missing or bypassed governance causes build failure\n\n### CI\n\nBuild MUST FAIL if:\n\n- A component resolves tokens without governance\n- Governance rules are duplicated elsewhere\n- Inventory or mapping differs from governance output\n\n---\n\n## Rationale\n\nWithout a single source of truth:\n\n- Rules fork silently\n- Fixes diverge\n- Components behave inconsistently\n- Architecture becomes opinion-based\n\nWith governance:\n\n- Behavior is deterministic\n- Drift is detectable\n- Refactors are safe\n- CanonRS behaves like a real framework\n\n---\n\n## Relationship to Other Rules\n\n- Rule #165 — CanonRS Workbench Is a Canonical Reference\n- Rule #170 — HTML and CSS Must Share the Same Contract\n- Rule #174 — Tokens Are Compile-Time Contracts\n- Rule #175 — UI Inventory Is Fixed and Canonical\n- Rule #178 — Contracts Are Read-Only APIs\n\n---\n\n## Canon Outcome\n\n- Governance decides\n- Others comply\n- Architecture is enforced\n- Drift is impossible\n\n---\n\n**If it is not governed, it is not canon.**"},{"number":177,"slug":"canonrs-css-generated-artifact-never-design-surface","title":"canonrs.css Is a Generated Artifact, Never a Design Surface","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["css","build","artifacts","generation"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Editing generated CSS files directly creates divergence between source and output, breaking reproducibility. This leads to inconsistent builds and debugging complexity.","problem":"generated css is manually edited causing non deterministic builds","solution":"edit only source files and regenerate css through build process","signals":["manual edit","build mismatch","lost changes"],"search_intent":"why not edit generated css files","keywords":["generated css artifact pattern","build determinism css","frontend generated files rule","css build pipeline integrity"],"body":"## Principle\n\n**The `canonrs.css` file is generated output—manual edits are forbidden and will be overwritten on next build.**\n\n- `canonrs.css` is read-only\n- All changes go through source files (tokens, presets, components)\n- Build script regenerates `canonrs.css` from scratch every time\n\n---\n\n## Problem\n\nWhen developers edit `canonrs.css` directly:\n\n- **Silent data loss** - next build overwrites manual changes\n- **Source divergence** - output doesn't match source\n- **Debugging nightmare** - changes appear in browser but not in git\n- **Team confusion** - some devs edit source, others edit output\n- **Build non-determinism** - same source produces different outputs\n\nReal bug: Manual fix in `canonrs.css` → worked locally → git ignored file → CI built different CSS → production broken.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```bash\n# Editing generated file\nvim styles/canonrs.css\n```\n```css\n/* styles/canonrs.css */\n@import \"./ui/button_ui.css\";\n@import \"./custom-fix.css\";  /* ❌ Manual addition */\n```\n```bash\n# Committing generated file\ngit add styles/canonrs.css\ngit commit -m \"fix: update button colors\"  # ❌ Wrong layer\n```\n\n**Why forbidden:** Next build overwrites this. Change is ephemeral. Source of truth is violated.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```bash\n# Edit source files\nvim styles/ui/button_ui.css          # Component CSS\nvim src/tokens/themes/presets/canonrs.ts  # Preset colors\nvim styles/themes/dark/ui.css        # Theme mappings\n```\n```bash\n# Regenerate canonrs.css\nnpm run build\n```\n```bash\n# Commit source changes only\ngit add styles/ui/button_ui.css\ngit add src/tokens/themes/presets/canonrs.ts\ngit commit -m \"feat: update button colors in canonrs preset\"\n```\n```bash\n# .gitignore ensures canonrs.css is never committed\necho \"styles/canonrs.css\" >> .gitignore\n```\n\n**Why correct:** Source is versioned. Output is ephemeral. Build is deterministic.\n\n---\n\n## Rationale\n\n### Build Determinism\n```\nSource (versioned) → Build Script → Output (ephemeral)\n```\n\nIf output is modified, build is no longer deterministic. CI and local environments diverge.\n\n### Single Source of Truth\n\nManual edits create **two sources of truth**:\n1. Source files (git)\n2. Generated file (local filesystem)\n\nThis violates DRY and causes drift.\n\n### Architectural Invariants\n\n1. **Generated files are artifacts** - never design surfaces\n2. **Source files are truth** - all changes happen here\n3. **Build is pure function** - same source → same output\n\n### Bugs Prevented\n\n- Developer edits `canonrs.css` → build overwrites → confusion\n- CI builds different CSS than local (manual edits not in git)\n- Merge conflicts in generated files\n- Unable to reproduce builds\n\n### Why Not Opinion\n\nThis is **build system hygiene**. Generated artifacts must be reproducible. Manual edits break reproducibility.\n\n---\n\n## Enforcement\n\n### Gitignore\n```gitignore\n# Generated CSS - DO NOT COMMIT\nstyles/canonrs.css\nstyles/.generated/\ndist/\n```\n\n### Pre Commit Hook\n```bash\n#!/bin/bash\n# .git/hooks/pre-commit\nif git diff --cached --name-only | grep \"canonrs.css\"; then\n  echo \"❌ ERROR: canonrs.css is generated. Edit source files instead.\"\n  exit 1\nfi\n```\n\n### Build Script Header\n```bash\n# scripts/core/generate-canonrs-entry.sh\necho \"/* AUTO-GENERATED - DO NOT EDIT */\" > \"$OUTPUT\"\necho \"/* Run: npm run build to regenerate */\" >> \"$OUTPUT\"\n```\n\n### File Permissions\n```bash\n# Make canonrs.css read-only after build\nchmod 444 styles/canonrs.css\n```\n\n### Review Checklist\n\n- [ ] `canonrs.css` is in `.gitignore`\n- [ ] No manual edits to `canonrs.css` in commits\n- [ ] All changes go through source files → rebuild\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf the build script doesn't produce the CSS you need, fix the build script—don't manually edit the output.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":178,"slug":"contracts-are-read-only-apis","title":"Contracts Are Read-Only APIs (DEPRECATED)","status":"DEPRECATED","severity":"NONE","category":"governance","tags":["contracts","api","governance","deprecation"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Deprecated rules must not be referenced or extended to avoid fragmentation of authority. Only canonical rules define current behavior.","problem":"deprecated rules are referenced causing fragmentation and duplication","solution":"reference only canonical active rules and ignore deprecated ones","signals":["duplicate rule","confusion","invalid reference"],"search_intent":"how to handle deprecated rules in architecture","keywords":["deprecated rules governance","canonical rule authority","api contract deprecation pattern","design system rule versioning"],"body":"## Deprecation Notice\n\nThis rule is **fully superseded** by:\n\n➡ **Canon Rule #177 — Contracts Are Read-Only APIs**\n\n- The content was duplicated verbatim.\n- Rule #177 is the **single canonical authority**.\n- This file exists **only to preserve historical numbering**.\n\n---\n\n## Enforcement\n\n- ❌ Do NOT reference this rule\n- ❌ Do NOT extend this rule\n- ❌ Do NOT reintroduce this number\n\nAll tooling, documentation, and governance MUST reference **Canon Rule #177** exclusively.\n\n---\n\n**Canonical rules do not fork.  \nHistory is preserved. Authority is singular.**"},{"number":179,"slug":"no-visual-surfaces-in-core-or-families","title":"No Visual Surface Tokens in Core or Families","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","theming","architecture","css"],"language":"EN","version":"1.0.0","date":"2026-01-29","intro":"Defining visual surfaces in foundational layers breaks theming flexibility and causes regressions in dark mode. Visual decisions must remain in the theme layer.","problem":"visual surface values are defined in core or family tokens breaking theme separation","solution":"define only semantic intent in core and resolve appearance in theme layer","signals":["dark mode break","visual drift","theme coupling"],"search_intent":"how to separate theme and core tokens correctly","keywords":["design system token layering","theme vs core tokens separation","css semantic tokens pattern","dark mode token architecture"],"body":"## Principle\n\n**Core and Family tokens MUST NOT define visual surface values; all visual surfaces MUST be resolved at the theme layer.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWhen visual surface values are defined in Core or Family tokens:\n\n- Dark mode breaks (e.g. white or high-luminance surfaces in dark themes)\n- Themes cannot adapt surfaces independently\n- Visual intent leaks into foundational layers\n- Recurrent regressions occur when adding new components or variants\n- Design contracts become ambiguous and non-scalable\n\nArchitectural impact:\n- Theme coupling\n- Visual drift across modes\n- Non-deterministic UI behavior in enterprise contexts\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n// Core or Family token defining a visual surface directly\nFamilyToken::new(\"color-info-light\", \"hsl(199 89% 96%)\")\nFamilyToken::new(\"alert-bg-info\", \"var(--color-info-light)\")\n```\n\nWhy this violates the rule:\n\n- Encodes appearance in a foundational layer\n- Is not theme-aware\n- Cannot adapt across light/dark or future themes\n- Breaks the Core → Theme → UI contract\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n// Core defines semantic intent only\nFamilyToken::new(\"semantic-info-surface\", \"var(--theme-info-surface)\")\n\n// Family consumes semantic surface\nFamilyToken::new(\"alert-bg-info\", \"var(--semantic-info-surface)\")\n```\n\n```css\n/* Theme resolves visual appearance */\n[data-theme=\"canonrs\"] {\n  --theme-info-surface: hsl(199 89% 96%);\n}\n\n[data-theme=\"canonrs\"].dark {\n  --theme-info-surface: hsl(199 60% 18%);\n}\n```\n\nWhy this complies with the rule:\n\n- Core defines intent, not appearance\n- Theme owns visual decisions\n- Families remain semantic and reusable\n- Dark mode and future themes are deterministic\n\n---\n\n## Rationale\n\nThis rule exists to enforce a strict architectural separation:\n\n- **Core** defines immutable semantic contracts\n- **Themes** define visual appearance\n- **Families** bind semantics to component meaning\n- **UI** implements behavior\n\nInvariants protected:\n- No visual values in foundational layers\n- Full theme control over surfaces\n- Predictable dark mode behavior\n- Enterprise-grade scalability\n\nThis is not an opinion:\n- Violations produce measurable regressions\n- The rule is testable and enforceable\n- The separation is required for multi-theme systems\n\n---\n\n## Enforcement\n\nThis rule is enforced by:\n\n- CI checks rejecting `*-light`, `*-surface` visual values in Core or Families\n- Static grep-based validation in build scripts\n- Mandatory code review checklist\n- Build-time failure on forbidden token patterns\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":180,"slug":"semantic-token-names-canonical-suffixes","title":"Semantic Token Names Must Follow Canonical Suffixes","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","naming","css","validation"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Inconsistent token naming breaks references between layers and causes silent styling failures. Standardized suffixes ensure predictable token resolution.","problem":"semantic tokens use inconsistent suffixes causing lookup failures","solution":"enforce only canonical suffixes bg fg and border for semantic tokens","signals":["missing style","token mismatch","naming drift"],"search_intent":"how to standardize semantic token naming css","keywords":["semantic token naming pattern","css token suffix rules","design system naming convention","token validation css"],"body":"## Principle\n\n**Semantic tokens must use standardized suffixes exclusively: `-bg`, `-fg`, `-border`—never custom variants like `-surface`, `-color`, or `-text`.**\n\n- Only three suffixes are allowed\n- No exceptions, no variations\n- Enforced by family-engine validation\n\n---\n\n## Problem\n\nWhen semantic tokens use non-standard suffixes:\n\n- **Family references token that doesn't exist** - `--semantic-info-surface` not found\n- **Theme defines correct name** - `--semantic-info-bg` exists\n- **CSS breaks silently** - component has no background, no error shown\n- **Maintenance nightmare** - every team invents different suffixes\n\nReal bug: Family referenced `--semantic-info-surface` → theme defined `--semantic-info-bg` → callout had transparent background.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```rust\n// family_e_feedback.rs\nFamilyToken::new(\"callout-bg-info\", \"var(--semantic-info-surface)\"), // ❌ -surface doesn't exist\nFamilyToken::new(\"alert-fg-error\", \"var(--semantic-error-color)\"),   // ❌ -color not allowed\nFamilyToken::new(\"badge-text\", \"var(--semantic-text-primary-fg)\"),   // ❌ inconsistent naming\n```\n```css\n/* themes/dark/ui.css */\n--semantic-info-surface: hsl(var(--color-muted));  /* ❌ Wrong suffix */\n--semantic-error-color: hsl(var(--color-destructive));  /* ❌ Wrong suffix */\n```\n\n**Why forbidden:** Family and theme use different naming → token lookup fails → visual breaks.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```rust\n// family_e_feedback.rs\nFamilyToken::new(\"callout-bg-info\", \"var(--semantic-info-bg)\"),    // ✅ Standard -bg\nFamilyToken::new(\"alert-fg-error\", \"var(--semantic-error-fg)\"),    // ✅ Standard -fg\nFamilyToken::new(\"badge-border\", \"var(--semantic-success-border)\"), // ✅ Standard -border\n```\n```css\n/* themes/dark/ui.css */\n--semantic-info-bg: hsl(var(--color-muted));       /* ✅ Correct suffix */\n--semantic-error-fg: hsl(var(--color-destructive-foreground)); /* ✅ Correct suffix */\n--semantic-success-border: hsl(var(--color-accent)); /* ✅ Correct suffix */\n```\n\n**Why correct:** Family and theme use identical naming convention. Token always resolves.\n\n---\n\n## Rationale\n\n### The Three Canonical Suffixes\n```\n-bg     → background-color\n-fg     → color (foreground/text)\n-border → border-color\n```\n\n**Why only three?**\n- Cover 99% of use cases\n- Prevent naming drift\n- Enable search/replace refactoring\n- Enforceable by regex\n\n### Architectural Invariants\n\n1. **Token names are contracts** - family and theme must agree\n2. **Suffixes are standardized** - no custom inventions\n3. **Validation is automatic** - build fails on mismatch\n\n### Bugs Prevented\n\n- Token mismatch (family references non-existent token)\n- Silent CSS failures (transparent backgrounds, invisible text)\n- Team confusion (everyone invents different suffixes)\n- Refactoring breaks (can't regex-replace `-surface` → `-bg`)\n\n### Why Not Opinion\n\nThis is **naming consistency**. Like variable naming in code—arbitrary choice, but once chosen, must be universal.\n\n---\n\n## Enforcement\n\n### Build-time validation (Rust)\n```rust\n// family_engine\nfn validate_semantic_reference(token: &FamilyToken) -> Result<(), Error> {\n  let value = &token.value;\n  if value.contains(\"--semantic-\") {\n    if !value.ends_with(\"-bg)\") && !value.ends_with(\"-fg)\") && !value.ends_with(\"-border)\") {\n      return Err(Error::InvalidSemanticSuffix {\n        token: token.name.clone(),\n        reference: value.clone(),\n        expected: \"Semantic tokens must end with -bg, -fg, or -border\"\n      });\n    }\n  }\n  Ok(())\n}\n```\n\n### Linter rule (CSS)\n```yaml\n# stylelint\ncustom-property-pattern:\n  - \"^semantic-(.*-)?(bg|fg|border)$\"\n  - message: \"Semantic tokens must use -bg, -fg, or -border suffix\"\n```\n\n### Error message (educational)\n```\n❌ ERROR in family-e-feedback.rs\n\n  Token 'callout-bg-info' references 'var(--semantic-info-surface)'\n\n  RULE: Canon Rule #180 - Semantic Token Names Must Follow Canonical Suffixes\n  \n  ALLOWED SUFFIXES:\n    --semantic-*-bg      (background)\n    --semantic-*-fg      (foreground/text)\n    --semantic-*-border  (border)\n\n  FIX: Change reference to:\n       var(--semantic-info-bg)\n\n  DOCS: /opt/docker/monorepo/libs/canonRS-rules/canon-rule-180\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf you need a semantic token that doesn't fit `-bg`, `-fg`, or `-border`, you're likely:\n1. Creating a component-specific token (belongs in family, not semantic)\n2. Misunderstanding semantic purpose (semantic = meaning, not property)\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":181,"slug":"token-cascade-is-architecture-not-convention","title":"Token Cascade Is Architecture, Not Convention","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["tokens","architecture","cascade","css"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Treating token cascade as convention leads to violations and inconsistent layering. The cascade must be enforced as a structural constraint.","problem":"token cascade is treated as convention causing violations and inconsistencies","solution":"enforce token cascade through build time validation and strict layering","signals":["layer violation","build failure","inconsistent usage"],"search_intent":"how to enforce token cascade architecture css","keywords":["token cascade architecture","css layering enforcement","design system cascade rules","frontend token validation"],"body":"## Principle\n\n**The token cascade (Preset → Theme → Semantic → Family → Component) is an architectural constraint enforced by the framework—not a coding convention.**\n\n- Every layer has one job\n- No layer can be skipped\n- Order is enforced by build system\n- Violations fail at build time, not runtime\n\n---\n\n## Problem\n\nWhen token cascade is treated as convention:\n\n- **Developers skip layers** - components reference presets directly\n- **Inconsistent usage** - some files follow cascade, others don't\n- **Refactoring breaks** - no guarantee layers are respected\n- **Onboarding confusion** - \"should I use `--color-*` or `--semantic-*`?\"\n\nReal result: Treating cascade as architecture (not convention) made theme switching work **perfectly on first try** after fixing token layer.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```css\n/* Component violates cascade - no enforcement */\n[data-button] {\n  background: hsl(var(--color-primary)); /* ❌ Skips semantic + family */\n}\n```\n\n```rust\n// Family violates cascade - no enforcement\nFamilyToken::new(\n  \"button-primary-bg\",\n  \"hsl(38 92% 50%)\"  // ❌ Skips semantic, hardcodes color\n)\n```\n\n```bash\n# Build succeeds despite violations\nnpm run build  # ✅ (but wrong)\n```\n\n**Why forbidden:** No enforcement = violations slip through. Architecture becomes suggestion. Eventually breaks.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n// Family engine validates tokens\nfn validate_family_token(token: &FamilyToken) -> Result<(), Error> {\n  if !token.value.starts_with(\"var(--semantic-\") {\n    return Err(Error::InvalidTokenReference(\n      format!(\"Family token '{}' must reference --semantic-*, got '{}'\",\n        token.name, token.value)\n    ));\n  }\n  Ok(())\n}\n```\n\n```bash\n# Build FAILS if cascade violated\nnpm run build\n# ❌ ERROR: Family token 'button-primary-bg' references '--color-primary' directly.\n#    Family tokens must reference semantic layer (--semantic-*).\n```\n\n```yaml\n# Linter enforces cascade\nstylelint:\n  rules:\n    canonrs/token-cascade:\n      - error\n      - layers:\n          - preset: \"--color-*\"\n          - semantic: \"--semantic-*\"\n          - family: \"--{component}-*\"\n        enforce-references: true\n```\n\n**Why correct:** Violations are impossible. Build fails before wrong code ships. Architecture is guaranteed.\n\n---\n\n## Rationale\n\n### Architecture vs Convention\n\n```\nConvention: \"Please follow this pattern\"\n            → Humans forget\n            → Violations accumulate\n            → Eventually breaks\n\nArchitecture: \"This pattern is enforced\"\n              → Build fails on violation\n              → Impossible to ship wrong code\n              → Always correct\n```\n\n### The Five Layers\n\n```\n1. PRESET     → Define data (colors, sizes)\n               ✅ Can reference: nothing\n               ❌ Cannot reference: anything\n\n2. THEME      → Define context (light/dark)\n               ✅ Can reference: --color-*\n               ❌ Cannot reference: --semantic-*, --family-*\n\n3. SEMANTIC   → Define meaning (action-primary, surface-bg)\n               ✅ Can reference: --color-*\n               ❌ Cannot reference: --family-*, components\n\n4. FAMILY     → Define contracts (button-*, card-*, table-*)\n               ✅ Can reference: --semantic-*\n               ❌ Cannot reference: --color-*\n\n5. COMPONENT  → Define usage (button, card, table CSS)\n               ✅ Can reference: --{family}-*\n               ❌ Cannot reference: --color-*, --semantic-*\n```\n\n**Each layer has allowed references. Violations fail build.**\n\n### Architectural Invariants\n\n1. **Dependency direction is one-way** - higher layers depend on lower, never reverse\n2. **No layer skipping** - each layer consumes only the layer directly below\n3. **Build-time validation** - impossible to ship violations\n4. **Self-documenting** - error messages teach correct usage\n\n### Bugs Prevented\n\n- Component referencing preset (skips 3 layers)\n- Theme referencing family (wrong direction)\n- Family hardcoding values (skips semantic)\n- Any cascade violation shipping to production\n\n### Why Not Opinion\n\nThis is **layered architecture** enforced by the compiler. Not stylistic preference—structural requirement.\n\n---\n\n## Enforcement\n\n### Build Time Validation\n\n```rust\n// family_engine validates all tokens\npub fn build() -> Result<(), Error> {\n  for family in FAMILIES {\n    for token in family.tokens {\n      validate_token_cascade(&token)?;\n    }\n  }\n  Ok(())\n}\n\nfn validate_token_cascade(token: &FamilyToken) -> Result<(), Error> {\n  // Family tokens MUST reference --semantic-*\n  if !token.value.contains(\"--semantic-\") {\n    return Err(Error::CascadeViolation {\n      token: token.name.clone(),\n      layer: \"family\",\n      expected: \"--semantic-*\",\n      got: token.value.clone(),\n    });\n  }\n  Ok(())\n}\n```\n\n### Linter\n\n```javascript\n// stylelint plugin\nmodule.exports = {\n  rules: {\n    \"canonrs/enforce-token-cascade\": (enabled, options) => {\n      return (root, result) => {\n        root.walkDecls((decl) => {\n          const layer = getFileLayer(decl.source.input.file);\n          const refs = extractVarReferences(decl.value);\n\n          refs.forEach((ref) => {\n            if (!isValidReference(layer, ref)) {\n              report({\n                message: `Layer '${layer}' cannot reference '${ref}'`,\n                node: decl,\n              });\n            }\n          });\n        });\n      };\n    },\n  },\n};\n```\n\n### Documentation Generation\n\n```bash\n# Generate cascade diagram from enforced rules\nnpm run docs:cascade\n# Outputs visual diagram showing allowed references\n```\n\n### Error Messages\n\n```\n❌ ERROR in family-c-forms.css\n\n  Family token 'button-primary-bg' references '--color-primary' directly.\n\n  RULE: Canon Rule #169 - Token Cascade Is Architecture\n  FIX:  Reference semantic layer instead:\n\n        --button-primary-bg: var(--semantic-action-primary-bg);\n\n  WHY:  Family tokens define contracts consumed by components.\n        They must reference semantic tokens so theme context\n        (light/dark) is correctly resolved.\n\n  DOCS: /opt/docker/monorepo/libs/canonRS-rules/canon-rule-179\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nThe cascade is the framework. Violating it is violating the architecture. If enforcement is too strict, relax the rule definition—don't bypass enforcement.\n\n---\n\n## Summary\n\n**The token cascade is not a suggestion. It's a compiler.**\n\n```\nPreset   → defines data\nTheme    → defines context\nSemantic → defines meaning\nFamily   → defines contract\nComponent → consumes contract\n```\n\n**Every violation is a build failure. This is not optional.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":182,"slug":"semantic-role-must-affect-visual-contrast","title":"Semantic Role Must Affect Visual Contrast","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["tokens","ui","contrast","accessibility"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"When semantic roles share identical visuals, users cannot distinguish context or feedback. Visual contrast must reflect semantic meaning.","problem":"different semantic roles share identical visuals causing user confusion","solution":"ensure each semantic role has distinct visual contrast and background","signals":["no contrast","user confusion","semantic collapse"],"search_intent":"how to enforce visual contrast for semantic roles","keywords":["semantic contrast ui design","design system visual hierarchy","feedback vs surface styling","ui contrast semantics"],"body":"## Principle\n\n**Components with different semantic roles must have visually distinct backgrounds—feedback states cannot share the same background as neutral surfaces.**\n\n- Neutral surfaces = generic containers\n- Feedback surfaces = contextual communication\n- Visual distinction reinforces semantic meaning\n\n---\n\n## Problem\n\nWhen semantically different components share identical backgrounds:\n\n- **User confusion** - can't distinguish feedback from content\n- **Semantic collapse** - meaning lost in visual uniformity\n- **Accessibility failure** - no visual hierarchy\n- **Enterprise UX violation** - feedback must be immediately recognizable\n\nReal case: Card (neutral) vs Callout (info feedback) → if identical backgrounds, user can't distinguish informational feedback from structural content.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```css\n/* themes/dark/ui.css */\n--semantic-surface-bg: hsl(var(--color-background));\n--semantic-info-bg: hsl(var(--color-background)); /* ❌ Same as surface */\n```\n\n```javascript\n// Browser result\ngetComputedStyle(card).backgroundColor ===\n  getComputedStyle(callout).backgroundColor;\n// true ❌ - Cannot distinguish semantic roles\n```\n\n**Why forbidden:** Feedback component visually identical to neutral container. User cannot identify informational intent.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```css\n/* themes/dark/ui.css */\n--semantic-surface-bg: hsl(var(--color-background)); /* Neutral container */\n--semantic-info-bg: hsl(var(--color-muted)); /* ✅ Visually distinct */\n--semantic-success-bg: hsl(var(--color-accent)); /* ✅ Visually distinct */\n--semantic-error-bg: hsl(var(--color-destructive)); /* ✅ Visually distinct */\n--semantic-warning-bg: hsl(var(--color-primary)); /* ✅ Visually distinct */\n```\n\n```javascript\n// Browser result\ngetComputedStyle(card).backgroundColor; // rgb(255, 255, 255) - white\ngetComputedStyle(callout).backgroundColor; // rgb(249, 250, 251) - subtle gray\n// Different ✅ - User can distinguish roles\n```\n\n**Why correct:** Each semantic role has unique visual treatment. Feedback is immediately recognizable.\n\n---\n\n## Rationale\n\n### Semantic Hierarchy\n\n```\nNeutral (surface):    White background, no intent\nInfo (feedback):      Subtle gray, \"here's information\"\nSuccess (feedback):   Green tint, \"action succeeded\"\nError (feedback):     Red tint, \"requires attention\"\nWarning (feedback):   Amber tint, \"proceed with caution\"\n```\n\nVisual distinction = semantic clarity.\n\n### Architectural Invariants\n\n1. **Feedback requires visual weight** - must be noticeable\n2. **Neutrality is the baseline** - surfaces are invisible containers\n3. **Context must be visible** - semantic role = visual role\n\n### UX Principles\n\n- **Scanability**: User can quickly identify feedback zones\n- **Hierarchy**: Important information visually elevated\n- **Consistency**: Same role = same visual treatment across app\n\n### Bugs Prevented\n\n- Info callout looks like regular content (user ignores)\n- Error state invisible (user doesn't notice failure)\n- Warning blends with content (user misses critical info)\n- Success confirmation not recognized (user retries action)\n\n### Why Not Opinion\n\nThis is **information design**. Different semantic meanings require different visual encodings. Not aesthetic—functional requirement.\n\n---\n\n## Enforcement\n\n### Design Review Checklist\n\n- [ ] Feedback components have distinct backgrounds from surfaces\n- [ ] Info/warning/error/success are visually distinguishable\n- [ ] Neutral containers use `--semantic-surface-bg`\n- [ ] Feedback uses `--semantic-{intent}-bg`\n\n### Visual Regression Test\n\n```typescript\ntest(\"semantic roles have distinct visual contrast\", () => {\n  const card = document.querySelector(\"[data-card]\");\n  const callout = document.querySelector(\"[data-callout]\");\n\n  const cardBg = getComputedStyle(card).backgroundColor;\n  const calloutBg = getComputedStyle(callout).backgroundColor;\n\n  expect(cardBg).not.toBe(calloutBg); // Must be visually distinct\n});\n```\n\n### Contrast Validation\n\n```bash\n# Automated contrast check\nnode scripts/validate-semantic-contrast.js\n# Fails if feedback tokens equal surface tokens\n```\n\n---\n\n## Exceptions\n\n**Minimal exceptions:**\n\nIf a feedback component is **explicitly neutral** (e.g., `<Callout variant=\"default\">`), it may use surface background—but this defeats the purpose of feedback and should be rare.\n\nGeneral rule: **If it's feedback, it must look like feedback.**\n\n---\n\n## Related Rules\n\n- **Canon Rule #171**: Themes Resolve Context, Not Palette\n- **Canon Rule #172**: Semantic Tokens Are the Only Bridge Between Theme and Families\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":183,"slug":"overlay-positioning-is-a-primitive","title":"Overlay Positioning Is a Primitive, Not a Controller Concern","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["overlays","primitives","geometry","controllers"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Handling overlay positioning in controllers leads to duplication, instability, and hydration issues. Positioning must be centralized and reusable.","problem":"overlay positioning is implemented in controllers causing duplication and instability","solution":"move all positioning logic into dedicated primitives","signals":["inline style","duplication","hydration break"],"search_intent":"how to centralize overlay positioning logic","keywords":["overlay positioning primitive","ui geometry architecture","popover positioning pattern","frontend overlay system"],"body":"## Principle\n\n**All overlay positioning logic MUST live in a dedicated primitive, never in controllers or CSS alone.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWithout this rule:\n\n- Overlays appear centered on screen instead of anchored to triggers\n- Controllers start performing layout calculations\n- Inline styles leak into runtime DOM\n- Tooltip, Popover, Dropdown implementations diverge\n- SSR/CSR hydration breaks due to structural mismatch\n\nArchitectural impact:\n\n- Controller bloat\n- Logic duplication\n- Non-reusable overlays\n- Unstable rendering contracts\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\nlet rect = anchor.get_bounding_client_rect();\noverlay.style().set_property(\"top\", \"...\").unwrap();\noverlay.style().set_property(\"left\", \"...\").unwrap();\n```\n\nThis violates the rule by coupling geometry calculation to controllers.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\nuse_floating_position(\n    \"trigger-id\",\n    \"overlay-id\",\n    FloatingConfig {\n        placement: Placement::Top,\n        offset: 8.0,\n        flip: true,\n    },\n);\n```\n\nThe primitive owns all positioning logic and exposes results via CSS variables.\n\n---\n\n## Rationale\n\nThis rule protects core architectural invariants:\n\n- Controllers manage state only\n- Geometry is reusable and centralized\n- CSS remains declarative\n- Overlays behave consistently across the system\n\nThis is not opinion — it prevents systemic architectural failure.\n\n---\n\n## Enforcement\n\n- Code review: no geometry logic outside primitives\n- CI grep for `getBoundingClientRect` usage\n- Overlay checklist during PR review\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":184,"slug":"floating-overlays-expose-position-only-via-css-vars","title":"Floating Overlays Expose Position Only via CSS Variables","status":"ENFORCED","severity":"CRITICAL","category":"styling-css","tags":["css","overlays","primitives","variables"],"language":"EN","version":"1.1.0","date":"2026-02-03","intro":"Exposing overlay positioning through multiple channels creates inconsistency and breaks rendering contracts. A single CSS variable interface ensures predictable integration.","problem":"overlay positioning is exposed through multiple mechanisms causing inconsistency","solution":"expose all overlay position values exclusively via css custom properties","signals":["inline styles","layout drift","inconsistent positioning"],"search_intent":"how to expose overlay position via css variables","keywords":["css variables overlay positioning","floating ui css contract","frontend overlay positioning pattern","css custom properties layout"],"body":"## Principle\n\n**Floating overlay primitives MUST expose computed positioning exclusively via CSS custom properties as a public contract.**\n\nThis rule defines the **external interface** between positioning logic and rendering.\n\n---\n\n## Contract Definition\n\n- Position values are exposed as:\n  - `--floating-x`\n  - `--floating-y`\n- No other positioning channel is allowed\n- Consumers (CSS, themes, animations) rely on this contract\n\n---\n\n## Forbidden Pattern\n\n❌ Inline positioning  \n❌ DOM mutation of `top`, `left`, `bottom`, `right`\n\n---\n\n## Canonical Pattern\n\n```css\n[data-overlay] {\n  transform: translate(var(--floating-x), var(--floating-y));\n}\n```\n\n```rust\nstyle.set_property(\"--floating-x\", \"128px\").ok();\nstyle.set_property(\"--floating-y\", \"42px\").ok();\n```\n\n---\n\n## Scope Clarification\n\n- This rule defines **WHAT is exposed**\n- Enforcement details live in **Canon Rule #187**\n\n---\n\n## Exceptions\n\n**None. This contract is absolute.**\n\n---"},{"number":185,"slug":"overlays-must-be-anchor-relative","title":"Overlays Must Be Anchor-Relative","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["overlays","positioning","primitives","ux"],"language":"EN","version":"1.1.0","date":"2026-02-03","intro":"Viewport-based overlay positioning breaks spatial consistency and user expectations. Overlays must maintain a clear relationship with their trigger element.","problem":"overlays are positioned relative to viewport instead of anchor element","solution":"position overlays strictly relative to explicit anchor elements","signals":["floating overlay","misaligned ui","position drift"],"search_intent":"how to position overlays relative to anchor","keywords":["overlay anchor positioning","ui positioning primitives","popover relative placement","frontend overlay architecture"],"body":"## Principle\n\n**Every overlay MUST be positioned relative to an explicit anchor element.**\n\nViewport-relative overlays are architecturally invalid.\n\n---\n\n## Forbidden Patterns\n\n❌ Viewport-centered overlays  \n❌ Fixed positioning without anchor  \n❌ Positioning without trigger reference  \n\n```css\n[data-dropdown] {\n  position: fixed;\n  top: 50%;\n}\n```\n\n---\n\n## Canonical Pattern\n\n```rust\nuse_floating_position(\n  \"trigger-id\",\n  \"overlay-id\",\n  FloatingConfig {\n    placement: Placement::Bottom,\n    offset: 8.0,\n    flip: true,\n  },\n);\n```\n\n---\n\n## Architectural Invariants\n\n- Spatial causality is preserved\n- UX remains predictable\n- Scroll and resize remain stable\n- Accessibility remains coherent\n\n---\n\n## Enforcement\n\n- Overlay primitives require `anchor_id`\n- CI rejects viewport-relative overlays\n- Review blocks unanchored overlays\n\n---\n\n## Exceptions\n\n**None. Anchor-relative is mandatory.**\n\n---"},{"number":186,"slug":"overlay-visibility-is-data-state","title":"Overlay Visibility Is Controlled Only via data-state","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","state","css","overlays"],"language":"EN","version":"1.1.0","date":"2026-02-03","intro":"Controlling visibility through DOM mutations or conditional rendering breaks SSR determinism. Visibility must be driven by declarative state.","problem":"overlay visibility is controlled imperatively causing hydration mismatch","solution":"control overlay visibility exclusively via data-state attributes and css","signals":["hydration mismatch","dom divergence","visibility bug"],"search_intent":"how to control overlay visibility with data state","keywords":["data state visibility css","hydration safe visibility pattern","overlay state css control","ssr visibility architecture"],"body":"## Principle\n\n**Overlay visibility MUST be controlled exclusively via the `data-state` attribute.**\n\nNo conditional DOM.  \nNo inline styles.  \nNo runtime structure changes.\n\n---\n\n## Forbidden Patterns\n\n❌ `display: none` set in Rust  \n❌ Conditional rendering  \n❌ Inline visibility styles  \n\n```rust\noverlay.style().set_property(\"display\", \"none\"); // ❌\n```\n\n---\n\n## Canonical Pattern\n\n```html\n<div data-overlay data-state=\"closed\"></div>\n```\n\n```css\n[data-overlay][data-state=\"closed\"] {\n  visibility: hidden;\n  pointer-events: none;\n}\n\n[data-overlay][data-state=\"open\"] {\n  visibility: visible;\n}\n```\n\n---\n\n## Architectural Invariants\n\n- SSR and CSR DOM are identical\n- Hydration is deterministic\n- State is inspectable and debuggable\n\n---\n\n## Enforcement\n\n- CI greps for display mutation\n- Overlays must always exist in DOM\n- CSS maps `data-state` → visibility\n\n---\n\n## Exceptions\n\n**None. This rule is absolute.**\n\n---"},{"number":187,"slug":"floating-primitives-expose-css-vars-only","title":"Floating Primitives Enforce CSS-Variable-Only Positioning","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["overlays","primitives","css","enforcement"],"language":"EN","version":"1.1.0","date":"2026-02-03","intro":"Allowing primitives to manipulate layout directly creates inconsistencies and breaks separation of concerns. Positioning must be enforced through a single contract.","problem":"primitives mutate layout properties directly causing inconsistency","solution":"restrict primitives to writing only css variables for positioning","signals":["inline top left","layout mutation","contract violation"],"search_intent":"how to enforce css variable positioning primitives","keywords":["floating ui css enforcement","overlay positioning contract","css variable layout control","frontend primitives architecture"],"body":"## Principle\n\n**Floating primitives MUST enforce that positioning is communicated only via CSS custom properties.**\n\nThis rule defines the **internal enforcement mechanism** of the overlay positioning contract.\n\n---\n\n## Enforcement Rules\n\n- Primitives may:\n  - Compute geometry\n  - Write to `--floating-*` CSS variables\n- Primitives MUST NOT:\n  - Mutate layout properties\n  - Set inline `top`, `left`, `bottom`, `right`\n  - Bypass CSS rendering\n\n---\n\n## Forbidden Pattern\n\n```rust\nelement.style().set_property(\"top\", \"120px\").unwrap(); // ❌\n```\n\n---\n\n## Canonical Pattern\n\n```rust\nstyle.set_property(\"--floating-x\", \"300px\").ok();\nstyle.set_property(\"--floating-y\", \"120px\").ok();\n```\n\n---\n\n## Relationship to Other Rules\n\n- **Canon Rule #183** — where positioning logic lives\n- **Canon Rule #184** — public positioning contract\n- **Canon Rule #187** — enforcement of that contract\n\n---\n\n## Exceptions\n\n**None. Enforcement is mandatory.**\n\n---"},{"number":188,"slug":"overlay-positioning-is-not-ui-logic","title":"Overlay Positioning Is Not UI Logic","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["overlays","architecture","controllers","primitives"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Embedding positioning logic in UI or controllers leads to duplication and divergence. Positioning must remain centralized and reusable.","problem":"overlay positioning logic is implemented in ui or controllers causing duplication","solution":"delegate positioning entirely to dedicated primitives","signals":["duplicate logic","geometry in ui","inconsistent overlays"],"search_intent":"how to separate overlay positioning from ui logic","keywords":["overlay positioning separation","ui vs primitive architecture","popover logic centralization","frontend overlay design pattern"],"body":"## Principle\n\n**Controllers and UI components MUST NOT compute or decide overlay positioning.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWithout this rule:\n\n- Controllers accumulate geometry logic\n- UI code becomes unportable\n- Multiple components reimplement positioning\n- Bugs multiply with every new overlay\n\nObservable failures:\n\n- Tooltip logic duplicated in multiple files\n- Dropdown behavior diverges from Popover\n- Fixes applied inconsistently\n\nArchitectural impact:\n\n- Loss of primitive abstraction\n- Explosion of ad-hoc logic\n- System becomes unscalable\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\nlet rect = trigger.get_bounding_client_rect();\nlet x = rect.left() + 10.0;\nlet y = rect.bottom() + 8.0;\n```\n\n```rust\n// Controller deciding placement\nif is_near_bottom {\n    placement = Top;\n}\n```\n\nThis embeds layout decisions in UI logic.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\nuse_floating_position(\n    \"trigger-id\",\n    \"overlay-id\",\n    FloatingConfig {\n        placement: Placement::Bottom,\n        offset: 8.0,\n        flip: true,\n    },\n);\n```\n\nControllers only declare intent, never geometry.\n\n---\n\n## Rationale\n\nThis rule preserves:\n\n- Reusability of overlays\n- Single source of truth for positioning\n- Predictable behavior across the system\n- Ability to evolve positioning without touching UI\n\nPositioning is infrastructure, not UI.\n\n---\n\n## Enforcement\n\n- Review checklist: no `getBoundingClientRect` in controllers\n- CI scan for geometry math outside primitives\n- Primitive API is the only allowed entry point\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":191,"slug":"pages-use-page-behaviors-only","title":"Pages Must Delegate Wiring to Page Behaviors Only","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["pages","behavior","architecture","composition"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Embedding logic or wiring directly in pages leads to uncontrolled growth and architectural drift. Pages must remain declarative entry points.","problem":"pages contain logic and wiring causing coupling and growth","solution":"delegate all wiring to page behaviors and keep pages logic free","signals":["logic in page","event listener","coupling"],"search_intent":"how to separate page logic from behavior layer","keywords":["page behavior architecture","frontend page composition pattern","ui wiring separation","behavior layer design"],"body":"## Principle\n\n**Pages MUST delegate all client-side wiring to a corresponding page-level behavior and MUST NOT contain logic themselves.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWithout this rule:\n\n- Pages start accumulating logic\n- Page files grow uncontrollably\n- Behavior usage becomes inconsistent\n- The distinction between framework behavior and product wiring blurs\n\nObservable failures:\n\n- Event listeners created inside pages\n- Pages importing framework primitives or behaviors directly\n- Logic duplicated across multiple pages\n\nArchitectural impact:\n\n- Loss of clear composition boundaries\n- Tight coupling between pages and implementation details\n- Reduced portability across products\n\n---\n\n## Canonical Architecture\n\n```\nrs-design/\n├── primitives/   → infrastructure (calculation, low-level)\n├── behaviors/    → reusable framework behavior\n└── ui/           → pure rendering (HTML + CSS hooks)\n\nproduct-site/\n├── pages/        → composition + markup (ZERO logic)\n└── behaviors/    → page wiring only (ZERO logic)\n```\n\nEach page has a **1:1 page behavior**.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n// page.rs\nuse rs_design::behaviors::datatable::DataTableBehavior;\n\npub fn Page() {\n    DataTableBehavior::new(...).init(); // ❌ page calling framework behavior\n}\n```\n\n```rust\n// page.rs\nuse web_sys::EventTarget;\n\nlet el = document.get_element_by_id(\"btn\").unwrap();\nel.add_event_listener_with_callback(...); // ❌ logic inside page\n```\n\nPages must never own wiring or logic.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n// pages/page_a.rs\nuse crate::behaviors::page_a_behavior::init_page_a_behavior;\n\npub fn PageA() {\n    init_page_a_behavior();\n}\n```\n\n```rust\n// behaviors/page_a_behavior.rs\nuse rs_design::behaviors::*;\n\npub fn init_page_a_behavior() {\n    DataTableBehavior::new(...).attach();\n    TooltipBehavior::new(...).attach();\n}\n```\n\n- Pages only delegate\n- Page behaviors only wire\n- Framework behaviors implement logic\n\n---\n\n## Rationale\n\nThis rule enforces a strict separation of concerns:\n\n- Pages define structure and composition\n- Page behaviors define wiring\n- Framework behaviors define behavior\n- Primitives define infrastructure\n\nThis guarantees:\n\n- Predictable page size\n- Zero logic leakage into pages\n- Clean product-level customization\n- Reusable and versioned framework behavior\n\nPages become declarative entry points, not execution units.\n\n---\n\n## Enforcement\n\n- CI: forbid DOM APIs inside `/pages`\n- Review: page files must only call page behavior\n- Naming rule: `page_name.rs` ↔ `page_name_behavior.rs`\n- Pages must not import `rs-design::primitives` or framework behaviors directly\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":192,"slug":"page-behaviors-do-not-implement-logic","title":"Page Behaviors Must Not Implement New Logic","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["behavior","pages","architecture","logic"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Allowing page behaviors to implement logic leads to duplication and loss of reuse. Behavior must remain centralized in framework layers.","problem":"page behaviors implement logic causing duplication and architecture collapse","solution":"restrict page behaviors to wiring and delegate logic to framework behaviors","signals":["logic in behavior","duplication","unscalable"],"search_intent":"how to prevent logic in page behaviors","keywords":["page behavior architecture rule","frontend behavior separation","logic centralization pattern","framework behavior design"],"body":"## Principle\n\n**Page behaviors MUST NOT implement business logic, calculations, or state machines.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWithout this rule:\n\n- Page behaviors slowly turn into controllers\n- Logic becomes duplicated across pages\n- Framework behaviors are bypassed\n- The architecture collapses into ad-hoc scripts\n\nObservable failures:\n\n- Conditional logic inside page behaviors\n- Geometry, filtering, or sorting logic in page wiring\n- One-off logic written “just for this page”\n\nArchitectural impact:\n\n- Loss of reuse\n- Unversioned behavior\n- Hard-to-test product code\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\npub fn init_page_behavior() {\n    if window_width < 768 {\n        reposition_overlay(); // ❌ logic\n    }\n}\n```\n\n```rust\nlet filtered = rows.iter().filter(...); // ❌ logic\n```\n\nPage behaviors must not decide *what* happens — only *what is connected*.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\npub fn init_page_behavior() {\n    DataTableBehavior::new(...).attach();\n    TooltipBehavior::new(...).attach();\n}\n```\n\nAll logic lives in **framework behaviors**.\n\n---\n\n## Rationale\n\nThis rule preserves:\n\n- Clear ownership of logic\n- Framework-level reuse\n- Deterministic behavior evolution\n- Clean product codebases\n\nPage behaviors are **composition glue**, not execution engines.\n\n---\n\n## Enforcement\n\n- Review checklist: no `if`, `match`, or calculations in page behaviors\n- CI: forbid math, filtering, or geometry outside `rs-design`\n- Page behaviors must only call `.new()` / `.attach()`\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":193,"slug":"framework-behaviors-are-the-only-source-of-interactivity","title":"Framework Behaviors Are the Only Source of Interactivity","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["behavior","ui","events","architecture"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Distributing interactivity across pages and UI components creates inconsistent behavior and increases bug surface. Interaction must be centralized.","problem":"interactivity is implemented outside framework behaviors causing inconsistency","solution":"implement all interactivity exclusively within framework behaviors","signals":["event in ui","inconsistent behavior","duplication"],"search_intent":"how to centralize interactivity in framework behaviors","keywords":["frontend behavior centralization","ui interaction architecture","event handling framework pattern","behavior layer design"],"body":"## Principle\n\n**All interactive behavior MUST be implemented exclusively inside framework behaviors.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWithout this rule:\n\n- Interactivity leaks into pages\n- Multiple interaction models coexist\n- UX becomes inconsistent across products\n\nObservable failures:\n\n- Event listeners in pages\n- UI components handling their own logic\n- Different behaviors for the same component\n\nArchitectural impact:\n\n- Loss of canonical behavior\n- Increased bug surface\n- Impossible to guarantee consistency\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n// UI component\n<button on:click=move |_| toggle()>\n```\n\n```rust\n// Page file\nelement.add_event_listener_with_callback(...);\n```\n\nUI renders. Pages compose. Behaviors act.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n// rs-design/behaviors/copyable.rs\npub struct CopyableBehavior { ... }\n```\n\n```rust\n// page behavior\nCopyableBehavior::new(\"copy-btn\", \"content\").attach();\n```\n\nThere is exactly **one canonical behavior** per interaction.\n\n---\n\n## Rationale\n\nThis rule guarantees:\n\n- Uniform UX across products\n- Single point of behavioral truth\n- Testable, versioned interactivity\n- Predictable system evolution\n\nBehavior is part of the framework, not the product.\n\n---\n\n## Enforcement\n\n- CI: forbid DOM APIs outside framework behaviors\n- Review: UI components must be stateless\n- Pages must never attach events directly\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":194,"slug":"ui-components-are-pure-render-functions","title":"UI Components Are Pure Render Functions","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["ui","render","ssr","architecture"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Mixing logic or side effects into UI components breaks deterministic rendering and hydration. Components must remain pure.","problem":"ui components contain logic or side effects causing hydration issues","solution":"ensure ui components are pure render functions with no side effects","signals":["effect in ui","dom access","hydration error"],"search_intent":"how to make ui components pure render functions","keywords":["pure ui component pattern","ssr safe components design","frontend render purity","component architecture rules"],"body":"## Principle\n\n**UI components MUST be pure render functions with no side effects or behavior.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWithout this rule:\n\n- UI starts executing logic\n- Rendering becomes stateful\n- Hydration mismatches occur\n- Behavior becomes untraceable\n\nObservable failures:\n\n- Effects inside UI components\n- DOM access during render\n- Conditional rendering based on runtime state\n\nArchitectural impact:\n\n- Broken SSR guarantees\n- Unpredictable hydration\n- Tight coupling between view and logic\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n#[component]\npub fn Button() {\n    Effect::new(|| { ... }); // ❌ side effect\n}\n```\n\n```rust\nlet el = document.get_element_by_id(...); // ❌ DOM access\n```\n\nUI must never *do* anything.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n#[component]\npub fn Button() -> impl IntoView {\n    view! {\n        <button data-button />\n    }\n}\n```\n\nBehavior is attached externally via behaviors.\n\n---\n\n## Rationale\n\nThis rule ensures:\n\n- Deterministic rendering\n- Stable SSR/CSR output\n- Maximum reusability\n- Clear mental model\n\nUI components describe structure — nothing else.\n\n---\n\n## Enforcement\n\n- Review: no effects, no DOM, no logic in `/ui`\n- CI: forbid `window`, `document`, `Effect` in UI\n- UI props must be serializable and declarative\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":195,"slug":"interactive-components-require-explicit-id","title":"Interactive Components Require Explicit ID","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["components","hydration","id","behavior"],"language":"EN","version":"1.0.0","date":"2026-02-02","intro":"Auto-generated or optional IDs break SSR and CSR determinism, causing hydration mismatches and unreliable behavior attachment. Stable identifiers must be explicit.","problem":"interactive components use non deterministic or optional ids causing hydration and behavior issues","solution":"require explicit deterministic id prop provided by consumer for all interactive components","signals":["hydration mismatch","missing id","behavior attach fail"],"search_intent":"how to fix hydration issues with component ids","keywords":["deterministic id ssr csr","component id requirement pattern","hydration id mismatch","frontend behavior registry id"],"body":"## Principle\n\n**Interactive components that attach client-side behaviors MUST require an explicit `id` prop without default value or automatic generation.**\n\n- ID must be deterministic across SSR and CSR\n- ID must be provided by the component consumer\n- No UUID, no global counters, no automatic fallbacks\n\n---\n\n## Problem\n\nWhat breaks **without** this rule:\n\n- **Hydration mismatch**: Auto-generated IDs differ between server and client render passes\n- **Non-deterministic behavior attachment**: Registry cannot reliably match DOM elements to behaviors\n- **Conditional rendering breaks**: Components rendered conditionally get different IDs on SSR vs CSR\n- **Testing fragility**: Non-deterministic IDs make assertions impossible\n- **Accessibility violations**: `aria-labelledby` and form associations break with changing IDs\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```rust\n// Auto-generated ID with UUID\n#[component]\npub fn Combobox(\n    #[prop(optional)] id: Option<String>,\n) -> impl IntoView {\n    let id = id.unwrap_or_else(|| {\n        format!(\"combobox-{}\", uuid::Uuid::new_v4())\n    });\n    // ...\n}\n\n// Auto-generated ID with global counter\nstatic COUNTER: AtomicUsize = AtomicUsize::new(0);\n\n#[component]\npub fn Combobox(\n    #[prop(optional)] id: Option<String>,\n) -> impl IntoView {\n    let id = id.unwrap_or_else(|| {\n        format!(\"combobox-{}\", COUNTER.fetch_add(1, Ordering::Relaxed))\n    });\n    // ...\n}\n\n// Default empty string\n#[component]\npub fn Combobox(\n    #[prop(default = String::new())] id: String,\n) -> impl IntoView {\n    // Behavior registry skips elements with empty id\n}\n```\n\nWhy this violates the rule:\n- UUID changes between SSR and CSR\n- Counter produces different sequences if render order changes\n- Empty ID causes behavior registry to skip the element\n- Developer gets no compile-time feedback that ID is required\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```rust\n#[component]\npub fn Combobox(\n    id: String, // Required, no default, no Option<T>\n    options: Vec<ComboboxOption>,\n    #[prop(default = \"Select...\".to_string())] placeholder: String,\n) -> impl IntoView {\n    view! {\n        <ComboboxPrimitive\n            id=id\n            data-combobox\n        >\n            // ...\n        </ComboboxPrimitive>\n    }\n}\n\n// Usage\nview! {\n    <Combobox id=\"user-role-select\" options=roles />\n}\n```\n\nWhy this complies:\n- Compiler enforces ID at call site\n- Same ID in SSR and CSR (deterministic)\n- Behavior registry reliably finds element\n- Tests use predictable IDs\n- Accessibility attributes reference stable IDs\n\n---\n\n## Rationale\n\nWhy this rule exists **architecturally**:\n\n### Invariants it protects\n- **SSR/CSR determinism**: Same component tree must produce identical DOM structure\n- **Behavior attachment contract**: Registry depends on stable, unique IDs to match elements\n\n### Contracts it enforces\n- Component consumer declares intent (explicit ID = explicit control)\n- No hidden side effects (no global state mutation)\n\n### Bugs it prevents\n- Hydration mismatches that cause silent failures or panics\n- Race conditions in behavior initialization\n- Flaky tests from non-deterministic IDs\n- Broken accessibility relationships\n\n### Why it is not opinion\nSSR and CSR must produce identical markup for hydration to work. Any non-deterministic ID generation violates this fundamental constraint.\n\n---\n\n## Enforcement\n\nHow this rule is validated:\n\n- **Code review**: All interactive components must have `id: String` (not `Option<String>`, not default)\n- **Component checklist**: When creating UI with behavior, verify ID is required prop\n- **Behavior registry**: Logs warning if element has `data-*` attribute but no `id`\n- **Testing**: Integration tests fail if hydration mismatch occurs\n\nFuture enforcement:\n- Lint rule to detect `#[prop(optional)] id` or `#[prop(default = ...)] id` in components with `data-*` attributes\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a component does not attach behaviors (e.g., pure presentational `<Button>` without `data-*` attribute), it does not need an `id` prop. But any component that participates in the behavior registry MUST require explicit ID.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-02-02)"},{"number":196,"slug":"ssr-csr-separation-wasm-bloat-prevention","title":"SSR/CSR Separation for WASM Bloat Prevention","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["wasm","ssr","csr","build"],"language":"EN","version":"1.0.0","date":"2026-02-02","intro":"Mixing SSR and CSR code causes large WASM bundles, increased build time, and runtime inefficiency. Clear separation ensures optimal performance.","problem":"ssr and csr code are compiled together causing wasm bloat and inefficiency","solution":"separate modules strictly by compilation target using feature flags","signals":["large wasm","slow build","memory bloat"],"search_intent":"how to reduce wasm size by","keywords":["wasm bloat prevention","ssr csr separation rust","leptos feature flags wasm","frontend wasm optimization"],"body":"## Principle\n\n**Design system code MUST be separated by compilation target: SSR-only code compiles only for server, CSR-only code compiles only for WASM client.**\n\n- UI components, layouts, blocks, primitives = `#[cfg(feature = \"ssr\")]`\n- Behaviors (event handlers, DOM manipulation) = `#[cfg(feature = \"hydrate\")]`\n- Shared types (used by both) = no feature gate\n- Design tokens, utils = `#[cfg(feature = \"ssr\")]` (CSS already has tokens)\n\n---\n\n## Problem\n\nWhat breaks **without** this rule:\n\n- **WASM bloat**: 22MB+ bundle includes entire SSR rendering engine\n- **Build time explosion**: Compiling HTML rendering logic to WASM unnecessarily\n- **Runtime overhead**: Loading megabytes of unused code in browser\n- **Memory waste**: Client holds full component tree in WASM memory\n- **Network cost**: Users download 10-20MB of SSR code they never execute\n\nReal measurement from CanonRS pre-separation:\n- WASM size: 22MB (debug), 8-10MB (release)\n- After separation: 500KB-2MB (release)\n- **Reduction: 80-95%**\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```rust\n// lib.rs - NO SEPARATION\npub mod ui;           // ❌ Compiles to WASM unnecessarily\npub mod blocks;       // ❌ HTML rendering in client\npub mod primitives;   // ❌ SSR logic in WASM\npub mod behaviors;    // ⚠️ No hydrate guard\npub mod design;       // ❌ 400KB tokens in WASM when CSS has them\n\n// Result: 22MB WASM with full SSR engine\n```\n\nWhy this violates the rule:\n- Every module compiles for both targets\n- WASM includes HTML composition, slot logic, SSR utilities\n- Design tokens duplicated (CSS already has them)\n- No architectural boundary between server and client\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```rust\n// lib.rs - CLEAN SEPARATION\n#![recursion_limit = \"512\"]\n\n// SSR-only: HTML rendering, composition, structure\n#[cfg(feature = \"ssr\")]\npub mod blocks;\n\n#[cfg(feature = \"ssr\")]\npub mod ui;\n\n#[cfg(feature = \"ssr\")]\npub mod layouts;\n\n#[cfg(feature = \"ssr\")]\npub mod primitives;\n\n#[cfg(feature = \"ssr\")]\npub mod components;\n\n#[cfg(feature = \"ssr\")]\npub mod providers;\n\n#[cfg(feature = \"ssr\")]\npub mod design;  // Tokens already in CSS\n\n#[cfg(feature = \"ssr\")]\npub mod utils;\n\n// CSR-only: Event handlers, DOM mutation\n#[cfg(feature = \"hydrate\")]\npub mod behaviors;\n\n// Shared: Types used by both SSR and CSR\npub mod shared;  // TocItem, enums, trait definitions\n\n// Re-exports (conditional)\n#[cfg(feature = \"ssr\")]\npub use ui::button::Button;\n\n#[cfg(feature = \"ssr\")]\npub use blocks::Card;\n```\n\nWhy this complies:\n- Clear compilation boundary\n- WASM contains only client-side event runtime\n- SSR contains only server-side rendering logic\n- Shared types available to both without duplication\n- Design tokens excluded from WASM (CSS has them)\n\n---\n\n## Rationale\n\nWhy this rule exists **architecturally**:\n\n### Invariants it protects\n- **WASM = event runtime, NOT rendering engine**\n- **SSR = HTML generation, NOT shipped to client**\n- **Shared = minimal type definitions only**\n\n### Contracts it enforces\n- If it renders HTML → SSR only\n- If it composes UI → SSR only\n- If it reacts to events → CSR only\n- If it's a type used by both → Shared\n\n### Bugs it prevents\n- 10-20MB WASM bundles from SSR code leakage\n- `spawn_local` panics from SSR code running in hydrate context\n- Memory bloat from unused component definitions in client\n- Build time waste compiling SSR logic to WASM\n\n### Why it is not opinion\nWASM size directly impacts:\n- Time to Interactive (TTI)\n- First Contentful Paint (FCP)\n- Mobile network cost\n- Browser memory pressure\n\n20MB vs 2MB = 10x difference in load time on 3G network.\n\n---\n\n## Enforcement\n\nHow this rule is validated:\n\n**Build-time:**\n- `cargo leptos build --release` must produce WASM <3MB (optimized)\n- CI fails if WASM exceeds 5MB threshold\n- `wasm-opt -Oz` applied in release profile\n\n**Code review:**\n- New module in `rs-design/src/` requires explicit `#[cfg]`\n- No `pub mod` without feature gate except `shared`\n- Re-exports must match module feature gates\n\n**Testing:**\n- `cargo build --target wasm32-unknown-unknown --features hydrate` must NOT include SSR modules\n- `cargo build --features ssr` must NOT include hydrate-only code\n\n**Measurement:**\n```bash\n# Check WASM size\nls -lh target/site/pkg/*.wasm\n\n# Analyze bloat sources\ntwiggy top target/site/pkg/*.wasm\n\n# Verify separation\ncargo tree --features hydrate --target wasm32-unknown-unknown | grep \"rs-design\"\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nEvery module must be explicitly categorized:\n- SSR-only → `#[cfg(feature = \"ssr\")]`\n- CSR-only → `#[cfg(feature = \"hydrate\")]`\n- Both → no feature gate (must justify in code review)\n\nThe only valid \"both\" case: pure type definitions in `shared`.\n\n---\n\n## Related Rules\n\n- **Canon Rule #195**: Interactive components require explicit ID (hydrate needs IDs, SSR doesn't)\n- **Canon Rule #XXX** (future): WASM budget enforcement in CI\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-02-02)\n  - Established SSR/CSR separation requirement\n  - Documented 80-95% WASM size reduction\n  - Defined feature gate patterns"},{"number":197,"slug":"feature-flags-are-architectural-boundaries","title":"Feature Flags Are Architectural Boundaries","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["feature-flags","architecture","build","boundaries"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Using feature flags as conditional shortcuts breaks architectural boundaries and introduces cross-target leakage. Flags must define strict separation.","problem":"feature flags are misused causing cross target code leakage and instability","solution":"treat feature flags as hard architectural boundaries defining code location","signals":["type mismatch","duplicate crates","hydration panic"],"search_intent":"how to use feature flags as architecture boundaries","keywords":["feature flags architecture rust","ssr csr boundary enforcement","build graph separation","frontend feature flag misuse"],"body":"## Principle\n\n**Feature flags define hard architectural boundaries, not conditional shortcuts.**\n\nA feature flag decides **where code is allowed to exist**, not merely if it compiles.\n\n---\n\n## Problem\n\nMisusing feature flags causes:\n\n- SSR code compiled into WASM\n- CSR code linked into server binaries\n- Duplicate runtime crates\n- Non-deterministic build graphs\n\nObserved failures:\n- `axum::Body` type mismatches\n- Multiple `axum-core` versions\n- Hydration panics\n- WASM bloat\n\n---\n\n## Canonical Feature Roles\n\n```\nssr     → server rendering, adapters, HTML\nhydrate → client behaviors, DOM, WASM\nnone    → pure shared contracts only\n```\n\n---\n\n## Forbidden Patterns\n\n❌ `cfg(any(feature = \"ssr\", feature = \"hydrate\"))`  \n❌ Runtime logic inside shared crates  \n❌ Feature flags used to “hide” architecture violations  \n\n---\n\n## Canonical Pattern\n\n```rust\n#[cfg(feature = \"ssr\")]\npub mod ui;\n\n#[cfg(feature = \"hydrate\")]\npub mod behaviors;\n\npub mod shared;\n```\n\n---\n\n## Enforcement\n\n- Every `#[cfg]` maps to a runtime boundary\n- CI validates dependency graph per feature\n- Feature misuse is a build failure\n\n---\n\n## Exceptions\n\n**None. Feature flags are architectural contracts.**\n\n---"},{"number":198,"slug":"runtime-crates-must-not-leak-cross-target-types","title":"Runtime Crates Must Not Leak Cross-Target Types","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["runtime","types","workspace","boundaries"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Sharing runtime-specific types across targets breaks type identity and introduces fragile dependencies. Boundaries must remain strict.","problem":"runtime types leak across targets causing type conflicts and instability","solution":"use shared contract types and isolate runtime specific implementations","signals":["type mismatch","duplicate dependency","integration failure"],"search_intent":"how to isolate runtime types across targets","keywords":["runtime type isolation","cross target rust types","shared contract pattern","workspace architecture boundaries"],"body":"## Principle\n\n**Runtime crates must never exchange concrete runtime types across targets.**\n\nSSR and CSR are different runtimes.  \nTheir types must never cross boundaries.\n\n---\n\n## Problem\n\nWhen runtime types leak:\n\n- Multiple versions of the same crate are linked\n- Type identity breaks (`axum::Body ≠ leptos::Body`)\n- Errors surface deep in dependency graphs\n- Integration becomes fragile\n\n---\n\n## Forbidden Patterns\n\n❌ Shared crates depending on `axum`, `leptos`, `web-sys`  \n❌ Returning runtime-specific types from framework APIs  \n❌ Passing runtime objects across crate boundaries  \n\n---\n\n## Canonical Pattern\n\n```rust\n// shared\npub struct RouteInfo {\n    pub path: String,\n}\n\n// ssr\nhandle_route(RouteInfo);\n\n// csr\nhydrate_route(RouteInfo);\n```\n\nShared defines **contracts**, runtimes execute.\n\n---\n\n## Enforcement\n\n- `cargo tree --features hydrate` must exclude SSR runtimes\n- Shared crates compile without feature flags\n- CI fails on duplicated runtime crates\n\n---\n\n## Exceptions\n\n**None. Runtime isolation is mandatory.**\n\n---"},{"number":199,"slug":"server-adapters-are-integration-code","title":"Server Adapters Are Integration Code","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["server","adapters","integration","ssr"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Embedding server-specific logic into framework code couples architecture to a specific runtime and breaks portability. Adapters must remain isolated.","problem":"server adapter logic leaks into framework causing coupling and instability","solution":"isolate server adapters as integration layer separate from framework","signals":["framework coupling","runtime leak","migration issue"],"search_intent":"how to separate server adapters from framework","keywords":["server adapter architecture","integration layer pattern","framework agnostic design","ssr adapter separation"],"body":"## Principle\n\n**Server adapters are integration glue, not framework architecture.**\n\nThe framework must remain server-agnostic.\n\n---\n\n## Problem\n\nWhen adapters leak into framework code:\n\n- Framework becomes locked to one server\n- Migration cost explodes\n- Runtime types propagate incorrectly\n- Build graphs destabilize\n\n---\n\n## Forbidden Patterns\n\n❌ Framework crates importing `axum::*`  \n❌ UI or behaviors referencing server extractors  \n❌ Adapter-specific types in shared APIs  \n\n---\n\n## Canonical Pattern\n\n```rust\n// framework\npub fn render_app() -> String {}\n\n// integration\nRouter::new().route(\"/\", get(|| render_app()));\n```\n\nAdapters connect systems — they do not define them.\n\n---\n\n## Enforcement\n\n- Adapters allowed only in integration crates\n- Zero adapter dependencies in design-system crates\n- Review rejects adapter imports outside integration layer\n\n---\n\n## Exceptions\n\n**None. Adapters are replaceable by definition.**\n\n---"},{"number":200,"slug":"shared-crates-must-be-zero-dependency-runtime","title":"Shared Crates Must Be Zero-Dependency Runtime","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["shared","runtime","contracts","workspace"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Adding runtime dependencies to shared crates breaks portability and contaminates architecture. Shared must remain pure contract layer.","problem":"shared crates include runtime dependencies causing architecture leakage","solution":"restrict shared crates to pure types with no runtime dependencies","signals":["runtime import","dependency leak","build failure"],"search_intent":"how to keep shared crates runtime agnostic","keywords":["shared crate architecture rust","zero dependency runtime pattern","contract layer design","workspace isolation rules"],"body":"## Principle\n\n**Shared crates contain only pure, runtime-agnostic contracts.**\n\nShared is not a common runtime.  \nShared is a **contract layer**.\n\n---\n\n## Allowed Contents\n\n✅ structs  \n✅ enums  \n✅ traits  \n✅ constants  \n✅ serde-compatible models  \n\n---\n\n## Forbidden Contents\n\n❌ DOM access  \n❌ Server adapters  \n❌ Async runtimes  \n❌ Effects, hooks, behaviors  \n❌ Runtime-bound dependencies  \n\n---\n\n## Canonical Pattern\n\n```rust\n// shared\npub enum ThemeMode {\n    Light,\n    Dark,\n}\n```\n\n```rust\n// ssr\napply_theme(ThemeMode::Dark);\n\n// csr\nhydrate_theme(ThemeMode::Dark);\n```\n\n---\n\n## Enforcement\n\n- Shared crates compile for all targets\n- Dependency audit shows zero runtime crates\n- CI fails on any runtime import\n\n---\n\n## Exceptions\n\n**None. Shared must remain pure forever.**\n\n---"},{"number":201,"slug":"presets-only-color-source","title":"Presets Are the Only Source of Color Values","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","colors","theming","css"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Defining color values outside presets creates duplication and breaks palette switching. Color data must remain centralized.","problem":"color values are defined outside presets causing duplication and inconsistency","solution":"define all color values exclusively within preset definitions","signals":["color drift","palette break","hardcoded color"],"search_intent":"how to centralize color values in design system","keywords":["design system color source","preset color architecture","css color token centralization","theming palette management"],"body":"## Principle\n\n**Absolute color values (HSL, hex, RGB) must only exist in preset definitions.**\n\n- No color literals in themes\n- No color literals in semantic tokens\n- No color literals in family tokens\n- Presets are the single source of truth for palette\n\n---\n\n## Problem\n\nWhen themes or semantic tokens define colors directly:\n\n- **Preset is overridden** - palette switching breaks\n- **Light mode inconsistency** - different colors than dark\n- **Maintenance nightmare** - color changes require editing multiple files\n- **Cascade violations** - override order becomes unpredictable\n\nReal bug: `themes/light/ui.css` defined `hsl(210 40% 98%)` → preset amber was ignored → button turned gray in light mode.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```css\n/* themes/light/ui.css */\n@layer theme {\n  [data-theme=\"canonrs\"]:not(.dark) {\n    --semantic-action-primary-bg: hsl(210 40% 98%); /* ❌ Hardcoded color */\n  }\n}\n```\n```css\n/* tokens/base/ui.css */\n:root {\n  --semantic-surface-bg: hsl(0 0% 100%); /* ❌ Hardcoded white */\n}\n```\n\n**Why forbidden:** Preset values cannot override these. Palette switching is impossible.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```css\n/* themes/light/ui.css */\n@layer theme {\n  [data-theme=\"canonrs\"]:not(.dark) {\n    --semantic-action-primary-bg: hsl(var(--color-primary)); /* ✅ References preset */\n  }\n}\n```\n```typescript\n/* src/tokens/themes/presets/canonrs.ts */\nexport const canonrsTheme: ThemeDefinition = {\n  modes: {\n    light: {\n      colors: {\n        primary: { h: 38, s: 92, l: 50 }  // ✅ Color defined here only\n      }\n    }\n  }\n};\n```\n\n**Why correct:** Changing preset changes all derived tokens. One source of truth.\n\n---\n\n## Rationale\n\n### Architectural Invariants\n\n1. **Palette is a design decision** - must be centralized\n2. **Themes resolve context** - not palette\n3. **Components are palette-agnostic** - they reference tokens, not colors\n\n### Bugs Prevented\n\n- Preset switching breaking visual consistency\n- Light/dark mode color mismatches\n- Maintenance drift (colors defined in 5 places)\n- Override cascade conflicts\n\n### Why Not Opinion\n\nColor values are **data**. Data must have a single source. This is database normalization applied to CSS.\n\n---\n\n## Enforcement\n\n### Build-time validation\n```bash\n# Fail if themes contain color literals\ngrep -r \"hsl([0-9]\" styles/themes/ && exit 1\n```\n\n### Linter rule\n```yaml\n# stylelint\nrules:\n  color-no-literal-in-themes:\n    - error\n    - except: [\"presets/*.css\"]\n```\n\n### Review checklist\n\n- [ ] All color changes go through preset files\n- [ ] Theme files only reference `var(--color-*)`\n- [ ] Semantic tokens reference `var(--color-*)` or `var(--semantic-*)`\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a color value must exist, it belongs in a preset. If it's not palette-dependent, it's not a color token—it's a component-specific value and belongs in that component's CSS.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":202,"slug":"themes-resolve-context-not-palette","title":"Themes Resolve Context, Not Palette","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["themes","tokens","css","context"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Themes defining color values override presets and break palette consistency. Themes must only map semantic meaning to preset values.","problem":"themes define color values directly causing palette inconsistency","solution":"themes must reference preset tokens and never define color values","signals":["theme override","duplicate color","semantic drift"],"search_intent":"how to separate themes from color palette","keywords":["theme vs preset separation","design system context mapping","css theme architecture","semantic token mapping"],"body":"## Principle\n\n**Theme files (light/dark) map semantic meaning to preset variables—never define color values.**\n\n- Theme = context resolver (light vs dark)\n- Preset = palette definer (amber, blue, green)\n- Theme references preset, never replaces it\n\n---\n\n## Problem\n\nWhen themes define color values instead of referencing presets:\n\n- **Preset switching breaks** - theme hardcoded values override preset\n- **Duplicate truth** - same color defined in preset AND theme\n- **Semantic drift** - `--semantic-action-primary-bg` means different things in different contexts\n- **Maintenance explosion** - changing brand color requires editing themes AND presets\n\nReal bug: Theme defined `--semantic-action-primary-bg: hsl(38 92% 50%)` → when preset changed, theme didn't follow → inconsistent UI.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```css\n/* themes/light/ui.css */\n@layer theme {\n  [data-theme=\"canonrs\"]:not(.dark) {\n    /* ❌ Theme defining color value */\n    --semantic-action-primary-bg: hsl(38 92% 50%);\n    --semantic-surface-bg: hsl(0 0% 100%);\n  }\n}\n```\n\n**Why forbidden:** Theme is now the source of truth, not the preset. Palette switching is impossible.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```css\n/* themes/light/ui.css */\n@layer theme {\n  [data-theme=\"canonrs\"]:not(.dark) {\n    /* ✅ Theme mapping semantic → preset */\n    --semantic-action-primary-bg: hsl(var(--color-primary));\n    --semantic-action-primary-fg: hsl(var(--color-primary-foreground));\n    --semantic-surface-bg: hsl(var(--color-background));\n    --semantic-surface-fg: hsl(var(--color-foreground));\n  }\n}\n```\n```typescript\n/* Preset defines palette */\nexport const canonrsTheme: ThemeDefinition = {\n  modes: {\n    light: {\n      colors: {\n        primary: { h: 38, s: 92, l: 50 },\n        background: { h: 0, s: 0, l: 100 }\n      }\n    }\n  }\n};\n```\n\n**Why correct:** Theme resolves \"what does primary mean in light mode\" by pointing to preset. Preset defines \"what color is primary.\"\n\n---\n\n## Rationale\n\n### Separation of Concerns\n```\nPreset:  \"Primary is amber (38 92% 50%)\"\nTheme:   \"In light mode, action-primary uses preset's primary\"\nFamily:  \"Buttons use semantic action-primary\"\n```\n\nEach layer has **one job**.\n\n### Architectural Invariants\n\n1. **Palette changes happen in one place** (preset)\n2. **Context changes happen in one place** (theme)\n3. **Component contracts stay stable** (families/components)\n\n### Bugs Prevented\n\n- Preset amber → theme blue (user sees blue, preset ignored)\n- Light/dark inconsistency (different hardcoded values)\n- Brand color changes requiring 50+ file edits\n\n### Why Not Opinion\n\nThis is **referential integrity**. Theme is a foreign key to preset. Denormalization breaks data consistency.\n\n---\n\n## Enforcement\n\n### Static analysis\n```bash\n# Themes must not contain color literals\ngrep -E \"hsl\\([0-9]|#[0-9a-fA-F]{3,6}\" styles/themes/**/*.css && exit 1\n```\n\n### Linter rule\n```yaml\n# CSS variable references only\nthemes/**/*.css:\n  - no-color-literals\n  - require-var-reference: [\"--color-*\"]\n```\n\n### Review checklist\n\n- [ ] Theme files contain only `var(--color-*)` references\n- [ ] Semantic tokens map to preset tokens\n- [ ] No HSL/hex/RGB values in theme files\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a value must be hardcoded, it's not context-dependent—it's not a theme concern. Move it to component CSS or base tokens.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":203,"slug":"semantic-tokens-bridge-theme-families","title":"Semantic Tokens Are the Only Bridge Between Theme and Families","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","semantic","architecture","css"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Skipping semantic tokens couples components directly to palette and breaks context resolution. A strict token cascade is required.","problem":"components or families bypass semantic tokens causing coupling and context loss","solution":"enforce semantic tokens as the only bridge between theme and families","signals":["token bypass","context loss","refactor break"],"search_intent":"how to enforce semantic token layer design system","keywords":["semantic token architecture","design system token cascade","frontend dependency inversion tokens","css token layering pattern"],"body":"## Principle\n\n**Family tokens must reference semantic tokens exclusively—never preset or theme tokens directly.**\n\n- Families bind to `--semantic-*`\n- Components bind to `--family-*` (e.g., `--button-primary-bg`)\n- No component or family skips the semantic layer\n\n---\n\n## Problem\n\nWhen families or components reference preset/theme tokens directly:\n\n- **Context is lost** - component doesn't know \"why\" it's using a color\n- **Theme switching breaks** - components hardwired to specific palette\n- **Semantic drift** - \"primary\" means different things in different components\n- **Refactor impossible** - changing semantic meaning requires editing every component\n\nReal bug: Button used `--color-primary` directly → worked in dark, broke in light (context wasn't resolved).\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```css\n/* family-c-forms.css */\n[data-theme=\"canonrs\"] {\n  --button-primary-bg: hsl(var(--color-primary)); /* ❌ Skips semantic layer */\n}\n```\n```css\n/* button_ui.css */\n[data-button][data-ui-variant=\"solid\"] {\n  background: hsl(var(--color-primary)); /* ❌ Component directly using preset */\n}\n```\n\n**Why forbidden:** Component is now coupled to preset. Semantic meaning is lost. Refactoring semantic layer has no effect.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```css\n/* themes/dark/ui.css - Theme resolves context */\n@layer theme {\n  [data-theme=\"canonrs\"].dark {\n    --semantic-action-primary-bg: hsl(var(--color-primary));\n    --semantic-action-primary-fg: hsl(var(--color-primary-foreground));\n  }\n}\n```\n```css\n/* family-c-forms.css - Family binds to semantic */\n[data-theme=\"canonrs\"] {\n  --button-primary-bg: var(--semantic-action-primary-bg);\n  --button-primary-fg: var(--semantic-action-primary-fg);\n}\n```\n```css\n/* button_ui.css - Component uses family */\n[data-button][data-ui-variant=\"solid\"] {\n  background: var(--button-primary-bg);\n  color: var(--button-primary-fg);\n}\n```\n\n**Why correct:** Each layer has one job. Component knows \"I'm a primary action,\" not \"I'm amber.\"\n\n---\n\n## Rationale\n\n### Token Cascade\n```\nPreset:    --color-primary (palette)\n           ↓\nTheme:     --semantic-action-primary-bg (context)\n           ↓\nFamily:    --button-primary-bg (contract)\n           ↓\nComponent: background: var(--button-primary-bg) (consumption)\n```\n\n**Every layer is replaceable without breaking downstream layers.**\n\n### Architectural Invariants\n\n1. **Semantic is the contract** - families and components depend on it\n2. **Preset/theme can change** - semantic absorbs the change\n3. **Components are context-agnostic** - they consume intent, not color\n\n### Bugs Prevented\n\n- Button breaks when semantic \"primary\" changes meaning\n- Light/dark inconsistency (component bypasses context resolution)\n- Impossible to globally change \"what primary means\"\n\n### Why Not Opinion\n\nThis is **dependency inversion**. High-level components depend on abstraction (semantic), not concretion (preset).\n\n---\n\n## Enforcement\n\n### Static analysis\n```bash\n# Families must not reference --color-*\ngrep -r \"var(--color-\" styles/.generated/family-*.css && exit 1\n```\n\n### Linter rule\n```yaml\n# Family tokens\nfamily-*.css:\n  - no-direct-preset-reference\n  - require-semantic-tokens-only\n\n# Component CSS\n**/*_ui.css:\n  - no-preset-tokens\n  - no-semantic-tokens\n  - require-family-tokens-only\n```\n\n### Review checklist\n\n- [ ] Families reference only `--semantic-*`\n- [ ] Components reference only `--button-*`, `--card-*`, etc (family tokens)\n- [ ] No `--color-*` or `--semantic-*` in component CSS\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a component needs a value that doesn't fit the semantic layer, create a new semantic token. If it's truly one-off, it's not a token—hardcode it in that component.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":204,"slug":"theme-files-must-be-last-in-cascade","title":"Theme Files Must Be Last in the Cascade","status":"ENFORCED","severity":"HIGH","category":"styling-css","tags":["css","cascade","themes","order"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Incorrect import order causes theme overrides to be ignored and breaks light dark context resolution. CSS cascade must enforce deterministic override priority.","problem":"theme files are imported before other css causing overrides to fail","solution":"import theme files last in the css cascade after all tokens and components","signals":["theme not applied","wrong colors","override failure"],"search_intent":"how to fix css cascade theme override order","keywords":["css cascade theme order","theme override last import","design system css order","light dark css issue"],"body":"## Principle\n\n**Theme files (light/dark overrides) must be imported after all other token and component CSS.**\n\n- Themes are contextual overrides\n- Overrides only work if they come last\n- Order: presets → base → families → components → themes\n\n---\n\n## Problem\n\nWhen theme files are imported early in the cascade:\n\n- **Overrides don't apply** - subsequent imports overwrite theme values\n- **Context resolution fails** - semantic tokens defined after theme win\n- **Light/dark broken** - theme class has no effect\n- **Debugging nightmare** - values appear correct in DevTools but don't render\n\nReal bug: Theme imported before families → family tokens overwrote theme values → light mode showed wrong colors.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```css\n/* canonrs.css - WRONG ORDER */\n@import \"./.generated/themes.css\";          /* ❌ Theme too early */\n@import \"./tokens/base/ui.css\";\n@import \"./.generated/family-c-forms.css\";  /* This overwrites theme */\n@import \"./ui/button_ui.css\";\n```\n\n**Why forbidden:** Families and components imported after themes can redefine the same variables, overwriting theme context.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```css\n/* canonrs.css - CORRECT ORDER */\n@import \"./.generated/core.css\";\n@import \"./.generated/themes.css\";          /* Presets */\n@import \"./tokens/base/globals.css\";\n@import \"./tokens/base/core.css\";\n@import \"./tokens/base/ui.css\";\n@import \"./tokens/base/layout.css\";\n@import \"./tokens/base/blocks.css\";\n@import \"./.generated/family-a-overlay.css\";\n@import \"./.generated/family-b-selection.css\";\n@import \"./.generated/family-c-forms.css\";\n/* ... all families ... */\n@import \"./ui/accordion_ui.css\";\n@import \"./ui/button_ui.css\";\n/* ... all components ... */\n@import \"./layouts/page_layout_layout.css\";\n/* ... all layouts ... */\n@import \"./blocks/card_block.css\";\n/* ... all blocks ... */\n@import \"./themes/light/ui.css\";            /* ✅ Theme overrides LAST */\n@import \"./themes/dark/ui.css\";             /* ✅ Theme overrides LAST */\n```\n\n**Why correct:** Themes override everything. No CSS after themes can interfere with context resolution.\n\n---\n\n## Rationale\n\n### CSS Cascade Rules\n\nCSS is **last-write-wins** for same-specificity rules. Themes must write last.\n\n### Architectural Invariants\n\n1. **Base tokens define defaults**\n2. **Families define contracts**\n3. **Components consume contracts**\n4. **Themes provide context** (last layer, highest priority for same variables)\n\n### Bugs Prevented\n\n- Light mode showing dark colors (theme overridden)\n- Dark mode class having no effect\n- Semantic tokens not resolving correctly\n- Component styles ignoring theme context\n\n### Why Not Opinion\n\nThis is **CSS specificity and cascade order**. Not subjective. Themes are overrides—overrides must come last.\n\n---\n\n## Enforcement\n\n### Build-time validation\n```bash\n# Script: scripts/core/generate-canonrs-entry.sh\n# Themes MUST be the last @imports\necho \"@import \\\"./themes/light/ui.css\\\";\" >> \"$OUTPUT\"\necho \"@import \\\"./themes/dark/ui.css\\\";\" >> \"$OUTPUT\"\n# Nothing after this point\n```\n\n### Linter rule\n```yaml\n# stylelint - custom rule\nimport-order:\n  - presets\n  - base\n  - families\n  - components\n  - themes  # Must be last\n```\n\n### Review checklist\n\n- [ ] `canonrs.css` imports themes as final step\n- [ ] No CSS files imported after `themes/*.css`\n- [ ] `generate-canonrs-entry.sh` enforces theme order\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf you need a value to override themes, it's not a theme concern—it's a component-specific override and belongs in that component's CSS with higher specificity.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":205,"slug":"ui-tokens-bind-semantic-never-presets","title":"UI Tokens Must Bind to Semantic Tokens, Never to Presets","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","semantic","css","components"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Direct binding to preset tokens bypasses context resolution and breaks theme behavior. Semantic layer must remain the abstraction boundary.","problem":"family tokens reference preset tokens directly bypassing semantic layer","solution":"bind ui tokens exclusively to semantic tokens not preset tokens","signals":["theme ignored","context loss","token bypass"],"search_intent":"how to enforce semantic token binding in ui","keywords":["semantic token binding ui","design system token layering","preset vs semantic tokens","css token architecture"],"body":"## Principle\n\n**Family tokens (UI contracts like `--button-primary-bg`) must reference semantic tokens (`--semantic-*`), never preset tokens (`--color-*`).**\n\n- Families consume semantic layer\n- Components consume family layer\n- No layer skipping\n\n---\n\n## Problem\n\nWhen family tokens reference presets directly:\n\n- **Context is bypassed** - theme light/dark has no effect\n- **Semantic meaning is lost** - \"primary\" becomes \"color X\" instead of \"action intent\"\n- **Refactoring breaks** - changing semantic layer doesn't propagate\n- **Testing impossible** - can't verify semantic correctness without inspecting RGB\n\nReal bug: `--button-primary-bg: hsl(var(--color-primary))` → button ignored theme context → broke in light mode.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```css\n/* family-c-forms.css */\n[data-theme=\"canonrs\"] {\n  /* ❌ Family directly referencing preset */\n  --button-primary-bg: hsl(var(--color-primary));\n  --button-primary-fg: hsl(var(--color-primary-foreground));\n}\n```\n```rust\n// family_c_forms.rs\nFamilyToken::new(\n  \"button-primary-bg\",\n  \"var(--color-primary)\"  // ❌ Skips semantic layer\n)\n```\n\n**Why forbidden:** Family token is now coupled to preset. Theme can't inject context. Semantic contract is violated.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```css\n/* themes/dark/ui.css - Theme defines semantic meaning */\n@layer theme {\n  [data-theme=\"canonrs\"].dark {\n    --semantic-action-primary-bg: hsl(var(--color-primary));\n  }\n}\n```\n```rust\n// family_c_forms.rs - Family binds to semantic\nFamilyToken::new(\n  \"button-primary-bg\",\n  \"var(--semantic-action-primary-bg)\"  // ✅ Consumes semantic contract\n)\n```\n```css\n/* button_ui.css - Component consumes family */\n[data-button][data-ui-variant=\"solid\"] {\n  background: var(--button-primary-bg);  // ✅ Stable contract\n}\n```\n\n**Why correct:** Each layer respects the contract. Family is stable. Theme injects context. Component is agnostic.\n\n---\n\n## Rationale\n\n### Token Flow\n```\nPreset:   --color-primary (data)\n          ↓\nTheme:    --semantic-action-primary-bg (meaning)\n          ↓\nFamily:   --button-primary-bg (contract)\n          ↓\nComponent: background: var(--button-primary-bg) (usage)\n```\n\n**Families are contracts, not implementations.**\n\n### Architectural Invariants\n\n1. **Families define interfaces** - what tokens components need\n2. **Semantic defines implementation** - what those tokens mean\n3. **Theme injects context** - how meaning changes in light/dark\n4. **Preset defines data** - what colors exist\n\n### Bugs Prevented\n\n- Button using wrong color in light mode (bypassed theme)\n- Semantic refactor having no effect (family hardwired to preset)\n- Component coupled to palette (can't change palette independently)\n\n### Why Not Opinion\n\nThis is **Dependency Inversion Principle**. Families depend on abstraction (semantic), not concretion (preset).\n\n---\n\n## Enforcement\n\n### Build-time validation\n```bash\n# Family engine must only generate semantic references\ngrep \"var(--color-\" styles/.generated/family-*.css && exit 1\n```\n\n### Linter rule (Rust)\n```rust\n// In family_engine\nfn validate_token(token: &FamilyToken) {\n  if token.value.contains(\"--color-\") {\n    panic!(\"Family token {} references preset directly. Use --semantic-* instead.\", token.name);\n  }\n}\n```\n\n### Review checklist\n\n- [ ] All family tokens reference `--semantic-*`\n- [ ] No `--color-*` in family definitions\n- [ ] Family engine validates semantic-only references\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a family needs a value that isn't semantic, the semantic layer is incomplete. Add the missing semantic token.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":206,"slug":"ui-inventory-is-fixed","title":"UI Inventory Is Fixed and Canonical","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["ui","inventory","design-system","contracts"],"language":"EN","version":"1.1.0","date":"2026-01-29","intro":"Uncontrolled UI component growth leads to breaking changes and inconsistent APIs. UI inventory must be explicit and versioned.","problem":"ui component set changes without control causing breaking changes and drift","solution":"enforce fixed ui inventory with build time validation and versioning","signals":["unexpected component","inventory drift","build failure"],"search_intent":"how to enforce fixed ui component inventory","keywords":["ui inventory governance","design system component control","frontend api stability","component registry validation"],"body":"## Principle\n\n**The UI inventory is a fixed, canonical contract.**\n\n- UI count is explicit\n- UI set is enumerable\n- Any drift is a breaking change\n\n---\n\n## Canonical Statement\n\nAs of this rule:\n\n- **Total UI components: 78**\n- **Source of truth:** filesystem inventory\n- **Location:** `packages-rust/rs-design/src/ui`\n\nThis number is **authoritative**.\n\n---\n\n## Invariants\n\n- Every UI component:\n  - MUST live under `src/ui/<component>`\n  - MUST be discoverable by build-time scan\n  - MUST consume **only family tokens (public API)**\n\n- The UI inventory:\n  - MUST NOT change silently\n  - MUST NOT be inferred implicitly\n  - MUST be versioned intentionally\n\n---\n\n## Enforcement\n\n### Build Time Enforcement\n\n- `build.rs` MUST:\n  - Discover UI directories\n  - Emit a deterministic list\n  - Compare against expected count (**78**)\n\n### Failure Conditions\n\nBuild **MUST FAIL** if:\n\n- UI count ≠ **78**\n- A UI directory is added without version bump\n- A UI directory is removed without version bump\n- A UI directory bypasses discovery\n\n---\n\n## Forbidden Patterns\n\n### Forbidden\n\n- \"We added one more UI, no big deal\"\n- Dynamic or runtime UI discovery\n- UI components outside `src/ui`\n- UI count changing without rule update\n\n---\n\n## Allowed Patterns\n\n### Allowed\n\n- Explicit version bump + rule update\n- Regenerated inventory with review\n- CI-enforced diff on UI list\n\n---\n\n## Rationale\n\nUI components are **public API surface**.\n\nAn uncontrolled UI set leads to:\n\n- Undocumented breaking changes\n- Token misuse\n- Design drift\n- Inconsistent consumer expectations\n\nA fixed inventory turns the design system into a **real framework**.\n\n---\n\n## Canon Outcome\n\n✅ UI inventory is explicit\n✅ Drift is impossible\n✅ Governance is enforced\n✅ CI blocks invalid changes\n\n---\n\n**UI count is not metadata.\nUI count is contract.**\n\n---\n\n**Version History:**\n- 1.0.0 (2026-01-28): Initial - 175 components (incorrect)\n- 1.1.0 (2026-01-29): Corrected - 78 components (filesystem reality)"},{"number":207,"slug":"preset-switching-never-changes-component-css","title":"Preset Switching Must Never Change Component CSS","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","preset","css","architecture"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Coupling components to preset values prevents runtime theming and requires rebuilds. Proper token indirection enables dynamic switching.","problem":"components depend on preset values causing rebuild and coupling","solution":"ensure components use tokens so preset changes only affect variable values","signals":["hardcoded color","rebuild required","theme break"],"search_intent":"how to make components preset agnostic","keywords":["preset switching css tokens","component token indirection","design system theming runtime","frontend preset architecture"],"body":"## Principle\n\n**Changing theme preset (amber → blue → green) must only change CSS variable values—never component structure or selectors.**\n\n- Preset switching is runtime variable substitution\n- No component rebuild required\n- No component CSS changes\n\n---\n\n## Problem\n\nWhen preset changes require component edits:\n\n- **Not scalable** - can't ship multiple presets to users\n- **Build-time coupling** - preset becomes a compile-time constant\n- **User theming impossible** - users can't customize colors\n- **Maintenance explosion** - adding preset = editing components\n\nReal test: Amber preset worked in dark AND light **without touching any component CSS**—proof of correct architecture.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```css\n/* button_ui.css - Component hardcoded to preset */\n[data-button][data-ui-variant=\"solid\"] {\n  background: hsl(38 92% 50%);  /* ❌ Amber hardcoded */\n}\n```\n```css\n/* button_ui.css - Component aware of preset */\n[data-theme=\"amber\"] [data-button] {\n  background: hsl(38 92% 50%);\n}\n\n[data-theme=\"blue\"] [data-button] {\n  background: hsl(210 100% 50%);\n}\n```\n\n**Why forbidden:** Component must be rebuilt for every preset. User theming is impossible.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```css\n/* button_ui.css - Preset-agnostic */\n[data-button][data-ui-variant=\"solid\"] {\n  background: var(--button-primary-bg);  /* ✅ Token reference only */\n  color: var(--button-primary-fg);\n}\n```\n```typescript\n/* presets/amber.ts */\nexport const amber: ThemeDefinition = {\n  modes: {\n    dark: {\n      colors: {\n        primary: { h: 38, s: 92, l: 50 }  /* Amber */\n      }\n    }\n  }\n};\n```\n```typescript\n/* presets/blue.ts */\nexport const blue: ThemeDefinition = {\n  modes: {\n    dark: {\n      colors: {\n        primary: { h: 210, s: 100, l: 50 }  /* Blue */\n      }\n    }\n  }\n};\n```\n```html\n<!-- Runtime preset switching -->\n<html data-theme=\"amber\" class=\"dark\">\n<html data-theme=\"blue\" class=\"dark\">\n```\n\n**Why correct:** Component CSS never changes. Only CSS variable values change. Instant theme switching.\n\n---\n\n## Rationale\n\n### Token Indirection\n```\nComponent:  background: var(--button-primary-bg)\n            ↓\nFamily:     --button-primary-bg: var(--semantic-action-primary-bg)\n            ↓\nTheme:      --semantic-action-primary-bg: hsl(var(--color-primary))\n            ↓\nPreset:     --color-primary: 38 92% 50%  (ONLY THING THAT CHANGES)\n```\n\nPreset swap = change one value. Entire UI updates.\n\n### Architectural Invariants\n\n1. **Components are data-agnostic** - they reference contracts\n2. **Contracts are stable** - token names never change\n3. **Values are dynamic** - presets provide different values for same tokens\n\n### Bugs Prevented\n\n- Component rebuild required for preset change\n- User can't customize colors\n- Theme switching requires page reload\n- Multiple presets bloat bundle size\n\n### Why Not Opinion\n\nThis is **dependency injection**. Components depend on abstractions (tokens), not concrete values (colors). Allows runtime substitution.\n\n---\n\n## Enforcement\n\n### Build verification\n```bash\n# Component CSS must not change between builds with different presets\nPRESET=amber npm run build && cp dist/canonrs.css canonrs-amber.css\nPRESET=blue npm run build && cp dist/canonrs.css canonrs-blue.css\n\n# Component sections must be identical\ndiff <(grep -A 50 \"button_ui.css\" canonrs-amber.css) \\\n     <(grep -A 50 \"button_ui.css\" canonrs-blue.css) \\\n  && echo \"✅ Components are preset-agnostic\"\n```\n\n### Architecture test\n```typescript\n// Test: switching preset without rebuild\ntest(\"preset switching updates colors without component changes\", () => {\n  const root = document.documentElement;\n  \n  // Amber preset\n  root.setAttribute(\"data-theme\", \"amber\");\n  expect(getComputedStyle(button).backgroundColor).toBe(\"rgb(245, 158, 11)\");\n  \n  // Blue preset (no rebuild, just change attribute)\n  root.setAttribute(\"data-theme\", \"blue\");\n  expect(getComputedStyle(button).backgroundColor).toBe(\"rgb(59, 130, 246)\");\n  \n  // Component CSS never changed\n  expect(button.style.cssText).toBe(\"\");\n});\n```\n\n### Review checklist\n\n- [ ] Components reference only family tokens\n- [ ] No color values in component CSS\n- [ ] Preset switching is attribute change, not rebuild\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf changing preset requires changing component CSS, the token architecture is broken. Fix the token layer.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":208,"slug":"working-theme-toggle-proves-correct-token-architecture","title":"A Working Theme Toggle Is Proof of Correct Token Architecture","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["tokens","theming","architecture","validation"],"language":"EN","version":"1.0.0","date":"2026-01-30","intro":"Visual bugs during theme toggle indicate token architecture issues, not logic errors. Proper separation ensures automatic updates via CSS.","problem":"theme toggle breaks visuals due to incorrect token architecture","solution":"fix token cascade and mappings instead of modifying javascript or components","signals":["wrong colors","toggle bug","visual mismatch"],"search_intent":"why theme toggle breaks ui tokens architecture","keywords":["theme toggle architecture test","css token cascade debug","design system toggle issue","frontend theming validation"],"body":"## Principle\n\n**If theme toggle breaks visual rendering, the error is in token architecture—not JavaScript or component logic.**\n\n- Theme toggle is an architectural litmus test\n- Visual bugs during toggle indicate token layer failures\n- Working toggle = correct separation of concerns\n\n---\n\n## Problem\n\nWhen theme toggle causes visual bugs:\n\n- **Token cascade is broken** - values not resolving correctly\n- **Semantic layer incomplete** - tokens missing for some contexts\n- **Component coupling** - components hardcoded to one mode\n- **Theme overrides wrong** - cascade order incorrect\n\nReal discovery: JavaScript toggle code was **correct from day one**. Visual bugs were 100% caused by token layer mistakes (hardcoded colors, wrong cascade order, missing semantic mappings).\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```javascript\n// ❌ Attempting to fix visual bugs in JavaScript\nconst toggle = () => {\n  html.classList.toggle(\"dark\");\n  \n  // ❌ Manually forcing colors (workaround for broken tokens)\n  if (html.classList.contains(\"dark\")) {\n    document.querySelectorAll('[data-button]').forEach(btn => {\n      btn.style.backgroundColor = \"#F59E0B\";  // ❌ Symptom treatment\n    });\n  }\n};\n```\n\n**Why forbidden:** This treats symptoms, not root cause. Token architecture is broken. Fix tokens, not JavaScript.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```bash\n# Step 1: Verify JavaScript is correct\n# Toggle adds/removes .dark class?\ndocument.documentElement.classList.contains('dark')  # Should toggle\n\n# Step 2: Verify preset is loaded\ngetComputedStyle(document.documentElement).getPropertyValue('--color-primary')\n# Should return preset value (e.g., \"38 92% 50%\")\n\n# Step 3: Verify semantic resolution\ngetComputedStyle(document.documentElement).getPropertyValue('--semantic-action-primary-bg')\n# Should change when toggling .dark\n\n# Step 4: Verify family binding\ngetComputedStyle(document.documentElement).getPropertyValue('--button-primary-bg')\n# Should match semantic token\n\n# Step 5: Verify component consumption\ngetComputedStyle(document.querySelector('[data-button]')).backgroundColor\n# Should match family token\n```\n\n**Fix tokens at the failing layer. Never fix in JavaScript.**\n\n---\n\n## Rationale\n\n### Theme Toggle as Architectural Test\n```\nToggle adds .dark class\n  ↓\nCSS cascade resolves [data-theme].dark selector\n  ↓\nSemantic tokens update via theme overrides\n  ↓\nFamily tokens reference semantic (no change needed)\n  ↓\nComponents reference family (no change needed)\n  ↓\nVisual updates automatically\n```\n\n**If this chain breaks, the break is in CSS, not JS.**\n\n### Architectural Invariants\n\n1. **Toggle changes one thing** - adds/removes `.dark` class\n2. **CSS does the rest** - cascade resolves everything\n3. **Components are passive** - they consume tokens, don't react to toggle\n\n### Bugs Prevented\n\n- Fixing token bugs in JavaScript (wrong layer)\n- Adding toggle logic to components (wrong abstraction)\n- Forcing colors via inline styles (bypasses architecture)\n- Believing toggle code is the problem (it almost never is)\n\n### Why Not Opinion\n\nThis is **separation of concerns testing**. If changing context (dark/light) breaks rendering, context isn't properly abstracted.\n\n---\n\n## Enforcement\n\n### Architecture validation test\n```typescript\ntest(\"theme toggle updates all components via tokens\", () => {\n  const button = document.querySelector('[data-button]');\n  const card = document.querySelector('[data-card]');\n  \n  // Dark mode\n  document.documentElement.classList.add('dark');\n  const darkBg = getComputedStyle(button).backgroundColor;\n  const darkCardBg = getComputedStyle(card).backgroundColor;\n  \n  // Light mode\n  document.documentElement.classList.remove('dark');\n  const lightBg = getComputedStyle(button).backgroundColor;\n  const lightCardBg = getComputedStyle(card).backgroundColor;\n  \n  // Colors changed\n  expect(darkBg).not.toBe(lightBg);\n  expect(darkCardBg).not.toBe(lightCardBg);\n  \n  // JavaScript did not touch component styles\n  expect(button.style.cssText).toBe(\"\");\n  expect(card.style.cssText).toBe(\"\");\n});\n```\n\n### Debug checklist (when toggle breaks)\n\n- [ ] Verify JavaScript: does `.dark` class toggle?\n- [ ] Verify preset: does `--color-primary` exist?\n- [ ] Verify theme: does `--semantic-*` change with `.dark`?\n- [ ] Verify family: does `--button-*` reference `--semantic-*`?\n- [ ] Verify component: does component use `--button-*` (not hardcoded)?\n- [ ] Verify cascade: are themes imported last?\n\n### Review principle\n\n> **If toggle breaks visuals, assume tokens are wrong—not JavaScript.**\n\nInvestigate in this order:\n1. Token definitions (preset, semantic, family)\n2. CSS cascade order\n3. Component token references\n4. JavaScript (last resort, usually correct)\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf toggle requires JavaScript beyond class manipulation, the token architecture is fundamentally broken.\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version (2026-01-30)"},{"number":209,"slug":"axum-version-must-match-adapter","title":"Axum Version Must Match Adapter Version","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["axum","dependencies","ssr","build"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Version mismatches between adapter and framework cause type conflicts and build instability. Dependency alignment is mandatory.","problem":"axum versions differ between adapter and app causing type mismatch","solution":"use exact axum version defined by adapter without override","signals":["type mismatch","build error","duplicate crate"],"search_intent":"how to fix axum version mismatch error","keywords":["axum version mismatch","rust dependency conflict axum","ssr adapter compatibility","cargo dependency alignment"],"body":"## Principle\n\n**An application MUST use the exact Axum version required by its server adapter.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWhen Axum versions diverge between the application and the adapter layer:\n\n- Type mismatches between identical-looking types\n- `Body` incompatibilities at compile time\n- Multiple `axum_core` versions in the dependency graph\n- Non-obvious, hard-to-debug build failures\n\nThis breaks SSR routing and streaming deterministically.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```toml\nleptos_axum = \"0.8\"\naxum = \"0.7\"\n```\n\nThe application is overriding a framework dependency owned by the adapter.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```toml\nleptos_axum = \"0.8\"\naxum = \"0.8\"\n```\n\nThe adapter defines the framework version contract.\n\n---\n\n## Rationale\n\nAdapters are integration boundaries.\n\n- They define concrete framework versions\n- They encapsulate compatibility guarantees\n- Allowing apps to override breaks type identity\n- This is a contract, not a preference\n\n---\n\n## Enforcement\n\n- `cargo tree | grep axum` must show a single major version\n- CI must fail on multiple `axum_core` versions\n- Code review checklist item\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":210,"slug":"cdylib-mandatory-for-hydration","title":"cdylib Is Mandatory for Hydration","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["wasm","hydration","build","rust"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Missing crate type prevents wasm generation and causes silent hydration failures. Explicit configuration is required.","problem":"cdylib crate type is missing causing wasm generation failure","solution":"declare cdylib crate type to ensure proper wasm output","signals":["no wasm","hydration fail","build error"],"search_intent":"how to fix missing cdylib wasm hydration","keywords":["cdylib wasm rust","leptos hydration build error","wasm crate type config","frontend rust wasm setup"],"body":"## Principle\n\n**Any Leptos application that supports hydration MUST declare `cdylib` as a crate type.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWithout `cdylib`:\n\n- WASM artifacts are not generated\n- `wasm-bindgen` fails late\n- Hydration silently breaks\n- Build errors appear far from the root cause\n\nThis creates false-positive “successful” builds.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```toml\n[package]\nname = \"app\"\n```\n\nNo explicit library crate type for WASM output.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```toml\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n```\n\nThis guarantees correct WASM emission.\n\n---\n\n## Rationale\n\nHydration is a cross-compilation contract.\n\n- WASM output is mandatory\n- Tooling assumes `cdylib`\n- Implicit defaults are unsafe\n- This prevents late-stage failures\n\n---\n\n## Enforcement\n\n- CI validates presence of `cdylib`\n- Missing crate-type is a build error\n- Review checklist item\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":211,"slug":"ssr-meta-requires-html-shell","title":"SSR Meta Requires Explicit HTML Shell","status":"ENFORCED","severity":"HIGH","category":"core-runtime","tags":["ssr","html","meta","structure"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Missing html shell causes runtime errors and invalid meta rendering. SSR requires explicit document structure.","problem":"ssr meta is used without html shell causing runtime failure","solution":"provide explicit html shell with head and body for ssr rendering","signals":["runtime panic","missing head","meta error"],"search_intent":"how to fix leptos meta ssr shell error","keywords":["ssr html shell leptos","meta rendering error ssr","frontend html structure requirement","leptos meta fix"],"body":"## Principle\n\n**When using `leptos_meta` in SSR, an explicit HTML shell MUST be provided.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWithout an explicit shell:\n\n- Runtime panics occur\n- Meta tags cannot resolve a `<head>`\n- HTML structure becomes ambiguous\n- Errors surface only at runtime\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\nprovide_meta_context();\nview! { <Title/> <Meta/> <Body/> }\n```\n\nNo enclosing HTML document.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\npub fn shell(options: LeptosOptions) -> impl IntoView {\n    view! {\n        <!DOCTYPE html>\n        <html>\n            <head>\n                <HydrationScripts options/>\n                <MetaTags/>\n            </head>\n            <body>\n                <App/>\n            </body>\n        </html>\n    }\n}\n```\n\n---\n\n## Rationale\n\nMeta rendering requires a concrete document boundary.\n\n- Head/body separation is mandatory\n- Implicit containers are invalid\n- SSR must be structurally explicit\n\n---\n\n## Enforcement\n\n- SSR builds must expose `shell()`\n- Missing shell fails review\n- Runtime panic considered violation\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":212,"slug":"hydration-bootstrap-tooling-owned","title":"Hydration Bootstrap Is Tooling-Owned","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","wasm","bootstrap","tooling"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Manual wasm bootstrap conflicts with tooling lifecycle and causes duplicated initialization. Ownership must remain in tooling.","problem":"application controls wasm bootstrap causing lifecycle conflicts","solution":"delegate bootstrap to tooling and expose only entrypoint","signals":["double init","unexpected behavior","bootstrap conflict"],"search_intent":"how to fix wasm bootstrap conflict leptos","keywords":["wasm bootstrap leptos","hydration lifecycle control","frontend wasm startup pattern","tooling owned bootstrap"],"body":"## Principle\n\n**The WASM hydration bootstrap MUST be owned by the build tooling, not the application.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nManual bootstrap:\n\n- Conflicts with `cargo-leptos`\n- Breaks tool-controlled lifecycle\n- Causes double initialization\n- Produces non-deterministic behavior\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n#[wasm_bindgen(start)]\nfn run() {\n    hydrate();\n}\n```\n\nApplication hijacks the bootstrap phase.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n#[cfg(feature = \"hydrate\")]\nfn main() {\n    app::hydrate();\n}\n```\n\nTooling controls startup; app exposes entrypoint only.\n\n---\n\n## Rationale\n\nBootstrap is infrastructure responsibility.\n\n- Tools orchestrate WASM lifecycle\n- Apps must remain passive\n- Prevents duplicated initialization\n- Enforces clear ownership\n\n---\n\n## Enforcement\n\n- Static analysis for `wasm_bindgen(start)`\n- CI failure on forbidden pattern\n- Mandatory code review check\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":213,"slug":"streaming-ssr-requires-executor","title":"Streaming SSR Requires Explicit Executor Initialization","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["ssr","async","executor","streaming"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Streaming SSR fails silently or crashes when no async executor is initialized. Deterministic scheduling requires explicit executor setup.","problem":"streaming ssr runs without executor causing runtime failures","solution":"initialize async executor explicitly before rendering","signals":["runtime panic","stream hang","async failure"],"search_intent":"how to fix streaming ssr executor init","keywords":["streaming ssr executor rust","async executor initialization","leptos streaming issue","tokio executor setup"],"body":"## Principle\n\n**Streaming SSR MUST explicitly initialize an async executor before rendering.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWithout executor initialization:\n\n- Runtime panics occur\n- Errors are silent or misleading\n- Streaming hangs or crashes\n- Debugging becomes impractical\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n#[tokio::main]\nasync fn main() {\n    render_app();\n}\n```\n\nNo executor initialization.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n#[tokio::main]\nasync fn main() {\n    any_spawner::Executor::init_tokio();\n    render_app();\n}\n```\n\nExecutor is explicitly defined.\n\n---\n\n## Rationale\n\nStreaming SSR depends on async scheduling.\n\n- Executors are not implicit\n- Tooling does not auto-initialize\n- Explicit init guarantees determinism\n\n---\n\n## Enforcement\n\n- Build-time lint for missing initialization\n- Runtime panic treated as rule violation\n- Review checklist requirement\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":214,"slug":"lib-owns-app-structure","title":"lib.rs Owns Application Structure and UI Semantics","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["architecture","ui","ssr","csr"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Distributing UI structure across entrypoints breaks determinism and creates divergence between SSR and CSR. A single semantic root is required.","problem":"application structure is defined outside lib rs causing architectural drift","solution":"centralize all ui composition and routing in lib rs","signals":["ui in main","routing drift","meta inconsistency"],"search_intent":"how to centralize app structure in lib rs","keywords":["lib rs architecture leptos","ui composition centralization","ssr csr semantic root","frontend app structure pattern"],"body":"## Principle\n\n**`lib.rs` is the sole owner of application structure, UI composition, routing, and semantic intent.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWhen `lib.rs` is treated as a secondary or optional file:\n\n- UI logic leaks into `main.rs`\n- SSR and CSR responsibilities blur\n- Meta configuration becomes inconsistent\n- Hydration and routing behavior diverge\n- The application loses a single semantic root\n\nThis leads to architectural drift and non-deterministic rendering behavior.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n// main.rs\nRouter::new()\n    .leptos_routes(&opts, routes, || view! { <App/> })\n```\n\nUI structure is being constructed outside `lib.rs`.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n// lib.rs\n#[component]\npub fn App() -> impl IntoView {\n    provide_meta_context();\n    view! {\n        <Router>\n            <Routes>...</Routes>\n        </Router>\n    }\n}\n```\n\n`lib.rs` defines what the application **is**, not how it is served.\n\n---\n\n## Rationale\n\nThis rule enforces a single semantic root.\n\n- UI composition must be centralized\n- SSR and CSR must share the same semantic tree\n- `lib.rs` is the canonical description of the app\n- Prevents duplication and divergence\n\nThis is an architectural invariant, not a stylistic choice.\n\n---\n\n## Enforcement\n\n- Code review: UI code in `main.rs` is forbidden\n- CI lint: forbid `view! { <App/> }` outside `lib.rs`\n- Structural audits\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":215,"slug":"main-owns-runtime-bootstrap","title":"main.rs Owns Runtime and Server Bootstrap Exclusively","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["bootstrap","runtime","ssr","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Mixing application logic into bootstrap breaks determinism and entangles SSR CSR execution paths. Runtime initialization must remain isolated.","problem":"main rs contains application logic causing coupling and instability","solution":"restrict main rs to runtime initialization and server bootstrap only","signals":["logic in main","feature entanglement","bootstrap error"],"search_intent":"how to separate runtime bootstrap from app logic","keywords":["main rs bootstrap pattern","runtime initialization separation","ssr architecture rust","frontend bootstrap isolation"],"body":"## Principle\n\n**`main.rs` MUST contain only runtime initialization and server bootstrap logic.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWhen `main.rs` contains application logic:\n\n- Build features become entangled\n- SSR/CSR branches become unsafe\n- UI logic is duplicated or conditionally compiled\n- Bootstrap order becomes fragile\n\nThis breaks determinism and makes the application untestable in isolation.\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n#[tokio::main]\nasync fn main() {\n    view! { <App/> }\n}\n```\n\nRendering logic in the bootstrap layer.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n#[cfg(feature = \"ssr\")]\n#[tokio::main]\nasync fn main() {\n    any_spawner::Executor::init_tokio();\n    start_server();\n}\n```\n\nBootstrap only. No UI semantics.\n\n---\n\n## Rationale\n\nBootstrap is infrastructural.\n\n- It wires runtimes, servers, and ports\n- It must be replaceable without touching UI\n- It must never affect application semantics\n\nThis separation is mandatory for enterprise-grade systems.\n\n---\n\n## Enforcement\n\n- CI: fail if `view!` macro appears in `main.rs`\n- Code review checklist\n- Static analysis\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":216,"slug":"app-must-not-define-html","title":"App Components Must Not Define HTML Structure","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["html","ssr","components","structure"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Embedding document structure in components causes invalid markup and hydration failures. HTML boundaries must remain in shell.","problem":"components define html structure causing invalid ssr output","solution":"limit html document structure to shell and keep components content only","signals":["invalid html","hydration error","meta failure"],"search_intent":"how to prevent html structure in components","keywords":["ssr html structure separation","component document boundary","frontend html shell pattern","leptos invalid html fix"],"body":"## Principle\n\n**Application components MUST NOT define `<html>`, `<head>`, or `<body>` structure.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nWhen components define HTML structure:\n\n- Meta injection breaks\n- Hydration becomes undefined\n- Multiple head/body contexts appear\n- SSR output becomes invalid HTML\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n#[component]\nfn App() -> impl IntoView {\n    view! {\n        <Html/>\n        <Head/>\n        <Body>...</Body>\n    }\n}\n```\n\nStructural HTML inside UI components.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n#[component]\nfn App() -> impl IntoView {\n    view! {\n        <Title/>\n        <Meta/>\n        <Router/>\n    }\n}\n```\n\nStructural HTML belongs to the shell only.\n\n---\n\n## Rationale\n\nHTML structure is a document-level concern.\n\n- UI components describe content, not documents\n- SSR shells own the document boundary\n- Prevents invalid markup and runtime panics\n\n---\n\n## Enforcement\n\n- Static scan for `<html>` in components\n- SSR runtime validation\n- Review enforcement\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":217,"slug":"entrypoints-must-be-explicit","title":"Entry Points Must Be Explicit, Public, and Isolated","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["entrypoints","features","ssr","csr"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Implicit entrypoints cause build ambiguity and feature leakage across targets. Explicit contracts ensure correct integration.","problem":"entrypoints are implicit or private causing build ambiguity","solution":"define public feature isolated entrypoints for ssr and csr","signals":["unresolved import","feature leak","build failure"],"search_intent":"how to define explicit entrypoints ssr csr","keywords":["entrypoint visibility rust","ssr csr entrypoint pattern","feature isolation build","frontend integration contract"],"body":"## Principle\n\n**All SSR and CSR entrypoints MUST be explicit, public, and feature-isolated.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nImplicit or private entrypoints cause:\n\n- Unresolvable imports\n- Feature flag leakage\n- Build-time ambiguity\n- Tooling incompatibility\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n#[cfg(feature = \"hydrate\")]\nfn hydrate() { ... }\n```\n\nEntrypoint not exported.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n#[cfg(feature = \"hydrate\")]\npub fn hydrate() { ... }\n\n#[cfg(feature = \"ssr\")]\npub fn shell(opts: LeptosOptions) -> impl IntoView { ... }\n```\n\nEntrypoints are explicit contracts.\n\n---\n\n## Rationale\n\nEntrypoints are integration surfaces.\n\n- Tooling depends on visibility\n- Features must not overlap\n- Isolation guarantees determinism\n\n---\n\n## Enforcement\n\n- Build fails on unresolved imports\n- CI validates `pub` visibility\n- Feature-gated symbol checks\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":218,"slug":"legacy-rendering-apis-forbidden","title":"Legacy Rendering APIs Are Forbidden","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["ssr","rendering","deprecated","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Using legacy rendering APIs bypasses canonical integration paths and introduces hidden technical debt. Forward-only architecture must be enforced.","problem":"deprecated rendering apis are used causing architectural inconsistency","solution":"use only canonical rendering integration paths and remove legacy apis","signals":["legacy api","hydration mismatch","integration break"],"search_intent":"how to replace legacy rendering api leptos","keywords":["legacy rendering api removal","ssr canonical integration","frontend api deprecation","leptos rendering migration"],"body":"## Principle\n\n**Deprecated or legacy rendering APIs MUST NOT be used in new or migrated systems.**\n\n- Objective\n- Verifiable\n- One clear boundary\n\n---\n\n## Problem\n\nUsing legacy APIs:\n\n- Bypasses canonical shell handling\n- Breaks meta integration\n- Causes hydration mismatches\n- Locks architecture to obsolete patterns\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\nrender_app_to_stream(|| view! { <App/> })\n```\n\nManual streaming without shell integration.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\nRouter::new()\n    .leptos_routes(&opts, routes, || shell(opts.clone()))\n```\n\nCanonical integration path.\n\n---\n\n## Rationale\n\nFramework evolution invalidates old entrypoints.\n\n- CanonRS enforces forward-only architecture\n- Legacy APIs create hidden technical debt\n- Migration must be explicit and complete\n\n---\n\n## Enforcement\n\n- Static analysis for forbidden symbols\n- CI block on legacy imports\n- Mandatory migration checklist\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":219,"slug":"leptos-consistent-ports","title":"Leptos Product Must Declare Consistent Ports Across Workspace and Local Config","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["ports","leptos","workspace","config"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Port mismatches between workspace and product configs cause runtime inconsistency and broken development workflows. Configuration must be deterministic.","problem":"ports differ between workspace and local config causing runtime inconsistency","solution":"ensure site addr and reload port match across all configurations","signals":["wrong port","connection refused","reload failure"],"search_intent":"how to fix leptos port mismatch config","keywords":["leptos port mismatch","workspace config alignment","frontend dev server port","cargo leptos serve issue"],"body":"## Principle\n\n**`site-addr` and `reload-port` MUST match exactly between `[[workspace.metadata.leptos]]` and product `Leptos.toml`.**\n\n---\n\n## Problem\n\nWithout port consistency:\n- Server starts on wrong port (e.g., 3000 vs 3004)\n- Hot reload connects to wrong port, fails silently\n- Developer confusion: \"cargo leptos serve\" uses different port than \"cargo run\"\n- CI/CD deploys to unexpected ports\n\n**Observable symptoms**:\n```\n# Terminal 1\ncargo leptos serve\n🚀 Listening on http://127.0.0.1:3000\n\n# Terminal 2 (user expects 3004)\ncurl http://127.0.0.1:3004\nConnection refused\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```toml\n# ROOT Cargo.toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nsite-addr = \"127.0.0.1:3000\"\nreload-port = 3001\n\n# products/canonrs-site/Leptos.toml\n[package.metadata.leptos]\nsite-addr = \"127.0.0.1:3004\"  # ❌ DIVERGENT\nreload-port = 3005             # ❌ DIVERGENT\n```\n\n**Why this violates**: `cargo-leptos` may use either config depending on invocation context, causing unpredictable behavior.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```toml\n# ROOT Cargo.toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nsite-addr = \"127.0.0.1:3000\"\nreload-port = 3001\n\n# products/canonrs-site/Leptos.toml\n[package.metadata.leptos]\nsite-addr = \"127.0.0.1:3000\"  # ✅ MATCHES\nreload-port = 3001             # ✅ MATCHES\n```\n\n**Why this complies**: Single source of truth for runtime ports, regardless of build invocation.\n\n---\n\n## Rationale\n\n### Architectural invariants\n1. **Deterministic deployment**: Port configuration must be unambiguous\n2. **Hot reload contract**: Reload port mismatch breaks development loop\n3. **Multi-product workspaces**: Each product needs unique ports, but internal consistency\n\n### Bugs prevented\n- Silent hot reload failures (connects to wrong port)\n- Production deploys on dev ports\n- Port conflicts when running multiple products\n\n### Why not opinion\nPort binding is an OS-level contract. Divergent configuration creates race conditions and undefined behavior at runtime.\n\n---\n\n## Enforcement\n\n### CI validation\n```bash\n#!/bin/bash\n# check-leptos-ports.sh\n\nfor product in products/*/Leptos.toml; do\n    product_name=$(basename $(dirname $product))\n    \n    # Extract ports from workspace\n    workspace_site_addr=$(grep -A 20 \"name = \\\"$product_name\\\"\" Cargo.toml | grep site-addr | head -1)\n    workspace_reload=$(grep -A 20 \"name = \\\"$product_name\\\"\" Cargo.toml | grep reload-port | head -1)\n    \n    # Extract from local\n    local_site_addr=$(grep site-addr $product)\n    local_reload=$(grep reload-port $product)\n    \n    if [[ \"$workspace_site_addr\" != \"$local_site_addr\" ]]; then\n        echo \"❌ Port mismatch in $product_name\"\n        exit 1\n    fi\ndone\n```\n\n### Build-time check\n```toml\n# Cargo.toml (via build.rs)\n[package.metadata.leptos.validation]\nenforce-port-consistency = true\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nPorts must be deterministic. If a product needs different ports in different environments, use environment variables at runtime, not config divergence.\n\n---\n\n## Version History\n\n- **1.0.0** (2026-02-03) — Initial version"},{"number":220,"slug":"unique-build-targets","title":"Workspace Metadata Must Define Unique Build Targets Per Product","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["workspace","build","targets","leptos"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Shared build directories cause artifact collisions and corrupted outputs across products. Isolation is required for deterministic builds.","problem":"multiple products share same build output causing overwrite and corruption","solution":"assign unique site root per product to isolate build outputs","signals":["artifact overwrite","wrong wasm","build conflict"],"search_intent":"how to fix shared build output leptos workspace","keywords":["leptos site root conflict","workspace build isolation","wasm artifact overwrite","frontend multi product build"],"body":"## Principle\n\n**Each `[[workspace.metadata.leptos]]` block MUST specify a unique `site-root` to prevent output collision.**\n\n---\n\n## Problem\n\nWithout unique build targets:\n- Products overwrite each other's outputs\n- `target/site/pkg/` contains mixed WASM from multiple products\n- Hot reload serves wrong product's assets\n- CI artifacts are corrupted (mixed binaries)\n\n**Observable symptoms**:\n```bash\ncargo leptos build --package canonrs-site\n# Output: target/site/\n\ncargo leptos build --package canonrs-workbench\n# Output: target/site/  ❌ OVERWRITES PREVIOUS\n\nls target/site/pkg/\ncanonrs_workbench.wasm  # ❌ Where is canonrs_site.wasm?\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nsite-root = \"target/site\"      # ❌ SHARED\nsite-pkg-dir = \"pkg\"\n\n[[workspace.metadata.leptos]]\nname = \"canonrs-workbench\"\nsite-root = \"target/site\"      # ❌ COLLISION\nsite-pkg-dir = \"pkg\"\n```\n\n**Why this violates**: Both products write to same directory, last build wins.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nsite-root = \"target/site\"           # ✅ UNIQUE\nsite-pkg-dir = \"pkg\"\n\n[[workspace.metadata.leptos]]\nname = \"canonrs-workbench\"\nsite-root = \"target/site-workbench\" # ✅ UNIQUE\nsite-pkg-dir = \"pkg\"\n\n[[workspace.metadata.leptos]]\nname = \"core-auth-frontend\"\nsite-root = \"target/site-auth\"      # ✅ UNIQUE\nsite-pkg-dir = \"pkg\"\n```\n\n**Why this complies**: Each product has isolated output directory, no collision possible.\n\n---\n\n## Rationale\n\n### Architectural invariants\n1. **Build isolation**: Each product is independently deployable\n2. **Artifact integrity**: Mixed outputs break deployment pipelines\n3. **Parallel builds**: Unique targets enable `cargo build -j`\n\n### Bugs prevented\n- Deployed wrong product to production (mixed WASM)\n- Hot reload serves stale or wrong product\n- CI cache corruption (artifacts from different products)\n\n### Why not opinion\nFile system collisions are race conditions. Shared output directories violate the principle of build isolation.\n\n---\n\n## Enforcement\n\n### CI validation\n```bash\n#!/bin/bash\n# check-unique-site-roots.sh\n\nsite_roots=$(grep -h \"site-root\" Cargo.toml | sort)\nunique_roots=$(echo \"$site_roots\" | uniq)\n\nif [[ \"$site_roots\" != \"$unique_roots\" ]]; then\n    echo \"❌ Duplicate site-root detected\"\n    echo \"$site_roots\"\n    exit 1\nfi\n```\n\n### Linter rule\n```rust\n// workspace-lint/src/leptos.rs\nfn validate_site_roots(metadata: &[LeptosMetadata]) -> Result<()> {\n    let roots: HashSet<_> = metadata.iter().map(|m| &m.site_root).collect();\n    \n    if roots.len() != metadata.len() {\n        bail!(\"Duplicate site-root in workspace.metadata.leptos\");\n    }\n    \n    Ok(())\n}\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nEven for single-product workspaces, use unique `site-root` to future-proof against adding more products.\n\n---\n\n## Version History\n\n- **1.0.0** (2026-02-03) — Initial version"},{"number":221,"slug":"critical-config-alignment","title":"Leptos.toml Critical Fields Must Align With Workspace Metadata","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["config","workspace","leptos","build"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Divergent configuration fields create inconsistent builds and runtime behavior across tools. Critical fields must remain aligned.","problem":"critical config fields differ causing inconsistent builds and runtime mismatch","solution":"ensure critical fields match exactly between workspace and local configs","signals":["feature mismatch","port conflict","build divergence"],"search_intent":"how to align leptos config workspace and local","keywords":["leptos config alignment","workspace metadata consistency","frontend build config mismatch","cargo leptos config issue"],"body":"## Principle\n\n**`site-addr`, `reload-port`, `bin-features`, and `lib-features` MUST match exactly between workspace metadata and product Leptos.toml. Path fields (`style-file`, `assets-dir`) MAY differ due to context (root-relative vs product-relative).**\n\n---\n\n## Problem\n\nWithout alignment on critical fields:\n- Features diverge: SSR builds with hydrate features, or vice-versa\n- Ports mismatch (covered by Rule #219, but reinforced here)\n- Build flags inconsistent between `cargo leptos build` and `cargo build --features`\n\n**Observable symptoms**:\n```rust\n// Workspace says: lib-features = [\"hydrate\"]\n// Leptos.toml says: lib-features = [\"hydrate\", \"csr\"]\n\n// Result: cargo leptos build uses \"hydrate\"\n//         cargo build uses \"hydrate,csr\"\n// Divergent binaries!\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```toml\n# ROOT Cargo.toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nbin-features = [\"ssr\"]\nlib-features = [\"hydrate\"]\n\n# products/canonrs-site/Leptos.toml\n[package.metadata.leptos]\nbin-features = [\"ssr\", \"debug\"]  # ❌ DIVERGENT\nlib-features = [\"hydrate\", \"csr\"] # ❌ DIVERGENT\n```\n\n**Why this violates**: Feature flags define compilation boundaries. Divergence creates two different binaries depending on build tool.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```toml\n# ROOT Cargo.toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nsite-addr = \"127.0.0.1:3000\"\nreload-port = 3001\nbin-features = [\"ssr\"]\nlib-features = [\"hydrate\"]\nstyle-file = \"products/canonrs-site/style/output.css\"  # Root-relative\nassets-dir = \"products/canonrs-site/public\"            # Root-relative\n\n# products/canonrs-site/Leptos.toml\n[package.metadata.leptos]\nsite-addr = \"127.0.0.1:3000\"     # ✅ MATCHES\nreload-port = 3001                # ✅ MATCHES\nbin-features = [\"ssr\"]            # ✅ MATCHES\nlib-features = [\"hydrate\"]        # ✅ MATCHES\nstyle-file = \"public/style/output.css\"  # ✅ Product-relative (OK)\nassets-dir = \"public\"                    # ✅ Product-relative (OK)\n```\n\n**Why this complies**: Critical runtime/compilation fields are identical. Path fields differ legitimately due to invocation context.\n\n---\n\n## Rationale\n\n### Architectural invariants\n1. **Feature flag determinism**: Compilation must be reproducible regardless of tool\n2. **Configuration DRY**: Critical values defined once, paths adjusted for context\n3. **Tool interoperability**: `cargo leptos`, `cargo build`, and CI all produce same binary\n\n### Bugs prevented\n- Hydration mismatches (SSR built with different features than WASM)\n- Port conflicts (different tools bind different ports)\n- Deployment failures (CI builds different binary than local)\n\n### Why not opinion\nFeature flags alter the compiled artifact at the ABI level. Divergent flags = different binaries = undefined behavior.\n\n---\n\n## Enforcement\n\n### CI validation\n```bash\n#!/bin/bash\n# check-critical-alignment.sh\n\nCRITICAL_FIELDS=(\"site-addr\" \"reload-port\" \"bin-features\" \"lib-features\")\n\nfor field in \"${CRITICAL_FIELDS[@]}\"; do\n    workspace_val=$(grep -A 20 \"workspace.metadata.leptos\" Cargo.toml | grep \"$field\")\n    local_val=$(grep \"$field\" products/*/Leptos.toml)\n    \n    # Compare (ignoring path context)\n    if [[ \"$workspace_val\" != \"$local_val\" ]]; then\n        echo \"❌ Mismatch on $field\"\n        exit 1\n    fi\ndone\n```\n\n### Review checklist\n```markdown\n- [ ] Ports match between workspace and Leptos.toml\n- [ ] bin-features are identical\n- [ ] lib-features are identical\n- [ ] Paths are context-appropriate (root vs product)\n```\n\n---\n\n## Exceptions\n\n**Paths are exempt**: `style-file`, `assets-dir`, `site-root` MAY differ because:\n- Workspace metadata uses root-relative paths\n- Leptos.toml uses product-relative paths\n- Both resolve to the same filesystem location\n\nAll other fields have **no exceptions**.\n\n---\n\n## Version History\n\n- **1.0.0** (2026-02-03) — Initial version"},{"number":222,"slug":"cargo-metadata-resolvable","title":"Workspace Members Must Be Fully Resolvable by Cargo Metadata","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["workspace","cargo","metadata","build"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Invalid workspace member paths break cargo metadata resolution and cause partial builds or missing crates. All members must resolve correctly.","problem":"workspace members do not resolve to valid cargo toml files causing incomplete builds","solution":"ensure all workspace member paths resolve and appear in cargo metadata output","signals":["missing package","partial build","metadata mismatch"],"search_intent":"how to fix cargo workspace members not found","keywords":["cargo metadata workspace issue","workspace member path error","rust workspace resolution","cargo build workspace failure"],"body":"## Principle\n\n**All paths in `[workspace.members]` MUST resolve to valid `Cargo.toml` files and appear in `cargo metadata --format-version 1` output.**\n\n---\n\n## Problem\n\nWithout resolvable members:\n- `cargo build --workspace` fails silently or partially\n- IDE tooling (rust-analyzer) cannot find crates\n- `cargo test --workspace` skips broken members\n- Dependency graph is incomplete, causing version conflicts\n\n**Observable symptoms**:\n```bash\ncargo metadata --format-version 1 | jq '.workspace_members | length'\n4  # Expected 5 members\n\ncargo build --workspace\nerror: package `canonrs-site` not found in workspace\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```toml\n[workspace]\nmembers = [\n    \"canonrs-site\",                    # ❌ Missing 'products/' prefix\n    \"packages-rust/canonrs-ssr\",       # ❌ Missing 'rs-canonrs/' level\n    \"products/canonrs-workbench/src\",  # ❌ Points to src/, not root\n]\n```\n\n**Why this violates**: Cargo cannot find `Cargo.toml` at these paths from workspace root.\n\n**Validation**:\n```bash\nls canonrs-site/Cargo.toml\n# ls: cannot access 'canonrs-site/Cargo.toml': No such file or directory\n```\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```toml\n[workspace]\nmembers = [\n    \"packages-rust/rs-canonrs/canonrs\",\n    \"packages-rust/rs-canonrs/canonrs-ssr\",\n    \"packages-rust/rs-canonrs/canonrs-csr\",\n    \"packages-rust/rs-canonrs/canonrs-shared\",\n    \"products/canonrs-site\",\n    \"products/canonrs-workbench\",\n]\nresolver = \"2\"\n```\n\n**Why this complies**: Each path points to directory containing `Cargo.toml`.\n\n**Validation**:\n```bash\nfor member in $(grep -A 10 'members =' Cargo.toml | grep '\"' | tr -d '\", '); do\n    if [[ ! -f \"$member/Cargo.toml\" ]]; then\n        echo \"❌ Invalid member: $member\"\n        exit 1\n    fi\ndone\n```\n\n---\n\n## Rationale\n\n### Architectural invariants\n1. **Workspace integrity**: Cargo must know all crates to resolve deps correctly\n2. **Tool compatibility**: rust-analyzer, cargo-leptos, CI all depend on metadata\n3. **Build reproducibility**: Missing members cause non-deterministic builds\n\n### Bugs prevented\n- Partial builds (some crates ignored)\n- IDE autocomplete failures (crates not indexed)\n- Version resolution errors (incomplete dependency graph)\n- CI cache misses (workspace structure unknown)\n\n### Why not opinion\n`cargo metadata` is the canonical source of truth for Rust tooling. Invalid members break the entire ecosystem contract.\n\n---\n\n## Enforcement\n\n### CI validation (strict)\n```bash\n#!/bin/bash\n# validate-workspace-members.sh\n\n# Get expected members from Cargo.toml\nexpected=$(grep -A 50 'members =' Cargo.toml | grep '\"' | tr -d '\", ' | sort)\n\n# Get actual members from cargo metadata\nactual=$(cargo metadata --format-version 1 --no-deps | \\\n         jq -r '.workspace_members[]' | \\\n         cut -d' ' -f1 | sort)\n\nif [[ \"$expected\" != \"$actual\" ]]; then\n    echo \"❌ Workspace member mismatch\"\n    echo \"Expected:\"\n    echo \"$expected\"\n    echo \"Actual:\"\n    echo \"$actual\"\n    exit 1\nfi\n\necho \"✅ All members resolvable\"\n```\n\n### Build-time check\n```bash\n# In CI pipeline\ncargo metadata --format-version 1 > /dev/null || {\n    echo \"❌ cargo metadata failed - invalid workspace\"\n    exit 1\n}\n```\n\n### Pre-commit hook\n```bash\n#!/bin/bash\n# .git/hooks/pre-commit\n\nfor member in $(grep -A 50 'members =' Cargo.toml | grep '\"' | tr -d '\", '); do\n    if [[ ! -f \"$member/Cargo.toml\" ]]; then\n        echo \"❌ Cannot commit: invalid workspace member $member\"\n        exit 1\n    fi\ndone\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nEvery entry in `members = [...]` must resolve to a valid Cargo.toml. Use glob patterns (`\"products/*\"`) only if ALL directories match.\n\n---\n\n## Version History\n\n- **1.0.0** (2026-02-03) — Initial version"},{"number":223,"slug":"feature-flag-isolation","title":"Feature Flag Scopes Must Not Leak Between Products","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["features","workspace","ssr","csr"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Feature flag leakage across products causes incorrect compilation and bloated binaries. Isolation is required for deterministic builds.","problem":"feature flags leak between products causing incorrect builds and bloat","solution":"define explicit package scoped feature flags with no cross contamination","signals":["feature leak","wrong build","dependency bloat"],"search_intent":"how to isolate feature flags in workspace","keywords":["feature flag isolation rust","workspace feature leakage","ssr csr feature separation","cargo feature boundaries"],"body":"## Principle\n\n**Each `[[workspace.metadata.leptos]]` block MUST define `bin-features` and `lib-features` that apply ONLY to its specified `bin-package` and `lib-package`, with no cross-contamination.**\n\n---\n\n## Problem\n\nWithout feature isolation:\n- SSR packages (`canonrs-ssr`) compile with hydrate features\n- Hydrate builds include server-only dependencies (tokio, axum)\n- WASM binaries bloated with unused SSR code\n- Violates Rule #196 (SSR/CSR separation) at workspace level\n\n**Observable symptoms**:\n```bash\ncargo build --package canonrs-ssr --features hydrate\n# ❌ SSR package compiling with CSR features\n\ncargo tree -p canonrs-site --features hydrate | grep tokio\n# ❌ WASM build includes tokio (SSR-only dep)\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```toml\n# ROOT Cargo.toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\n# ❌ No bin-package/lib-package specified\nbin-features = [\"ssr\"]\nlib-features = [\"hydrate\"]\n\n# Result: ALL workspace members get these features!\n\n[[workspace.metadata.leptos]]\nname = \"canonrs-workbench\"\nbin-features = [\"ssr\", \"debug\"]  # ❌ Leaks to canonrs-site\nlib-features = [\"hydrate\"]\n```\n\n**Why this violates**: Without explicit package binding, features apply globally or unpredictably.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nbin-package = \"canonrs-site\"      # ✅ EXPLICIT\nlib-package = \"canonrs-site\"      # ✅ EXPLICIT\nbin-features = [\"ssr\"]            # Only for canonrs-site bin\nlib-features = [\"hydrate\"]        # Only for canonrs-site lib\n\n[[workspace.metadata.leptos]]\nname = \"canonrs-workbench\"\nbin-package = \"canonrs-workbench\" # ✅ EXPLICIT\nlib-package = \"canonrs-workbench\" # ✅ EXPLICIT\nbin-features = [\"ssr\"]            # Only for workbench bin\nlib-features = [\"hydrate\"]        # Only for workbench lib\n\n# canonrs-ssr, canonrs-csr are NOT in leptos metadata\n# They are NOT products, they are libraries\n```\n\n**Why this complies**: Each product's features are isolated to its own package. Library crates (`canonrs-ssr`) are never directly built with Leptos features.\n\n---\n\n## Rationale\n\n### Architectural invariants\n1. **Feature boundaries**: SSR and hydrate must never coexist in same artifact\n2. **Package isolation**: Products are independent, libraries are context-free\n3. **Build determinism**: Feature flags are explicit per package\n\n### Bugs prevented\n- SSR code in WASM (hydration mismatches)\n- Tokio/Axum in browser builds (bloat + compile errors)\n- Violates Canon Rule #196 (SSR/CSR separation)\n- Violates Canon Rule #197 (feature flag boundaries)\n\n### Why not opinion\nFeature flags control compilation at the ABI level. Cross-contamination creates binaries that violate architectural contracts.\n\n---\n\n## Enforcement\n\n### CI validation\n```bash\n#!/bin/bash\n# check-feature-isolation.sh\n\n# Extract all bin-package and lib-package names\npackages=$(grep -E \"(bin-package|lib-package)\" Cargo.toml | \\\n           awk -F'\"' '{print $2}' | sort -u)\n\n# Verify each is a member\nfor pkg in $packages; do\n    if ! cargo metadata --format-version 1 --no-deps | \\\n         jq -r '.workspace_members[]' | grep -q \"$pkg\"; then\n        echo \"❌ Feature leak: $pkg not in workspace\"\n        exit 1\n    fi\ndone\n\n# Verify SSR/CSR libs are NOT in leptos metadata\nif grep -q \"canonrs-ssr\" Cargo.toml | grep \"bin-package\"; then\n    echo \"❌ SSR library should not be a leptos product\"\n    exit 1\nfi\n```\n\n### Static analysis\n```rust\n// workspace-lint/src/features.rs\nfn validate_feature_isolation(metadata: &WorkspaceMetadata) -> Result<()> {\n    for leptos_config in &metadata.leptos_configs {\n        let bin_pkg = &leptos_config.bin_package;\n        let lib_pkg = &leptos_config.lib_package;\n        \n        // Verify packages exist\n        ensure!(\n            metadata.members.contains(bin_pkg),\n            \"bin-package {} not in workspace\", bin_pkg\n        );\n        \n        // Verify no -ssr/-csr libs are products\n        ensure!(\n            !bin_pkg.ends_with(\"-ssr\") && !bin_pkg.ends_with(\"-csr\"),\n            \"SSR/CSR libraries cannot be leptos products\"\n        );\n    }\n    \n    Ok(())\n}\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nLibrary crates (`canonrs-ssr`, `canonrs-csr`) NEVER appear in `[[workspace.metadata.leptos]]`. Only final products (apps) do.\n\n---\n\n## Version History\n\n- **1.0.0** (2026-02-03) — Initial version"},{"number":224,"slug":"hyphen-naming-convention","title":"Workspace Package Names Must Use Hyphens Consistently","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["naming","workspace","cargo","convention"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Inconsistent naming between hyphens and underscores causes tool resolution issues and build failures. Naming must be uniform.","problem":"mixed hyphen and underscore naming causes tool resolution errors","solution":"use hyphens consistently across package bin and config names","signals":["binary not found","name mismatch","build error"],"search_intent":"how to fix cargo naming hyphen underscore issue","keywords":["cargo naming convention hyphen","rust package naming rules","workspace naming consistency","binary name mismatch rust"],"body":"## Principle\n\n**All package names, bin names, and leptos config names MUST use hyphens (`-`), never underscores (`_`), with the understanding that Rust internally converts hyphens to underscores for module names.**\n\n---\n\n## Problem\n\nWithout hyphen consistency:\n- cargo-leptos searches for `canonrs-site` but finds `canonrs_site`\n- Binary outputs have unpredictable names (`canonrs_site` vs `canonrs-site`)\n- `use canonrs_site::App` vs `canonrs-site` package name creates confusion\n- CI scripts fail with \"file not found\"\n\n**Observable symptoms**:\n```bash\ncargo leptos build\nError: Could not read \"target/debug/canonrs-site\"\n\nls target/debug/\ncanonrs_site  # ❌ Name mismatch\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```toml\n# ROOT Cargo.toml\n[[workspace.metadata.leptos]]\nname = \"canonrs_site\"           # ❌ UNDERSCORE\nbin-package = \"canonrs-site\"    # ❌ INCONSISTENT\n\n# products/canonrs-site/Cargo.toml\n[package]\nname = \"canonrs-site\"           # ❌ INCONSISTENT\n\n[[bin]]\nname = \"canonrs_site\"           # ❌ UNDERSCORE\n\n# Leptos.toml\noutput-name = \"canonrs_site\"    # ❌ UNDERSCORE\n```\n\n**Why this violates**: Mixed naming creates unpredictable tool behavior. Cargo and cargo-leptos have different resolution rules.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```toml\n# ROOT Cargo.toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"           # ✅ HYPHEN\nbin-package = \"canonrs-site\"    # ✅ HYPHEN\nlib-package = \"canonrs-site\"    # ✅ HYPHEN\n\n# products/canonrs-site/Cargo.toml\n[package]\nname = \"canonrs-site\"           # ✅ HYPHEN\n\n[[bin]]\nname = \"canonrs-site\"           # ✅ HYPHEN\npath = \"src/main.rs\"\n\n# Leptos.toml\n[package]\nname = \"canonrs-site\"           # ✅ HYPHEN\n\n[package.metadata.leptos]\noutput-name = \"canonrs-site\"    # ✅ HYPHEN\n```\n\n**Why this complies**: Single naming convention across all tools. Rust automatically converts to `canonrs_site` for module imports.\n\n**Usage in code**:\n```rust\n// imports use underscore (auto-converted by Rust)\nuse canonrs_site::App;\n\n// But package/bin/config all use hyphen\n```\n\n---\n\n## Rationale\n\n### Architectural invariants\n1. **Tool compatibility**: cargo, cargo-leptos, rustc all handle hyphens correctly\n2. **File system**: Hyphens are valid in all contexts (URLs, files, DNS)\n3. **Rust convention**: `cargo new` defaults to hyphens\n\n### Bugs prevented\n- \"Binary not found\" errors at runtime\n- CI deployment failures (wrong artifact name)\n- cargo-leptos unable to locate output\n- Developer confusion (three different names for same thing)\n\n### Why not opinion\nCargo's naming convention is hyphen-first. Fighting this creates friction with the entire Rust ecosystem.\n\n---\n\n## Enforcement\n\n### CI validation\n```bash\n#!/bin/bash\n# check-naming-consistency.sh\n\n# Check workspace metadata\nif grep -E 'name = \"[^\"]*_[^\"]*\"' Cargo.toml | grep workspace.metadata; then\n    echo \"❌ Underscore found in workspace.metadata.leptos name\"\n    exit 1\nfi\n\n# Check all Cargo.toml files\nfind . -name Cargo.toml -exec grep -H 'name = \"[^\"]*_[^\"]*\"' {} \\; && {\n    echo \"❌ Underscore found in package names\"\n    exit 1\n}\n\n# Check Leptos.toml files\nfind . -name Leptos.toml -exec grep -H 'name = \"[^\"]*_[^\"]*\"' {} \\; && {\n    echo \"❌ Underscore found in Leptos config\"\n    exit 1\n}\n\necho \"✅ All names use hyphens\"\n```\n\n### Linter rule\n```rust\n// workspace-lint/src/naming.rs\nfn validate_hyphen_convention(config: &CargoToml) -> Result<()> {\n    if config.package.name.contains('_') {\n        bail!(\"Package name must use hyphens: {}\", config.package.name);\n    }\n    \n    for bin in &config.bin {\n        if bin.name.contains('_') {\n            bail!(\"Bin name must use hyphens: {}\", bin.name);\n        }\n    }\n    \n    Ok(())\n}\n```\n\n### cargo-deny config\n```toml\n# deny.toml\n[bans]\ndeny = [\n    { name = \"*_*\", reason = \"Package names must use hyphens\" },\n]\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nEven internal crates use hyphens: `canonrs-ssr`, `canonrs-csr`, `canonrs-shared` (not `canonrs_ssr`).\n\n**Note**: In Rust code, imports automatically use underscores:\n```rust\nuse canonrs_site::App;  // ✅ This is automatic, not a violation\n```\n\n---\n\n## Version History\n\n- **1.0.0** (2026-02-03) — Initial version"},{"number":225,"slug":"workspace-exact-version-policy-v2","title":"Workspace Dependency Version Policy Must Prevent ABI Drift Without Blocking Patch Compatibility","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["dependencies","versioning","workspace","abi"],"language":"EN","version":"2.0.0","date":"2026-02-04","intro":"Uncontrolled semver ranges or over pinning cause ABI conflicts or ecosystem friction. A balanced strategy ensures stability and compatibility.","problem":"dependency versions drift causing abi conflicts and inconsistent builds","solution":"pin minor versions allow patch updates and enforce lockfile with duplicate prevention","signals":["duplicate crate","type mismatch","abi conflict"],"search_intent":"how to prevent dependency version drift rust workspace","keywords":["rust workspace version policy","abi drift dependency rust","cargo lockfile enforcement","duplicate crate prevention"],"body":"## Principle\n\n**Workspace-level runtime dependencies MUST prevent multiple-version ABI conflicts while preserving controlled patch compatibility.**\n\nThe objective is:\n- Zero duplicate critical crates in the dependency graph\n- Deterministic builds\n- No accidental semver drift\n- No artificial ecosystem lock\n\n---\n\n## Problem\n\nUsing wide semver ranges:\n\n```toml\naxum = \"0.8\"\nleptos = \"0.8\"\n```\n\nallows Cargo to resolve different patch versions across the graph:\n\n```\naxum 0.8.7\naxum 0.8.9\n```\n\nThis causes:\n\n- Type mismatches across crate boundaries\n- Duplicate symbol linkage\n- Subtle ABI conflicts\n- Non-deterministic CI vs local builds\n\nBut over-constraining with `=X.Y.Z` everywhere may:\n\n- Break compatibility with upstream ecosystem crates\n- Cause unnecessary resolution conflicts\n- Increase maintenance burden\n\n---\n\n## Canonical Version Strategy\n\n### Critical Runtime Crates\n\n```toml\n[workspace.dependencies]\n\n# Server stack\naxum = \"0.8.8\"\ntower = \"0.5.3\"\ntokio = { version = \"1.42.0\", features = [\"full\"] }\n\n# Leptos ecosystem\nleptos = \"0.8.15\"\nleptos_meta = \"0.8.5\"\nleptos_router = \"0.8.11\"\nleptos_axum = \"0.8.7\"\n\n# WASM\nwasm-bindgen = \"0.2.95\"\n```\n\n✔ Minor version stability  \n✔ Patch updates allowed  \n✔ Compatible with ecosystem constraints  \n✔ Prevents accidental major drift  \n\n---\n\n### Lockfile Is The Real Freeze Point\n\nDeterminism is enforced by:\n\n```\nCargo.lock (committed)\n```\n\nNot by forcing `=X.Y.Z` everywhere.\n\nBuild reproducibility depends on:\n- Committed lockfile\n- CI `cargo fetch --locked`\n- `cargo build --locked`\n\n---\n\n### Duplicate Version Detection\n\nMultiple versions of critical crates are forbidden.\n\nVerification:\n\n```bash\ncargo tree --workspace -d | grep -E \"axum|leptos|tokio\"\n```\n\nMust produce zero duplicate entries.\n\nEnforced via:\n\n```toml\n# deny.toml\n[bans]\nmultiple-versions = \"deny\"\n\n[[bans.deny]]\nname = \"axum\"\n\n[[bans.deny]]\nname = \"leptos\"\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```toml\naxum = \"0.8\"\nleptos = \"0.8\"\ntokio = \"1\"\n```\n\nToo permissive.\n\n---\n\n### Forbidden\n\n```toml\naxum = \"=0.8.8\"\n```\n\nBlocks compatible ecosystem patch updates unnecessarily.\n\n---\n\n## Rationale\n\nRust uses nominative typing:\n\n```\naxum::Body (0.8.7) ≠ axum::Body (0.8.9)\n```\n\nVersion drift at patch level can still produce separate crate instantiations.\n\nThe correct balance:\n\n- Minor stability for API surface\n- Patch flexibility\n- Graph duplication prohibition\n\n---\n\n## Enforcement\n\n### CI\n\n```bash\ncargo fetch --locked\ncargo build --locked\n\n# No duplicates\ncargo tree --workspace -d && exit 1\n```\n\n### Pre-commit\n\n```bash\ncargo update -p axum --dry-run\n```\n\nMust not upgrade across minor without architectural review.\n\n---\n\n## Exceptions\n\nAllowed:\n- Dev-dependencies\n- Build-dependencies\n- Proc-macro crates\n- Crates proven ABI-isolated\n\nForbidden:\n- Multiple versions of any runtime crate in final artifact\n- Unbounded semver ranges\n\n---\n\n## Summary\n\nThis rule enforces:\n\n- Graph purity\n- ABI safety\n- Ecosystem compatibility\n- Deterministic builds\n\nWithout over-constraining legitimate patch evolution.\n\n---\n\nVersion 2.0 replaces strict `=X.Y.Z` pinning with **controlled minor pinning + lockfile enforcement + duplicate banning**."},{"number":226,"slug":"root-relative-paths","title":"Workspace Leptos Paths Must Be Root-Relative","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["paths","workspace","leptos","build"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Relative paths tied to product directories break when tooling runs from workspace root. Paths must resolve deterministically.","problem":"paths are product relative causing missing assets and build failures","solution":"define all leptos paths relative to workspace root","signals":["404 css","missing assets","build error"],"search_intent":"how to fix leptos path resolution workspace","keywords":["leptos path root relative","workspace asset resolution","cargo leptos path issue","frontend path config error"],"body":"## Principle\n\n**All path fields in `[[workspace.metadata.leptos]]` (`style-file`, `assets-dir`) MUST be relative to the workspace root, not to the product directory.**\n\n---\n\n## Problem\n\nWithout root-relative paths:\n- CSS files return 404 (cargo-leptos runs from workspace root)\n- Assets not found during build\n- Works locally (if in product dir) but fails in CI\n- Hot reload serves wrong files\n\n**Observable symptoms**:\n```bash\ncd /opt/docker/monorepo  # Workspace root\ncargo leptos serve\n\n# Browser console:\nFailed to load resource: the server responded with 404 (Not Found)\n/style/output.css\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```toml\n# ROOT Cargo.toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nstyle-file = \"style/output.css\"      # ❌ Relative to where?\nassets-dir = \"public\"                 # ❌ Ambiguous\nsite-root = \"target/site\"\n```\n\n**Why this violates**: When `cargo-leptos` runs from workspace root, it looks for `/style/output.css` (doesn't exist) instead of `/products/canonrs-site/style/output.css`.\n\n**Test**:\n```bash\nls style/output.css\n# ls: cannot access 'style/output.css': No such file or directory\n\nls products/canonrs-site/style/output.css\n# File exists ✓\n```\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```toml\n# ROOT Cargo.toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nbin-package = \"canonrs-site\"\nlib-package = \"canonrs-site\"\nstyle-file = \"products/canonrs-site/style/output.css\"  # ✅ From root\nassets-dir = \"products/canonrs-site/public\"            # ✅ From root\nsite-root = \"target/site\"                              # ✅ Root-relative\n```\n\n**Why this complies**: Paths resolve correctly when cargo-leptos runs from workspace root.\n\n**Verification**:\n```bash\n# From workspace root\nls products/canonrs-site/style/output.css  # ✅ Exists\nls products/canonrs-site/public            # ✅ Exists\n```\n\n---\n\n## Rationale\n\n### Architectural invariants\n1. **Build context**: cargo-leptos always executes from workspace root\n2. **CI consistency**: CI clones at root level, not product level\n3. **Multi-product**: Multiple products need unambiguous paths\n\n### Bugs prevented\n- CSS 404 errors in production\n- Assets missing in deployed builds\n- \"Works on my machine\" (when running from product dir)\n- Hot reload serving wrong product's files\n\n### Why not opinion\nFile system paths are absolute or relative to a working directory. Cargo-leptos's working directory is the workspace root by definition.\n\n---\n\n## Enforcement\n\n### CI validation\n```bash\n#!/bin/bash\n# check-leptos-paths.sh\n\n# Extract style-file and assets-dir from workspace metadata\npaths=$(grep -A 20 'workspace.metadata.leptos' Cargo.toml | \\\n        grep -E '(style-file|assets-dir)' | \\\n        awk -F'\"' '{print $2}')\n\nfor path in $paths; do\n    # Check if path starts with products/ or packages/\n    if [[ ! \"$path\" =~ ^(products|packages) ]]; then\n        echo \"❌ Path must be root-relative: $path\"\n        echo \"Expected: products/<product>/$path\"\n        exit 1\n    fi\n    \n    # Verify path exists\n    if [[ ! -e \"$path\" ]]; then\n        echo \"❌ Path does not exist: $path\"\n        exit 1\n    fi\ndone\n\necho \"✅ All paths are root-relative and exist\"\n```\n\n### Build-time check\n```bash\n# In cargo-leptos wrapper script\nfor cfg in $(toml get Cargo.toml workspace.metadata.leptos); do\n    style_file=$(echo $cfg | jq -r '.style_file')\n    \n    if [[ ! -f \"$style_file\" ]]; then\n        echo \"❌ style-file not found: $style_file\"\n        echo \"Hint: Use products/<name>/style/...\"\n        exit 1\n    fi\ndone\n```\n\n### Review checklist\n```markdown\n- [ ] style-file starts with `products/<product>/`\n- [ ] assets-dir starts with `products/<product>/`\n- [ ] Both paths resolve from workspace root\n- [ ] `ls <path>` succeeds from root directory\n```\n\n---\n\n## Exceptions\n\n### Shared assets (rare)\n```toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nstyle-file = \"shared/style/common.css\"  # ✅ OK if truly shared\nassets-dir = \"products/canonrs-site/public\"\n```\n\n**Allowed only if**:\n- Asset is genuinely shared by multiple products\n- Path is still root-relative\n- Documented why shared\n\n**All other cases: No exceptions.**\n\n---\n\n## Version History\n\n- **1.0.0** (2026-02-03) — Initial version"},{"number":227,"slug":"workspace-dependency-inheritance","title":"Products Must Inherit Workspace Dependencies","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["dependencies","workspace","cargo","versioning"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Duplicating dependency versions in products causes drift and conflicts. Workspace must remain the single source of truth.","problem":"dependencies are redeclared in products causing version drift","solution":"use workspace true inheritance for all shared dependencies","signals":["version conflict","duplicate dependency","build failure"],"search_intent":"how to inherit dependencies workspace cargo","keywords":["cargo workspace dependency inheritance","rust dependency duplication issue","workspace true cargo","version drift rust"],"body":"## Principle\n\n**Product crates MUST use `{ workspace = true }` for all dependencies declared in `[workspace.dependencies]`, never redeclaring versions.**\n\n---\n\n## Problem\n\nWithout workspace inheritance:\n- Version drift: Products use different versions than workspace declares\n- Duplicate dependency declarations cause conflicts\n- Updates require changing multiple Cargo.toml files\n- Violates DRY (Don't Repeat Yourself)\n\n**Observable symptoms**:\n```bash\ncargo tree -p canonrs-site -d\n# Shows multiple versions of same crate\n\nerror: failed to select a version for `axum`\ncandidate versions: 0.8.7, 0.8.8\nrequired by canonrs-site, leptos_axum\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```toml\n# ROOT Cargo.toml\n[workspace.dependencies]\nleptos = \"=0.8.15\"\naxum = \"=0.8.8\"\n\n# products/canonrs-site/Cargo.toml\n[dependencies]\nleptos = \"=0.8.15\"              # ❌ DUPLICATED\naxum = { version = \"=0.8.8\", optional = true }  # ❌ DUPLICATED\n```\n\n**Why this violates**: Version is declared twice. If workspace updates to 0.8.16, product still uses 0.8.15.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```toml\n# ROOT Cargo.toml\n[workspace.dependencies]\nleptos = \"=0.8.15\"\nleptos_meta = \"=0.8.5\"\nleptos_router = \"=0.8.11\"\nleptos_axum = \"=0.8.7\"\naxum = \"=0.8.8\"\ntower-http = { version = \"=0.6.8\", features = [\"fs\"] }\ntokio = { version = \"=1.42.0\", features = [\"full\"] }\n\n# products/canonrs-site/Cargo.toml\n[dependencies]\nleptos = { workspace = true }                    # ✅ INHERITED\nleptos_meta = { workspace = true }               # ✅ INHERITED\nleptos_router = { workspace = true }             # ✅ INHERITED\nleptos_axum = { workspace = true, optional = true }\naxum = { workspace = true, optional = true }\ntower-http = { workspace = true, optional = true }\ntokio = { workspace = true, optional = true }\n\n# Local-only deps (not in workspace)\ncanonrs = { path = \"../../packages-rust/rs-canonrs/canonrs\" }\n```\n\n**Why this complies**: Single source of truth for versions. Workspace update propagates automatically.\n\n---\n\n## Rationale\n\n### Architectural invariants\n1. **Version consistency**: All crates use exact same version\n2. **Update atomicity**: Change version once, applies everywhere\n3. **Dependency graph**: Cargo resolver sees unified versions\n\n### Bugs prevented\n- Version conflicts between products\n- Forgotten updates (one product on old version)\n- Build cache invalidation (different versions = different artifacts)\n- ABI conflicts (see Rule #225)\n\n### Why not opinion\nCargo workspaces exist specifically to solve this problem. Not using them defeats the purpose of having a workspace.\n\n---\n\n## Enforcement\n\n### CI validation\n```bash\n#!/bin/bash\n# check-workspace-inheritance.sh\n\n# Get deps declared in workspace\nworkspace_deps=$(grep -A 100 '\\[workspace.dependencies\\]' Cargo.toml | \\\n                 grep -E '^\\w+' | awk -F'=' '{print $1}' | tr -d ' ')\n\n# Check each product\nfor product in products/*/Cargo.toml; do\n    for dep in $workspace_deps; do\n        # If product uses this dep, check it inherits from workspace\n        if grep -q \"^$dep \" \"$product\"; then\n            if ! grep -q \"$dep.*workspace.*true\" \"$product\"; then\n                echo \"❌ $product: $dep must use { workspace = true }\"\n                exit 1\n            fi\n        fi\n    done\ndone\n\necho \"✅ All products inherit workspace dependencies\"\n```\n\n### cargo-deny config\n```toml\n# deny.toml\n[bans]\ndeny = [\n    { duplicate-versions = \"deny\" },\n]\n\n# Deny any product declaring version for workspace dep\n[[bans.deny]]\npattern = \"leptos.*version\"\nreason = \"Must use { workspace = true }\"\n```\n\n### Review checklist\n```markdown\n- [ ] No version= in product Cargo.toml for workspace deps\n- [ ] All shared deps use { workspace = true }\n- [ ] Optional deps add `optional = true` but still inherit version\n- [ ] cargo tree shows no duplicate versions\n```\n\n---\n\n## Exceptions\n\n### Product-specific dependencies\n```toml\n# products/canonrs-site/Cargo.toml\n[dependencies]\n# Workspace deps - INHERITED\nleptos = { workspace = true }\n\n# Product-specific - OK to declare version\nweb-sys = { version = \"0.3\", features = [\"Window\", \"Storage\"] }\ncanonrs = { path = \"../../packages-rust/rs-canonrs/canonrs\" }\n```\n\n**Allowed if**:\n- Dependency is NOT in `[workspace.dependencies]`\n- Dependency is unique to this product\n- Path dependencies (local crates)\n\n### Dev/build dependencies\n```toml\n[dev-dependencies]\n# Can declare versions if NOT in workspace.dependencies\ncriterion = \"0.5\"\n\n[build-dependencies]\n# Can declare versions if NOT in workspace.dependencies\ncc = \"1.0\"\n```\n\n**All workspace-declared deps: No exceptions.**\n\n---\n\n## Version History\n\n- **1.0.0** (2026-02-03) — Initial version"},{"number":228,"slug":"wasm-target-configuration","title":"Workspace Must Define WASM Target Configuration","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["wasm","target","build","cargo"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Missing wasm target configuration causes build failures and inconsistent outputs. Explicit setup ensures reproducibility.","problem":"wasm target is not configured causing build failures","solution":"define wasm target configuration in cargo config with optimization flags","signals":["target not found","build fail","wasm error"],"search_intent":"how to configure wasm target cargo","keywords":["wasm target cargo config","rust wasm build setup","wasm optimization flags","frontend wasm compilation"],"body":"## Principle\n\n**Workspace MUST have `.cargo/config.toml` configuring `wasm32-unknown-unknown` target with optimization flags.**\n\n---\n\n## Problem\n\nWithout WASM target configuration:\n- `cargo build --target wasm32-unknown-unknown` fails with \"target not found\"\n- WASM builds use default optimization (bloated output)\n- Inconsistent builds between developers\n- CI fails on WASM compilation\n\n**Observable symptoms**:\n```bash\ncargo build --target wasm32-unknown-unknown\nerror: failed to run custom build command\ntarget `wasm32-unknown-unknown` not found\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```\n# No .cargo/config.toml exists\n```\n\n**Why this violates**: Cargo doesn't know how to build for WASM target.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```toml\n# .cargo/config.toml (workspace root)\n[build]\ntarget-dir = \"target\"\n\n[target.wasm32-unknown-unknown]\nrustflags = [\n    \"-C\", \"opt-level=z\",\n    \"-C\", \"lto=fat\",\n    \"-C\", \"codegen-units=1\",\n]\n```\n\n**Why this complies**: \n- Defines WASM target explicitly\n- Optimizes for size (`opt-level=z`)\n- Enables LTO for smaller binaries\n- Single codegen unit for maximum optimization\n\n---\n\n## Rationale\n\n### Architectural invariants\n1. **Target availability**: WASM must be buildable without manual setup\n2. **Size optimization**: Browser WASM needs aggressive size reduction\n3. **Build consistency**: All developers use same optimization flags\n\n### Bugs prevented\n- \"Target not found\" build failures\n- Bloated WASM (10MB+ vs 500KB optimized)\n- Inconsistent performance between dev/prod\n- CI/CD failures on WASM builds\n\n### Why not opinion\nWASM target doesn't exist by default in Cargo. Configuration is mandatory, not optional.\n\n---\n\n## Enforcement\n\n### CI validation\n```bash\n#!/bin/bash\n# check-wasm-target.sh\n\nif [[ ! -f .cargo/config.toml ]]; then\n    echo \"❌ .cargo/config.toml not found\"\n    exit 1\nfi\n\nif ! grep -q \"wasm32-unknown-unknown\" .cargo/config.toml; then\n    echo \"❌ WASM target not configured\"\n    exit 1\nfi\n\n# Verify target actually works\ncargo build --target wasm32-unknown-unknown --lib -p canonrs-site --no-default-features --features=hydrate || {\n    echo \"❌ WASM build failed\"\n    exit 1\n}\n\necho \"✅ WASM target configured\"\n```\n\n### Required setup\n```bash\n# Install target (one-time per machine)\nrustup target add wasm32-unknown-unknown\n\n# Verify\nrustup target list --installed | grep wasm32\n```\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nAll Leptos workspaces MUST configure WASM target. Without it, hydrate builds fail.\n\n---\n\n## Version History\n\n- **1.0.0** (2026-02-03) — Initial version"},{"number":229,"slug":"design-system-css-artifact-isolation","title":"Design System CSS Must Be Consumed as an Independent Artifact","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["css","design-system","build","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-03","intro":"Coupling design system css into product pipelines creates hidden dependencies and breaks versioning. Artifact isolation is required.","problem":"design system css is integrated into product build pipelines causing coupling","solution":"consume design system css as standalone prebuilt artifact","signals":["path coupling","build dependency","css override"],"search_intent":"how to isolate design system css artifact","keywords":["design system css isolation","frontend css artifact pattern","tailwind design system coupling","css build separation"],"body":"## Principle\n\n**The CanonRS design system CSS MUST be consumed by products as an independent, prebuilt artifact, never as an input to utility or product-specific build pipelines.**\n\n- One-directional dependency\n- No build-time coupling\n- No path leakage\n- No implicit regeneration\n\n---\n\n## Problem\n\nWhen design system CSS is merged into, imported by, or rebuilt inside product pipelines:\n\n- Products become coupled to internal CanonRS paths\n- Design system updates force product rebuilds\n- Utility frameworks (Tailwind) gain authority over design tokens\n- Build graphs become cyclic or implicit\n- Versioning and rollback become impossible\n\n**Observable symptoms**:\n- Tailwind `@import` referencing `rs-canonrs/`\n- Products breaking when CanonRS directory layout changes\n- CSS changes requiring full product rebuilds\n- Inability to ship CanonRS updates independently\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```css\n/* Tailwind input.css */\n@import \"../../../packages-rust/rs-canonrs/styles/canonrs.css\";\n```\n\n```js\n// tailwind.config.js\ncontent: [\n  \"../../packages-rust/rs-canonrs/**/*\"\n]\n```\n\n```html\n<!-- Product shell -->\n<link rel=\"stylesheet\" href=\"../../rs-canonrs/styles/canonrs.css\">\n```\n\n**Why this violates the rule**:\n- Product now depends on CanonRS internal layout\n- Tailwind becomes a transitive owner of design tokens\n- Breaks artifact isolation and versioning\n- Violates Canon Rule #158 (immutable design system contracts)\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```text\nCanonRS (build once)\n└── styles/canonrs.css   ← versioned artifact\n\nProduct\n├── public/\n│   ├── canonrs.css      ← copied or published artifact\n│   └── output.css       ← utilities (Tailwind)\n```\n\n```html\n<!-- Product HTML shell -->\n<link rel=\"stylesheet\" href=\"/canonrs.css\">   <!-- Design system -->\n<link rel=\"stylesheet\" href=\"/output.css\">    <!-- Utilities -->\n```\n\n**Why this complies**:\n- CanonRS is built independently\n- Products consume a stable artifact\n- Tailwind remains utilities-only\n- No path coupling, no rebuild cascade\n\n---\n\n## Rationale\n\n### Architectural invariants protected\n\n1. **Artifact isolation**\n   - Design system is a distributable unit\n   - Products do not rebuild it\n\n2. **Dependency direction**\n   ```\n   Design System → Product\n   Utilities      → Product\n   ```\n   Never the reverse.\n\n3. **Version governance**\n   - CanonRS can be versioned, cached, rolled back\n   - Products opt-in to upgrades explicitly\n\n4. **Scalability**\n   - 1 design system\n   - N products\n   - N utility strategies (Tailwind, Uno, none)\n\n### Bugs prevented\n\n- Accidental token overrides\n- CSS order instability\n- Hidden rebuild dependencies\n- CI-only failures (“works locally”)\n- Design drift across products\n\n### Why this is not opinion\n\nThis rule enforces **build graph correctness**.\nMixing design system CSS into product pipelines creates implicit dependencies that tooling cannot reason about or cache safely.\n\nThis is a structural invariant, not a stylistic choice.\n\n---\n\n## Enforcement\n\n### CI checks\n\n```bash\n#!/bin/bash\n# forbid-design-system-imports.sh\n\n# Tailwind must not import canonrs.css\nif grep -R \"canonrs.css\" products/*/style | grep \"@import\"; then\n  echo \"❌ CanonRS CSS must not be imported into Tailwind\"\n  exit 1\nfi\n\n# No product may reference rs-canonrs paths\nif grep -R \"rs-canonrs/styles\" products/; then\n  echo \"❌ Products must not reference CanonRS internal paths\"\n  exit 1\nfi\n```\n\n### Review checklist\n\n- [ ] canonrs.css served as standalone asset\n- [ ] No Tailwind `@import` of design system\n- [ ] No relative paths to rs-canonrs in products\n- [ ] CSS order: CanonRS → Utilities\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nIf a product requires customization:\n- Override via utilities\n- Extend via new CanonRS families\n- Never inline or recompile the design system\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":230,"slug":"wasm-artifact-budget-is-hard-contract","title":"WASM Artifact Budget Is a Hard Contract","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["wasm","build","performance","budget"],"language":"EN","version":"1.0.0","date":"2025-02-04","intro":"Unbounded wasm size growth causes severe performance issues and goes unnoticed without enforcement. Size must be treated as a hard constraint.","problem":"wasm bundle grows without limits causing performance degradation","solution":"define and enforce wasm size budget at build time with failure on exceed","signals":["large wasm","slow load","bundle bloat"],"search_intent":"how to enforce wasm size budget build","keywords":["wasm size budget enforcement","frontend wasm performance","bundle size control rust","wasm build optimization guard"],"body":"## Principle\n\n**Every WASM artifact MUST declare and enforce a maximum size budget at build time, failing compilation if exceeded.**\n\n- Budget is declared explicitly (not implicit)\n- Enforcement happens at build time (not deployment)\n- Violations block merge/deploy\n\n---\n\n## Problem\n\nWithout size budgets:\n\n- WASM bundles grow silently from 2MB → 143MB (71x regression, real incident)\n- Performance degradation goes unnoticed until production\n- Client devices download bloated bundles\n- No early warning system for architectural violations\n- Developers unaware of cross-compilation leaks\n\n### Real Incident (2025-02-04)\n- SSR code leaked into WASM build\n- Bundle grew from 2MB to 143MB\n- No build-time detection\n- Discovered only during manual testing\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```makefile\n# Makefile without budget\nbuild:\n\tcargo leptos build --release\n\t# ❌ No size validation\n```\n\n**Why this violates the rule:**\n- Silent regressions\n- No feedback loop\n- Production incidents\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```makefile\n# Makefile with hard limit\nMAX_WASM_MB := 10\n\nbuild:\n\tcargo leptos build --release\n\t@$(MAKE) check-wasm-size\n\ncheck-wasm-size:\n\t@if [ -f \"target/site/pkg/app.wasm\" ]; then \\\n\t\tWASM_SIZE=$$(du -m target/site/pkg/app.wasm | cut -f1); \\\n\t\techo \"WASM: $${WASM_SIZE}MB / $(MAX_WASM_MB)MB\"; \\\n\t\tif [ $$WASM_SIZE -gt $(MAX_WASM_MB) ]; then \\\n\t\t\techo \"❌ CANON VIOLATION: WASM exceeds budget\"; \\\n\t\t\texit 1; \\\n\t\tfi; \\\n\tfi\n```\n\n**Why this complies:**\n- Explicit budget declaration\n- Automatic enforcement\n- Fast failure feedback\n- Prevents bad merges\n\n---\n\n## Rationale\n\n### Architectural Invariants\n1. **Client Performance Contract**: WASM size directly impacts load time\n2. **Separation Verification**: Size explosion indicates architecture leak\n3. **Resource Constraints**: Mobile/low-bandwidth users\n\n### Why Not Opinion\n- 10MB WASM = ~3s download on 3G\n- 143MB WASM = unusable on mobile\n- Size is objective, measurable metric\n\n### Prevention Class\n- SSR code in WASM builds\n- Duplicate dependencies\n- Debug symbols in release\n- Feature leaks\n\n---\n\n## Enforcement\n\n### Build-Time (Required)\n```bash\nmake build  # Fails if budget exceeded\n```\n\n### CI/CD (Required)\n```yaml\n- name: Enforce WASM Budget\n  run: |\n    cd products/canonrs-site\n    make check-wasm-size\n```\n\n### Pre-Merge Checklist\n- [ ] Build passes with size check\n- [ ] WASM < declared budget\n- [ ] No warnings ignored\n\n---\n\n## Exceptions\n\n**No exceptions. This rule is absolute.**\n\nBudgets may be adjusted through architectural review, but:\n- Must be justified with performance analysis\n- Requires explicit approval\n- Budget increase ≠ rule suspension\n\n---\n\n## Related Rules\n\n- **Rule #221**: Separate Build Bins by Target\n- **Rule #226**: No SSR in WASM Builds\n- **Rule #228**: Feature Flags Control Compilation Scope\n\n---\n\n## Version History\n\n- **1.0.0** (2025-02-04) — Initial version post-143MB incident"},{"number":231,"slug":"ssr-link-isolation-refined","title":"SSR Crates Must Be Link-Time Isolated From WASM Graph","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["wasm","ssr","features","linking"],"language":"EN","version":"1.1.0","date":"2026-02-04","intro":"Server dependencies leaking into wasm builds cause bloated binaries and runtime failures. Isolation must be enforced at dependency graph level.","problem":"server dependencies leak into wasm graph causing bloat and invalid builds","solution":"enforce strict separation of ssr and wasm graphs through features bins and dependency isolation","signals":["wasm bloat","unexpected dependency","build anomaly"],"search_intent":"how to isolate ssr from wasm rust","keywords":["ssr wasm dependency isolation","rust feature graph separation","cargo link graph wasm","leptos ssr csr isolation"],"body":"## Principle\n\n**No server-side crate or server-only dependency may appear in the WASM target dependency graph — even transitively.**\n\nThis must be enforced at:\n\n- Cargo resolution phase\n- Feature resolution phase\n- Link graph phase\n\nNot merely at `cfg` usage level.\n\n---\n\n## Clarification: Why cfg Alone Is Insufficient\n\nIncorrect assumption:\n\n```rust\n#[cfg(feature = \"ssr\")]\nuse axum::Router;\n```\n\nThis removes code usage during compilation,\nbut DOES NOT guarantee dependency isolation if:\n\n- Feature is enabled in the crate\n- Dependency is not properly feature-gated\n- Bin separation is not enforced\n\nCargo resolves dependencies before dead-code elimination.\n\n---\n\n## Real Root Cause of Leakage\n\nLeakage occurs when:\n\n1. Single crate builds both SSR and CSR\n2. Optional deps are enabled globally\n3. Features are not bound to specific bins\n4. Library crate exposes SSR deps transitively\n\nExample violation:\n\n```toml\n[dependencies]\naxum = { optional = true }\n\n[features]\nssr = [\"axum\"]\nhydrate = []\n```\n\nIf bin does not require-features isolation,\nresolver may include `axum` in graph.\n\n---\n\n## Canonical Isolation Model\n\n### Separate Binaries\n\n```toml\n[[bin]]\nname = \"app-ssr\"\npath = \"src/bin/ssr.rs\"\nrequired-features = [\"ssr\"]\n\n[[bin]]\nname = \"app-csr\"\npath = \"src/bin/csr.rs\"\nrequired-features = [\"hydrate\"]\n```\n\n---\n\n### Feature Scoped Dependencies\n\n```toml\n[dependencies]\nleptos = { version = \"0.8\", default-features = false }\n\naxum = { version = \"0.8\", optional = true }\ntower = { version = \"0.5\", optional = true }\n\n[features]\nssr = [\"leptos/ssr\", \"axum\", \"tower\"]\nhydrate = [\"leptos/hydrate\"]\n```\n\n---\n\n### Hard Compile Guard\n\nIn SSR-only crate:\n\n```rust\n#[cfg(target_arch = \"wasm32\")]\ncompile_error!(\"❌ SSR crate compiled for WASM target\");\n```\n\n---\n\n## Graph Verification\n\n```bash\ncargo tree --target wasm32-unknown-unknown -p app-csr \\\n  | grep -E \"axum|tower|tokio\"\n```\n\nMust return nothing.\n\nThis validates link-time purity.\n\n---\n\n## Architectural Invariants\n\n- SSR and WASM artifacts must have disjoint link graphs\n- No server runtime symbols in browser binary\n- Feature resolution must not cross product boundaries\n- Library crates must not leak server deps transitively\n\n---\n\n## Enforcement in CI\n\n```bash\n# WASM graph purity check\nif cargo tree --target wasm32-unknown-unknown -p app-csr \\\n   | grep -E \"axum|tower|tokio\"; then\n    echo \"❌ SSR dependency leaked into WASM graph\"\n    exit 1\nfi\n```\n\n---\n\n## Forbidden Patterns\n\n### ❌ Single-bin architecture\n### ❌ Global feature flags\n### ❌ SSR deps without optional gating\n### ❌ Products exposing server crates via shared libs\n\n---\n\n## Rationale\n\nRust linking is graph-based.\n\nEven if `cfg` removes code, Cargo:\n\n1. Resolves dependency graph\n2. Determines features\n3. Links reachable artifacts\n\nGraph isolation is stronger than conditional compilation.\n\n---\n\n## Exceptions\n\nNone.\n\nIf code must be shared:\n\n- Extract shared logic into feature-neutral crate\n- Keep server adapter layer separate\n- WASM crate must not depend on server adapter crate\n\n---\n\n## Summary\n\nThis rule enforces:\n\n- Link-graph integrity\n- WASM artifact purity\n- Feature boundary enforcement\n- Zero SSR contamination\n\nVersion 1.1 clarifies that leakage is caused by\nfeature resolution and bin architecture,\nnot merely by `cfg` usage."},{"number":232,"slug":"builds-deterministic-from-workspace-root","title":"Product Builds Must Be Deterministic From Workspace Root","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["build","workspace","paths","determinism"],"language":"EN","version":"1.0.0","date":"2025-02-04","intro":"Builds that depend on current working directory cause inconsistent behavior across environments. Determinism requires root based execution.","problem":"builds depend on working directory causing inconsistent and failing builds","solution":"execute all builds from workspace root using explicit root relative paths","signals":["path error","ci failure","inconsistent build"],"search_intent":"how to make builds deterministic workspace","keywords":["workspace deterministic build","cwd independent build","makefile root path rust","ci reproducible builds"],"body":"## Principle\n\n**All product builds MUST be executed from workspace root with zero dependence on current working directory.**\n\n- `cwd = /workspace/root` always\n- No relative path assumptions\n- Reproducible across environments\n\n---\n\n## Problem\n\nWithout deterministic builds:\n\n- \"Works on my machine\" syndrome\n- CI failures due to path assumptions\n- Flaky builds depending on shell history\n- Makefiles assume wrong working directory\n- Relative imports break unpredictably\n\n### Symptom Examples\n```bash\n# Works\ncd /workspace/products/app && make build  ✅\n\n# Breaks\ncd /workspace && make -C products/app build  ❌\n# Error: ../../../assets/style.css not found\n```\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n```makefile\n# Makefile assumes cwd=product dir\nbuild:\n\ttrunk build --release\n\tcp ../../../assets/style.css dist/\n\t# ❌ Breaks if cwd ≠ expected\n```\n\n**Why this violates:**\n- Fragile relative paths\n- Breaks in CI/containers\n- Non-reproducible\n\n---\n\n## Canonical Pattern\n\n### Canonical\n```makefile\n# Makefile with explicit root reference\nWORKSPACE_ROOT := $(shell git rev-parse --show-toplevel)\nPRODUCT_DIR := $(WORKSPACE_ROOT)/products/app\nASSETS_DIR := $(WORKSPACE_ROOT)/assets\n\nbuild:\n\tcd $(PRODUCT_DIR) && trunk build --release\n\tcp $(ASSETS_DIR)/style.css $(PRODUCT_DIR)/dist/\n```\n\n**Or use workspace configuration:**\n```toml\n# Cargo.toml\n[[workspace.metadata.leptos]]\nassets-dir = \"assets\"  # Relative to workspace root\nsite-root = \"products/app/target/site\"\n```\n\n**Why this complies:**\n- No cwd assumptions\n- Works from any directory\n- CI-friendly\n\n---\n\n## Rationale\n\n### Invariants Protected\n1. **Reproducibility**: Same command = same result\n2. **CI/CD Reliability**: No environment-specific paths\n3. **Developer Experience**: Works regardless of shell state\n4. **Container Builds**: No hidden dependencies on host paths\n\n### Why Not Opinion\nBuild determinism is fundamental to:\n- Continuous Integration\n- Supply chain security (reproducible builds)\n- Multi-developer workflows\n- Automated deployments\n\n### What This Enables\n- Docker builds without volume mounts\n- CI caching (paths are stable)\n- Parallel builds (no race conditions)\n- Build from any directory\n\n---\n\n## Enforcement\n\n### Makefile Pattern (Required)\n```makefile\n# Always determine workspace root\nWORKSPACE_ROOT := $(shell git rev-parse --show-toplevel)\n\n# Or use relative upward traversal with validation\nWORKSPACE_ROOT := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))/../..)\n\n# Fail if not in git repo\nifeq ($(WORKSPACE_ROOT),)\n$(error \"Must be run from within git repository\")\nendif\n```\n\n### CI Validation (Required)\n```yaml\n# Test build from different directories\n- name: Test Build Reproducibility\n  run: |\n    cd /tmp\n    make -C /workspace/products/app build\n    \n    cd /workspace\n    make -C products/app build\n    \n    # Both should produce identical artifacts\n    diff /tmp/result /workspace/result\n```\n\n### Pre-Commit Check\n```bash\n# Test from random directory\ncd $(mktemp -d)\nmake -C $WORKSPACE/products/app build || \\\n  echo \"❌ Build is CWD-dependent\"\n```\n\n---\n\n## Exceptions\n\n**No exceptions for product builds.**\n\nInternal tool scripts may use cwd if:\n1. Clearly documented as developer-only\n2. Never used in CI/CD\n3. Not part of release process\n\n---\n\n## Related Rules\n\n- **Rule #221**: Separate Build Bins by Target\n- **Rule #226**: Workspace-Relative Build Paths\n- **Rule #228**: Feature Flags Control Compilation\n\n---\n\n## Implementation Checklist\n\nFor each product Makefile:\n- [ ] Defines `WORKSPACE_ROOT` explicitly\n- [ ] All paths relative to `WORKSPACE_ROOT`\n- [ ] Fails fast if not in workspace\n- [ ] Tested from multiple directories\n- [ ] CI builds from workspace root\n\n---\n\n## Version History\n\n- **1.0.0** (2025-02-04) — Initial version, consolidates Rules #221, #226, #228"},{"number":233,"slug":"css-cascade-is-build-enforced-order","title":"CSS Cascade Order Is Build-Enforced Architecture","status":"ENFORCED","severity":"CRITICAL","category":"styling-css","tags":["css","cascade","build","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Incorrect css ordering causes token conflicts and unstable themes. Order must be enforced at build level.","problem":"css layers are loaded in wrong order causing inconsistencies and overrides","solution":"enforce strict cascade order at bundler level with fixed sequence","signals":["token override","theme break","specificity issue"],"search_intent":"how to enforce css cascade order build","keywords":["css cascade architecture order","design system css layering","frontend css build order","token specificity issues css"],"body":"## Principle\n\nThe CanonRS CSS cascade order is an architectural invariant enforced by the bundler.\n\nIt is not configurable.\nIt is not flexible.\nIt is not opinion.\n\n---\n\n## Mandatory Order\n\n1. Primitives\n2. Foundation\n3. Themes\n4. Semantic\n5. Families\n6. Root\n7. Variants\n8. UI\n9. Blocks\n10. Layouts\n11. Globals\n\nAny deviation is a structural violation.\n\n---\n\n## Forbidden Pattern\n\n- Reordering files manually\n- Injecting CSS before semantic layer\n- Loading Families before Themes\n- UI depending on layer order assumptions\n\n---\n\n## Canonical Enforcement\n\nBundler must concatenate layers strictly in declared sequence.\n\nNo `@layer` directives allowed in final artifact.\n\n---\n\n## Rationale\n\nImproper ordering causes:\n\n- Token shadowing\n- Inconsistent specificity\n- Theme instability\n- Debug complexity\n- Undocumented coupling\n\nOrder is enforced at build time, never by convention.\n\n---\n\n## Exceptions\n\n**No exceptions.**"},{"number":234,"slug":"tokens-engine-single-source-of-truth","title":"Tokens Engine Is the Single Source of CSS Truth","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","css","architecture","design-system"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Multiple sources defining tokens cause inconsistencies and loss of control. Tokens must originate from a single engine.","problem":"tokens are defined outside canonical engine causing inconsistency","solution":"generate all tokens exclusively from tokens engine and consume as artifact","signals":["token override","duplicate definition","css conflict"],"search_intent":"how to centralize design tokens engine","keywords":["design tokens single source","css token engine architecture","frontend token generation","design system token governance"],"body":"## Principle\n\nAll design tokens MUST originate exclusively from the CanonRS Tokens Engine.\n\nNo other file, layer, or tool is allowed to define, override, or regenerate token values.\n\n---\n\n## Absolute Constraints\n\n- `.generated/` directory is **read-only**\n- `canonrs.bundle.css` is a **compiled artifact**\n- No product may define token variables manually\n- UI, Blocks, and Layouts cannot declare new tokens\n- Utility frameworks have zero authority over token definition\n\n---\n\n## Forbidden Pattern\n\n```css\n:root {\n  --color-background: hsl(0 0% 100%); /* ❌ Manual token */\n}\n```\n\n```css\n@layer base {\n  --color-primary: red; /* ❌ Tailwind controlling tokens */\n}\n```\n\n---\n\n## Canonical Model\n\nTokens flow only in this direction:\n\n```\nTokens Engine (Rust)\n        ↓\n.generated/*.css\n        ↓\ncanonrs.bundle.css\n        ↓\nProducts consume as artifact\n```\n\nNo lateral generation allowed.\n\n---\n\n## Rationale\n\nThis protects:\n\n1. Determinism\n2. Governance\n3. Version control\n4. Artifact isolation\n5. Enterprise rollback capability\n\nTokens are architecture, not styling preference.\n\n---\n\n## Enforcement\n\n- CI fails if token vars are defined outside tokens engine\n- `.generated/` is git-ignored and must never be edited\n- Any manual token declaration is a hard violation\n\n---\n\n## Exceptions\n\n**No exceptions.**"},{"number":235,"slug":"ui-must-consume-semantic-not-theme","title":"UI Layer Must Consume Semantic Tokens, Never Theme Directly","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","ui","semantic","theming"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Direct theme usage in ui creates coupling and breaks theme abstraction. Semantic layer must remain the interface.","problem":"ui consumes theme tokens directly causing tight coupling and instability","solution":"restrict ui to semantic tokens only and isolate theme mapping","signals":["theme coupling","hardcoded color","refactor break"],"search_intent":"how to enforce semantic tokens in ui","keywords":["semantic token ui binding","design system theme separation","css token abstraction layer","frontend token usage pattern"],"body":"## Principle\n\nUI components MUST consume only semantic tokens (`--color-*`).\n\nUI MUST NOT reference:\n\n- `--theme-*`\n- primitive tokens\n- raw HSL values\n- hardcoded colors\n\n---\n\n## Forbidden Pattern\n\n```css\n.button {\n  background: var(--theme-surface-bg); /* ❌ Theme direct usage */\n}\n```\n\n```css\n.card {\n  background: hsl(0 0% 100%); /* ❌ Raw color */\n}\n```\n\n---\n\n## Canonical Pattern\n\nSemantic defines bridge:\n\n```\n--color-background → --theme-surface-bg\n```\n\nUI consumes only:\n\n```\nbackground: hsl(var(--color-background));\n```\n\n---\n\n## Rationale\n\nThis guarantees:\n\n- Theme swappability\n- Multi-theme coexistence\n- Semantic abstraction\n- No UI ↔ Theme coupling\n\nTheme changes must never require UI modification.\n\n---\n\n## Enforcement\n\nCI must search for:\n\n- `--theme-` usage in UI layer\n- raw HSL values inside UI\n- hardcoded colors\n\nAll are violations.\n\n---\n\n## Exceptions\n\n**No exceptions.**"},{"number":236,"slug":"cli-defines-workspace-topology","title":"CanonRS CLI Defines Workspace Topology","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["cli","workspace","architecture","topology"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Manual workspace structure changes cause instability and break architectural guarantees. Topology must be controlled centrally.","problem":"workspace structure is modified manually causing inconsistency and breakage","solution":"delegate all workspace topology changes exclusively to cli","signals":["metadata mismatch","structure drift","build failure"],"search_intent":"how to enforce cli workspace topology","keywords":["workspace topology cli control","cargo workspace structure governance","frontend monorepo architecture","build system topology control"],"body":"## Principle\n\nThe CanonRS CLI is the single authority responsible for defining, generating, and reorganizing workspace topology.\n\nManual structural edits to the workspace are prohibited.\n\n---\n\n## Architectural Boundary\n\nWorkspace structure includes:\n\n- `[workspace.members]`\n- `[[workspace.metadata.leptos]]`\n- product directories\n- package directories\n- naming conventions\n- site-root definitions\n- bin/lib package bindings\n- feature isolation mappings\n\nOnly the CLI may modify these.\n\n---\n\n## Forbidden Pattern\n\nManual editing:\n\n```toml\n[workspace]\nmembers = [\n  \"products/new-app\"  # ❌ added manually\n]\n```\n\nManual folder manipulation:\n\n```\nmkdir products/new-app   # ❌ outside CLI\n```\n\nManual leptos metadata injection:\n\n```toml\n[[workspace.metadata.leptos]]\nname = \"new-app\"  # ❌ manual entry\n```\n\n---\n\n## Canonical Flow\n\nAll structural changes must follow:\n\n```\ncanonrs new product\ncanonrs new package\ncanonrs reorganize\ncanonrs sync\n```\n\nThe CLI updates:\n\n- Cargo.toml\n- Leptos metadata\n- directory structure\n- feature boundaries\n- naming compliance\n- workspace determinism guarantees\n\n---\n\n## Rationale\n\nManual topology mutations cause:\n\n- Broken cargo metadata\n- Feature leakage\n- Duplicate site-roots\n- Port mismatches\n- Non-deterministic builds\n- SSR/CSR contamination\n- Naming drift\n\nWorkspace topology is architecture, not configuration.\n\n---\n\n## Enforcement\n\n- CI validates workspace integrity via `cargo metadata`\n- CI checks that no manual diff exists outside CLI-managed blocks\n- Workspace Cargo.toml may contain protected regions:\n  \n```toml\n# --- BEGIN CLI MANAGED ---\n# (auto-generated content)\n# --- END CLI MANAGED ---\n```\n\nEditing inside managed blocks is a violation.\n\n---\n\n## Exceptions\n\nManual edits allowed only for:\n\n- dependency version bumps (within policy)\n- comments\n- documentation sections\n\nStructural changes: **No exceptions.**\n\n---\n\n## Related Rules\n\n- Rule #222 — Workspace Members Must Be Fully Resolvable\n- Rule #223 — Feature Flag Isolation\n- Rule #224 — Naming Consistency\n- Rule #232 — Deterministic Root Builds\n\n---\n\n## Version History\n\n- **1.0.0** (2026-02-13) — Initial formalization of CLI authority"},{"number":237,"slug":"cli-owns-leptos-metadata","title":"CLI Owns Leptos Metadata Blocks","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["cli","leptos","workspace","metadata"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Manual edits to leptos metadata cause configuration drift and runtime errors. Ownership must remain centralized.","problem":"leptos metadata is edited manually causing inconsistency and failures","solution":"restrict all metadata changes to cli managed regions only","signals":["port mismatch","config drift","build issue"],"search_intent":"how to manage leptos metadata cli","keywords":["leptos metadata management cli","workspace config governance","frontend metadata control","cargo leptos config sync"],"body":"## Principle\n\nAll `[[workspace.metadata.leptos]]` blocks are generated and synchronized exclusively by the CanonRS CLI.\n\nManual edits are forbidden.\n\n---\n\n## Problem\n\nManual editing of leptos metadata causes:\n\n- Port divergence (Rule #219)\n- site-root collisions (Rule #220)\n- Feature leaks (Rule #223)\n- Path errors (Rule #226)\n- Hydration mismatches\n- Build nondeterminism\n\nLeptos metadata defines runtime architecture, not convenience config.\n\n---\n\n## Forbidden Pattern\n\n```toml\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\nsite-addr = \"127.0.0.1:3009\"  # ❌ manual change\n```\n\n---\n\n## Canonical Pattern\n\nLeptos metadata exists inside a CLI managed region:\n\n```toml\n# --- BEGIN CLI MANAGED LEPTOS ---\n[[workspace.metadata.leptos]]\nname = \"canonrs-site\"\n...\n# --- END CLI MANAGED LEPTOS ---\n```\n\nOnly the CLI modifies this region.\n\n---\n\n## Enforcement\n\n- CI verifies no diff inside managed block.\n- CLI regenerates metadata on `canonrs sync`.\n- Build fails if workspace metadata checksum changes unexpectedly.\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":238,"slug":"workspace-graph-must-be-acyclic","title":"Workspace Dependency Graph Must Be Acyclic","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["dependencies","graph","architecture","workspace"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Circular dependencies break build stability and violate architectural layering. Dependency graph must remain acyclic.","problem":"dependency cycles exist causing instability and architectural violations","solution":"enforce directed acyclic graph with strict dependency direction","signals":["cycle detected","build instability","resolution error"],"search_intent":"how to fix circular dependency rust workspace","keywords":["rust dependency cycle fix","workspace graph acyclic","cargo dependency cycle error","frontend architecture layering"],"body":"## Principle\n\nThe workspace crate dependency graph must be a Directed Acyclic Graph (DAG).\n\nNo circular dependencies are allowed at any layer.\n\n---\n\n## Problem\n\nCircular dependencies cause:\n\n- Incremental build instability\n- Cargo resolution errors\n- Implicit feature activation\n- Architectural boundary violations\n- Logical layer inversion (UI importing products, etc.)\n\n---\n\n## Forbidden Pattern\n\n```\ncanonrs-ui → canonrs-site\ncanonrs-site → canonrs-ui   ❌ cycle\n```\n\nOr transitive:\n\n```\nA → B → C → A  ❌\n```\n\n---\n\n## Canonical Pattern\n\nLayering example:\n\n```\ntokens-engine\n   ↓\nrs-design\n   ↓\ncanonrs\n   ↓\nproducts/*\n```\n\nDependency direction always flows downward.\n\nNever upward.\n\n---\n\n## Enforcement\n\nCI validation:\n\n```bash\ncargo metadata --format-version 1 \\\n| jq '.packages[].dependencies'\n```\n\nStatic detection of back-references via graph traversal.\n\n---\n\n## Exceptions\n\nNone.\n\nArchitectural cycles are fatal errors.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":239,"slug":"cli-sync-must-be-idempotent","title":"CLI Sync Operations Must Be Idempotent","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["cli","workspace","determinism","build"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Non idempotent cli operations cause unstable builds and unnecessary diffs. Outputs must remain deterministic.","problem":"cli operations produce different outputs on repeated runs causing instability","solution":"ensure cli sync produces identical results across executions","signals":["git diff","unstable output","rebuild loop"],"search_intent":"how to make cli operations idempotent","keywords":["cli idempotent operations","deterministic build output","workspace sync stability","frontend cli consistency"],"body":"## Principle\n\nRunning `canonrs sync` multiple times without structural changes must produce zero diffs.\n\nCLI operations must be idempotent.\n\n---\n\n## Problem\n\nNon-idempotent generators cause:\n\n- Git noise\n- Infinite reformat loops\n- CI instability\n- Phantom diffs\n- Timestamp pollution\n- Rebuild cascades\n\n---\n\n## Forbidden Pattern\n\n```\ncanonrs sync\ngit diff → changes\n\ncanonrs sync\ngit diff → different changes again  ❌\n```\n\n---\n\n## Canonical Pattern\n\n```\ncanonrs sync\ngit diff → clean\n\ncanonrs sync\ngit diff → clean\n```\n\nOutput must be deterministic:\n\n- Same ordering\n- Same formatting\n- Same whitespace\n- Stable timestamps\n- Stable dependency ordering\n\n---\n\n## Enforcement\n\nCI pipeline:\n\n```bash\ncanonrs sync\nif ! git diff --quiet; then\n  echo \"❌ CLI not idempotent\"\n  exit 1\nfi\n```\n\n---\n\n## Exceptions\n\nNone.\n\nCLI determinism is mandatory for architectural trust.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":240,"slug":"generated-artifacts-must-not-be-committed","title":"Generated Artifacts Must Not Be Committed","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["build","artifacts","ci","tokens"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Committing generated artifacts causes merge conflicts and version drift. Build outputs must remain reproducible and excluded from source control.","problem":"generated files are committed causing conflicts drift and stale artifacts","solution":"exclude all generated outputs via gitignore and regenerate during build or ci","signals":["merge conflict","diff noise","stale artifact"],"search_intent":"why not commit generated files build","keywords":["generated artifacts gitignore","build reproducibility rust","ci regenerate artifacts","css bundle commit issue"],"body":"## Principle\n\nGenerated artifacts must never be committed to the repository.\n\nOnly source of truth is committed.\nGenerated output is reproducible.\n\n---\n\n## Applies To\n\n- styles/.generated/*\n- canonrs.bundle.css\n- CLI-generated metadata blocks\n- Auto-generated workspace files\n- Any deterministic build output\n\n---\n\n## Problem\n\nCommitting generated files causes:\n\n- Merge conflicts\n- Diff pollution\n- Version skew\n- Stale artifacts\n- False architectural drift\n\n---\n\n## Forbidden Pattern\n\n```\ngit add styles/.generated/*\ngit commit -m \"update generated css\"\n```\n\n---\n\n## Canonical Pattern\n\n```\n.gitignore\n-------------\nstyles/.generated/\ncanonrs.bundle.css\n```\n\nGeneration happens:\n\n- On build\n- On CLI sync\n- In CI before deploy\n\n---\n\n## Enforcement\n\nCI must regenerate artifacts and verify:\n\n```\ncanonrs sync\ncargo run --bin tokens-engine\ngit diff --quiet\n```\n\nIf diff exists → build fails.\n\n---\n\n## Exceptions\n\nNone.\n\nGenerated files are build products, not architecture.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":241,"slug":"tokens-engine-is-build-step-not-runtime","title":"Tokens Engine Is Build Infrastructure, Not Runtime Code","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["tokens","build","runtime","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Including build tooling in runtime creates dependency leaks and bloated binaries. Tokens engine must remain isolated.","problem":"tokens engine is used at runtime causing dependency leakage and architectural violation","solution":"restrict tokens engine to build time and prevent runtime imports","signals":["dependency leak","runtime bloat","build graph issue"],"search_intent":"why tokens engine not runtime dependency","keywords":["tokens engine build only","runtime dependency leak rust","design system generator separation","frontend build vs runtime"],"body":"## Principle\n\nThe tokens-engine is a build-time tool.\n\nIt must never be a runtime dependency.\n\n---\n\n## Problem\n\nIf tokens-engine becomes runtime:\n\n- Product crates depend on build tooling\n- Runtime binaries grow incorrectly\n- Architectural layering collapses\n- Feature flags leak\n- Workspace graph becomes unstable\n\n---\n\n## Forbidden Pattern\n\n```\ncanonrs-ui → canonrs-tokens (bin)\nproducts → tokens-engine\n```\n\nOr:\n\n```\nuse canonrs_tokens::bin::*;\n```\n\n---\n\n## Canonical Pattern\n\n```\ntokens-engine (bin)\n   ↓ generates CSS\nrs-design (library)\n   ↓ consumed by\ncanonrs-ui\n   ↓ consumed by\nproducts\n```\n\nNo runtime code depends on generator code.\n\n---\n\n## Enforcement\n\n- tokens-engine exists only as binary.\n- Library modules expose data structures only.\n- Runtime crates must not reference generator modules.\n\n---\n\n## Exceptions\n\nNone.\n\nBuild tooling must remain isolated.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":242,"slug":"cascade-order-is-a-contract","title":"Cascade Order Is a Contract, Not a Preference","status":"ENFORCED","severity":"CRITICAL","category":"styling-css","tags":["css","cascade","tokens","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Changing cascade order breaks token resolution and causes visual instability. Order must be treated as architectural contract.","problem":"css cascade order changes causing token conflicts and unstable ui","solution":"enforce fixed cascade order generated by system and forbid manual changes","signals":["override failure","theme leak","visual instability"],"search_intent":"how to enforce css cascade contract","keywords":["css cascade contract order","design system layering css","token shadowing css issue","frontend cascade enforcement"],"body":"## Principle\n\nThe CSS cascade order is part of the architectural contract.\n\nReordering layers is a breaking change.\n\n---\n\n## Canonical Cascade Order\n\n1. PRIMITIVES\n2. FOUNDATION (CORE)\n3. THEMES\n4. SEMANTIC\n5. FAMILIES\n6. ROOT\n7. VARIANTS\n8. UI\n9. BLOCKS\n10. LAYOUTS\n11. GLOBALS (final)\n\nThis order is fixed.\n\n---\n\n## Problem\n\nChanging cascade order causes:\n\n- Semantic override failures\n- Theme leakage\n- Token shadowing\n- Visual instability\n- Undetectable regressions\n\n---\n\n## Forbidden Pattern\n\n```\n@import \"./.generated/root.css\";\n@import \"./.generated/semantic.css\";  ❌ wrong order\n```\n\nOr manual reordering in canonrs.css.\n\n---\n\n## Canonical Pattern\n\nCascade order is generated automatically by entry_generator.\n\nManual edits are forbidden.\n\n---\n\n## Enforcement\n\n- Entry generator defines order explicitly.\n- CI checks hash of canonrs.css header section.\n- Any reordering requires new rule + major version bump.\n\n---\n\n## Exceptions\n\nNone.\n\nCascade is architecture.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":243,"slug":"data-theme-is-the-only-activation-boundary","title":"data-theme Is the Only Theme Activation Boundary","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["css","theming","tokens","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Multiple theme activation mechanisms create conflicts and inconsistent behavior. A single boundary ensures deterministic theming.","problem":"themes use multiple activation mechanisms causing conflicts and leaks","solution":"restrict theme activation exclusively to data theme attribute","signals":["theme leak","partial override","inconsistent behavior"],"search_intent":"how to enforce single theme activation css","keywords":["data theme css pattern","frontend theme activation boundary","design system theming control","css theme isolation"],"body":"## Principle\n\n[data-theme] is the only valid activation boundary for themes.\n\nNo theme may rely on:\n\n- :root overrides\n- body classes\n- global resets\n- implicit dark-mode inheritance\n\n---\n\n## Problem\n\nMultiple activation mechanisms cause:\n\n- Theme leakage\n- Partial overrides\n- Conflicting scopes\n- Unpredictable dark behavior\n\n---\n\n## Forbidden Pattern\n\n```\n:root { --theme-bg: ... }          ❌\nbody.dark { ... }                  ❌\nhtml[data-theme=...] .component    ❌\n```\n\n---\n\n## Canonical Pattern\n\n```\n[data-theme=\"clean-slate\"] { ... }\n[data-theme=\"clean-slate\"].dark { ... }\n```\n\nEverything theme-bound must be inside [data-theme].\n\n---\n\n## Enforcement\n\n- theme_generator outputs only [data-theme] scoped variables\n- semantic layer consumes theme tokens only\n- root layer never references preset values directly\n\n---\n\n## Exceptions\n\nNone.\n\n[data-theme] is a hard boundary.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":244,"slug":"semantic-layer-is-mandatory-abstraction","title":"Semantic Layer Is a Mandatory Abstraction","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["tokens","semantic","css","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Skipping semantic layer creates coupling and breaks reusability. Semantic tokens must be the abstraction boundary.","problem":"components use theme tokens directly causing coupling and instability","solution":"enforce semantic layer as mandatory bridge between theme and ui","signals":["token bypass","theme coupling","refactor break"],"search_intent":"why semantic tokens required design system","keywords":["semantic token abstraction layer","design system token bridge","frontend token architecture","css semantic layer"],"body":"## Principle\n\nUI, Blocks and Root must consume semantic tokens only.\n\nNever theme tokens directly.\n\n---\n\n## Problem\n\nDirect theme consumption causes:\n\n- Tight coupling to preset vocabulary\n- Impossible re-theming\n- Architectural collapse\n- Token naming chaos\n\n---\n\n## Forbidden Pattern\n\n```\nbackground: var(--theme-surface-bg); ❌ inside UI\nborder: var(--theme-border);         ❌ inside component\n```\n\n---\n\n## Canonical Pattern\n\n```\nbackground: var(--color-background);\nborder: var(--color-border);\n```\n\nWhere:\n\n--color-* → semantic bridge → --theme-* → normalized preset value\n\n---\n\n## Cascade Responsibility\n\nPreset → Theme → Semantic → Families → Root → UI\n\nSkipping semantic layer is forbidden.\n\n---\n\n## Enforcement\n\n- semantic_generator defines all --color-* bridges\n- root consumes semantic only\n- UI consumes semantic only\n\n---\n\n## Exceptions\n\nNone.\n\nSemantic is mandatory.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":245,"slug":"families-must-not-leak-global-state","title":"Families Must Not Leak Global State","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["tokens","families","architecture","css"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Families overriding global tokens break cascade determinism and theme stability. Isolation is required.","problem":"family tokens override global layers causing unpredictable state","solution":"restrict families to domain scoped tokens without overriding system layers","signals":["override conflict","theme instability","cascade break"],"search_intent":"how to prevent token override in families","keywords":["family token isolation","design system layering css","token override prevention","frontend cascade architecture"],"body":"## Principle\n\nFamily tokens exist for component-domain grouping.\n\nThey must not:\n\n- Override semantic tokens\n- Override theme tokens\n- Mutate root contract\n- Redefine primitives\n\n---\n\n## Problem\n\nIf families override globals:\n\n- State becomes unpredictable\n- Visual contract breaks\n- Theme switching becomes unstable\n- Cascade loses determinism\n\n---\n## Patterns\n\n### Forbidden Pattern\n\nInside family-x:\n\n```\n--color-background: ...\n--theme-primary: ...\n--root-bg: ...\n```\n\n---\n\n### Canonical Pattern\n\nFamily defines domain-scoped variables only:\n\n```\n--overlay-bg\n--selection-active-bg\n--form-field-border\n--feedback-success-border\n```\n\nFamilies provide structure.\nThey do not override system layers.\n\n---\n## Contract\n\n### Enforcement\n\n- families are generated after semantic\n- families are scoped to [data-theme]\n- no family token may shadow semantic/global tokens\n\n---\n\n### Exceptions\n\nNone.\n\nFamilies are domain isolation, not system overrides.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":246,"slug":"css-bundle-must-be-layer-free","title":"CSS Bundle Must Be Layer-Free","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["css","bundle","build","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Layer directives in final css introduce runtime instability and tool interference. Bundle must be flattened.","problem":"css bundle contains layer or import directives causing instability","solution":"flatten bundle fully and remove all structural directives","signals":["layer directive","import leak","runtime instability"],"search_intent":"how to remove css layer directives bundle","keywords":["css bundle flattening","remove @layer css build","frontend css artifact generation","design system css build output"],"body":"## Principle\n\nThe final distributed artifact (`canonrs.bundle.css`) MUST NOT contain:\n\n- @layer directives\n- @import statements\n- Nested composition logic\n\nThe bundle must be fully flattened and deterministic.\n\n---\n\n## Problem\n\nIf @layer or @import leaks into the bundle:\n\n- CSS order becomes tool-dependent\n- Runtime cascade becomes unstable\n- Products gain implicit authority over CanonRS\n- Tailwind or other processors alter system hierarchy\n\n---\n\n## Forbidden Pattern\n\nInside canonrs.bundle.css:\n\n```\n@layer components { ... }   ❌\n@import \"./something.css\";  ❌\n```\n\n---\n\n## Canonical Pattern\n\nFlat, ordered CSS:\n\n```\n:root { ... }\n[data-theme=\"...\"] { ... }\n[data-family-x] { ... }\n[data-ui] { ... }\n```\n\nNo structural directives allowed in final artifact.\n\n---\n\n## Enforcement\n\n- bundler.rs removes @layer\n- bundler resolves all @import recursively\n- CI rejects bundle containing @layer or @import\n\n---\n\n## Exceptions\n\nNone.\n\nThe bundle is a deployment artifact, not a development input.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":247,"slug":"entry-order-is-architectural","title":"CSS Entry Order Is Architectural, Not Cosmetic","status":"ENFORCED","severity":"CRITICAL","category":"styling-css","tags":["css","order","tokens","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Entry order defines authority in cascade and cannot be changed without breaking system behavior. Order is structural.","problem":"entry order changes cause instability and incorrect overrides","solution":"enforce fixed entry generation order and forbid manual edits","signals":["override break","token shadowing","inconsistent ui"],"search_intent":"why css entry order matters architecture","keywords":["css entry order architecture","cascade authority layers","design system import order","frontend css generation order"],"body":"## Principle\n\nThe entry generation order in canonrs.css is a hard architectural contract.\n\nOrder defines authority.\n\n---\n\n## Canonical Order\n\n1. PRIMITIVES\n2. FOUNDATION (core)\n3. THEMES\n4. SEMANTIC\n5. FAMILIES\n6. ROOT\n7. VARIANTS\n8. UI\n9. BLOCKS\n10. LAYOUTS\n11. GLOBALS (final overrides)\n\n---\n\n## Problem\n\nChanging order causes:\n\n- Semantic shadowing\n- Root override loss\n- UI consuming unstable tokens\n- Non-deterministic visual output\n\n---\n\n## Forbidden Pattern\n\n```\n@import \"./.generated/root.css\";\n@import \"./.generated/semantic.css\";  ❌ wrong order\n```\n\n---\n\n## Why This Is Not Optional\n\nCascade order equals system authority.\n\nReordering = rewriting architecture.\n\n---\n\n## Enforcement\n\n- entry_generator hardcodes order\n- Any manual edit to canonrs.css is forbidden\n- CI verifies import order\n\n---\n\n## Exceptions\n\nNone.\n\nEntry order is immutable unless versioned major rewrite.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":248,"slug":"tokens-engine-is-the-only-source-of-truth","title":"Tokens Engine Is the Only Source of Truth","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["tokens","css","governance","build"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Manual edits to generated token files create drift and break reproducibility. Single source is required.","problem":"generated token files are edited manually causing drift and inconsistency","solution":"restrict all token generation to engine and forbid manual edits","signals":["diff drift","inconsistent css","build mismatch"],"search_intent":"why generated tokens must not be edited","keywords":["tokens engine single source","generated css immutability","design system governance tokens","frontend token build pipeline"],"body":"## Principle\n\nNo CSS token file inside .generated/ may be edited manually.\n\nAll generated CSS must originate from tokens-engine.\n\n---\n\n## Problem\n\nManual edits create:\n\n- Drift between Rust token definitions and CSS output\n- Irreproducible builds\n- Impossible diff tracking\n- Silent regression during rebuild\n\n---\n\n## Forbidden Pattern\n\nEditing:\n\n```\nstyles/.generated/core.css\nstyles/.generated/semantic.css\nstyles/.generated/family-x.css\n```\n\n---\n\n## Canonical Pattern\n\nSingle authority:\n\nRust token definitions →\ntokens-engine →\n.generated/ →\ncanonrs.css →\ncanonrs.bundle.css\n\n---\n\n## Enforcement\n\n- .generated directory must be git-ignored\n- CI rejects diffs in .generated\n- Any edit requires modifying Rust token definitions\n\n---\n\n## Exceptions\n\nNone.\n\nGenerated files are artifacts, not editable source.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":249,"slug":"products-must-not-depend-on-generated-css","title":"Products Must Not Depend on .generated CSS","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["css","products","architecture","tokens"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Direct dependency on internal css layers couples products to implementation details and breaks versioning. Isolation is required.","problem":"products depend on internal generated css causing coupling and fragility","solution":"consume only final bundled css artifact and forbid internal layer usage","signals":["path coupling","import break","refactor failure"],"search_intent":"how to decouple products from design system css","keywords":["design system css isolation","frontend css artifact usage","token layer coupling issue","css bundle consumption pattern"],"body":"## Principle\n\nProducts MUST consume only the distributed artifact:\n\n    canonrs.bundle.css\n\nProducts MUST NOT reference:\n\n    styles/.generated/*\n    canonrs.css (entry file)\n    internal token layer files\n\n---\n\n## Problem\n\nWhen products reference internal CSS layers:\n\n- They become coupled to CanonRS internals\n- Directory refactors break products\n- Token evolution becomes blocked\n- Versioning becomes impossible\n\n---\n\n## Forbidden Pattern\n\n<link rel=\"stylesheet\" href=\"../../rs-canonrs/styles/.generated/root.css\">  ❌  \n<link rel=\"stylesheet\" href=\"../../rs-canonrs/styles/canonrs.css\">         ❌\n\n---\n\n## Canonical Pattern\n\n<link rel=\"stylesheet\" href=\"/canonrs.bundle.css\">  ✅\n\nSingle artifact consumption only.\n\n---\n\n## Enforcement\n\n- CI scans products for references to `.generated`\n- CI forbids imports of canonrs.css\n- Only bundle may be served publicly\n\n---\n\n## Exceptions\n\nNone.\n\nProducts consume artifacts, never internal structure.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":250,"slug":"design-system-must-not-depend-on-products","title":"Design System Must Not Depend on Products","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["workspace","dependencies","architecture","graph"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Reverse dependencies from design system to products create cycles and break architectural layering. Dependency direction must be strictly enforced.","problem":"design system depends on product crates causing cycles and instability","solution":"enforce one directional dependency flow from core to products only","signals":["dependency cycle","build instability","feature leakage"],"search_intent":"how to prevent dependency cycles design system","keywords":["design system dependency rules","rust workspace dependency direction","avoid circular dependencies frontend","architecture layering enforcement"],"body":"## Principle\n\nCrates classified as `design-system`, `ui`, `tokens-engine`, or `shared`\nMUST NEVER depend on `product` crates.\n\nDependency flow is strictly:\n\nCore → Products  \nNever the reverse.\n\n---\n\n## Problem\n\nReverse dependency creates:\n\n- Cycles in graph\n- Hidden feature leakage\n- Build instability\n- Impossible artifact isolation\n\n---\n\n## Forbidden Pattern\n\n```toml\n[dependencies]\ncanonrs-site = { path = \"../../products/canonrs-site\" }\n```\n\n---\n\n## Canonical Flow\n\nAllowed:\n\n```\ncanonrs (design-system)\ncanonrs-ui\ncanonrs-interactive\ncanonrs-providers\ncanonrs-site (product)\n```\n\nDisallowed:\n\n```\ncanonrs → canonrs-site\n```\n\n---\n\n## Rationale\n\nProducts consume platform.\nPlatform never consumes product.\n\nArchitectural gravity must be downward.\n\n---\n\n## Enforcement\n\nCI must:\n\n- Detect dependencies from non-product crate to product crate\n- Fail build if detected\n\n---\n\n## Exceptions\n\nNone."},{"number":251,"slug":"artifacts-must-be-statically-identifiable","title":"All Build Artifacts Must Be Statically Identifiable","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["build","artifacts","naming","determinism"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Dynamic artifact naming causes cache issues and deployment confusion. Artifacts must be deterministic and identifiable.","problem":"artifacts use dynamic naming causing cache corruption and instability","solution":"use deterministic naming conventions tied to source identity","signals":["cache issue","mismatch artifact","deployment confusion"],"search_intent":"how to enforce deterministic artifact naming","keywords":["artifact naming deterministic build","css wasm naming convention","frontend build artifact control","cache safe file naming"],"body":"## Principle\n\nAll generated artifacts (CSS, WASM, bundles, manifests)\nMUST have deterministic file names and identifiable source ownership.\n\n---\n\n## Problem\n\nDynamic artifact naming causes:\n\n- Cache corruption\n- Hard-to-debug mismatches\n- Deployment confusion\n- CI instability\n\n---\n\n## Canonical Requirements\n\n- CSS artifacts must have canonical naming\n- WASM artifacts must match product hyphenated name\n- No runtime-generated filenames\n- No implicit temp bundles\n\n---\n\n## Example\n\nAllowed:\n\n```\ncanonrs.bundle.css\ncanonrs-site.wasm\n```\n\nDisallowed:\n\n```\nbundle-20260213.css\nwasm_build_3434.wasm\n```\n\n---\n\n## Rationale\n\nArtifacts are part of architecture.\nEphemeral names break reproducibility.\n\n---\n\n## Enforcement\n\nCI must:\n\n- Detect timestamp-based filenames\n- Detect random hash-only filenames unless explicitly versioned\n\n---\n\n## Exceptions\n\nVersion-hashed filenames allowed ONLY if hash is content-derived and deterministic."},{"number":252,"slug":"theme-switching-must-be-attribute-driven","title":"Theme Switching Must Be Attribute-Driven","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["css","theming","runtime","attributes"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Imperative theme manipulation introduces inconsistencies and hydration issues. Theme switching must rely on declarative attributes.","problem":"themes are applied via javascript mutations causing instability and conflicts","solution":"apply themes exclusively via dom data attributes and css cascade","signals":["hydration mismatch","style conflict","debug difficulty"],"search_intent":"how to implement css attribute theme switching","keywords":["data theme attribute switching","css theming declarative pattern","frontend theme toggle css","avoid js theme mutation"],"body":"## Principle\n\nTheme switching MUST occur exclusively via DOM attribute mutation:\n\n    <html data-theme=\"canonrs-theme\">\n\nJavaScript style mutation is forbidden.\n\n---\n\n## Problem\n\nImperative theme systems cause:\n\n- Inline style pollution\n- Hydration mismatches\n- CSS specificity conflicts\n- Impossible debugging\n\n---\n\n## Canonical Pattern\n\nCSS:\n\n[data-theme=\"canonrs-theme\"] { ... }\n[data-theme=\"clean-slate\"] { ... }\n\nRuntime:\n\ndocument.documentElement.setAttribute(\"data-theme\", \"clean-slate\");\n\n---\n\n## Forbidden\n\n- Modifying CSS variables individually at runtime\n- Injecting `<style>` blocks\n- Using JS-based theming engines\n\n---\n\n## Rationale\n\nTheme authority belongs to CSS cascade, not JavaScript.\n\n---\n\n## Exceptions\n\nNone."},{"number":253,"slug":"token-cascade-order-is-immutable","title":"Token Cascade Order Is Immutable","status":"ENFORCED","severity":"CRITICAL","category":"styling-css","tags":["css","tokens","cascade","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Reordering token cascade breaks resolution guarantees and causes silent regressions. Order must remain fixed.","problem":"token cascade order is modified causing resolution failures","solution":"enforce immutable cascade order through generators and ci checks","signals":["token mismatch","silent regression","style break"],"search_intent":"why token cascade order must be fixed","keywords":["token cascade order css","design system layering immutability","frontend css resolution order","token dependency chain"],"body":"## Principle\n\nToken generation order is fixed:\n\n1. Primitives\n2. Foundation\n3. Themes\n4. Semantic\n5. Families\n6. Root\n7. Variants\n8. UI\n9. Blocks\n10. Layouts\n11. Globals\n\nReordering is forbidden without rule revision.\n\n---\n\n## Problem\n\nChanging cascade order:\n\n- Breaks variable resolution\n- Produces silent style regressions\n- Invalidates semantic mapping guarantees\n\n---\n\n## Enforcement\n\n- entry_generator.rs must preserve canonical order\n- CI must diff import order\n- Any order change requires architecture review\n\n---\n\n## Rationale\n\nCascade order is architecture, not implementation detail.\n\n---\n\n## Exceptions\n\nNone."},{"number":254,"slug":"canonrs-must-build-without-products","title":"CanonRS Must Build Independently of Products","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["build","design-system","independence","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Design system depending on products creates coupling and blocks independent versioning. Build must remain isolated.","problem":"design system depends on products causing coupling and circular dependencies","solution":"ensure design system builds independently without product context","signals":["build failure","dependency cycle","artifact coupling"],"search_intent":"how to decouple design system from products","keywords":["design system independent build","frontend architecture decoupling","rust workspace isolation","design system versioning independence"],"body":"## Principle\n\nCanonRS (tokens-engine + UI + CSS) MUST compile and generate artifacts without any product crate present.\n\n---\n\n## Problem\n\nIf CanonRS depends on product context:\n\n- Circular dependency appears\n- Artifact governance breaks\n- CLI orchestration becomes mandatory\n- Design system cannot be versioned independently\n\n---\n\n## Canonical Condition\n\nFrom:\n\npackages-rust/rs-canonrs/\n\nThe following must work:\n\ncargo build\ncargo run --bin tokens-engine\n\nWithout any product.\n\n---\n\n## Forbidden\n\n- Importing product paths\n- Accessing product config\n- Reading product Leptos.toml\n\n---\n\n## Rationale\n\nDesign system is infrastructure.\nInfrastructure must be product-agnostic.\n\n---\n\n## Exceptions\n\nNone."},{"number":255,"slug":"cli-authority-over-workspace-generation","title":"CLI Is the Sole Authority Over Workspace Generation","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["cli","workspace","architecture","generation"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Manual workspace edits break determinism and architecture control. Generation must be centralized.","problem":"workspace structure is modified manually causing instability and drift","solution":"delegate all workspace generation exclusively to cli","signals":["config drift","build mismatch","manual override"],"search_intent":"why cli should control workspace generation","keywords":["workspace generation cli control","build topology governance","frontend monorepo cli authority","cargo workspace automation"],"body":"## Principle\n\n**The CanonRS CLI is the only authority allowed to generate, update, or restructure `.canonrs/workspace`.**\n\n- Workspace topology is not user-defined.\n- Products do not control workspace metadata.\n- Profiles and Leptos metadata are injected exclusively by the CLI.\n- Manual modifications are forbidden.\n\n---\n\n## Rationale\n\nThe workspace defines:\n\n- Build graph\n- Leptos metadata\n- Release profiles\n- Mode-specific behavior\n\nIf users edit this structure manually, architectural determinism is lost.\n\n---\n\n## Forbidden\n\n❌ Editing `.canonrs/workspace/Cargo.toml` manually  \n❌ Committing workspace topology changes directly  \n❌ Defining workspace metadata in product Cargo.toml  \n\n---\n\n## Required\n\n✅ Workspace must be generated through `canonrs dev` or `canonrs build`  \n✅ Workspace content must always be reproducible  \n✅ CLI must enforce topology deterministically  \n\n---\n\nThis rule ensures CanonRS governs build topology, not the application."},{"number":256,"slug":"generated-workspace-is-ephemeral","title":"Generated Workspace Is Ephemeral and Immutable","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["workspace","build","artifacts","determinism"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Treating generated workspace as source code breaks reproducibility and build determinism. It must remain disposable.","problem":"generated workspace is treated as source causing drift and instability","solution":"treat workspace as ephemeral artifact and regenerate via cli","signals":["manual edit","git commit workspace","build inconsistency"],"search_intent":"why generated workspace should not be versioned","keywords":["ephemeral workspace build","generated workspace immutability","frontend build artifact governance","cli workspace regeneration"],"body":"## Principle\n\n**`.canonrs/workspace` is an ephemeral artifact and must never be treated as source code.**\n\n- It is generated.\n- It is disposable.\n- It is reproducible.\n- It is immutable by humans.\n\n---\n\n## Characteristics\n\n- May be deleted at any time.\n- Always regenerable via CLI.\n- Must not be version controlled.\n- Must not be manually edited.\n\n---\n\n## Architectural Impact\n\nThis enforces:\n\n- Deterministic builds\n- Centralized profile governance\n- Canonical build topology\n- Clean product boundaries\n\n---\n\n## Forbidden\n\n❌ Adding workspace to git  \n❌ Editing generated files  \n❌ Injecting manual build flags  \n\n---\n\nThe workspace exists to isolate CanonRS build mechanics from product code."},{"number":257,"slug":"tokens-engine-is-mandatory-pre-build-step","title":"Tokens Engine Is a Mandatory Pre-Build Step","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["build","tokens","css","pipeline"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Skipping token generation leads to incomplete css cascade and inconsistent builds. Generation must be enforced.","problem":"tokens engine is not executed before build causing missing css","solution":"enforce tokens engine execution before any build or dev process","signals":["missing css","style break","build warning"],"search_intent":"how to enforce prebuild token generation","keywords":["tokens engine prebuild step","css generation pipeline","frontend build order enforcement","design system build sequence"],"body":"## Principle\n\n**The tokens-engine must execute before any CanonRS application build or development server starts.**\n\nNo application may compile without a fully generated CSS cascade.\n\n---\n\n## Build Order\n\n1. tokens-engine generates CSS\n2. Workspace topology validated/generated\n3. cargo leptos watch/build executes\n\nThis order is mandatory and non-bypassable.\n\n---\n\n## Architectural Boundaries\n\n- CSS is not handwritten for runtime.\n- CSS is not generated during application compilation.\n- tokens-engine is the sole authority over cascade generation.\n\n---\n\n## Enforcement\n\n- CLI must always execute tokens-engine first.\n- Direct `cargo build` is unsupported.\n- Missing generated CSS must raise warnings.\n\n---\n\nThis ensures styling architecture remains deterministic and centralized."},{"number":258,"slug":"mode-drives-build-profiles","title":"Mode Drives Build Profiles","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["cli","build","profiles","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Manual profile configuration creates inconsistency and misalignment with execution mode. Profiles must be derived from mode.","problem":"build profiles are defined manually causing mismatch with execution mode","solution":"derive build profiles exclusively from mode configuration via cli","signals":["profile mismatch","build inconsistency","mode conflict"],"search_intent":"how to map build profiles to mode","keywords":["build profile mapping mode","cli driven build config","ssr csr profile alignment","frontend build profiles rust"],"body":"## Principle\n\n**Application mode (ssr | csr | hybrid) defined in `canonrs.toml` is the single source of truth for build profile selection.**\n\nProfiles must never be manually defined inside product `Cargo.toml`.\n\n---\n\n## Deterministic Mapping\n\n| Mode   | Profile           | Purpose |\n|--------|-------------------|----------|\n| ssr    | canonrs-ssr       | Server-first build |\n| csr    | canonrs-csr       | WASM-optimized build |\n| hybrid | canonrs-hybrid    | Dual-target build |\n\n---\n\n## Enforcement\n\n- CLI injects correct `[profile.canonrs-*]`\n- Products cannot override profiles\n- `[profile.*]` inside product Cargo.toml is forbidden\n\n---\n\n## Rationale\n\nMode is architectural intent.\nProfiles are execution strategy.\n\nThey must always be aligned."},{"number":259,"slug":"products-must-not-define-build-topology","title":"Products Must Not Define Build Topology","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["architecture","build","workspace","cli"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Allowing products to control build topology creates inconsistency and breaks framework governance. Build structure must remain centralized.","problem":"products define build topology causing inconsistency and architectural drift","solution":"delegate all build topology control exclusively to framework cli","signals":["config drift","workspace mismatch","build inconsistency"],"search_intent":"why products should not define build topology","keywords":["build topology control cli","workspace governance rust","frontend architecture separation","leptos metadata ownership"],"body":"## Principle\n\n**Products define business logic only. Build topology is governed exclusively by CanonRS CLI.**\n\nProducts must not control:\n\n- Workspace layout\n- Leptos metadata\n- Build targets\n- Profile configuration\n\n---\n\n## Architectural Separation\n\nProduct responsibilities:\n- Domain logic\n- UI composition\n- Routes\n- Providers\n\nFramework responsibilities:\n- CSS cascade\n- Profile injection\n- Workspace structure\n- Compilation strategy\n\n---\n\n## Forbidden\n\n❌ Defining `[workspace]` inside product  \n❌ Overriding leptos metadata  \n❌ Custom target management  \n\n---\n\nThis protects framework integrity and ensures multi-product consistency."},{"number":260,"slug":"cli-autodiscovery-must-be-explicit-or-fail","title":"CLI Autodiscovery Must Be Explicit or Fail","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["cli","config","discovery","determinism"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Implicit fallback paths hide configuration errors and create unpredictable behavior. Discovery must be explicit.","problem":"cli uses silent fallback paths causing hidden misconfiguration","solution":"enforce explicit discovery with hard failure when root is not found","signals":["misconfiguration","unexpected path","partial build"],"search_intent":"how to enforce explicit cli discovery","keywords":["cli root discovery rules","explicit config resolution","fail fast cli pattern","workspace root detection rust"],"body":"## Principle\n\n**CanonRS CLI must never rely on silent fallback paths.**\n\nRoot discovery must follow this strict order:\n\n1. `$CANONRS_ROOT` environment variable\n2. Directory traversal up to defined depth\n3. Hard failure with explicit instruction\n\n---\n\n## Forbidden\n\n❌ Hardcoded filesystem fallbacks  \n❌ Implicit default paths  \n❌ Silent misconfiguration  \n\n---\n\n## Required Failure Behavior\n\nIf root cannot be found:\n\n- CLI must stop execution\n- Clear remediation steps must be printed\n- No partial build may occur\n\n---\n\nThis prevents invisible configuration drift and ensures explicit operational intent."},{"number":261,"slug":"css-size-drift-must-be-monitored","title":"CSS Bundle Size Drift Must Be Monitored","status":"ENFORCED","severity":"HIGH","category":"build-tooling","tags":["css","build","performance","monitoring"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Unmonitored css growth leads to performance degradation and architectural decay. Size must be tracked and enforced.","problem":"css bundle grows silently causing performance issues and bloat","solution":"define size baseline and enforce limits via ci checks","signals":["bundle bloat","slow load","unexpected growth"],"search_intent":"how to monitor css bundle size ci","keywords":["css bundle size monitoring","frontend performance css size","ci size threshold check","design system css bloat"],"body":"## Principle\n\ncanonrs.bundle.css MUST have a declared size expectation.\n\nSignificant size growth must be detected during CI.\n\n---\n\n## Problem\n\nSilent CSS growth leads to:\n\n- Unbounded token expansion\n- Duplicate families\n- Forgotten experimental layers\n- Performance regressions\n\n---\n\n## Canonical Pattern\n\nBaseline example:\n\ncanonrs.bundle.css ≈ X KB\n\nCI must fail if growth exceeds declared threshold (ex: +15%).\n\n---\n\n## Enforcement Example\n\ncheck-css-size.sh:\n\nSIZE=$(du -k canonrs.bundle.css | cut -f1)\nMAX=800\n\nif [ \"$SIZE\" -gt \"$MAX\" ]; then\necho \"❌ CSS exceeds expected budget\"\nexit 1\nfi\n\n---\n\n## Rationale\n\nArchitecture scale must be observable.\n\nUnbounded CSS growth is architectural decay.\n\n---\n\n## Exceptions\n\nBudget may be raised via architectural review.\n\nSilent growth is forbidden.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":262,"slug":"no-runtime-css-regeneration","title":"No Runtime CSS Regeneration","status":"ENFORCED","severity":"CRITICAL","category":"build-tooling","tags":["css","runtime","build","determinism"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Runtime css generation introduces nondeterminism and hydration issues. Styles must be fully static.","problem":"css is generated or mutated at runtime causing inconsistency and mismatch","solution":"generate css only at build time and serve as static artifact","signals":["hydration mismatch","style drift","cache inconsistency"],"search_intent":"why css should not be generated at runtime","keywords":["runtime css generation problem","static css build architecture","frontend deterministic styling","avoid dynamic css injection"],"body":"## Principle\n\nCanonRS CSS MUST be generated at build time only.\n\nNo runtime CSS generation, mutation, or injection is allowed.\n\n---\n\n## Problem\n\nRuntime generation introduces:\n\n- Non-deterministic styling\n- Hydration mismatches\n- Debug vs release divergence\n- Impossible caching guarantees\n\n---\n\n## Forbidden Pattern\n\nGenerating tokens in:\n\n- WASM at runtime\n- SSR boot phase\n- Client-side JS\n- Inline style injection\n\n---\n\n## Canonical Pattern\n\nBuild phase:\n\ntokens-engine →\ngenerated files →\nbundle →\nserved static\n\nRuntime:\nStatic CSS only.\n\n---\n\n## Rationale\n\nDesign system is infrastructure.\n\nInfrastructure must be deterministic.\n\n---\n\n## Enforcement\n\n- No dynamic CSS string building in codebase\n- No token computation in WASM\n- CI scans for `document.styleSheets.insertRule`\n\n---\n\n## Exceptions\n\nNone.\n\nAll style authority belongs to build system.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":263,"slug":"workspace-must-declare-crate-boundary-visibility","title":"Workspace Crate Boundaries Must Be Explicitly Declared","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["workspace","architecture","roles","metadata"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Implicit crate roles create ambiguity and increase coupling risks. Boundaries must be explicit.","problem":"crates do not declare roles causing unclear responsibilities and coupling","solution":"require explicit role metadata declaration for every crate","signals":["role ambiguity","dependency confusion","coupling risk"],"search_intent":"how to define crate roles workspace","keywords":["crate role metadata rust","workspace architecture roles","cargo metadata canonrs","frontend crate boundaries"],"body":"## Principle\n\nEvery crate inside the CanonRS workspace MUST declare its architectural role explicitly in its `Cargo.toml`.\n\nCrates MUST identify themselves as one of:\n\n- `design-system`\n- `ui`\n- `interactive`\n- `tokens-engine`\n- `server`\n- `shared`\n- `providers`\n- `cli`\n- `product`\n- `tooling`\n\n---\n\n## Problem\n\nWithout declared architectural roles:\n\n- Crates drift in responsibility\n- Dependency layering becomes unclear\n- Cyclic coupling risk increases\n- Refactors break invisible contracts\n\n---\n\n## Canonical Pattern\n\n```toml\n[package.metadata.canonrs]\nrole = \"design-system\"\nlayer = \"core\"\n```\n\n---\n\n## Rationale\n\nArchitecture must not be implicit.\nCargo knows crates.\nCanonRS must know _what they are_.\n\nThis rule enforces semantic clarity at crate boundary level.\n\n---\n\n## Enforcement\n\n- CI must validate presence of `[package.metadata.canonrs]`\n- Role must be one of the allowed values\n- Layer hierarchy must not be violated\n\n---\n\n## Exceptions\n\nNo exceptions.\n\nAll CanonRS crates must self-declare role."},{"number":264,"slug":"behavior-registry-is-single-entry-point","title":"Behavior Registry Is the Single Runtime Entry Point","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["behavior","runtime","wasm","registry"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Multiple behavior initialization paths cause duplication and nondeterministic runtime behavior. A single entry point is required.","problem":"behaviors are initialized in multiple places causing duplication and instability","solution":"centralize all behavior initialization through a single registry bootstrap","signals":["duplicate listeners","init conflict","runtime anomaly"],"search_intent":"how to centralize behavior initialization","keywords":["behavior registry pattern","single runtime entrypoint wasm","frontend behavior initialization","leptos behavior bootstrap"],"body":"## Principle\n\n**All behaviors MUST be registered exclusively through the Behavior Registry and initialized via a single runtime bootstrap call.**\n\n- Single entry point\n- No parallel init paths\n- Deterministic runtime wiring\n\n---\n\n## Problem\n\nWithout this rule:\n\n- Multiple init systems compete\n- Behaviors attach twice\n- Order-dependent bugs appear\n- MutationObserver conflicts\n- Inconsistent CSR activation\n\nObservable symptoms:\n\n- `init_x()` called manually per component\n- Different apps registering behaviors differently\n- Duplicate listeners\n- Random runtime anomalies\n\nArchitectural impact:\n\n- Breaks deterministic hydration\n- Breaks runtime uniformity across apps\n- Destroys plug-and-play architecture\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\n// Manual init inside component\nresize_handle_behavior::register();\nsetup_resize(container);\n\n// Direct init without registry\ninit_sidebar();\n```\n\nWhy this violates the rule:\n\nIt bypasses centralized discovery and lifecycle control.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n#[cfg(feature = \"hydrate\")]\npub fn hydrate() {\n    leptos::mount::hydrate_body(App);\n    canonrs::behaviors::init_canonrs_behaviors();\n}\n```\n\nBehavior internally:\n\n```rust\nregister_behavior(\"data-resize-container\", Box::new(...));\n```\n\nWhy this complies:\n\n- Single bootstrap\n- Central registry control\n- MutationObserver unified\n- Zero per-component setup\n\n---\n\n## Rationale\n\nThis protects:\n\n- Runtime determinism\n- Zero-config consumption\n- Portable app integration\n- CanonRS behavior auto-discovery model\n\nRegistry is architectural infrastructure.\nIt is not optional plumbing.\n\n---\n\n## Enforcement\n\n- CI blocks any `init_*` functions outside registry\n- Code review rejects manual event listener attachment without registry\n- All behaviors must expose `register()`\n- Bootstrap must contain exactly one `init_canonrs_behaviors()`\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version\n\n\n\n# ==========================================================="},{"number":265,"slug":"interactive-owns-state-behavior-does-not","title":"Interactive Owns All State — Behavior Owns None","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["state","behavior","reactivity","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"State duplication between behavior and interactive layers causes divergence and hydration issues. Ownership must be singular.","problem":"behavior holds state causing divergence and inconsistent rendering","solution":"restrict all state to interactive layer and keep behavior stateless","signals":["state divergence","hydration mismatch","debug difficulty"],"search_intent":"why behavior should not manage state","keywords":["state ownership interactive layer","behavior stateless pattern","reactive state architecture","leptos state separation"],"body":"## Principle\n\n**All feature state MUST live inside Interactive. Behavior MUST NOT maintain internal mutable state beyond ephemeral runtime tracking.**\n\n---\n\n## Problem\n\nWithout this rule:\n\n- Behavior holds hidden state\n- State diverges between SSR and CSR\n- Debugging becomes impossible\n- Hydration mismatches appear\n\nObservable symptoms:\n\n- `Rc<RefCell<...>>` storing business logic\n- Sorting state in behavior\n- Pagination index stored in behavior\n- Filter strings stored in DOM only\n\nArchitectural impact:\n\n- Split source of truth\n- CSR-only correctness\n- SSR inconsistency\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\nstruct DataTableState {\n    current_page: usize,\n    filter_query: String,\n}\n```\n\nor\n\n```rust\nlet state = Rc<RefCell<MyComplexState>>::new(...);\n```\n\nWhy this violates:\n\nBehavior becomes orchestrator.\nIt duplicates Interactive responsibilities.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n// Interactive owns state\npub struct DataTableState {\n    pub current_page: RwSignal<usize>,\n    pub filter_query: RwSignal<String>,\n}\n\n// Behavior only dispatches\ncontainer.dispatch_event(&event);\n```\n\nWhy this complies:\n\n- Single source of truth\n- State is reactive\n- SSR-safe\n- Behavior purely runtime bridge\n\n---\n\n## Rationale\n\nProtects:\n\n- State determinism\n- SSR/CSR symmetry\n- Debug visibility\n- Predictable architecture layering\n\nState ownership is not optional.\nIt defines the CanonRS architecture.\n\n---\n\n## Enforcement\n\n- Reject any complex state structs inside behavior\n- Reject `Rc<RefCell>` for business logic\n- Allow only ephemeral flags (`is_resizing`, etc.)\n- Mandatory state presence inside Interactive for features\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":266,"slug":"primitives-must-not-generate-ids","title":"Primitives Must Never Generate IDs","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["primitives","ids","ssr","hydration"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Auto-generated ids create nondeterministic markup and hydration issues. Identity must be externally controlled.","problem":"primitives generate ids causing mismatch between ssr and csr","solution":"require ids to be passed externally and forbid internal generation","signals":["hydration warning","id mismatch","runtime conflict"],"search_intent":"why primitives should not generate ids","keywords":["deterministic id generation ssr","leptos hydration id mismatch","frontend component identity","primitive id contract"],"body":"## Principle\n\n**Primitives MUST receive IDs externally and MUST NOT generate, mutate, or auto-derive IDs internally.**\n\n---\n\n## Problem\n\nWithout this rule:\n\n- SSR and CSR ID mismatch\n- Non-deterministic markup\n- Hydration warnings\n- Broken behavior attachment\n\nObservable symptoms:\n\n- `format!(\"button-{}\", counter)`\n- Static mut counters\n- Atomic counters in primitives\n- Random UUID generation\n\nArchitectural impact:\n\n- Hydration instability\n- Cross-instance conflicts\n- Hard-to-debug runtime bugs\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\nlet id = format!(\"btn-{}\", COUNTER.fetch_add(1, Ordering::SeqCst));\n```\n\nWhy this violates:\n\nID generation is side-effectful.\nPrimitive becomes stateful.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\n#[prop(optional)] id: Option<String>\n```\n\nInteractive:\n\n```rust\nlet table_id = \"users-table\".to_string();\n```\n\nWhy this complies:\n\n- Deterministic render\n- Identity controlled at higher layer\n- SSR consistent with CSR\n\n---\n\n## Rationale\n\nProtects:\n\n- Hydration determinism\n- Identity contract integrity\n- Behavior registry reliability\n\nID is architectural identity.\nIt cannot be implicit.\n\n---\n\n## Enforcement\n\n- Static grep for `format!(\".*{}\")` patterns in primitives\n- Reject use of atomic counters\n- Reject UUID generation\n- Review checklist requires ID prop presence\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":267,"slug":"data-attributes-are-contract-not-style","title":"Data Attributes Are Contract, Not Styling Mechanism","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["data-attributes","contracts","css","behavior"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Using classes for state or behavior creates coupling and ambiguity. Structural contracts must be explicit.","problem":"classes are used to represent state causing coupling and ambiguity","solution":"use data attributes to represent structural and behavioral contracts","signals":["class misuse","state ambiguity","coupling issue"],"search_intent":"when to use data attributes vs classes","keywords":["data attributes contract pattern","css class misuse state","frontend semantic attributes","ui behavior contract attributes"],"body":"## Principle\n\n**Data-attributes MUST represent structural or behavioral contracts, not visual styling shortcuts.**\n\n---\n\n## Problem\n\nWithout this rule:\n\n- CSS and architecture couple incorrectly\n- Classes abused for state contracts\n- Visual variants leak into runtime logic\n- Style and behavior semantics blur\n\nObservable symptoms:\n\n- `class=\"collapsed\"`\n- `class=\"variant-solid\"`\n- Using class to represent feature activation\n\nArchitectural impact:\n\n- Hard-coded styling logic\n- Contract ambiguity\n- Fragile UI-state coupling\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\nclass=\"density-compact\"\nclass=\"resize-enabled\"\n```\n\nWhy this violates:\n\nClasses are presentational.\nThey cannot represent canonical state contract.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\ndata-density=\"compact\"\ndata-resize-enabled=\"true\"\nattr:data-ui-variant=\"solid\"\n```\n\nWhy this complies:\n\n- Explicit contract\n- Behavior-friendly\n- CSS-token compatible\n- Machine-readable invariant\n\n---\n\n## Rationale\n\nProtects:\n\n- Structural clarity\n- Behavior registry compatibility\n- Token-based CSS layering\n- Declarative architecture\n\nData-attributes are semantic contracts.\nClasses are layout utilities.\n\nThese must never be conflated.\n\n---\n\n## Enforcement\n\n- Code review rejects structural contract encoded as class\n- Linter rule for `variant-` prefix in class\n- CSS contract mapped only via `[data-*]` selectors\n- Documentation requires feature attributes defined explicitly\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":268,"slug":"behavior-cleanup-is-mandatory","title":"Behavior Cleanup Is Mandatory and Deterministic","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["behavior","cleanup","wasm","listeners"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Missing cleanup in behaviors causes memory leaks and duplicated listeners in SPA environments. Lifecycle must be deterministic.","problem":"event listeners and side effects are not cleaned up causing leaks and duplication","solution":"implement deterministic cleanup using lifecycle hooks for all behaviors","signals":["memory leak","duplicate listener","event storm"],"search_intent":"how to cleanup event listeners wasm","keywords":["wasm event listener cleanup","behavior lifecycle rust","frontend memory leak listeners","leptos on_cleanup pattern"],"body":"## Principle\n\n**Every Behavior must implement deterministic cleanup of listeners, guards, and side-effects.**\n\n---\n\n## Problem\n\nWithout deterministic cleanup:\n\n- Memory leaks in SPA navigation\n- Duplicate listeners\n- Intermittent runtime bugs\n- Event storms\n- Hard-to-reproduce state corruption\n\n---\n\n## Forbidden Pattern\n\n### Forbidden\n\n```rust\ncontainer.add_event_listener_with_callback(\"click\", cb)?;\nclosure.forget(); // no cleanup ever\n```\n\nListener lives forever. No detach. No guard reset.\n\n---\n\n## Canonical Pattern\n\n### Canonical\n\n```rust\nlet closure = Closure::wrap(Box::new(move |_| { ... }) as Box<dyn FnMut(_)>);\n\ncontainer.add_event_listener_with_callback(\n    \"click\",\n    closure.as_ref().unchecked_ref()\n)?;\n\non_cleanup(move || {\n    let _ = container.remove_event_listener_with_callback(\n        \"click\",\n        closure.as_ref().unchecked_ref()\n    );\n    let _ = container.remove_attribute(\"data-x-attached\");\n});\n```\n\n---\n\n## Rationale\n\nPrevents:\n- Memory leaks\n- Double registration\n- Runtime instability\n\nCleanup is not optional. It is architectural integrity.\n\n---\n\n## Enforcement\n\n- Code review mandatory\n- CI lint for missing guard removal\n- Behavior template compliance check\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":269,"slug":"custom-events-are-runtime-contracts","title":"Custom Events Are Public Runtime Contracts","status":"ENFORCED","severity":"HIGH","category":"behavior","tags":["events","runtime","contracts","behavior"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Unstructured event naming creates hidden coupling and breaking changes. Events must follow stable contracts.","problem":"custom events lack naming contract causing instability and breakage","solution":"use canonical namespaced event pattern canon component action","signals":["event mismatch","silent failure","integration break"],"search_intent":"how to name custom events consistently","keywords":["custom event naming convention","frontend runtime contracts events","canonical event namespace pattern","behavior event stability"],"body":"## Principle\n\n**All canon:* custom events are part of the public runtime API and semver-bound.**\n\n---\n\n## Problem\n\nWithout formal contract:\n\n- Breaking rename breaks Interactive\n- Hidden coupling\n- Silent behavior failures\n\n---\n\n## Forbidden Pattern\n\n```rust\nCustomEvent::new(\"resize-event\")\n```\n\nUnscoped, unstable, non-canonical.\n\n---\n\n## Canonical Pattern\n\n```rust\nCustomEvent::new(\"canon:resize:move\")\n```\n\nPattern: `canon:{component}:{action}`\n\n---\n\n## Rationale\n\nRuntime events must be versioned surfaces.\n\n---\n\n## Enforcement\n\n- Event naming audit\n- CI string pattern validation\n\n---\n\n## Exceptions\n\n> No exceptions.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":270,"slug":"behavior-dom-scope-isolation","title":"Behavior Must Never Traverse Beyond Its Container Scope","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["dom","scope","behavior","isolation"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Global DOM queries introduce coupling and unpredictable side effects across components. Scope must be isolated.","problem":"behavior queries global dom causing cross component interference","solution":"restrict all dom queries to container scoped selectors","signals":["unexpected interaction","coupling","runtime bug"],"search_intent":"how to scope dom queries behavior","keywords":["dom query scope isolation","frontend behavior container pattern","avoid document query selector","component isolation dom"],"body":"## Principle\n\n**Behavior must operate strictly within its container element boundary.**\n\n---\n\n## Problem\n\nGlobal DOM queries cause:\n\n- Cross-component interference\n- Coupling\n- Unpredictable runtime effects\n\n---\n\n## Forbidden Pattern\n\n```rust\ndocument().query_selector_all(\".datatable-row\")\n```\n\n---\n\n## Canonical Pattern\n\n```rust\ncontainer.query_selector_all(\"[data-datatable-row]\")\n```\n\n---\n\n## Rationale\n\nIsolation preserves composability.\n\n---\n\n## Enforcement\n\n- Disallow document-wide query except registry\n- Static review\n\n---\n\n## Exceptions\n\nRegistry initialization only.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":271,"slug":"behavior-idempotent-registration","title":"Behavior Execution Must Be Idempotent","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["behavior","idempotency","runtime","events"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Repeated behavior registration causes duplicate listeners and inconsistent runtime state. Execution must be idempotent.","problem":"behavior registration runs multiple times causing duplication and instability","solution":"guard registration using container flags to ensure single execution","signals":["duplicate event","multiple trigger","runtime anomaly"],"search_intent":"how to make behavior idempotent","keywords":["idempotent behavior registration","frontend event duplication fix","wasm behavior guard pattern","leptos container attribute guard"],"body":"## Principle\n\n**Running register() multiple times must not change system state.**\n\n---\n\n## Problem\n\nHot reload may re-register behavior.\n\nWithout idempotence:\n\n- Duplicate listeners\n- Multiple event triggers\n\n---\n\n## Forbidden Pattern\n\nNo guard attribute.\n\n---\n\n## Canonical Pattern\n\n```rust\nif container.get_attribute(\"data-x-attached\").as_deref() == Some(\"1\") {\n    return Ok(());\n}\n```\n\n---\n\n## Rationale\n\nDeterministic runtime.\n\n---\n\n## Enforcement\n\nGuard required in all behaviors.\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":272,"slug":"no-global-mutable-wasm-state","title":"Global Mutable State Is Forbidden in wasm Scope","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["state","wasm","global","mutability"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Global mutable state in wasm introduces race conditions and hidden coupling. State must be localized and reactive.","problem":"global mutable state exists causing race conditions and coupling","solution":"store all state in reactive signals or scoped structures","signals":["race condition","memory leak","state conflict"],"search_intent":"why global state is bad wasm","keywords":["wasm global mutable state issue","reactive state rust leptos","avoid static mut rust wasm","frontend state isolation"],"body":"## Principle\n\n**No global mutable state may exist in wasm runtime.**\n\n---\n\n## Problem\n\nGlobal state causes:\n\n- Race conditions\n- Hidden coupling\n- Memory leaks\n\n---\n\n## Forbidden Pattern\n\n```rust\nstatic mut GLOBAL_COUNTER: i32 = 0;\n```\n\n---\n\n## Canonical Pattern\n\nState lives inside ComponentState or Interactive signals.\n\n---\n\n## Rationale\n\nStateless wasm bridge.\n\n---\n\n## Enforcement\n\nClippy + code review.\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":273,"slug":"mutationobserver-explicit-justification","title":"MutationObserver Requires Explicit Architectural Justification","status":"ENFORCED","severity":"HIGH","category":"behavior","tags":["mutationobserver","performance","dom","behavior"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Uncontrolled MutationObserver usage degrades performance and causes layout thrashing. Usage must be justified.","problem":"mutationobserver is used without control causing performance degradation","solution":"require explicit architectural approval before using mutationobserver","signals":["performance drop","layout thrash","observer spam"],"search_intent":"when to use mutationobserver safely","keywords":["mutationobserver performance issues","dom observer control pattern","frontend performance dom watch","behavior dom monitoring rules"],"body":"## Principle\n\n**MutationObserver must never be used implicitly.**\n\n---\n\n## Problem\n\nUncontrolled observers cause:\n\n- Performance degradation\n- Layout thrashing\n\n---\n\n## Enforcement\n\nMandatory architecture review approval.\n\n---\n\n## Exceptions\n\nDocumented reactive DOM sync only.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":274,"slug":"interactive-feature-isolation","title":"Interactive Features Must Be Strictly Isolated","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["interactive","state","isolation","signals"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Cross feature state mutation creates hidden dependencies and regression chains. Features must remain isolated.","problem":"features share state causing coupling and unpredictable regressions","solution":"scope all signals and state per feature module","signals":["hidden dependency","regression chain","state conflict"],"search_intent":"how to isolate feature state signals","keywords":["feature state isolation signals","reactive state modularization","frontend signal scoping","leptos feature boundaries"],"body":"## Principle\n\n**Each feature must operate independently without signal cross-coupling.**\n\n---\n\n## Problem\n\nCross-feature mutation leads to:\n\n- Hidden dependencies\n- Regression chains\n\n---\n\n## Enforcement\n\nFeature signals must be scoped to module.\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":275,"slug":"interactive-container-bound-hooks","title":"Interactive Hooks Must Be Container-Bound","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["interactive","hooks","container","state"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Global hook binding breaks isolation and introduces cross component interference. Hooks must be scoped.","problem":"hooks attach globally causing interference and coupling","solution":"bind all hooks explicitly to container id","signals":["unexpected behavior","cross interaction","state leak"],"search_intent":"how to scope hooks to container","keywords":["container bound hooks pattern","frontend hook isolation","reactive hook scoping rust","leptos container id binding"],"body":"## Principle\n\n**Hooks must bind strictly to container_id.**\n\n---\n\n## Problem\n\nGlobal hook attachment breaks isolation.\n\n---\n\n## Enforcement\n\nAll hooks require explicit container_id.\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":276,"slug":"interactive-ssr-determinism","title":"Interactive Must Remain Deterministic Under SSR Without wasm","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["ssr","wasm","determinism","interactive"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Interactive logic depending on wasm execution breaks SSR determinism and causes hydration mismatches. SSR must be independent.","problem":"interactive rendering depends on wasm causing mismatch between ssr and csr","solution":"gate all wasm logic and ensure deterministic ssr output","signals":["hydration mismatch","ssr divergence","runtime inconsistency"],"search_intent":"how to ensure ssr deterministic interactive","keywords":["ssr deterministic rendering leptos","wasm gating interactive logic","frontend hydration consistency","ssr csr symmetry rust"],"body":"## Principle\n\n**Interactive rendering must not depend on wasm execution.**\n\n---\n\n## Problem\n\nSSR mismatch causes hydration errors.\n\n---\n\n## Enforcement\n\nAll wasm logic must be cfg-gated.\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":277,"slug":"interactive-must-not-emit-dom-events","title":"Interactive Must Not Emit DOM Events","status":"ENFORCED","severity":"HIGH","category":"behavior","tags":["interactive","events","runtime","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Emitting DOM events from the interactive layer breaks separation between runtime behavior and state orchestration. This introduces hidden coupling and unpredictable execution flows.","problem":"interactive layer dispatches dom events directly breaking separation of concerns","solution":"restrict dom event dispatching exclusively to behavior layer","signals":["custom event dispatch","event bubbling misuse","runtime coupling"],"search_intent":"why interactive layer should not dispatch dom events","keywords":["dom event dispatch architecture","interactive vs behavior separation","frontend event layer design","runtime event coupling issues"],"body":"## Principle\n\n**Interactive layer must never dispatch DOM events directly.**\n\n---\n\n## Problem\n\nViolates separation between runtime and state orchestration.\n\n---\n\n## Enforcement\n\nDOM dispatch limited to Behavior only.\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":278,"slug":"primitive-must-not-encode-design-tokens","title":"Primitive Must Not Encode Design Tokens in Rust","status":"ENFORCED","severity":"CRITICAL","category":"design-system","tags":["primitives","tokens","css","architecture"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Embedding design tokens directly in Rust primitives breaks token cascade and theming flexibility. Visual decisions must remain in the CSS layer.","problem":"design tokens are hardcoded in rust primitives breaking theming and token cascade","solution":"move all visual decisions to css using data attributes and token system","signals":["inline styles","var(-- usage in rust","hardcoded classes"],"search_intent":"how to prevent hardcoded design tokens in components","keywords":["design tokens rust primitives","css token separation architecture","avoid inline styles components","data attribute styling system"],"body":"## Principle\n\n**Primitives must never hardcode design tokens, CSS variables, colors, spacing, or layout values in Rust.**\n\n---\n\n## Problem\n\nWhen tokens leak into Rust:\n\n- Token cascade is broken\n- Theming becomes impossible\n- Structural layer becomes presentation layer\n- Build-time guarantees are lost\n\n---\n\n## Forbidden Pattern\n\n```rust\nstyle=\"padding: 16px;\"\nstyle=\"color: var(--color-primary);\"\nclass=\"bg-primary text-white\"\n```\n\nRust must not encode visual decisions.\n\n---\n\n## Canonical Pattern\n\n```rust\n<div data-button=\"\" class=class>\n```\n\nCSS layer resolves tokens based on data-attributes.\n\n---\n\n## Rationale\n\nProtects:\n- Token cascade integrity\n- Theme override safety\n- Separation of structure and presentation\n\n---\n\n## Enforcement\n\n- Code review\n- Static grep for `var(--` inside primitives\n- CI audit for inline styles\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":279,"slug":"ui-must-not-know-behavior","title":"UI Layer Must Not Be Aware of Behavior Layer","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["ui","behavior","layering","ssr"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"When UI depends on behavior modules, architectural boundaries collapse and SSR determinism is compromised. Layers must remain strictly isolated.","problem":"ui layer references behavior layer causing coupling and ssr issues","solution":"enforce strict separation where ui communicates only via attributes and behavior reacts independently","signals":["behavior import in ui","conditional runtime logic","tight coupling"],"search_intent":"how to separate ui and behavior layers","keywords":["ui behavior separation architecture","ssr safe layering frontend","data attribute driven behavior","frontend decoupling patterns"],"body":"## Principle\n\n**UI must never import, reference, register, or conditionally depend on Behavior modules.**\n\n---\n\n## Problem\n\nIf UI references Behavior:\n\n- Layer boundaries collapse\n- SSR determinism risks increase\n- Runtime coupling emerges\n- Testability degrades\n\n---\n\n## Forbidden Pattern\n\n```rust\nuse crate::behavior::resize;\n\nif resize_enabled {\n    register_resize();\n}\n```\n\nUI does not orchestrate runtime.\n\n---\n\n## Canonical Pattern\n\n```rust\n<XPrimitive\n    attr:data-resizable=\"true\"\n/>\n```\n\nBehavior registry reacts independently at runtime.\n\n---\n\n## Rationale\n\nPreserves:\n- Strict layering\n- Predictable SSR\n- Independent evolution of runtime bridge\n\n---\n\n## Enforcement\n\n- Dependency graph audit\n- UI crate must not depend on behavior crate\n\n---\n\n## Exceptions\n\n> No exceptions.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":280,"slug":"interactive-must-be-feature-flag-safe","title":"Interactive Must Be Safe Under Feature Flag Removal","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["interactive","feature-flags","hydration","ssr"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Feature flags that alter DOM structure create hydration mismatches and unstable rendering. Structural output must remain consistent.","problem":"feature flags change dom structure causing hydration mismatch","solution":"preserve structural output and control behavior via attributes","signals":["hydration mismatch","dom divergence","conditional rendering bugs"],"search_intent":"how to use feature flags without breaking hydration","keywords":["feature flags ssr hydration","dom stability rendering","conditional rendering pitfalls","frontend feature isolation"],"body":"## Principle\n\n**Disabling any optional feature must not alter structural rendering or break hydration.**\n\n---\n\n## Problem\n\nFeature coupling causes:\n\n- Hidden rendering differences\n- Inconsistent DOM trees\n- Hydration mismatches\n- Regression chains\n\n---\n\n## Forbidden Pattern\n\n```rust\nif resize_enabled {\n    view! { <ResizeHandlePrimitive /> }\n} else {\n    view! {}\n}\n```\n\nFeature removal must not change structural contracts without controlled fallback.\n\n---\n\n## Canonical Pattern\n\n```rust\n<ResizeHandlePrimitive\n    attr:data-enabled=move || resize_enabled.get().to_string()\n/>\n```\n\nStructural output remains stable.\n\n---\n\n## Rationale\n\nProtects:\n- SSR determinism\n- DOM stability\n- Feature isolation\n\n---\n\n## Enforcement\n\n- Review: toggle each feature individually\n- Hydration test matrix\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial version"},{"number":281,"slug":"no-cross-crate-layer-leaks","title":"Cross-Crate Layer Leakage Is Forbidden","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["workspace","layers","modularity","rust"],"language":"EN","version":"1.0.0","date":"2026-02-13","intro":"Accessing internal layers across crates creates hidden coupling and breaks modular guarantees. Public boundaries must be respected.","problem":"cross crate access to internal layers breaks modular architecture","solution":"restrict access to public apis and enforce visibility boundaries","signals":["internal module import","tight coupling","semver break risk"],"search_intent":"how to prevent cross crate coupling rust","keywords":["rust crate boundaries architecture","layer isolation workspace","pub(crate) enforcement rust","modular system design rust"],"body":"## Principle\n\n**A crate must never access another crate’s internal layer (primitive/ui/interactive/behavior) bypassing public boundaries.**\n\n---\n\n## Problem\n\nCross-layer leaks create:\n\n- Hidden tight coupling\n- Refactor paralysis\n- Broken semver guarantees\n- Implicit contracts\n\n---\n\n## Forbidden Pattern\n\n```rust\nuse canonrs_ui_interactive::internal::resize_logic;\n```\n\nAccessing internal logic directly breaks boundaries.\n\n---\n\n## Canonical Pattern\n\n```rust\nuse canonrs_ui::Button;\nuse canonrs_ui_interactive::DataTableInteractive;\n```\n\nOnly public API surfaces are consumed.\n\n---\n\n## Rationale\n\nEnsures:\n- Package immutability\n- Version isolation\n- Layer purity\n- Enterprise maintainability\n\n---\n\n## Enforcement\n\n- Visibility restrictions (pub(crate))\n- CI dependency graph validation\n- Review checklist\n\n---\n\n## Exceptions\n\n> **No exceptions. This rule is absolute.**\n\n---\n\n## Version History\n\n- **1.0.0** — Initial version"},{"number":282,"slug":"no-raw-text-nodes","title":"No Raw Text Nodes in SSR Boundaries","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","ssr","dom","leptos"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Raw text nodes rendered directly in SSR cause hydration mismatches when WASM expects structured elements.","problem":"text nodes rendered in SSR differ from expected element nodes in hydration phase","solution":"always wrap text content in explicit HTML elements","signals":["hydration panic","expected text node mismatch","tachys failed_to_cast_text_node"],"search_intent":"how to fix leptos hydration text node mismatch","keywords":["leptos hydration error","ssr text node mismatch","wasm dom hydration","tachys hydration panic"],"body":"## Principle\n\nText must never exist as a root render node.\n\nAll text must be encapsulated in a stable element.\n\n---\n\n## Problem\n\nRaw text:\n\n- produces TEXT node in SSR\n- may produce ELEMENT in WASM\n- breaks hydration contract\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\n\"Hello world\"\n```\n\n---\n\n### Canonical Pattern\n\n```\n<span>\"Hello world\"</span>\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- no raw text nodes in primitives or UI\n- all text must be wrapped\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":283,"slug":"stable-dom-structure-required","title":"Stable DOM Structure Required for Hydration","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","ssr","dom","structure"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Hydration requires identical DOM structure between server and client. Any structural divergence causes runtime failure.","problem":"ssr and wasm generate different dom trees causing hydration mismatch","solution":"ensure identical node hierarchy regardless of state","signals":["hydration panic","dom mismatch warning","inconsistent node tree"],"search_intent":"how to fix ssr wasm dom mismatch leptos","keywords":["dom structure hydration","ssr mismatch leptos","wasm hydration failure","deterministic dom tree"],"body":"## Principle\n\nStructure must be deterministic.\n\nState may change content, never structure.\n\n---\n\n## Problem\n\nConditional rendering:\n\n- changes node hierarchy\n- breaks hydration\n- causes unrecoverable panic\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nif condition {\n  <div/>\n} else {\n  <span/>\n}\n```\n\n---\n\n### Canonical Pattern\n\n```\n<div>\n  {if condition { \"A\" } else { \"B\" }}\n</div>\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- node tree must be identical in SSR and WASM\n- only content may vary\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":284,"slug":"children-must-be-structurally-stable","title":"Children Must Be Structurally Stable","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["children","composition","dom","structure"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Dynamic children insertion or removal alters DOM shape and breaks hydration consistency.","problem":"optional children modify node count causing mismatch between ssr and client","solution":"ensure children count and position remain stable regardless of state","signals":["hydration mismatch","missing node errors","inconsistent children length"],"search_intent":"how to prevent children mismatch in leptos components","keywords":["leptos children mismatch","dom children stability","ssr hydration children","component composition rules"],"body":"## Principle\n\nChildren define structure, not state.\n\n---\n\n## Problem\n\nOptional nodes:\n\n- change child count\n- reorder DOM\n- break hydration\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\n{if show_hint {\n  <Hint/>\n}}\n```\n\n---\n\n### Canonical Pattern\n\n```\n<Hint hidden={!show_hint}/>\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- children count must remain constant\n- optional content must be hidden, not removed\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":285,"slug":"overlay-must-use-container-pattern","title":"Overlay Must Use Container Pattern","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["overlay","layout","composition","ui"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Overlay components require structural separation from content to ensure proper layering and interaction control.","problem":"overlay implemented as standalone content breaks layering and interaction model","solution":"use container pattern separating content and overlay layers","signals":["overlay not blocking interaction","overlay rendered as content","layering issues"],"search_intent":"how to implement overlay pattern correctly","keywords":["overlay container pattern","ui layering architecture","blocking overlay design","frontend overlay structure"],"body":"## Principle\n\nOverlay is a layer, not content.\n\n---\n\n## Problem\n\nStandalone overlay:\n\n- cannot cover content\n- breaks composition\n- fails interaction control\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\n<Overlay>content</Overlay>\n```\n\n---\n\n### Canonical Pattern\n\n```\n<OverlayContainer>\n  <Content/>\n  <Overlay/>\n</OverlayContainer>\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- overlay must be separate from content\n- container defines layering context\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":286,"slug":"overlay-must-block-interaction","title":"Overlay Must Block Interaction","status":"ENFORCED","severity":"HIGH","category":"behavior","tags":["overlay","interaction","ux","pointer-events"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Overlays must prevent user interaction with underlying content during blocking states.","problem":"overlay does not block pointer events allowing unintended interaction","solution":"enforce pointer-event blocking and layering via z-index","signals":["click through overlay","focus leakage","interaction during loading"],"search_intent":"how to block interaction with overlay","keywords":["overlay pointer events css","blocking ui interaction","z-index overlay behavior","frontend loading overlay"],"body":"## Principle\n\nOverlay must intercept interaction.\n\n---\n\n## Problem\n\nWithout blocking:\n\n- users interact with hidden content\n- state becomes inconsistent\n- UX breaks\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\n.overlay {\n  pointer-events: none;\n}\n```\n\n---\n\n### Canonical Pattern\n\n```\n.overlay {\n  pointer-events: all;\n  position: absolute;\n  inset: 0;\n  z-index: 10;\n}\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- overlay must block pointer interaction\n- overlay must visually cover content\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":287,"slug":"state-must-control-visibility","title":"State Must Control Visibility","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["state","visibility","dom","css"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Component state must explicitly control DOM visibility. Implicit or disconnected state leads to inconsistent UI behavior.","problem":"state is defined but does not affect visibility or rendering","solution":"bind state directly to DOM visibility via attributes or CSS selectors","signals":["state changes without visual effect","inconsistent UI behavior","hidden elements still interactive"],"search_intent":"how to bind state to visibility in leptos","keywords":["state visibility binding","leptos state dom control","css state selectors","reactive ui visibility"],"body":"## Principle\n\nState without effect is invalid.\n\n---\n\n## Problem\n\nUnbound state:\n\n- creates dead logic\n- breaks UX expectations\n- introduces bugs\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\ndata-rs-state=\"loading\"\n<!-- no visual change -->\n```\n\n---\n\n### Canonical Pattern\n\n```\n[data-rs-state=\"loading\"] .overlay {\n  display: flex;\n}\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- every state must control DOM or behavior\n- no passive state declarations\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":288,"slug":"aria-hidden-must-match-visibility","title":"aria-hidden Must Match Visibility","status":"ENFORCED","severity":"HIGH","category":"accessibility","tags":["aria","accessibility","visibility","dom"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"ARIA visibility must reflect actual DOM visibility. Mismatches cause incorrect screen reader behavior.","problem":"aria-hidden does not match real visibility state","solution":"synchronize aria-hidden with actual DOM visibility","signals":["screen reader reads hidden content","accessibility audit failures","inconsistent aria state"],"search_intent":"how to fix aria-hidden mismatch","keywords":["aria hidden accessibility","dom visibility aria","screen reader mismatch","wcag aria rules"],"body":"## Principle\n\nARIA must reflect reality.\n\n---\n\n## Problem\n\nMismatch:\n\n- breaks accessibility\n- confuses assistive tech\n- invalidates semantic contract\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\naria-hidden=\"true\"\n<!-- element visible -->\n```\n\n---\n\n### Canonical Pattern\n\n```\naria-hidden={state == \"idle\"}\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- aria-hidden must reflect visibility\n- no divergence allowed\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":289,"slug":"overlay-must-be-visually-present","title":"Overlay Must Be Visually Present","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["overlay","ui","feedback","visual"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"An overlay must provide visible feedback. Invisible overlays break user perception and interaction clarity.","problem":"overlay exists structurally but has no visual representation","solution":"ensure overlay has visible feedback such as spinner, backdrop, or indicator","signals":["invisible overlay","user confusion","no loading feedback"],"search_intent":"how to create visible loading overlay","keywords":["loading overlay ui","spinner overlay design","visual feedback loading","frontend ux overlay"],"body":"## Principle\n\nOverlay must communicate state.\n\n---\n\n## Problem\n\nInvisible overlay:\n\n- provides no feedback\n- confuses users\n- breaks UX expectations\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\n<div data-rs-loading-overlay></div>\n```\n\n---\n\n### Canonical Pattern\n\n```\n<div data-rs-loading-overlay>\n  <Spinner/>\n</div>\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- overlay must render visible content\n- feedback must be perceivable\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":290,"slug":"interactive-components-must-emit-events","title":"Interactive Components Must Emit Events","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["events","interaction","components","architecture"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Interactive components must emit events to integrate with the system. Silent state changes break composability.","problem":"components mutate state without emitting events","solution":"emit standardized events such as rs-change on state updates","signals":["no event emitted","integration failure","non-reactive components"],"search_intent":"how to emit events in ui components","keywords":["custom event frontend","rs-change event pattern","component interaction events","ui event architecture"],"body":"## Principle\n\nInteraction must be observable.\n\n---\n\n## Problem\n\nSilent components:\n\n- cannot be composed\n- cannot trigger reactions\n- break system integration\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nstate = \"on\"\n<!-- no event -->\n```\n\n---\n\n### Canonical Pattern\n\n```\ndispatchEvent(new CustomEvent(\"rs-change\", { detail: value }))\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- all interactive components must emit events\n- rs-change is required for value updates\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":291,"slug":"data-rs-value-is-canonical-output","title":"data-rs-value Is Canonical Output","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["data-attributes","value","state","integration"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Components must expose their state through a canonical data attribute to enable system-wide integration.","problem":"component state is not accessible externally","solution":"expose value via data-rs-value attribute","signals":["missing data-rs-value","integration failure","state inaccessible"],"search_intent":"how to expose component value via dom","keywords":["data attribute state","dom value exposure","frontend component integration","canonical value pattern"],"body":"## Principle\n\nValue must be externalized.\n\n---\n\n## Problem\n\nHidden state:\n\n- cannot be consumed\n- breaks integration\n- isolates components\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nstate stored internally only\n```\n\n---\n\n### Canonical Pattern\n\n```\ndata-rs-value=\"selected-option\"\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- all interactive components must expose data-rs-value\n- value must reflect current state\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":292,"slug":"select-is-canonical-interaction-model","title":"Select Is Canonical Interaction Model","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["select","interaction","architecture","canonical"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"All interactive components must follow a unified interaction contract. Divergence breaks composability.","problem":"components implement inconsistent interaction patterns","solution":"standardize all interactions on Select model: state + value + event","signals":["inconsistent component behavior","missing events","integration failures"],"search_intent":"how to standardize interaction patterns in ui components","keywords":["select pattern ui","interaction architecture frontend","unified component behavior","canonical ui model"],"body":"## Principle\n\nSelect defines interaction contract.\n\n---\n\n## Problem\n\nWithout standard:\n\n- components diverge\n- integration breaks\n- system becomes inconsistent\n\n---\n\n## Contract\n\n- must expose data-rs-value\n- must expose data-rs-state\n- must emit rs-change\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":293,"slug":"no-duplicate-state-outside-dom","title":"No Duplicate State Outside DOM","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["state","dom","duplication","architecture"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"State duplication between DOM and internal logic leads to divergence and inconsistency.","problem":"state exists in multiple sources causing desynchronization","solution":"use DOM as single source of truth via data attributes","signals":["inconsistent UI state","mismatch between DOM and logic","stale values"],"search_intent":"how to avoid duplicated state in frontend","keywords":["single source of truth dom","state duplication frontend","dom state pattern","reactive ui state"],"body":"## Principle\n\nDOM is the source of truth.\n\n---\n\n## Problem\n\nDuplicated state:\n\n- diverges\n- becomes stale\n- breaks reactivity\n\n---\n\n## Contract\n\n- state must be reflected in DOM\n- no parallel state storage\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":294,"slug":"no-hidden-interactive-elements","title":"No Hidden Interactive Elements","status":"ENFORCED","severity":"CRITICAL","category":"accessibility","tags":["accessibility","focus","aria","dom"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Hidden elements must not receive focus or interaction. Violations break accessibility and UX.","problem":"interactive elements exist inside hidden containers","solution":"disable or remove interactivity when element is hidden","signals":["focus leaks","keyboard navigation issues","accessibility violations"],"search_intent":"how to prevent focus on hidden elements","keywords":["hidden focus accessibility","aria hidden focus issue","keyboard navigation hidden elements","wcag focus rules"],"body":"## Principle\n\nHidden means non-interactive.\n\n---\n\n## Problem\n\nHidden interactive elements:\n\n- trap focus\n- confuse users\n- violate WCAG\n\n---\n\n## Contract\n\n- no focusable elements inside aria-hidden\n- no interactive behavior when hidden\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":295,"slug":"no-conditional-rendering-for-structure","title":"No Conditional Rendering for Structure","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","structure","conditional","ssr"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Conditional rendering that changes DOM structure breaks hydration guarantees.","problem":"if/else modifies node structure between SSR and client","solution":"use stable structure and conditionally change content or visibility","signals":["hydration mismatch","missing nodes","runtime panic"],"search_intent":"how to avoid conditional dom mismatch in ssr","keywords":["conditional rendering ssr","hydration mismatch leptos","stable dom structure","ssr best practices"],"body":"## Principle\n\nStructure must be constant.\n\n---\n\n## Problem\n\nConditional DOM:\n\n- alters hierarchy\n- breaks hydration\n- causes runtime errors\n\n---\n\n## Contract\n\n- no structural branching\n- only content variation allowed\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":296,"slug":"components-must-be-composable","title":"Components Must Be Composable","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["composition","architecture","integration"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Components must integrate seamlessly with other system parts. Isolation breaks system value.","problem":"components cannot interact with others","solution":"design components with standardized contracts","signals":["isolated components","integration failure","duplicated logic"],"search_intent":"how to design composable components","keywords":["component composition frontend","reusable ui architecture","composable design system","integration patterns"],"body":"## Principle\n\nComponents are building blocks.\n\n---\n\n## Problem\n\nNon-composable components:\n\n- cannot scale\n- increase complexity\n- reduce reuse\n\n---\n\n## Contract\n\n- must follow canonical contracts\n- must emit events and expose state\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":297,"slug":"overlay-state-must-be-deterministic","title":"Overlay State Must Be Deterministic","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["overlay","state","deterministic"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Overlay behavior must be fully determined by state. Implicit conditions lead to inconsistency.","problem":"overlay visibility depends on implicit logic","solution":"bind overlay behavior strictly to explicit state","signals":["inconsistent overlay visibility","unpredictable UI","state mismatch"],"search_intent":"how to control overlay visibility via state","keywords":["overlay state control","deterministic ui state","reactive overlay behavior","frontend state patterns"],"body":"## Principle\n\nState drives behavior.\n\n---\n\n## Problem\n\nImplicit logic:\n\n- creates bugs\n- breaks predictability\n\n---\n\n## Contract\n\n- overlay visibility must depend only on state\n- no implicit conditions\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":298,"slug":"dom-is-single-source-of-truth","title":"DOM Is Single Source of Truth","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["dom","architecture","state"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"All system state must be derivable from DOM to ensure consistency across layers.","problem":"state is stored outside DOM and becomes inconsistent","solution":"use DOM attributes as canonical state representation","signals":["state divergence","inconsistent rendering","debugging complexity"],"search_intent":"how to use dom as source of truth","keywords":["dom state architecture","frontend state source of truth","data attributes state","reactive dom pattern"],"body":"## Principle\n\nDOM is canonical.\n\n---\n\n## Problem\n\nExternal state:\n\n- diverges\n- becomes stale\n- increases complexity\n\n---\n\n## Contract\n\n- all state must be visible in DOM\n- no hidden internal state\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":299,"slug":"no-implicit-visual-state","title":"No Implicit Visual State","status":"ENFORCED","severity":"HIGH","category":"design-system","tags":["visual","state","css"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Visual state must be explicitly defined. Implicit styling leads to inconsistency.","problem":"visual changes occur without explicit state mapping","solution":"map all visual changes to explicit state attributes","signals":["inconsistent styling","unclear state representation","css conflicts"],"search_intent":"how to map state to visual changes","keywords":["state driven css","visual state mapping","design system consistency","frontend styling state"],"body":"## Principle\n\nVisual state must be explicit.\n\n---\n\n## Problem\n\nImplicit visuals:\n\n- break consistency\n- confuse developers\n\n---\n\n## Contract\n\n- all visuals must map to state\n- no implicit styling\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":300,"slug":"no-side-effects-in-primitives","title":"No Side Effects in Primitives","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["primitives","side-effects","architecture"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Primitives must be pure and predictable. Side effects break determinism and SSR safety.","problem":"primitives contain logic or side effects","solution":"keep primitives pure and move logic to behaviors","signals":["unpredictable rendering","hydration issues","side effects in view"],"search_intent":"how to keep components pure in leptos","keywords":["pure components frontend","no side effects ui","leptos primitives rules","ssr safe components"],"body":"## Principle\n\nPrimitives are pure.\n\n---\n\n## Problem\n\nSide effects:\n\n- break SSR\n- reduce predictability\n- introduce bugs\n\n---\n\n## Contract\n\n- primitives must be pure functions\n- no side effects allowed\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":301,"slug":"events-must-be-standardized","title":"Events Must Be Standardized","status":"ENFORCED","severity":"HIGH","category":"behavior","tags":["events","standardization","architecture"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Event naming and structure must be standardized to ensure interoperability.","problem":"custom or inconsistent event naming breaks integration","solution":"use canonical event names such as rs-change","signals":["inconsistent event names","integration issues","duplicated logic"],"search_intent":"how to standardize events in ui systems","keywords":["event naming frontend","standardized events ui","rs-change pattern","component event architecture"],"body":"## Principle\n\nEvents must be predictable.\n\n---\n\n## Problem\n\nCustom events:\n\n- break integration\n- increase complexity\n\n---\n\n## Contract\n\n- use canonical event names\n- follow system-wide standards\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":302,"slug":"state-must-be-serializable-in-dom","title":"State Must Be Serializable in DOM","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["dom","state","serialization"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"All component state must be externally observable through the DOM to guarantee SSR/CSR consistency, enable behavior integration, and support system-wide orchestration.","problem":"component state is hidden in memory and not exposed","solution":"serialize all state into data-rs-* attributes","signals":["hydration mismatch","integration failure","state inaccessible"],"search_intent":"how to expose component state through dom","keywords":["dom state serialization","data attributes state","ssr consistency","frontend integration"],"body":"## Principle\n\nState must be externalized and observable.\n\n---\n\n## Problem\n\nWhen state exists only in memory:\n\n- behaviors cannot consume it\n- external systems cannot react\n- SSR and client diverge\n- debugging becomes opaque\n\nThis creates systemic isolation and breaks Canon integration contracts.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nlet selected = create_signal(\"a\");\n```\n\n(no DOM representation)\n\n---\n\n### Canonical Pattern\n\n```\ndata-rs-value=\"a\"\ndata-rs-state=\"active\"\n```\n\nState is always reflected in DOM.\n\n---\n\n## Contract\n\n### Enforcement\n\n- all state must exist in DOM\n- use data-rs-* attributes exclusively\n- value must always reflect current state\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":303,"slug":"no-random-or-time-based-rendering","title":"No Random or Time-Based Rendering","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["hydration","determinism","ssr"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Rendering must be deterministic between SSR and client. Randomness or time-based values introduce divergence and break hydration.","problem":"render output differs between server and client","solution":"remove randomness or isolate to client-only execution","signals":["hydration mismatch","flickering UI","inconsistent DOM"],"search_intent":"how to prevent random rendering issues in ssr","keywords":["ssr determinism","hydration mismatch random","time rendering bug","frontend consistency"],"body":"## Principle\n\nRendering must be deterministic.\n\n---\n\n## Problem\n\nRandom or time-based rendering:\n\n- produces different DOM trees\n- breaks hydration alignment\n- causes runtime panic\n- invalidates SSR guarantees\n\nThis is a structural violation of Canon runtime.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nlet id = rand::random();\nlet now = Date::now();\n```\n\n---\n\n### Canonical Pattern\n\n```\nlet id = stable_id;\n```\n\n```\nif is_client() {\n  generate_runtime_value();\n}\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- no randomness in SSR\n- no time-based rendering in SSR\n- all values must be deterministic\n\n---\n\n### Exceptions\n\nClient-only islands.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":304,"slug":"no-optional-dom-nodes","title":"No Optional DOM Nodes","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["dom","hydration","structure"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Conditional DOM nodes break structural determinism required for hydration.","problem":"DOM structure differs between SSR and client","solution":"always render nodes and control visibility via attributes","signals":["hydration panic","node mismatch","inconsistent structure"],"search_intent":"how to maintain stable dom structure in ssr","keywords":["stable dom structure","ssr mismatch","hydration bug","frontend rendering rules"],"body":"## Principle\n\nDOM structure must be stable.\n\n---\n\n## Problem\n\nConditional rendering:\n\n- changes node count\n- shifts DOM tree\n- breaks hydration expectations\n- creates runtime instability\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\n{show && view! { <div/> }}\n```\n\n---\n\n### Canonical Pattern\n\n```\n<div hidden={!show}></div>\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- no conditional DOM nodes\n- use hidden / aria-hidden\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":305,"slug":"all-text-must-be-wrapped","title":"All Text Must Be Wrapped","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["text","hydration","dom"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Raw text nodes introduce implicit DOM structures that break hydration expectations.","problem":"text nodes mismatch between SSR and client","solution":"wrap all text inside explicit elements","signals":["expected text node error","hydration panic","string mismatch"],"search_intent":"how to fix hydration errors caused by text nodes","keywords":["text node hydration","leptos mismatch","wrap text dom","ssr bug"],"body":"## Principle\n\nText must be explicit.\n\n---\n\n## Problem\n\nRaw text nodes:\n\n- create implicit DOM\n- mismatch expected structure\n- break hydration casting\n- cause runtime panic\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\n<div>\"Hello\"</div>\n```\n\n---\n\n### Canonical Pattern\n\n```\n<div><span>\"Hello\"</span></div>\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- no raw text nodes\n- all text must be wrapped\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":306,"slug":"components-must-declare-behavior","title":"Components Must Declare Behavior","status":"ENFORCED","severity":"HIGH","category":"behavior","tags":["behavior","architecture"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Interactive components must explicitly declare their behavior to ensure predictable attachment.","problem":"behavior is implicit or missing","solution":"declare behavior via data-rs-behavior","signals":["missing interaction","behavior not attached","inconsistent UX"],"search_intent":"how to attach behavior to components","keywords":["component behavior binding","data rs behavior","frontend interaction","ui architecture"],"body":"## Principle\n\nBehavior must be explicit.\n\n---\n\n## Problem\n\nImplicit behavior:\n\n- cannot be reliably attached\n- breaks behavior registry\n- causes inconsistent interaction\n- increases coupling\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\n<button onclick=\"...\">\n```\n\n---\n\n### Canonical Pattern\n\n```\ndata-rs-behavior=\"toggle\"\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- all interactive components must declare behavior\n- use data-rs-behavior\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":307,"slug":"no-inline-style-for-logic","title":"No Inline Style for Logic","status":"ENFORCED","severity":"HIGH","category":"styling-css","tags":["css","inline-style"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Inline styles must not encode logic or dynamic state.","problem":"logic embedded in style attributes","solution":"move logic to state attributes and CSS","signals":["style conditions","inconsistent rendering","duplication"],"search_intent":"how to avoid logic in css","keywords":["css architecture","inline style logic","state driven css","frontend patterns"],"body":"## Principle\n\nLogic must not live in style.\n\n---\n\n## Problem\n\nInline logic:\n\n- breaks separation of concerns\n- duplicates logic\n- reduces maintainability\n- complicates theming\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nstyle={if active { \"color:red\" } else { \"\" }}\n```\n\n---\n\n### Canonical Pattern\n\n```\ndata-rs-state=\"active\"\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- no logic in style\n- styling must derive from state\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":308,"slug":"all-interactions-must-emit-events","title":"All Interactions Must Emit Events","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["events","interaction"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"All state changes must emit events to enable reactive integration across the system.","problem":"state changes are not externally observable","solution":"emit canonical events after state changes","signals":["silent updates","integration failure","missing reactions"],"search_intent":"how to emit events on state change","keywords":["ui events architecture","state change events","frontend events","rs change pattern"],"body":"## Principle\n\nInteractions must be observable.\n\n---\n\n## Problem\n\nSilent changes:\n\n- break integration\n- isolate components\n- prevent system reaction\n- reduce composability\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nstate.set(\"on\");\n```\n\n---\n\n### Canonical Pattern\n\n```\ndispatchEvent(new CustomEvent(\"rs-change\"))\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- emit event on every state change\n- use canonical event naming\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":309,"slug":"no-component-specific-event-names","title":"No Component-Specific Event Names","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["events","naming"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Event naming must be standardized to enable cross-component integration.","problem":"components emit custom event names","solution":"use canonical event names","signals":["inconsistent events","integration complexity","duplicated logic"],"search_intent":"how to standardize ui event names","keywords":["event naming frontend","canonical events","ui architecture patterns","event consistency"],"body":"## Principle\n\nEvents must be unified.\n\n---\n\n## Problem\n\nCustom event names:\n\n- break integration\n- increase complexity\n- prevent reuse\n- create fragmentation\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\ndispatchEvent(new CustomEvent(\"toggle-change\"))\n```\n\n---\n\n### Canonical Pattern\n\n```\ndispatchEvent(new CustomEvent(\"rs-change\"))\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- no component-specific event names\n- use canonical event vocabulary\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":310,"slug":"components-must-be-ssr-safe","title":"Components Must Be SSR Safe","status":"ENFORCED","severity":"CRITICAL","category":"core-runtime","tags":["ssr","hydration"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"All components must render identically in SSR and client environments.","problem":"component output differs between environments","solution":"ensure deterministic and environment-independent rendering","signals":["hydration mismatch","runtime panic","inconsistent UI"],"search_intent":"how to build ssr safe components","keywords":["ssr safe components","hydration consistency","frontend rendering safety","leptos ssr"],"body":"## Principle\n\nSSR and client must match.\n\n---\n\n## Problem\n\nEnvironment divergence:\n\n- creates different DOM\n- breaks hydration\n- causes runtime failure\n- invalidates SSR guarantees\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nif is_client() { render_a } else { render_b }\n```\n\n---\n\n### Canonical Pattern\n\n```\nrender_same_structure()\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- identical DOM in SSR and client\n- no environment-based divergence\n\n---\n\n### Exceptions\n\nClient-only islands.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":311,"slug":"no-implicit-dom-order-dependency","title":"No Implicit DOM Order Dependency","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["dom","order"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Component behavior must not depend on implicit DOM ordering.","problem":"logic relies on child position","solution":"use explicit attributes and selectors","signals":["fragile components","order-based bugs","unpredictable behavior"],"search_intent":"how to avoid dom order dependency","keywords":["dom order dependency","frontend architecture patterns","robust components","selector based logic"],"body":"## Principle\n\nOrder must not define behavior.\n\n---\n\n## Problem\n\nOrder-based logic:\n\n- breaks when structure changes\n- creates hidden coupling\n- reduces flexibility\n- causes instability\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nchildren[0]\n```\n\n---\n\n### Canonical Pattern\n\n```\nquerySelector(\"[data-rs-role='label']\")\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- no reliance on DOM order\n- use explicit selectors\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":312,"slug":"behaviors-must-be-idempotent","title":"Behaviors Must Be Idempotent","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["behavior","idempotency","runtime"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Behaviors may attach multiple times due to hydration, re-rendering, or hot-reload. They must not duplicate effects or side effects.","problem":"multiple attachments create duplicated listeners and inconsistent state","solution":"ensure behavior attachment is idempotent using guards","signals":["duplicated events","multiple listeners firing","unstable toggle behavior"],"search_intent":"how to make frontend behaviors idempotent","keywords":["idempotent behavior dom","attach guard frontend","duplicate event listeners","wasm hydration behavior"],"body":"## Principle\n\nBehavior must be safe to attach multiple times.\n\n---\n\n## Problem\n\nWhen behaviors attach more than once:\n\n- duplicate listeners accumulate\n- state toggles inconsistently\n- performance degrades\n- side effects multiply\n\nHydration and hot-reload make this scenario inevitable.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nelement.addEventListener(\"click\", handler);\n```\n\n(no guard)\n\n---\n\n### Canonical Pattern\n\n```\nif (!element.hasAttribute(\"data-rs-attached\")) {\n  element.setAttribute(\"data-rs-attached\", \"1\");\n  element.addEventListener(\"click\", handler);\n}\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- all behaviors must use attach guards\n- use data-rs-*-attached pattern\n- listeners must be registered once\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":313,"slug":"no-dom-mutation-outside-behavior-layer","title":"No DOM Mutation Outside Behavior Layer","status":"ENFORCED","severity":"CRITICAL","category":"component-architecture","tags":["dom","mutation","architecture"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"DOM mutation must be centralized in the behavior layer to ensure predictability and separation of concerns.","problem":"components mutate DOM directly","solution":"restrict DOM mutation to behavior layer only","signals":["unpredictable UI updates","duplicated logic","inconsistent DOM state"],"search_intent":"how to centralize dom mutations in frontend","keywords":["dom mutation architecture","behavior layer pattern","frontend separation of concerns","ui mutation control"],"body":"## Principle\n\nDOM mutation belongs to behavior layer only.\n\n---\n\n## Problem\n\nWhen UI or primitives mutate DOM:\n\n- logic becomes fragmented\n- SSR/CSR consistency breaks\n- behavior duplication occurs\n- debugging becomes difficult\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nview! {\n  <div onclick=...>\n}\n```\n\n---\n\n### Canonical Pattern\n\n```\ndata-rs-behavior=\"toggle\"\n```\n\nMutation handled in behavior.\n\n---\n\n## Contract\n\n### Enforcement\n\n- no DOM mutation in primitives or UI\n- behaviors own all DOM changes\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":314,"slug":"state-must-not-live-in-behavior","title":"State Must Not Live in Behavior","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["state","behavior","architecture"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Behavior must not own state. State must be stored in DOM or signals to remain observable and consistent.","problem":"state stored inside behavior logic","solution":"externalize state to DOM attributes","signals":["hidden state","inconsistent behavior","debugging difficulty"],"search_intent":"how to manage state outside behavior","keywords":["state externalization dom","behavior state anti pattern","frontend architecture","observable state"],"body":"## Principle\n\nState must not be hidden.\n\n---\n\n## Problem\n\nState inside behavior:\n\n- becomes invisible\n- cannot be shared\n- breaks integration\n- creates inconsistencies\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nlet isOpen = false;\n```\n\n---\n\n### Canonical Pattern\n\n```\ndata-rs-state=\"open\"\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- no state stored in behavior\n- state must exist in DOM\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":315,"slug":"every-state-change-must-update-dom","title":"Every State Change Must Update DOM","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["state","dom","reactivity"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"State changes must always be reflected in DOM to maintain synchronization across system layers.","problem":"state changes not reflected in DOM","solution":"update data-rs-* attributes on every change","signals":["stale UI","mismatch between state and DOM","integration failure"],"search_intent":"how to sync state with dom","keywords":["state dom sync","reactive dom update","frontend state architecture","data attribute sync"],"body":"## Principle\n\nDOM is the source of truth.\n\n---\n\n## Problem\n\nIf DOM is not updated:\n\n- behaviors read stale state\n- integrations break\n- UI becomes inconsistent\n- debugging becomes unreliable\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nstate.set(\"on\");\n```\n\n(no DOM update)\n\n---\n\n### Canonical Pattern\n\n```\nelement.setAttribute(\"data-rs-state\", \"on\");\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- every state change updates DOM\n- DOM must reflect current state\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":316,"slug":"no-implicit-default-state","title":"No Implicit Default State","status":"ENFORCED","severity":"HIGH","category":"state-reactivity","tags":["state","defaults"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Default state must be explicitly declared to avoid ambiguity and inconsistent initialization.","problem":"state defaults are implicit","solution":"always define explicit initial state","signals":["inconsistent initial render","unpredictable behavior","missing attributes"],"search_intent":"how to define default state in components","keywords":["default state frontend","explicit state initialization","component state patterns","predictable ui"],"body":"## Principle\n\nState must be explicit.\n\n---\n\n## Problem\n\nImplicit defaults:\n\n- create ambiguity\n- lead to inconsistent rendering\n- break determinism\n- complicate debugging\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nlet state;\n```\n\n---\n\n### Canonical Pattern\n\n```\ndata-rs-state=\"idle\"\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- all components must declare default state\n- no implicit initialization\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":317,"slug":"all-components-must-be-composable","title":"All Components Must Be Composable","status":"ENFORCED","severity":"HIGH","category":"component-architecture","tags":["composition","architecture"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Components must be designed for composition without hidden constraints or coupling.","problem":"components cannot be reused or composed","solution":"design components with explicit contracts","signals":["rigid components","duplication","poor reuse"],"search_intent":"how to design composable components","keywords":["component composition","reusable ui components","frontend architecture patterns","composability"],"body":"## Principle\n\nComposition is mandatory.\n\n---\n\n## Problem\n\nNon-composable components:\n\n- increase duplication\n- reduce flexibility\n- break scalability\n- create tight coupling\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\ncomponent requires fixed structure\n```\n\n---\n\n### Canonical Pattern\n\n```\ncomponent accepts children\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- components must accept composition\n- no rigid internal structure\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":318,"slug":"no-hidden-side-effects","title":"No Hidden Side Effects","status":"ENFORCED","severity":"CRITICAL","category":"state-reactivity","tags":["side-effects","state"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"All side effects must be explicit and traceable to avoid unpredictable behavior.","problem":"side effects occur implicitly","solution":"make all side effects explicit","signals":["unexpected updates","difficult debugging","inconsistent state"],"search_intent":"how to manage side effects in frontend","keywords":["side effects frontend","reactive architecture","explicit state changes","predictable ui"],"body":"## Principle\n\nSide effects must be explicit.\n\n---\n\n## Problem\n\nHidden side effects:\n\n- create unpredictability\n- break mental model\n- complicate debugging\n- cause race conditions\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nstate.set(\"x\") inside render\n```\n\n---\n\n### Canonical Pattern\n\n```\neffect(() => { ... })\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- no implicit side effects\n- all effects must be declared\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":319,"slug":"behaviors-must-be-stateless","title":"Behaviors Must Be Stateless","status":"ENFORCED","severity":"CRITICAL","category":"behavior","tags":["behavior","stateless"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Behaviors must not retain internal state to remain predictable and re-attachable.","problem":"behavior stores internal state","solution":"derive state from DOM","signals":["inconsistent behavior","state desync","debugging difficulty"],"search_intent":"how to build stateless behaviors","keywords":["stateless behavior dom","frontend behavior patterns","dom driven state","reactive ui"],"body":"## Principle\n\nBehavior must be stateless.\n\n---\n\n## Problem\n\nStateful behavior:\n\n- breaks reattachment\n- desynchronizes with DOM\n- creates hidden logic\n- causes inconsistency\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nlet open = false;\n```\n\n---\n\n### Canonical Pattern\n\n```\nelement.getAttribute(\"data-rs-state\")\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- no state in behavior\n- state must be read from DOM\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":320,"slug":"dom-is-the-single-source-of-truth","title":"DOM Is the Single Source of Truth","status":"ENFORCED","severity":"CRITICAL","category":"governance","tags":["dom","state","architecture"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"The DOM must be the canonical source of truth for all component state and interaction.","problem":"multiple sources of truth exist","solution":"centralize all state in DOM","signals":["state divergence","inconsistent UI","integration issues"],"search_intent":"why dom should be source of truth","keywords":["dom source of truth","frontend architecture","state consistency","ui synchronization"],"body":"## Principle\n\nDOM is authoritative.\n\n---\n\n## Problem\n\nMultiple sources:\n\n- create inconsistency\n- cause desynchronization\n- break integration\n- complicate debugging\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\nstate in memory + dom mismatch\n```\n\n---\n\n### Canonical Pattern\n\n```\ndata-rs-state=\"active\"\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- DOM is primary state source\n- all systems must read from DOM\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":321,"slug":"integration-must-be-declarative","title":"Integration Must Be Declarative","status":"ENFORCED","severity":"HIGH","category":"governance","tags":["integration","declarative"],"language":"EN","version":"1.0.0","date":"2026-04-02","intro":"Component integration must be defined declaratively through attributes, not imperative code.","problem":"integration is manual and imperative","solution":"use declarative data-rs-* attributes","signals":["complex wiring","tight coupling","brittle integrations"],"search_intent":"how to build declarative frontend integration","keywords":["declarative ui integration","data attributes integration","frontend architecture","decoupled components"],"body":"## Principle\n\nIntegration must be declarative.\n\n---\n\n## Problem\n\nImperative integration:\n\n- increases complexity\n- creates tight coupling\n- reduces scalability\n- breaks composability\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\n```\ncomponentA.listen(componentB)\n```\n\n---\n\n### Canonical Pattern\n\n```\ndata-rs-filter-target=\"table\"\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- integration must use attributes\n- no imperative wiring\n\n---\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 — Initial definition"},{"number":322,"slug":"island-dom-shape-must-be-static","title":"Island DOM Shape Must Be Static","status":"ENFORCED","severity":"CRITICAL","category":"islands-architecture","tags":["islands","hydration","dom","leptos"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"The DOM structure rendered by an island must be identical between SSR and hydration.","problem":"islands with dynamic DOM shape cause hydration mismatch and runtime panic","solution":"keep island view structure static and fixed","signals":["failed_to_cast_marker_node","hydration panic","tachys cursor error"],"search_intent":"leptos island hydration mismatch fix","keywords":["leptos islands hydration","island dom shape","tachys hydration error","SSR hydrate mismatch"],"body":"## Principle\n\nAn island view! macro must produce identical DOM structure on both SSR and client hydration.\n\n---\n\n## Problem\n\nWhen an island renders different DOM structures depending on runtime conditions or cfg flags, the tachys hydration cursor panics with failed_to_cast_marker_node.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\nTwo different view! blocks separated by cfg — one for SSR and one for hydrate — produce different DOM structures and always cause hydration panic.\n\n### Canonical Pattern\n\nOne single view! block. cfg is permitted only for event handlers and logic, never for the view structure itself.\n\n---\n\n## Contract\n\n### Enforcement\n\n- one view! block per island\n- cfg is permitted only for event handlers and logic\n- cfg must never alter the DOM structure\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":323,"slug":"island-props-must-be-serializable","title":"Island Props Must Be Serializable","status":"ENFORCED","severity":"CRITICAL","category":"islands-architecture","tags":["islands","props","serde","leptos"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"All props passed to a Leptos island must implement Serialize and Deserialize.","problem":"custom types without serde cause compile errors in island props","solution":"derive serde traits or use primitive-compatible types","signals":["island compile error","trait bound not satisfied","failed to parse path"],"search_intent":"leptos island props serialize deserialize","keywords":["leptos island props","serde island","island compile error","wasm props serialization"],"body":"## Principle\n\nIslands are hydrated from serialized props embedded in the HTML. Every prop type must be serializable.\n\n---\n\n## Problem\n\nCustom enums like DisabledState that do not derive Serialize cause compile errors when used as island props.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\nPassing a custom enum without serde as an island prop.\n\n### Canonical Pattern\n\nUse Option<bool>, Option<f64>, Option<String> in island props. Unwrap with defaults inside the function body.\n\n---\n\n## Contract\n\n### Enforcement\n\n- island props must use only serializable types: bool, f64, i64, String, Option<T>\n- custom enums used as island props must derive Serialize and Deserialize\n- prop(default = value) is not supported in islands - use Option<T> with unwrap_or\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":324,"slug":"island-view-must-be-isomorphic","title":"Island View Must Be Isomorphic","status":"ENFORCED","severity":"CRITICAL","category":"islands-architecture","tags":["islands","hydration","isomorphic","leptos"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"The first render of an island must produce identical output on SSR and client.","problem":"SSR and hydrate produce different HTML causing hydration panic","solution":"initialize signals with deterministic values derived from props","signals":["hydration mismatch","cursor panic","different attribute values SSR vs client"],"search_intent":"leptos island isomorphic render SSR hydrate","keywords":["isomorphic rendering leptos","SSR hydration match","island first render","deterministic island"],"body":"## Principle\n\nThe initial render of an island must be bit-for-bit identical between SSR and client hydration.\n\n---\n\n## Problem\n\nWhen signals are initialized with non-deterministic values or reactive attributes produce different output on SSR vs client, the hydration cursor panics.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\nReactive attributes that compute differently on SSR vs hydrate, producing different initial HTML.\n\n### Canonical Pattern\n\nSignals initialized from props. Same prop value on SSR and hydrate produces identical initial HTML.\n\n---\n\n## Contract\n\n### Enforcement\n\n- signals must be initialized from props, not from runtime state\n- reactive attributes must produce the same initial value on SSR and hydrate\n- no random, time-based, or environment-dependent initial values\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":325,"slug":"dynamic-lists-must-live-outside-islands","title":"Dynamic Lists Must Live Outside Islands","status":"ENFORCED","severity":"CRITICAL","category":"islands-architecture","tags":["islands","lists","hydration","leptos"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"Dynamic lists rendered inside islands cause hydration marker mismatches in Leptos 0.8.","problem":"iterators, For component and inner_html inside islands break hydration","solution":"move dynamic lists to SSR shell components outside the island","signals":["failed_to_cast_marker_node at list position","hydration panic on collection render","tachys cursor error inside island"],"search_intent":"leptos island dynamic list hydration error","keywords":["leptos island list","For component island","iterator island hydration","island children dynamic"],"body":"## Principle\n\nIslands must have a fixed number of child nodes. Dynamic collections must be rendered in SSR shell components.\n\n---\n\n## Problem\n\nAll dynamic list patterns inside islands cause hydration marker mismatch in Leptos 0.8: iterators with collect, For component, and inner_html with dynamic content.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\nRendering a dynamic list of marks or items inside the island view.\n\n### Canonical Pattern\n\nIsland has a fixed DOM shape. A SSR shell component wraps the island and renders the dynamic list outside of it.\n\n---\n\n## Contract\n\n### Enforcement\n\n- islands must not contain iterators, For, or inner_html with dynamic content\n- dynamic collections belong in component shell wrappers\n- island child count must be statically known at compile time\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":326,"slug":"island-event-handlers-must-be-cfg-gated","title":"Island Event Handlers Must Be Cfg-Gated","status":"ENFORCED","severity":"HIGH","category":"islands-architecture","tags":["islands","cfg","event-handlers","leptos"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"Event handler logic inside islands must be conditionally compiled with cfg feature flags.","problem":"web-sys and wasm-bindgen APIs are unavailable during SSR compilation","solution":"gate all client-only logic with cfg(feature = hydrate)","signals":["web_sys not found in SSR build","wasm_bindgen unavailable","JsCast not in scope"],"search_intent":"leptos island event handler SSR compile error","keywords":["leptos island cfg hydrate","web_sys SSR error","island event handler cfg","wasm_bindgen SSR"],"body":"## Principle\n\nIslands compile on both SSR and WASM targets. Client-only APIs must be cfg-gated to prevent SSR compilation failures.\n\n---\n\n## Problem\n\nweb_sys, wasm_bindgen and JsCast are only available in the WASM target. Using them without cfg gates causes SSR build failures.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\nUsing web_sys or JsCast inside an event handler without a cfg(feature = hydrate) gate.\n\n### Canonical Pattern\n\nTwo handler definitions: one with cfg(feature = hydrate) containing the real logic, one with cfg(not(feature = hydrate)) as a no-op. Both use the same variable name so the single view! block compiles on both sides.\n\n---\n\n## Contract\n\n### Enforcement\n\n- all web_sys, wasm_bindgen, JsCast usage must be inside cfg(feature = hydrate)\n- SSR no-op handlers must consume captured variables to avoid unused warnings\n- the view! block must use the same handler name on both sides\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":327,"slug":"signals-are-ssot-for-island-state","title":"Signals Are the SSOT for Island State","status":"ENFORCED","severity":"CRITICAL","category":"islands-architecture","tags":["islands","signals","state","leptos"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"In Leptos islands, signals are the single source of truth. DOM attributes are outputs, never inputs.","problem":"DOM mutation and attribute reading as state source breaks the reactive model","solution":"derive all state from signals initialized from props","signals":["reading data-rs-* attributes as state","set_attribute called directly","DOM scanning inside island"],"search_intent":"leptos island signal state management","keywords":["leptos signal SSOT","island state management","reactive state leptos","DOM attribute output"],"body":"## Principle\n\nState lives in signals. DOM reflects signals. Never the reverse.\n\n---\n\n## Problem\n\nThe behavior registry pattern reads DOM attributes as state and mutates them directly. This is architecturally incompatible with islands. Islands have a reactive owner and signals as their state model.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\nReading get_attribute to get state, calling set_attribute to update state, or scanning the DOM to find and update elements.\n\n### Canonical Pattern\n\nSignal initialized from prop. Reactive attributes in view! derive their values from the signal. No DOM reads or writes outside the reactive system.\n\n---\n\n## Contract\n\n### Enforcement\n\n- island state must live in signal()\n- data-rs-* attributes must be reactive outputs of signals\n- no set_attribute, get_attribute, or DOM scanning inside islands\n- behavior registry pattern is forbidden inside islands\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":328,"slug":"island-crate-must-be-linked-in-wasm-entry","title":"Island Crate Must Be Linked in WASM Entry Point","status":"ENFORCED","severity":"CRITICAL","category":"islands-architecture","tags":["islands","wasm","workspace","linking"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"In a Rust workspace, the crate containing islands must be explicitly linked in the WASM entry point crate.","problem":"islands defined in a separate crate are silently excluded from the WASM bundle","solution":"add the island crate as a dependency and use it explicitly in the client crate","signals":["island function missing from generated JS","window.__leptos_islands undefined","island never hydrates"],"search_intent":"leptos island workspace not working wasm","keywords":["leptos island workspace","island not found wasm","wasm bundle missing island","use crate as workspace"],"body":"## Principle\n\nwasm-bindgen only includes code reachable from the WASM entry point. Islands in separate crates must be explicitly referenced or they are silently excluded from the bundle.\n\n---\n\n## Problem\n\nIn a workspace where islands live in canonrs-server but the WASM entry is canonrs-client, the islands are silently excluded from the bundle. The JS output contains zero references to the island functions.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\nThe WASM entry crate does not declare the island crate as a dependency.\n\n### Canonical Pattern\n\nThe island crate is declared as an optional dependency in the WASM entry crate, activated via the hydrate feature. The lib.rs of the WASM entry includes use island_crate as _ with allow(unused_imports).\n\n---\n\n## Contract\n\n### Enforcement\n\n- the crate containing island components must be a dependency of the WASM entry crate\n- use island_crate as _ must appear in the WASM entry lib.rs\n- islands must be exported from the crate public API\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":329,"slug":"hydration-scripts-must-declare-islands-mode","title":"HydrationScripts Must Declare Islands Mode","status":"ENFORCED","severity":"CRITICAL","category":"islands-architecture","tags":["islands","hydration","bootstrap","leptos"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"The HydrationScripts component must receive islands=true to enable island bootstrap in Leptos 0.8.","problem":"without islands=true the runtime does not connect leptos-island elements to WASM functions","solution":"pass islands=true to HydrationScripts in the app shell","signals":["islands render as static HTML","no interaction after hydration","window.__leptos_islands undefined"],"search_intent":"leptos island not hydrating HydrationScripts","keywords":["HydrationScripts islands","leptos island bootstrap","island not interactive","leptos 0.8 islands setup"],"body":"## Principle\n\nHydrationScripts with islands=true generates the bootstrap code that connects leptos-island DOM elements to their corresponding WASM functions. Without this prop, islands are rendered as static HTML with no interactivity.\n\n---\n\n## Problem\n\nWithout islands=true, the Leptos runtime initializes but never scans the DOM for leptos-island elements. The WASM functions are exported but never called. Islands appear to render correctly but have zero interactivity.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\nHydrationScripts without the islands prop in an app that uses island components.\n\n### Canonical Pattern\n\nHydrationScripts with islands=true in the head of the app shell.\n\n---\n\n## Contract\n\n### Enforcement\n\n- HydrationScripts must include islands=true when any island component exists in the app\n- leptos::mount::hydrate_islands() must be called in the hydrate() wasm entry point\n- leptos must be declared with the islands feature in Cargo.toml\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":330,"slug":"island-props-must-use-serde-enums","title":"Island Props Must Use Serde Enums","status":"ENFORCED","severity":"CRITICAL","category":"islands-architecture","tags":["islands","props","serde","serialization"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"Props of Leptos islands that need to be serialized across the SSR/hydration boundary must use enums with serde derives. Primitive optional types silently fail serialization.","problem":"`Option<String>` and `Option<bool>` with `#[prop(optional)]` always serialize as `null` in `data-props`, making the value invisible to the client after hydration.","solution":"Define an enum with `#[derive(serde::Serialize, serde::Deserialize)]` for every prop that carries meaningful state across the SSR boundary.","signals":["prop always appears as `null` in `data-props` HTML attribute","client island never receives the intended value","state appears correct in SSR but resets after hydration"],"search_intent":"leptos island prop null after hydration, option string not serialized island","keywords":["leptos island props serde","island serialization","data-props null","option bool island"],"body":"## Principle\n\nThe Leptos island macro serializes component props into the `data-props` HTML attribute using serde. Only types that implement `Serialize + Deserialize` are correctly encoded. `Option<String>` with `into` and `Option<bool>` with `optional` collapse to `null` silently.\n\n---\n\n## Problem\n\nAn island prop declared as `Option<String>` or `Option<bool>` with `#[prop(optional)]` will always appear as `null` in the rendered HTML, even when a value is explicitly passed at the call site.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n```rust\n#[island]\npub fn ButtonIsland(\n    #[prop(optional, into)] state_hint: Option<String>,\n    #[prop(optional)] flag: Option<bool>,\n) -> impl IntoView { ... }\n```\n\n### Canonical Pattern\n```rust\n#[derive(Clone, Copy, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize)]\npub enum ButtonStateHint {\n    #[default] None,\n    First,\n    Last,\n    Hover,\n    Focus,\n}\n\n#[island]\npub fn ButtonIsland(\n    #[prop(optional)] state_hint: Option<ButtonStateHint>,\n) -> impl IntoView { ... }\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- every island prop that carries state across SSR must use a serde enum\n- `Option<String>` and `Option<bool>` are forbidden as island props\n- enums must derive `Serialize`, `Deserialize`, `Default`, and `PartialEq`\n\n### Exceptions\n\n- `Option<String>` is acceptable for purely cosmetic props (class, aria_label) that do not affect reactive state\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":331,"slug":"island-ssr-state-must-be-materialized","title":"Island SSR State Must Be Fully Materialized Without Signals","status":"ENFORCED","severity":"HIGH","category":"islands-architecture","tags":["islands","ssr","signals","hydration","state"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"Every initial state value rendered in SSR must be pre-computed as a static string. Reactive signals are not available during SSR and produce empty or incorrect output when used as the sole source of truth for `data-rs-state`.","problem":"Signal values like `is_hover.get()` return their initial value during SSR but the rendered attribute may be empty if the closure does not account for static initial state separately.","solution":"Pre-compute an `initial_state` string from all static booleans and use it as a fallback in the reactive closure.","signals":["`data-rs-state=\"\"` in SSR output even when initial state was passed","state appears only after user interaction, not on page load","CSS that depends on initial state does not apply on first render"],"search_intent":"leptos island ssr state empty, signal not rendered ssr, data-rs-state blank on load","keywords":["leptos ssr signal","island initial state","data-rs-state ssr","hydration state mismatch"],"body":"## Principle\n\nLeptos signals are reactive primitives for the client. During SSR, signal closures execute once with their initial value. If the closure returns an empty string when signals are false, the SSR output will be empty regardless of static props passed in.\n\n---\n\n## Problem\n```rust\n// ❌ broken — if initial_hover is true, signal starts true,\n// but the closure may return \"\" on SSR if not handled\nlet state = move || {\n    let mut s = vec![];\n    if is_hover.get() { s.push(\"hover\"); }\n    s.join(\" \")\n};\n```\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n\nUsing only signal getters in the `data-rs-state` closure without a static fallback.\n\n### Canonical Pattern\n```rust\n// Pre-compute static initial state for SSR\nlet initial_state = {\n    let mut s = vec![];\n    if disabled      { s.push(\"disabled\"); }\n    if is_first      { s.push(\"first\");    }\n    if is_last       { s.push(\"last\");     }\n    if initial_hover { s.push(\"hover\");    }\n    if initial_focus { s.push(\"focus\");    }\n    s.join(\" \")\n};\n\n// Reactive closure with SSR fallback\nlet state = move || {\n    let mut s = vec![];\n    if disabled        { s.push(\"disabled\"); }\n    if is_first        { s.push(\"first\");    }\n    if is_last         { s.push(\"last\");     }\n    if is_hover.get()  { s.push(\"hover\");    }\n    if is_active.get() { s.push(\"active\");   }\n    if is_focus.get()  { s.push(\"focus\");    }\n    s.join(\" \")\n};\n\n// In view: use initial_state as SSR fallback\ndata-rs-state=move || {\n    let s = state();\n    if s.is_empty() { initial_state.clone() } else { s }\n}\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- every island with initial state props must pre-compute `initial_state` as a static string\n- the `data-rs-state` attribute must use `initial_state` as fallback when the reactive closure returns empty\n- static positional states (first, last, disabled) must appear in both the static and reactive paths\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":332,"slug":"group-override-selector-must-match-base-specificity","title":"Group Override Selector Must Match Base Specificity","status":"ENFORCED","severity":"HIGH","category":"css-architecture","tags":["css","specificity","cascade","group","override"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"CSS selectors in group or container components that override styles of child components must have specificity greater than or equal to the base selector of the child component.","problem":"A group selector with fewer conditions than the child's base selector loses the cascade silently. The override is present in the CSS but never applies.","solution":"Replicate all `:not()` conditions from the base child selector in the group override selector.","signals":["group override CSS is present in the bundle but has no visual effect","computed styles show the child's base rule winning","adding `!important` fixes it (forbidden — signals a specificity problem)"],"search_intent":"css group override not working, specificity lost in cascade, child selector winning over parent context","keywords":["css specificity group","override cascade","not selector specificity","button group hover not applying"],"body":"## Principle\n\nCSS specificity is determined by the number and type of selectors. A group context selector (`[data-rs-button-group] [data-rs-button]`) does not automatically outrank a more complex base selector (`[data-rs-button]:not(...):not(...):not(...)`). The override must match or exceed the base selector's complexity.\n\n---\n\n## Problem\n```css\n/* button_ui.css — wins */\n[data-rs-button][data-rs-state~=\"hover\"]:not([data-rs-state~=\"disabled\"]):not([data-rs-variant=\"ghost\"]):not([data-rs-variant=\"link\"]) {\n  opacity: 0.92;\n}\n\n/* button_group_ui.css — loses (lower specificity) */\n[data-rs-button-group] [data-rs-button][data-rs-state~=\"hover\"] {\n  opacity: 0.92;\n}\n```\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n```css\n[data-rs-button-group] [data-rs-button][data-rs-state~=\"hover\"] {\n  opacity: var(--state-opacity-hover);\n}\n```\n\n### Canonical Pattern\n```css\n[data-rs-button-group][data-rs-state~=\"attached\"] [data-rs-button][data-rs-state~=\"hover\"]:not([data-rs-state~=\"disabled\"]):not([data-rs-variant=\"ghost\"]):not([data-rs-variant=\"link\"]) {\n  opacity: var(--state-opacity-hover);\n}\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- group override selectors must replicate all `:not()` conditions from the base child selector\n- `!important` is forbidden as a specificity workaround\n- group selectors must include their own state condition (e.g. `[data-rs-state~=\"attached\"]`) to add specificity\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":333,"slug":"island-descendant-selector-over-child-combinator","title":"Island CSS Must Use Descendant Selector Not Child Combinator","status":"ENFORCED","severity":"HIGH","category":"css-architecture","tags":["css","islands","selector","leptos","wasm"],"language":"EN","version":"1.0.0","date":"2026-04-04","intro":"CSS selectors targeting elements inside Leptos islands must use the descendant combinator (space) instead of the direct child combinator (`>`). Leptos injects `<leptos-island>` and `<leptos-children>` wrapper elements between a parent component and its slotted children, breaking child selectors.","problem":"A CSS rule using `>` between a parent component and an island child will never match because the DOM contains intermediate Leptos wrapper elements.","solution":"Always use the descendant combinator (space) when writing selectors that span across island boundaries.","signals":["CSS rule is present in bundle but never applies to island children","rule works in isolation but breaks when component is wrapped in an island","DevTools shows the selector does not match despite correct DOM attributes"],"search_intent":"leptos island child selector not working, css child combinator island, leptos-children breaks selector","keywords":["leptos island css","child combinator island","leptos-children wrapper","descendant selector leptos"],"body":"## Principle\n\nLeptos islands render wrapper elements in the DOM: `<leptos-island>` wraps the island component and `<leptos-children>` wraps slotted children. These elements sit between the parent and child in the DOM tree, making `>` (direct child) selectors unable to match across island boundaries.\n\n---\n\n## Problem\n\nDOM structure:\n```html\n<div data-rs-button-group=\"\">         <!-- parent -->\n  <leptos-children>                   <!-- injected by Leptos -->\n    <leptos-island>                   <!-- injected by Leptos -->\n      <button data-rs-button=\"\">      <!-- target -->\n```\n```css\n/* ❌ never matches — > skips leptos-children and leptos-island */\n[data-rs-button-group] > [data-rs-button] {\n  border-radius: 0;\n}\n```\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n```css\n[data-rs-button-group][data-rs-state~=\"attached\"] > [data-rs-button] {\n  border-radius: var(--button-group-radius-merge);\n}\n```\n\n### Canonical Pattern\n```css\n[data-rs-button-group][data-rs-state~=\"attached\"] [data-rs-button] {\n  border-radius: var(--button-group-radius-merge);\n}\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- `>` combinator is forbidden in any selector that crosses an island boundary\n- all group-to-child selectors must use the descendant combinator (space)\n- this applies to all CSS files in the design system that target island children\n\n### Exceptions\n\n- `>` is acceptable within a single non-island component where no Leptos wrappers exist\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition"},{"number":334,"slug":"island-state-must-propagate-via-context","title":"Island State Must Propagate Via Context","status":"ENFORCED","severity":"CRITICAL","category":"islands-architecture","tags":["islands","context","signal","state","leptos"],"language":"EN","version":"1.0.0","date":"2026-04-05","intro":"When multiple islands in the same tree need to share reactive state, the only correct pattern is `provide_context(RwSignal)` at the root island and `use_context` in child islands. Direct prop drilling of signals or DOM-based state sharing violates the signal contract.","problem":"Islands cannot share state via SSR component tree. Passing signals as props is not supported. DOM mutation as a workaround breaks the SSOT principle.","solution":"Root island publishes state via `provide_context`. Child islands consume via `use_context` captured before any closure.","signals":["child island does not react to parent state changes","state shared via DOM attributes instead of signals","`use_context` called inside event handler closure"],"search_intent":"leptos island shared state, provide_context island, tabs island state sync","keywords":["leptos provide_context","island context","RwSignal shared","tabs island pattern"],"body":"## Principle\n\nReactive state shared across island boundaries must flow through Leptos context. Any other mechanism — DOM mutation, eval, query_selector, prop serialization — is forbidden.\n\n---\n\n## Problem\n```rust\n// ❌ broken — DOM mutation as state\nel.set_attribute(\"data-rs-state\", \"active\").ok();\n\n// ❌ broken — use_context inside closure\non:click=move |_| {\n    if let Some(ctx) = use_context::<TabsContext>() { ... }\n}\n```\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n- `set_attribute` to sync state between islands\n- `query_selector` to find sibling islands\n- `eval` or `Function::new_no_args` to run JS\n- `use_context` called inside event handler or reactive closure\n\n### Canonical Pattern\n```rust\n// Root island — publishes state\n#[island]\npub fn TabsRootIsland(children: Children, #[prop(into)] initial: String) -> impl IntoView {\n    let active = RwSignal::new(initial);\n    provide_context(TabsContext(active));\n    view! { <div data-rs-tabs=\"\">{children()}</div> }\n}\n\n// Child island — consumes state, captures context before closure\n#[island]\npub fn TabsTriggerIsland(children: Children, #[prop(into)] value: String) -> impl IntoView {\n    let signal = use_context::<TabsContext>().map(|TabsContext(a)| a); // captured here\n    let v2 = value.clone();\n    let state = move || signal.map(|a| if a.get() == v2 { \"active\" } else { \"inactive\" }).unwrap_or(\"inactive\");\n    let on_click = {\n        let signal = signal;\n        move |_| { if let Some(a) = signal { a.set(value.clone()); } }\n    };\n    view! { <button data-rs-state=state on:click=on_click>{children()}</button> }\n}\n```\n\n---\n\n## Contract\n\n### Enforcement\n- shared island state must use `provide_context` + `use_context`\n- `use_context` must be called at component root, never inside closures\n- signal must be captured as `Option<RwSignal<T>>` before any closure\n\n### Exceptions\nNone.\n\n---\n\n## Version History\n- 1.0.0 - Initial definition"},{"number":335,"slug":"island-must-not-own-ui-structure","title":"Island Must Not Own UI Structure When Content Is Compositional","status":"ENFORCED","severity":"HIGH","category":"islands-architecture","tags":["islands","ssr","children","composition","structure"],"language":"EN","version":"1.0.0","date":"2026-04-05","intro":"Islands must control state, not render UI structure. When content is compositional — cards, tables, grids, rich components — it belongs in the SSR shell. Passing rich content as `Vec<String>` or serialized data to an island destroys composability and type safety.","problem":"Islands receive `Vec<TabItem { content: String }>` forcing all content to be serialized as plain strings, losing Leptos component composition.","solution":"Island wraps children via `Children`. SSR defines structure. Island provides context for state.","signals":["island prop contains HTML as string","`Vec<T>` with content fields passed to island","island renders cards, tables, or grids internally"],"search_intent":"leptos island children rich content, island vec string content, island composition pattern","keywords":["leptos island children","island SSR composition","island structure separation"],"body":"## Principle\n\nUI = f(state). Islands own the function. SSR owns the structure. Never invert this.\n\n---\n\n## Problem\n```rust\n// ❌ broken — content serialized as String, loses composition\n#[island]\npub fn TabsIsland(tabs: Vec<TabItem>, active: String) -> impl IntoView {\n    // TabItem { value: String, label: String, content: String }\n    // content: String cannot hold <Card />, <Table />, <Grid />\n}\n```\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n- `content: String` inside island props for rich UI content\n- island renders `<Card>`, `<Table>`, `<Grid>` internally from serialized data\n- `Vec<T>` where T contains UI content fields\n\n### Canonical Pattern\n```rust\n// ✅ Island owns state only\n#[island]\npub fn TabsRootIsland(children: Children, #[prop(into)] initial: String) -> impl IntoView {\n    let active = RwSignal::new(initial);\n    provide_context(TabsContext(active));\n    view! { <div data-rs-tabs=\"\">{children()}</div> }\n}\n\n// ✅ SSR defines structure with rich components\nview! {\n    <TabsRootIsland initial=\"proof\">\n        <TabsTriggerIsland value=\"proof\">\"Proof\"</TabsTriggerIsland>\n        <TabsContentIsland value=\"proof\">\n            <Card>...</Card>\n            <Table>...</Table>\n        </TabsContentIsland>\n    </TabsRootIsland>\n}\n```\n\n---\n\n## Contract\n\n### Enforcement\n- island props must never contain UI content as String\n- rich content must be passed via `Children`\n- island must not render layout components (Grid, Card, Table) from internal data\n\n### Exceptions\n- `TabsIsland` with `Vec<TabItem>` is allowed only when content is plain text (e.g. preview demos)\n\n---\n\n## Version History\n- 1.0.0 - Initial definition"},{"number":336,"slug":"token-references-must-resolve","title":"Token References Must Resolve to Existing Design Tokens","status":"ENFORCED","severity":"HIGH","category":"tokens","tags":["tokens","css","design-system","variables","contracts"],"language":"EN","version":"1.0.0","date":"2026-04-05","intro":"Every CSS custom property used in a component token must reference a design token that exists in the token system. Unresolved references produce silent visual failures — the browser uses the inherited or initial value with no error.","problem":"`--tabs-trigger-bg-active: var(--theme-primary-bg)` — `--theme-primary-bg` does not exist in the token system. The tab active state renders incorrectly with no build error.","solution":"Before declaring a token reference, verify the target token exists. Use the same token chain used by equivalent components (e.g. Button primary uses `--theme-action-primary-bg`).","signals":["active state renders with wrong color","component visually inconsistent with equivalent components","no build error despite broken visual"],"search_intent":"css variable not resolving, token reference broken, design token undefined","keywords":["css custom property undefined","token chain broken","theme-primary-bg missing"],"body":"## Principle\n\nToken references are contracts. A broken reference is a silent contract violation. The token system must be the single source of truth — every reference must be verifiable.\n\n---\n\n## Problem\n```rust\n// ❌ broken — --theme-primary-bg does not exist\nFamilyToken::new(\"tabs-trigger-bg-active\", \"var(--theme-primary-bg)\"),\nFamilyToken::new(\"tabs-trigger-fg-active\", \"var(--theme-primary-fg)\"),\n```\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n- referencing `--theme-primary-bg` or any token not defined in the bundle\n- copy-pasting token names without verifying they exist\n\n### Canonical Pattern\n```rust\n// ✅ verify equivalent component first (Button uses these)\nFamilyToken::new(\"tabs-trigger-bg-active\", \"var(--theme-action-primary-bg)\"),\nFamilyToken::new(\"tabs-trigger-fg-active\", \"var(--color-primary-foreground)\"),\n```\n\n---\n\n## Contract\n\n### Enforcement\n- every token value that references another token must be verified in `canonrs.bundle.css`\n- equivalent components must use the same token chain for the same semantic role\n- builder.md token declarations must not include prefixes that have no corresponding tokens\n\n### Exceptions\nNone.\n\n---\n\n## Version History\n- 1.0.0 - Initial definition"},{"number":337,"slug":"hover-must-not-override-active-state","title":"Hover State Must Not Override Active State","status":"ENFORCED","severity":"MEDIUM","category":"css-contracts","tags":["css","hover","active","state","specificity"],"language":"EN","version":"1.0.0","date":"2026-04-05","intro":"CSS hover styles applied without guarding against active state will override the active appearance on mouse-over, creating a visual regression where the selected item loses its active styling during hover.","problem":"`[data-rs-tabs-trigger]:hover` overrides `[data-rs-tabs-trigger][data-rs-state~=\"active\"]` when the user hovers over the active tab, removing the primary color and replacing it with the muted hover style.","solution":"All hover selectors on stateful components must exclude the active state via `:not([data-rs-state~=\"active\"])`.","signals":["active tab loses its color when hovered","selected item visually deselects on mouse-over","hover and active styles conflict"],"search_intent":"css hover overrides active, tab active color disappears on hover, hover state priority","keywords":["css specificity hover active","not selector active state","data-rs-state hover override"],"body":"## Principle\n\nActive state has semantic permanence. Hover is transient. Transient states must never override permanent ones.\n\n---\n\n## Problem\n```css\n/* ❌ broken — hover overrides active */\n[data-rs-tabs-trigger]:hover {\n  background: var(--tabs-trigger-bg-hover);\n  color: var(--tabs-trigger-fg-hover);\n}\n```\n\n---\n\n## Patterns\n\n### Forbidden Pattern\nHover selector without `:not([data-rs-state~=\"active\"])` on any component with an active state.\n\n### Canonical Pattern\n```css\n/* ✅ hover only applies when not active */\n[data-rs-tabs-trigger]:hover:not([data-rs-state~=\"active\"]):not([disabled]) {\n  background: var(--tabs-trigger-bg-hover);\n  color: var(--tabs-trigger-fg-hover);\n}\n```\n\n---\n\n## Contract\n\n### Enforcement\n- every `:hover` selector on a stateful component must include `:not([data-rs-state~=\"active\"])`\n- applies to: tabs, nav items, sidebar items, any component with persistent active state\n- disabled state must also be excluded: `:not([disabled])`\n\n### Exceptions\n- components with no active state (e.g. plain buttons without selection) are exempt\n\n---\n\n## Version History\n- 1.0.0 - Initial definition"},{"number":338,"slug":"island-boundary-rule","title":"Island Boundary Rule","status":"ENFORCED","severity":"HIGH","category":"leptos-architecture","tags":["island","ssr","leptos","layout","wasm","interaction"],"language":"EN","version":"1.0.0","date":"2026-04-07","intro":"Islands in CanonRS must never wrap layout structure. An island is a JS initialization boundary, not a layout container. Every component must render its full DOM in SSR via `#[component]`, and delegate JS initialization to a minimal `#[island]`.","problem":"A `#[island]` wrapping layout structure causes Leptos to emit an empty shell in SSR. The DOM never reaches the browser on first paint. Flex chains, height inheritance, and CSS selectors all fail silently.","solution":"Split every interactive component into three layers: SSR layout component, minimal init island, and optional interaction module.","signals":["component renders empty in SSR curl output","flex/grid layout collapses despite correct CSS","`height: 100%` has no effect on island children","`display: contents` on `leptos-island` is used as a workaround (forbidden)"],"search_intent":"leptos island empty SSR, island layout broken, flex chain broken leptos","keywords":["leptos island ssr","island boundary","flex layout island","wasm init pattern"],"body":"## Principle\n\nLeptos `#[island]` marks a hydration boundary. The server renders only the shell. Any layout structure inside an island is invisible on first paint.\n\nThe correct pattern separates concerns into three explicit layers:\n\n- Layer 1 — SSR Layout: `#[component]` renders full DOM in SSR\n- Layer 2 — Init Island: `#[island]` minimal, no children, calls init()\n- Layer 3 — Interaction: `interactions/*.rs` pointer/drag/keyboard (optional)\n\n---\n\n## Island Types\n\n### Type 1 — Passthrough Island\nComponent with no JS. Island is a re-export of the SSR component.\n\n### Type 2 — Init Island\nComponent with JS initialization. Layout stays in `#[component]`, init moves to minimal `#[island]`.\n\n### Type 3 — Interaction Island\nComponent with pointer/drag/keyboard complexity. Adds an `interactions/*.rs` module.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n`#[island]` wrapping layout — SSR emits empty shell, DOM never reaches browser.\n\n### Canonical Pattern\nLayout in `#[component]` renders full DOM in SSR. Init island is minimal with no children.\n\n---\n\n## Contract\n\n### Enforcement\n\n- `#[island]` must never contain children that are part of layout structure\n- `#[island]` must never be the direct parent of flex or grid children\n- init island must render `view! { <></> }` — no DOM output\n- `display: contents` on `leptos-island` is forbidden as a layout workaround\n- every component that requires JS must have an explicit Init island\n- init logic must never live inside a layout component\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition — derived from resizable layout failure (2026-04-07)"},{"number":339,"slug":"pointer-capture-must-register-on-document","title":"Pointer Capture Must Register Move and Up on Document","status":"ENFORCED","severity":"HIGH","category":"interaction-architecture","tags":["pointer","drag","event","capture","document","wasm","interaction"],"language":"EN","version":"1.0.0","date":"2026-04-07","intro":"Any interaction that uses pointer capture for drag behavior must register `pointermove` and `pointerup` on the `document`, not on the handle element. Registering on the element causes events to stop firing when the pointer leaves the element boundary during fast movement.","problem":"`pointermove` registered on the drag handle stops firing when the pointer moves faster than the element boundary. The drag freezes mid-interaction even though `setPointerCapture` was called.","solution":"Register `pointerdown` on the handle. Register `pointermove` and `pointerup` on `document`. Use `setPointerCapture` on the handle to ensure the pointer ID is tracked correctly.","signals":["drag stops when moving fast","bar does not follow pointer outside element bounds","`pointerup` not detected when releasing outside the handle","interaction works correctly on slow movement but breaks on fast movement"],"search_intent":"pointermove stops firing, drag freezes fast movement, pointer capture not working, pointerup not detected outside element","keywords":["pointermove document","pointer capture drag","setPointerCapture","pointerup outside element","wasm drag interaction"],"body":"## Principle\n\n`setPointerCapture` routes pointer events to the element but does not guarantee delivery if the listeners are on the element itself and the pointer leaves its bounds. Registering on `document` ensures all pointer events are captured regardless of position.\n\n---\n\n## Patterns\n\n### Forbidden Pattern\n```rust\n// pointermove on handle — stops firing when pointer leaves bounds\nhandle.add_event_listener_with_callback(\"pointerdown\", on_down).ok();\nhandle.add_event_listener_with_callback(\"pointermove\", on_move).ok();\nhandle.add_event_listener_with_callback(\"pointerup\", on_up).ok();\n```\n\n### Canonical Pattern\n```rust\n// pointerdown on handle, move and up on document\nhandle.add_event_listener_with_callback(\"pointerdown\", on_down).ok();\nhandle.set_pointer_capture(e.pointer_id()).ok(); // inside on_down\nlet doc: &web_sys::EventTarget = document.as_ref();\ndoc.add_event_listener_with_callback(\"pointermove\", on_move).ok();\ndoc.add_event_listener_with_callback(\"pointerup\", on_up).ok();\n```\n\n---\n\n## Contract\n\n### Enforcement\n\n- `pointerdown` is registered on the interactive element (handle, trigger, thumb)\n- `pointermove` must be registered on `document`\n- `pointerup` must be registered on `document`\n- `setPointerCapture` must be called inside `pointerdown` handler\n- pointer ID must be validated in `pointermove` and `pointerup` handlers\n- applies to: resizable, slider, carousel, any drag-based interaction\n\n### Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 1.0.0 - Initial definition — derived from resizable drag failure (2026-04-07)"},{"number":340,"slug":"passthrough-boundary-must-be-zero-logic","title":"Passthrough Boundary Must Be Zero Logic","status":"ENFORCED","severity":"CRITICAL","category":"boundary-architecture","tags":["boundary","passthrough","api","typing","ui","contract","rust"],"language":"EN","version":"2.0.0","date":"2026-04-16","intro":"Passthrough boundaries define a strict boundary between SSR and UI. They must not contain any logic, transformation, or interpretation of props. They forward typed data directly to UI components.","problem":"When passthrough boundaries perform parsing, enum mapping, fallback resolution, or conditional rendering, they introduce logic into a layer that must remain purely mechanical.","solution":"Passthrough boundaries must accept fully typed props and forward them directly to UI components. All transformations must occur before the boundary or inside the UI layer.","signals":[],"search_intent":"","keywords":[],"body":"## Principle\n\nPassthrough boundaries are **mechanical boundaries only**.\nThey do not interpret, decide, or transform.\n\n---\n\n## Architecture\n\n```\nComponent (SSR HTML)\n-> typed props\n-> Passthrough Boundary (#[component])\n-> UI component (rendering only)\n-> Primitive (HTML contract)\n```\n\n---\n\n## Boundary Types\n\nCanonRS defines three boundary types:\n\n- **Passthrough** (#[component]) — forwards props 1:1 to UI, zero logic\n- **Init** (#[component]) — declares behavior via data-rs-*, delegates to WASM init\n- **Interaction** (#[island]) — bootstraps interaction engine, delegates to WASM interaction\n\n---\n\n## Contract\n\n- MUST exist as a boundary component\n- MUST be implemented using `#[component]`\n- MUST NOT use `#[island]` macro\n- MUST forward props 1:1 to UI\n- MUST NOT contain logic (`if`, `match`, parsing)\n- MUST NOT mutate DOM\n- MUST NOT introduce state\n\n---\n\n## Signals (Violation Indicators)\n\n- `match`, `if`, or branching inside boundary\n- `unwrap_or`, `unwrap_or_default`\n- string to enum conversion\n- conditional rendering\n- parsing or normalization logic\n- props typed as `String` when enum exists\n\n---\n\n## Example\n\n### Forbidden\n\n```rust\n#[component]\npub fn RadioBoundary(selected: String) -> impl IntoView {\n    let state = if selected == \"true\" { SelectionState::Selected } else { SelectionState::Unselected };\n    view! { <RadioUi selected=state /> }\n}\n```\n\n### Canonical\n\n```rust\n#[component]\npub fn RadioBoundary(\n    #[prop(default = SelectionState::Unselected)] selected: SelectionState,\n) -> impl IntoView {\n    view! { <RadioUi selected=selected /> }\n}\n```\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 2.0.0 - Renamed island to boundary; added Architecture, Boundary Types, examples (2026-04-16)\n- 1.2.0 - Clarified boundary vs #[island] macro distinction (2026-04-11)\n- 1.1.0 - Clarified SSR-only boundary (2026-04-10)\n- 1.0.0 - Initial definition (2026-04-07)"},{"number":341,"slug":"init-boundary-must-be-dom-driven-zero-state","title":"Init Boundary Must Be DOM-Driven and Zero State","status":"ENFORCED","severity":"CRITICAL","category":"runtime-architecture","tags":["init","boundary","wasm","dom","state","loader"],"language":"EN","version":"4.0.0","date":"2026-04-16","intro":"Init boundaries define the scope for lightweight client-side behavior. They must not implement logic. All behavior is delegated to the WASM init layer, which operates directly on the DOM.","problem":"Placing logic, state, or event handling inside init boundaries creates duplication, breaks determinism, and conflicts with the CanonRS separation between UI and runtime layers.","solution":"Init boundaries must render SSR HTML and declare behavior via `data-rs-*`. The actual behavior is executed by the `canon-init-loader` and WASM init modules.","signals":[],"search_intent":"","keywords":[],"body":"## Principle\n\nInit boundaries are **zero-logic boundaries**.\n\nThey declare behavior — they do not execute it.\n\n---\n\n## Architecture\n\n```\nComponent (SSR HTML)\n-> data-rs-* attributes\n-> Init Boundary (#[component])\n-> canon-init-loader\n-> WASM init module\n-> DOM mutation\n-> CSS reaction\n```\n\n---\n\n## Decision Rule\n\nInit MUST be used when behavior is stateless and affects only the component itself.\n\nUse **interaction** instead when:\n\n- behavior affects multiple elements\n- behavior requires coordination between nodes\n- behavior requires memory of previous state\n- behavior includes keyboard navigation beyond simple focus\n- behavior enforces exclusivity (e.g. tabs, accordion, radio group)\n- behavior mutates layout or geometry\n\n---\n\n## Scope\n\n### Init MUST handle (via WASM, not boundary):\n\n- hover effects\n- simple click bindings\n- navigation triggers (`href`)\n- copy-to-clipboard\n- focus handling\n- attribute toggling\n\n### Init MUST NOT handle:\n\n- sorting\n- filtering\n- pagination\n- selection engines\n- drag interactions\n- complex keyboard navigation\n- multi-step state logic\n\n---\n\n## Boundary Contract\n\n- MUST exist as a boundary component\n- MUST use `#[component]` (NOT `#[island]`)\n- MUST render SSR HTML only\n- MUST declare behavior via `data-rs-*`\n- MUST NOT use `web_sys`\n- MUST NOT register event listeners\n- MUST NOT contain closures\n- MUST NOT contain logic (`if`, `match`, parsing)\n- MUST NOT store or manage state\n\n---\n\n## WASM Init Contract\n\n- MUST use `web_sys` for event handling\n- MUST query DOM via selectors\n- MUST read/write `data-rs-state`\n- MUST be idempotent (safe for MutationObserver)\n- MUST NOT depend on SSR hydration\n- MUST NOT store persistent state\n\n---\n\n## Signals (Violation Indicators)\n\n- presence of `#[island]`\n- usage of `web_sys` inside boundary\n- event listeners inside boundary\n- signals (`signal`, `RwSignal`)\n- local state variables (`is_open`, `active`)\n- conditional logic unrelated to DOM\n- parsing or transformation logic\n\n---\n\n## Relationship with Interaction Layer\n\n- Init = micro-interactions (stateless, DOM-driven)\n- Interaction = full behavior engine (stateful, complex)\n\nIf behavior requires coordination, persistence, or multi-step logic -> it MUST be an interaction module.\n\n---\n\n## Example\n\n### Forbidden\n\n```rust\n#[island]\npub fn NavItemInit() -> impl IntoView {\n    // logic inside boundary\n}\n```\n\n### Canonical\n\n```rust\n#[component]\npub fn NavItemBoundary() -> impl IntoView {\n    view! {\n        <a data-rs-interaction=\"init\">\n            <NavItem />\n        </a>\n    }\n}\n```\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 4.0.0 - Renamed island to boundary; added Decision Rule; updated examples (2026-04-16)\n- 3.0.0 - Init island defined as zero-logic boundary; behavior moved to WASM loader (2026-04-11)\n- 2.0.0 - Init moved to WASM loader (2026-04-11)\n- 1.0.0 - Initial definition (2026-04-07)"},{"number":342,"slug":"interaction-boundary-must-delegate-to-client-module","title":"Interaction Boundary Must Delegate to Client Module","status":"ENFORCED","severity":"CRITICAL","category":"interaction-architecture","tags":["interaction","boundary","wasm","client","behavior","dom"],"language":"EN","version":"4.0.0","date":"2026-04-16","intro":"Components with complex interaction must delegate all behavior to a client-side interaction module. The boundary defines the scope and may act as a bootstrap trigger, but must never implement interaction logic.","problem":"Placing interaction logic inside boundaries creates:\n- duplicated state\n- SSR/hydration inconsistencies\n- tight coupling between UI and behavior\n- non-reusable logic","solution":"All interaction logic must live in the client interaction layer.\nThe boundary exists as a scope delimiter and may trigger initialization, but does not contain behavior.","signals":[],"search_intent":"","keywords":[],"body":"## Principle\n\nInteraction belongs to the **client layer**, not the boundary.\n\n```\nPrimitive = contract\nUI = proxy\nBoundary = scope delimiter (and optional bootstrap)\nInteraction = behavior engine\nDOM = source of truth\n```\n\n---\n\n## Architecture\n\n```\nComponent (SSR HTML)\n-> data-rs-* attributes\n-> Interaction Boundary (#[island])\n-> canon-loader\n-> WASM interaction module\n-> DOM mutation\n-> CSS reaction\n```\n\n---\n\n## Decision Rule\n\nAn interaction module MUST be used when:\n\n- behavior affects multiple elements\n- behavior requires coordination between nodes\n- behavior requires memory of previous state\n- behavior includes keyboard navigation beyond simple focus\n- behavior enforces exclusivity (e.g. tabs, accordion, radio group)\n- behavior mutates layout or geometry\n\nOtherwise -> MUST be implemented as **init**.\n\n---\n\n## Interaction Types\n\n- **Selection** - tabs, menu, radio group, command\n- **Coordination** - accordion, navigation-menu\n- **Input Orchestration** - otp, combobox\n- **Layout** - resizable, drag/drop\n- **Virtualization** - list, table\n\n---\n\n## State Contract\n\n- DOM (`data-rs-state`) is the single source of truth\n- Interaction modules MUST read and write `data-rs-*` attributes\n- Interaction modules MUST NOT maintain internal persistent state\n- All state transitions MUST be reflected in DOM attributes\n\n---\n\n## Contract\n\n### Interaction Layer\n\n- MUST live in `canonrs-interactions-*`\n- MUST expose an initialization entrypoint\n- MUST operate via DOM (`data-rs-*`)\n- MUST be idempotent (safe for MutationObserver re-init)\n- MUST NOT depend on SSR hydration\n\n---\n\n### Boundary\n\n- MUST exist as a boundary component\n- MUST use `#[island]` for interaction bootstrap\n- MUST NOT implement interaction logic\n- MUST NOT contain pointer/drag/keyboard logic\n- MUST NOT manage interaction state\n\n---\n\n## Signals (Violation Indicators)\n\n- pointer or drag logic inside boundary\n- keyboard navigation logic inside boundary\n- layout mutation (`getBoundingClientRect`, inline styles)\n- signals controlling interaction state\n- loops managing interaction behavior\n\n---\n\n## Applies to\n\n- datatable\n- resizable\n- slider\n- drag/drop\n- virtual list\n- any complex interaction system\n\n---\n\n## Exceptions\n\nNone.\n\n---\n\n## Version History\n\n- 4.0.0 - Renamed island to boundary; added Decision Rule, Interaction Types, State Contract (2026-04-16)\n- 3.0.0 - Island redefined as mandatory boundary; clarified bootstrap role (2026-04-11)\n- 2.0.0 - Interaction defined as WASM-driven system (2026-04-10)\n- 1.0.0 - Initial definition (2026-04-07)"}]