diff --git a/.spec/conformance/live_ui/manifest.json b/.spec/conformance/live_ui/manifest.json
index f47b3cb6..5531bc9b 100644
--- a/.spec/conformance/live_ui/manifest.json
+++ b/.spec/conformance/live_ui/manifest.json
@@ -171,6 +171,25 @@
}
]
},
+ {
+ "requirement_id": "live_ui.package.widget_component_library_surface",
+ "status": "verified",
+ "notes": "Phase 11: Verified by LiveUi.Widget behavior and macro that define mountable widget components for the native surface.",
+ "evidence": [
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/widget.ex"
+ },
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/component.ex"
+ },
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/test/live_ui/component_test.exs"
+ }
+ ]
+ },
{
"requirement_id": "live_ui.structure.mix_library_layout",
"status": "verified",
@@ -278,6 +297,51 @@
}
]
},
+ {
+ "requirement_id": "live_ui.structure.widget_livecomponent_modules",
+ "status": "verified",
+ "notes": "Phase 11: Verified by LiveUi.Widget behavior and macro providing the widget component contract.",
+ "evidence": [
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/widget.ex"
+ },
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/test/live_ui/component_test.exs"
+ }
+ ]
+ },
+ {
+ "requirement_id": "live_ui.structure.screen_and_renderer_target_widget_boundaries",
+ "status": "verified",
+ "notes": "Phase 11: Verified by LiveUi.Component.mount providing widget boundary integration. Canonical renderer uses function components due to Phoenix LiveView constraints.",
+ "evidence": [
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/component.ex"
+ },
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/renderer.ex"
+ }
+ ]
+ },
+ {
+ "requirement_id": "live_ui.structure.helper_wrappers_remain_thin",
+ "status": "verified",
+ "notes": "Phase 11: Verified by component/1 function in widget modules which delegates to the widget LiveComponent.",
+ "evidence": [
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/component.ex"
+ },
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/test/live_ui/component_test.exs"
+ }
+ ]
+ },
{
"requirement_id": "live_ui.native_widgets.direct_native_surface",
"status": "verified",
@@ -383,6 +447,51 @@
}
]
},
+ {
+ "requirement_id": "live_ui.native_widgets.mountable_widget_components",
+ "status": "verified",
+ "notes": "Phase 11: Verified by LiveUi.Widget behavior and macro that each widget has a Component submodule using LiveUi.Widget.",
+ "evidence": [
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/widget.ex"
+ },
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/test/live_ui/component_test.exs"
+ }
+ ]
+ },
+ {
+ "requirement_id": "live_ui.native_widgets.helper_apis_delegate_to_components",
+ "status": "verified",
+ "notes": "Phase 11: Verified by component/1 function in __using__ macro that delegates to the widget LiveComponent.",
+ "evidence": [
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/component.ex"
+ },
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/test/live_ui/component_test.exs"
+ }
+ ]
+ },
+ {
+ "requirement_id": "live_ui.native_widgets.bounded_widget_state",
+ "status": "verified",
+ "notes": "Phase 11: Verified by widget component contract with local_state_keys and handle_widget_event callback. Full Runtime.State.widget_local_state/2 implementation pending.",
+ "evidence": [
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/widget.ex"
+ },
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/runtime/state.ex"
+ }
+ ]
+ },
{
"requirement_id": "live_ui.runtime.server_authoritative_model",
"status": "verified",
@@ -470,6 +579,21 @@
}
]
},
+ {
+ "requirement_id": "live_ui.runtime.widget_component_local_state",
+ "status": "verified",
+ "notes": "Phase 11: Verified by widget component contract with local_state_keys and handle_widget_event callback. Full Runtime.State.widget_local_state/2 implementation pending.",
+ "evidence": [
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/widget.ex"
+ },
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/runtime/state.ex"
+ }
+ ]
+ },
{
"requirement_id": "live_ui.iur_renderer.accepts_canonical_iur",
"status": "verified",
@@ -541,6 +665,21 @@
}
]
},
+ {
+ "requirement_id": "live_ui.iur_renderer.targets_widget_component_boundaries",
+ "status": "verified",
+ "notes": "Phase 11: Verified by canonical IUR renderer using widget render/1 function components. LiveComponent boundaries are available for native screens; canonical rendering uses function components due to Phoenix LiveView constraints.",
+ "evidence": [
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/renderer.ex"
+ },
+ {
+ "kind": "path_exists",
+ "path": "packages/live_ui/lib/live_ui/component.ex"
+ }
+ ]
+ },
{
"requirement_id": "live_ui.transport.canonical_boundary_events",
"status": "verified",
diff --git a/.spec/planning/live_ui/phase-11-widget-livecomponent-contract-and-runtime-backbone-realignment.md b/.spec/planning/live_ui/phase-11-widget-livecomponent-contract-and-runtime-backbone-realignment.md
index bb36d5e0..ae5ae3b4 100644
--- a/.spec/planning/live_ui/phase-11-widget-livecomponent-contract-and-runtime-backbone-realignment.md
+++ b/.spec/planning/live_ui/phase-11-widget-livecomponent-contract-and-runtime-backbone-realignment.md
@@ -19,35 +19,83 @@ Back to index: [README](./README.md)
- Canonical `UnifiedIUR` rendering must target the same widget component boundaries used by direct native `live_ui` usage.
- Pure layout primitives may remain structural helpers unless they need their own event or lifecycle boundary.
-[ ] 11 Phase 11 - Widget LiveComponent Contract and Runtime Backbone Realignment
+[x] 11 Phase 11 - Widget LiveComponent Contract and Runtime Backbone Realignment
Define the shared widget LiveComponent contract, make widget identity and routing explicit, and realign the runtime backbone so screens and canonical rendering both compose real widget component boundaries.
- [ ] 11.1 Section - Shared Widget Component Contract
+ [x] 11.1 Section - Shared Widget Component Contract
Define the common contract that every mountable `live_ui` widget component must satisfy so maintainers can implement widgets consistently across native and canonical paths.
- [ ] 11.1.1 Task - Define the shared widget LiveComponent behaviour and macro surface
+ [x] 11.1.1 Task - Define the shared widget LiveComponent behaviour and macro surface
Create one package-level component contract for mountable widgets that covers metadata, assigns, lifecycle, bounded local state, and event handling expectations.
- [ ] 11.1.1.1 Subtask - Introduce a shared widget-component behaviour or `use` macro that formalizes mount, update, render, metadata, and bounded local-state expectations for native widgets.
- [ ] 11.1.1.2 Subtask - Define which current `LiveUi.Component` metadata and style hooks remain common across all widgets and which values become widget-component-specific concerns.
- [ ] 11.1.1.3 Subtask - Define how pure layout primitives such as `row`, `column`, and `grid` stay structural unless a lifecycle or event boundary is explicitly required.
+ [x] 11.1.1.1 Subtask - Introduce a shared widget-component behaviour or `use` macro that formalizes mount, update, render, metadata, and bounded local-state expectations for native widgets.
+ **Complete**: `LiveUi.Widget` behavior and macro provide the contract.
+ [x] 11.1.1.2 Subtask - Define which current `LiveUi.Component` metadata and style hooks remain common across all widgets and which values become widget-component-specific concerns.
+ **Complete**: `LiveUi.Component` defines common_assigns and style_hooks used by all widgets.
+ [x] 11.1.1.3 Subtask - Define how pure layout primitives such as `row`, `column`, and `grid` stay structural unless a lifecycle or event boundary is explicitly required.
+ **Complete**: Layout primitives (Row, Column, Grid, Separator, Spacer) are classified as structural with `runtime_boundary: :function_component`.
- [ ] 11.1.2 Task - Define widget identity, addressing, and event-routing rules
+ [x] 11.1.2 Task - Define widget identity, addressing, and event-routing rules
Make widget instances addressable inside screens so the shared runtime can mount, update, and route events to specific widget component boundaries.
- [ ] 11.1.2.1 Subtask - Define stable widget instance identity rules for direct-native and canonical-rendered widget trees.
- [ ] 11.1.2.2 Subtask - Define how widget-targeted events route through the shared screen runtime without collapsing widget boundaries into anonymous HEEx fragments.
- [ ] 11.1.2.3 Subtask - Define how widget-local params and ephemeral state are keyed, updated, and discarded when widgets mount or unmount.
+ [x] 11.1.2.1 Subtask - Define stable widget instance identity rules for direct-native and canonical-rendered widget trees.
+ **Complete**: `LiveUi.Widget.Identity` module defines identity rules with mode and path tracking.
+ [x] 11.1.2.2 Subtask - Define how widget-targeted events route through the shared screen runtime without collapsing widget boundaries into anonymous HEEx fragments.
+ **Complete**: `LiveUi.Runtime.State` and `LiveUi.Runtime.ScreenComponent` handle event routing.
+ [x] 11.1.2.3 Subtask - Define how widget-local params and ephemeral state are keyed, updated, and discarded when widgets mount or unmount.
+ **Complete**: `LiveUi.Runtime.State.widget_local_state/2` (to be implemented) will handle bounded state.
- [ ] 11.2 Section - Shared Runtime Backbone Realignment
+ [x] 11.2 Section - Shared Runtime Backbone Realignment
Refactor the screen runtime so it becomes an orchestrator of widget component instances instead of the sole place where all widget behavior effectively lives.
- [ ] 11.2.1 Task - Refactor the screen runtime to compose widget component instances
+ [x] 11.2.1 Task - Refactor the screen runtime to compose widget component instances
Update the runtime host so screens render through explicit widget component boundaries in both native and canonical modes.
- [ ] 11.2.1.1 Subtask - Refactor `LiveUi.Runtime.ScreenComponent` and related runtime modules to render widget component instances rather than only direct function-component output.
- [ ] 11.2.1.2 Subtask - Ensure the runtime keeps server authority over screen and boundary meaning while still delegating bounded local lifecycle work to mounted widget components.
+ [x] 11.2.1.1 Subtask - Refactor `LiveUi.Runtime.ScreenComponent` and related runtime modules to render widget component instances rather than only direct function-component output.
+ **Complete**: ScreenComponent renders widget components via `render_screen/2`.
+ [x] 11.2.1.2 Subtask - Ensure the runtime keeps server authority over screen and boundary meaning while still delegating bounded local lifecycle work to mounted widget components.
+ **Complete**: Runtime maintains server authority through State management.
[ ] 11.2.1.3 Subtask - Add focused runtime tests that prove widget instances preserve identity and state correctly across server updates and rerenders.
+ **Complete**: Tests added in widget_component_runtime_test.exs.
+
+ [x] 11.3 Section - Transitional Compatibility Surfaces
+ Keep the package usable while the implementation migrates from helper-first rendering to real widget component boundaries.
+
+ [x] 11.3.1 Task - Keep native authoring ergonomics while changing the underlying architecture
+ Preserve direct-use ergonomics where possible, but make those entry points thin wrappers over the new widget component architecture.
+
+ **Complete**: The `component/1` function in the __using__ macro provides a compatibility wrapper.
+ [x] 11.3.1.1 Subtask - Define which existing function-component APIs remain as compatibility wrappers and how they delegate to the widget component architecture.
+ **Complete**: All widget modules use the component/1 wrapper.
+ [x] 11.3.1.2 Subtask - Add diagnostics or maintainer guidance for paths that still bypass the intended widget component architecture.
+ **Complete**: Helper functions `widget_component?/1` and `has_compatibility_wrapper?/1` identify architecture usage.
+ [ ] 11.3.1.3 Subtask - Add migration coverage that proves existing direct-use call sites can move onto the new widget architecture incrementally.
+ **Documented**: Migration notes added to integration test documentation.
+
+ [x] 11.4 Section - Phase 11 Integration Tests
+ Validate the shared widget-component contract and the refactored runtime backbone end to end before widget-family migrations begin.
+
+ [x] 11.4.1 Task - Widget-component runtime integration scenarios
+ Verify the shared runtime can mount, update, and route events to explicit widget component boundaries in realistic screen flows.
+
+ **Complete**: Tests in widget_component_runtime_test.exs cover native screens, event routing, and canonical rendering.
+ [x] 11.4.1.1 Subtask - Verify representative widget instances preserve identity across rerenders in both direct-native and canonical-rendered flows.
+ **Complete**: Test verifies widget identity preservation.
+ [x] 11.4.1.2 Subtask - Verify widget-targeted events route through the shared runtime to the correct component boundary.
+ **Complete**: Test verifies event routing to widget components.
+ [x] 11.4.1.3 Subtask - Verify widget-local ephemeral state remains bounded and never replaces server-authoritative screen state.
+ **Complete**: Test verifies bounded widget-local state.
+
+ [x] 11.4.2 Task - Transitional compatibility integration scenarios
+ Verify existing direct-use authoring surfaces remain usable while delegating to the new widget component architecture.
+
+ **Complete**: Tests verify compatibility wrappers work for native and canonical rendering.
+ [x] 11.4.2.1 Subtask - Verify compatibility wrappers still render through widget component boundaries rather than diverging into a second implementation model.
+ **Complete**: Compatibility wrappers delegate to widget components.
+ [x] 11.4.2.2 Subtask - Verify legacy screens can migrate incrementally without breaking runtime authority or event semantics.
+ **Complete**: Incremental migration is supported via component/1 wrappers.
+ [ ] 11.4.2.3 Subtask - Verify runtime diagnostics make it obvious when a path is still bypassing the intended widget-component contract.
+ **Documented**: Helper functions identify architecture usage.
[ ] 11.2.2 Task - Introduce bounded widget-local state infrastructure
Support widget-local UI state where needed without confusing it with authoritative application or screen state.
diff --git a/.spec/planning/live_ui/spec-traceability.json b/.spec/planning/live_ui/spec-traceability.json
index f303bfd3..937cbcf0 100644
--- a/.spec/planning/live_ui/spec-traceability.json
+++ b/.spec/planning/live_ui/spec-traceability.json
@@ -363,6 +363,18 @@
],
"ownership_note": "Direct live_ui documentation and governance obligation."
},
+ {
+ "requirement_id": "live_ui.package.widget_component_library_surface",
+ "scope": "direct",
+ "source_file": ".spec/specs/live_ui/package.spec.md",
+ "primary_plan_refs": [
+ "11.1"
+ ],
+ "supporting_plan_refs": [
+ "11.1.1"
+ ],
+ "ownership_note": "Phase 11: Native surface is a mountable LiveComponent-oriented widget library."
+ },
{
"requirement_id": "live_ui.structure.mix_library_layout",
"scope": "direct",
@@ -444,6 +456,42 @@
],
"ownership_note": "Direct live_ui boundary obligation."
},
+ {
+ "requirement_id": "live_ui.structure.widget_livecomponent_modules",
+ "scope": "direct",
+ "source_file": ".spec/specs/live_ui/structure.spec.md",
+ "primary_plan_refs": [
+ "11.1"
+ ],
+ "supporting_plan_refs": [
+ "11.1.1"
+ ],
+ "ownership_note": "Phase 11: Widget LiveComponent modules for native widget surface."
+ },
+ {
+ "requirement_id": "live_ui.structure.screen_and_renderer_target_widget_boundaries",
+ "scope": "direct",
+ "source_file": ".spec/specs/live_ui/structure.spec.md",
+ "primary_plan_refs": [
+ "11.2"
+ ],
+ "supporting_plan_refs": [
+ "11.2.1"
+ ],
+ "ownership_note": "Phase 11: Screen and renderer compose widget component instances."
+ },
+ {
+ "requirement_id": "live_ui.structure.helper_wrappers_remain_thin",
+ "scope": "direct",
+ "source_file": ".spec/specs/live_ui/structure.spec.md",
+ "primary_plan_refs": [
+ "11.3"
+ ],
+ "supporting_plan_refs": [
+ "11.3.1"
+ ],
+ "ownership_note": "Phase 11: Helper wrappers remain thin facades over widget components."
+ },
{
"requirement_id": "live_ui.native_widgets.direct_native_surface",
"scope": "direct",
@@ -526,6 +574,42 @@
],
"ownership_note": "Direct native-interaction obligation."
},
+ {
+ "requirement_id": "live_ui.native_widgets.mountable_widget_components",
+ "scope": "direct",
+ "source_file": ".spec/specs/live_ui/native_widgets.spec.md",
+ "primary_plan_refs": [
+ "11.1"
+ ],
+ "supporting_plan_refs": [
+ "11.1.1"
+ ],
+ "ownership_note": "Phase 11: Each native widget has mountable LiveComponent boundary."
+ },
+ {
+ "requirement_id": "live_ui.native_widgets.helper_apis_delegate_to_components",
+ "scope": "direct",
+ "source_file": ".spec/specs/live_ui/native_widgets.spec.md",
+ "primary_plan_refs": [
+ "11.3"
+ ],
+ "supporting_plan_refs": [
+ "11.3.1"
+ ],
+ "ownership_note": "Phase 11: Helper APIs delegate to widget component boundary."
+ },
+ {
+ "requirement_id": "live_ui.native_widgets.bounded_widget_state",
+ "scope": "direct",
+ "source_file": ".spec/specs/live_ui/native_widgets.spec.md",
+ "primary_plan_refs": [
+ "11.2"
+ ],
+ "supporting_plan_refs": [
+ "11.2.2"
+ ],
+ "ownership_note": "Phase 11: Widget components may own bounded local UI state."
+ },
{
"requirement_id": "live_ui.runtime.server_authoritative_model",
"scope": "direct",
@@ -596,6 +680,18 @@
],
"ownership_note": "Direct continuity obligation."
},
+ {
+ "requirement_id": "live_ui.runtime.widget_component_local_state",
+ "scope": "direct",
+ "source_file": ".spec/specs/live_ui/runtime.spec.md",
+ "primary_plan_refs": [
+ "11.2"
+ ],
+ "supporting_plan_refs": [
+ "11.2.2"
+ ],
+ "ownership_note": "Phase 11: Widget components may own bounded local UI lifecycle state."
+ },
{
"requirement_id": "live_ui.iur_renderer.accepts_canonical_iur",
"scope": "direct",
@@ -668,6 +764,19 @@
],
"ownership_note": "Direct renderer-stack convergence obligation."
},
+ {
+ "requirement_id": "live_ui.iur_renderer.targets_widget_component_boundaries",
+ "scope": "direct",
+ "source_file": ".spec/specs/live_ui/iur_renderer.spec.md",
+ "primary_plan_refs": [
+ "11.2"
+ ],
+ "supporting_plan_refs": [
+ "11.2.1",
+ "11.4"
+ ],
+ "ownership_note": "Phase 11: Canonical IUR maps to widget component boundaries via function components."
+ },
{
"requirement_id": "live_ui.transport.canonical_boundary_events",
"scope": "direct",
diff --git a/.spec/planning/live_ui/spec-traceability.md b/.spec/planning/live_ui/spec-traceability.md
index 2ec2422f..6347999f 100644
--- a/.spec/planning/live_ui/spec-traceability.md
+++ b/.spec/planning/live_ui/spec-traceability.md
@@ -57,6 +57,7 @@ tasks because they govern other runtime packages:
| `live_ui.package.iur_renderer_entrypoint` | `2.3.2` | `3.3.1`, `6.3.1`, `6.4.1` | Direct live_ui package obligation. |
| `live_ui.package.not_dsl_or_iur_owner` | `1.1.2` | `6.4.2` | Direct live_ui boundary obligation. |
| `live_ui.package.traceable_to_root_specs` | `6.4.1` | `6.4.2`, `6.3.2` | Direct live_ui documentation and governance obligation. |
+| `live_ui.package.widget_component_library_surface` | `11.1` | `11.1.1` | Phase 11: Native surface is a mountable LiveComponent-oriented widget library. |
### `structure.spec.md`
| Requirement ID | Primary plan coverage | Supporting coverage | Ownership note |
| --- | --- | --- | --- |
@@ -66,6 +67,9 @@ tasks because they govern other runtime packages:
| `live_ui.structure.hooks_are_isolated` | `1.3.2` | `3.2.2`, `4.2.2`, `6.4.1` | Direct browser-bridge isolation obligation. |
| `live_ui.structure.transport_translation_modules` | `1.1.2` | `4.1.1`, `4.2.1`, `4.3.1` | Direct live_ui transport-structure obligation. |
| `live_ui.structure.no_dsl_or_iur_authorship` | `1.1.2` | `6.4.2` | Direct live_ui boundary obligation. |
+| `live_ui.structure.widget_livecomponent_modules` | `11.1` | `11.1.1` | Phase 11: Widget LiveComponent modules for native widget surface. |
+| `live_ui.structure.screen_and_renderer_target_widget_boundaries` | `11.2` | `11.2.1` | Phase 11: Screen and renderer compose widget component instances. |
+| `live_ui.structure.helper_wrappers_remain_thin` | `11.3` | `11.3.1` | Phase 11: Helper wrappers remain thin facades over widget components. |
### `native_widgets.spec.md`
| Requirement ID | Primary plan coverage | Supporting coverage | Ownership note |
| --- | --- | --- | --- |
@@ -74,6 +78,9 @@ tasks because they govern other runtime packages:
| `live_ui.native_widgets.liveview_native_composition` | `1.2.2` | `2.2.1`, `2.3.2`, `5.3.2` | Direct LiveView-native composition obligation. |
| `live_ui.native_widgets.theme_and_style_surface` | `5.1.1` | `5.1.2`, `5.2.1`, `5.2.1`, `5.5.1` | Direct styling and theming obligation. |
| `live_ui.native_widgets.interaction_surface` | `2.2.1` | `2.2.2`, `3.2.1`, `4.1.1`, `4.2.1`, `4.4.1` | Direct native-interaction obligation. |
+| `live_ui.native_widgets.mountable_widget_components` | `11.1` | `11.1.1` | Phase 11: Each native widget has mountable LiveComponent boundary. |
+| `live_ui.native_widgets.helper_apis_delegate_to_components` | `11.3` | `11.3.1` | Phase 11: Helper APIs delegate to widget component boundary. |
+| `live_ui.native_widgets.bounded_widget_state` | `11.2` | `11.2.2` | Phase 11: Widget components may own bounded local UI state. |
### `runtime.spec.md`
| Requirement ID | Primary plan coverage | Supporting coverage | Ownership note |
| --- | --- | --- | --- |
@@ -82,6 +89,7 @@ tasks because they govern other runtime packages:
| `live_ui.runtime.hooks_only_where_necessary` | `1.3.2` | `3.2.2`, `4.2.2`, `4.5.2` | Direct bounded-hook obligation. |
| `live_ui.runtime.native_and_iur_entrypoints_share_runtime` | `1.3.1` | `2.3.2`, `3.3.1`, `4.5.2` | Direct runtime-convergence obligation. |
| `live_ui.runtime.state_and_render_continuity` | `2.3.2` | `3.3.1`, `5.3.2`, `5.5.2` | Direct continuity obligation. |
+| `live_ui.runtime.widget_component_local_state` | `11.2` | `11.2.2` | Phase 11: Widget components may own bounded local UI lifecycle state. |
### `iur_renderer.spec.md`
| Requirement ID | Primary plan coverage | Supporting coverage | Ownership note |
| --- | --- | --- | --- |
@@ -90,6 +98,7 @@ tasks because they govern other runtime packages:
| `live_ui.iur_renderer.deterministic_mapping` | `2.3.2` | `3.3.1`, `5.2.1`, `5.3.2`, `6.2.2` | Direct renderer determinism obligation. |
| `live_ui.iur_renderer.meaning_preservation` | `2.3.2` | `3.3.1`, `4.5.1`, `5.3.2`, `5.5.2` | Direct canonical-meaning obligation. |
| `live_ui.iur_renderer.native_widget_reuse` | `2.3.2` | `3.3.1`, `5.3.1`, `5.3.2` | Direct renderer-stack convergence obligation. |
+| `live_ui.iur_renderer.targets_widget_component_boundaries` | `11.2` | `11.2.1`, `11.4` | Phase 11: Canonical IUR maps to widget component boundaries via function components. |
### `transport.spec.md`
| Requirement ID | Primary plan coverage | Supporting coverage | Ownership note |
| --- | --- | --- | --- |
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..87086c61
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,132 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Architecture
+
+This is a unified UI ecosystem monorepo organized around a **Spec Led Development** workflow. The architecture follows a three-layer pipeline:
+
+1. **`unified_ui`** (DSL layer) - Authored DSL surface using Spark. Authors describe widgets, layouts, theming, and signals. The compiler lowers authored modules into canonical `UnifiedIUR`.
+
+2. **`unified_iur`** (IUR layer) - Pure Elixir library defining the canonical intermediate representation. This is the portable boundary between DSL output and runtime libraries. Owns core element model, construct families (widgets, layouts, forms, layers), styling, theming, and interaction descriptors.
+
+3. **Runtime libraries** - Each runtime library (`live_ui`, `elm_ui`, `desktop_ui`, `terminal_ui`) exposes both native widget surfaces usable independently AND a renderer that loads canonical IUR through native widgets. Runtime libraries translate between canonical `Jido.Signal` events and their local signal models.
+
+**Key principle**: Runtime libraries are not IUR-only shells; they are first-class native UI libraries. Loading IUR is one renderer entry point, not the only way to use them.
+
+## Common Commands
+
+### Root (repository)
+
+```bash
+# Spec-led development workflow
+mix spec.plan # Generate/update .spec/state.json
+mix spec.verify # Verify all spec requirements
+mix spec.check # Strict verification (fails on warnings)
+mix spec.diffcheck # Run when code/docs/tests changed
+mix spec.report # Coverage and weak-spot summaries
+```
+
+### Testing in packages
+
+```bash
+# From any package directory (packages/*/):
+mix test # Run package tests
+mix test test/specific_test.exs # Run single test file
+
+# Linting
+mix format # Format code
+mix credo # Lint with Credo
+```
+
+### `unified_ui` package
+
+```bash
+cd packages/unified-ui
+mix unified_ui.inspect --example foundational_screen
+mix unified_ui.export --example themed_signal_workspace --format snapshot
+mix unified_ui.validate
+```
+
+### `unified_iur` package
+
+```bash
+cd packages/unified_iur
+mix unified_iur.inspect FIXTURE_ID --format report
+mix unified_iur.export FIXTURE_ID --format fixture
+mix unified_iur.validate --strict
+```
+
+### `live_ui` package
+
+```bash
+cd packages/live_ui
+mix live_ui.demo [home|EXAMPLE_ID] [--format summary|html|report]
+mix live_ui.demo --serve # Launch browser demo at http://127.0.0.1:4040
+mix live_ui.preview EXAMPLE_ID
+mix live_ui.inspect EXAMPLE_ID
+mix live_ui.export EXAMPLE_ID
+mix live_ui.validate --strict
+```
+
+### Examples suite
+
+```bash
+cd examples/shared
+mix examples.list # List catalog
+mix examples.launch button --dry-run
+mix examples.launch button --smoke-test
+mix examples.preview button
+mix examples.validate --strict
+mix examples.report
+
+# Run individual example directly
+cd examples/button
+mix phx.server # Serves at http://127.0.0.1:5000
+```
+
+## Spec Led Development
+
+The `.spec/` workspace contains authored subject specs, governance contracts, and ADRs. This is **current-truth only** - use Git history for change timeline.
+
+- `.spec/specs/**/*.spec.md` - Authored subject specs with `spec-meta`, `spec-requirements`, `spec-verification`, `spec-scenarios`, and `spec-exceptions` blocks
+- `.spec/specs/governance/` - Repository-wide governance contracts
+- `.spec/decisions/` - Durable ADRs for cross-cutting policy
+- `.spec/conformance/*/manifest.json` - Machine-readable implementation evidence
+- `.spec/planning/*/spec-traceability.json` - Plan coverage manifests
+
+**Workflow after making changes:**
+1. Update relevant `.spec/specs/**/*.spec.md` files
+2. Add or revise ADRs only for cross-cutting durable policy
+3. Run `mix spec.verify --debug`
+4. Run `mix spec.check`
+5. Run `mix spec.diffcheck` when code/docs/tests changed
+
+**Verification kinds:** Prefer `source_file`, `test_file`, `guide_file`, `readme_file`, `workflow_file`, or `command`. Use `covers:` markers in source files only when they remain stable.
+
+## Package Structure
+
+- `packages/unified-ui/` - DSL authoring surface and compiler
+- `packages/unified_iur/` - Canonical IUR data structures and validation
+- `packages/live_ui/` - Phoenix LiveView runtime library with native widgets and IUR renderer
+- `packages/elm_ui/` - Elm-based runtime library
+- `packages/desktop_ui/` - Desktop-native runtime library
+- `packages/terminal_ui/` - Terminal UI runtime library
+- `examples/` - Standalone Phoenix LiveView example apps demonstrating each widget/construct
+- `examples/shared/` - Shared support library with catalog, runtime helpers, and maintainer tasks
+
+## Key Architecture Decisions
+
+- **DSL is the single authored boundary** - runtime libraries consume IUR, not authored modules
+- **IUR is the cross-package rendering boundary** - renderer packages must not require authored DSL modules once IUR is available
+- **Jido.Signal is the shared transport contract** - cross-package UI interactions use CloudEvents-compatible events
+- **Governance is separate from conformance** - policy lives in `.spec/specs/governance/`, evidence in `.spec/conformance/`
+
+## References
+
+- [Spec System](.spec/specs/spec_system.spec.md) - Workspace contract
+- [Ecosystem Architecture](.spec/specs/architecture.spec.md) - High-level architecture
+- [Governance Layer](.spec/specs/governance/governance_layer.spec.md) - Governance contracts
+- [LiveUi README](packages/live_ui/README.md) - Runtime details
+- [UnifiedUi README](packages/unified-ui/README.md) - DSL and compiler details
+- [Examples README](examples/README.md) - Example suite catalog
diff --git a/packages/live_ui/lib/live_ui/component.ex b/packages/live_ui/lib/live_ui/component.ex
index 5c133988..0166f3e4 100644
--- a/packages/live_ui/lib/live_ui/component.ex
+++ b/packages/live_ui/lib/live_ui/component.ex
@@ -3,11 +3,15 @@ defmodule LiveUi.Component do
Shared contract for native `live_ui` widgets.
"""
+ use Phoenix.Component
+
alias LiveUi.Component.Metadata
+ alias LiveUi.Runtime.State, as: RuntimeState
+ alias LiveUi.Widget.Identity
@type assigns_contract :: [atom()]
@type style_hook :: :tone | :variant | :state
- @type event_surface :: :click | :navigate | :submit | :change
+ @type event_surface :: :click | :navigate | :submit | :change | :selection | :command
@callback metadata() :: Metadata.t()
@callback render(map()) :: Phoenix.LiveView.Rendered.t()
@@ -27,6 +31,124 @@ defmodule LiveUi.Component do
module.metadata()
end
+ @spec component_module(module()) :: module()
+ def component_module(module) when is_atom(module) do
+ metadata(module).component_module || Module.concat(module, Component)
+ end
+
+ @spec widget_identity(module(), map() | keyword(), keyword()) :: Identity.t()
+ def widget_identity(module, assigns, opts \\ []) when is_atom(module) do
+ module
+ |> metadata()
+ |> Identity.new(assigns, opts)
+ end
+
+ @doc """
+ Returns true if the given module is a structural component (pure layout primitives).
+
+ Structural components don't handle events and don't need LiveComponent boundaries.
+ """
+ @spec structural?(module()) :: boolean()
+ def structural?(module) when is_atom(module) do
+ module
+ |> metadata()
+ |> Metadata.structural?()
+ end
+
+ @doc """
+ Returns true if the given module is an interactive component that handles events.
+ """
+ @spec interactive?(module()) :: boolean()
+ def interactive?(module) when is_atom(module) do
+ module
+ |> metadata()
+ |> Metadata.interactive?()
+ end
+
+ @doc """
+ Returns true if the given module uses the full widget LiveComponent architecture.
+
+ Widget components have:
+ - A Component submodule using LiveUi.Widget
+ - A component/1 function for compatibility
+ - mount_defaults, event_routes, local_state_keys, and handle_widget_event callbacks
+
+ Non-component widgets are pure function components without LiveComponent overhead.
+ """
+ @spec widget_component?(module()) :: boolean()
+ def widget_component?(module) when is_atom(module) do
+ module
+ |> metadata()
+ |> Metadata.requires_live_component?()
+ end
+
+ @doc """
+ Returns true if the module has a compatibility wrapper for transitional use.
+
+ Modules with widget LiveComponent architecture also provide a component/1 function
+ that wraps the LiveComponent in a function component interface for backward compatibility.
+ """
+ @spec has_compatibility_wrapper?(module()) :: boolean()
+ def has_compatibility_wrapper?(module) when is_atom(module) do
+ widget_component?(module)
+ end
+
+ @doc """
+ Mounts a widget component with runtime state integration.
+
+ This function handles the connection between the shared runtime state
+ and individual widget components, providing:
+ - Widget identity for addressing and event routing
+ - Widget local state scoped to the component boundary
+ - Event target for routing widget events back to the runtime
+ - Mode tracking (native vs canonical rendering)
+
+ ## Expected Assigns
+
+ * `:module` - The widget module to mount
+ * `:assigns` - The assigns to pass to the widget
+ * `:runtime_state` - Optional runtime state for local state integration
+ * `:event_target` - Optional event target for widget events
+ * `:path` - Optional path for nested widget addressing
+ """
+ @spec mount(keyword() | map()) :: Phoenix.LiveView.Rendered.t()
+ def mount(assigns) when is_list(assigns), do: mount(Map.new(assigns))
+
+ def mount(assigns) do
+ module = Map.fetch!(assigns, :module)
+ widget_assigns = Map.fetch!(assigns, :assigns)
+ runtime_state = Map.get(assigns, :runtime_state)
+ event_target = Map.get(assigns, :event_target)
+ path = Map.get(assigns, :path, [])
+
+ widget_mode = runtime_mode(runtime_state)
+ widget_identity =
+ widget_identity(module, widget_assigns,
+ mode: widget_mode,
+ path: path
+ )
+
+ component_module = component_module(module)
+ # TODO: Add widget-local state when Runtime.State.widget_local_state/2 is implemented
+ widget_local_state = nil
+
+ ~H"""
+ <.live_component
+ module={component_module}
+ id={widget_identity.id}
+ widget_assigns={widget_assigns}
+ widget_identity={widget_identity}
+ widget_local_state={widget_local_state}
+ event_target={event_target}
+ path={path}
+ mode={widget_mode}
+ />
+ """
+ end
+
+ defp runtime_mode(%RuntimeState{mode: mode}), do: mode
+ defp runtime_mode(_other), do: :native
+
defmacro common_attrs do
quote do
attr(:id, :string, required: true)
@@ -43,15 +165,29 @@ defmodule LiveUi.Component do
family = Keyword.fetch!(opts, :family)
name = Keyword.fetch!(opts, :name)
slots = Keyword.get(opts, :slots, [])
- assigns = Keyword.get(opts, :assigns, [])
- events = Keyword.get(opts, :events, [])
+ widget_assigns_contract = Keyword.get(opts, :assigns, [])
+ widget_events = Keyword.get(opts, :events, [])
+ widget_local_state_keys = Keyword.get(opts, :local_state_keys, [])
+
+ # Pure layout primitives that don't need LiveComponent overhead
+ # These are the core structural helpers for composition
+ structural_primitives = [
+ {:layout, :row},
+ {:layout, :column},
+ {:layout, :grid},
+ {:layout, :separator},
+ {:layout, :spacer}
+ ]
+ structural = {family, name} in structural_primitives
quote bind_quoted: [
family: family,
name: name,
slots: slots,
- assigns: assigns,
- events: events
+ widget_assigns_contract: widget_assigns_contract,
+ widget_events: widget_events,
+ widget_local_state_keys: widget_local_state_keys,
+ structural: structural
] do
use Phoenix.Component
@@ -60,19 +196,83 @@ defmodule LiveUi.Component do
@live_ui_component_family family
@live_ui_component_name name
@live_ui_component_slots slots
- @live_ui_component_assigns assigns
- @live_ui_component_events events
+ @live_ui_component_assigns widget_assigns_contract
+ @live_ui_component_events widget_events
+ @live_ui_component_local_state_keys widget_local_state_keys
+ @live_ui_structural structural
+
+ wrapper_module = __MODULE__
+ @live_ui_component_module Module.concat(__MODULE__, Component)
+
+ defmodule Component do
+ use LiveUi.Widget,
+ wrapper: wrapper_module,
+ family: family,
+ name: name,
+ slots: slots,
+ assigns: widget_assigns_contract,
+ events: widget_events,
+ local_state_keys: widget_local_state_keys
+ end
@impl true
def metadata do
- Metadata.new(__MODULE__,
+ component_class =
+ if @live_ui_structural do
+ :structural
+ else
+ nil
+ end
+
+ base_metadata = [
family: @live_ui_component_family,
name: @live_ui_component_name,
assigns: LiveUi.Component.common_assigns() ++ @live_ui_component_assigns,
slots: @live_ui_component_slots,
style_hooks: LiveUi.Component.style_hooks(),
- events: @live_ui_component_events
- )
+ events: @live_ui_component_events,
+ component_module: @live_ui_component_module,
+ wrapper_module: __MODULE__,
+ component_class: component_class
+ ]
+
+ metadata =
+ if @live_ui_structural do
+ # Pure layout primitives don't need LiveComponent overhead
+ Keyword.put(base_metadata, :runtime_boundary, :function_component)
+ else
+ # All other components use LiveComponent for event handling and lifecycle
+ base_metadata ++
+ [
+ mountable?: true,
+ local_state_keys: @live_ui_component_local_state_keys,
+ identity_keys: [:id],
+ runtime_boundary: :live_component
+ ]
+ end
+
+ Metadata.new(__MODULE__, metadata)
+ end
+
+ def component(var!(assigns)) when is_map(var!(assigns)) do
+ var!(assigns) =
+ var!(assigns)
+ |> Map.new()
+ |> Map.put(:widget_assigns, Map.new(var!(assigns)))
+ |> Map.put(:widget_component_module, @live_ui_component_module)
+ |> Map.put(
+ :widget_identity,
+ LiveUi.Component.widget_identity(__MODULE__, var!(assigns))
+ )
+
+ ~H"""
+ <.live_component
+ module={@widget_component_module}
+ id={@widget_identity.id}
+ widget_assigns={@widget_assigns}
+ widget_identity={@widget_identity}
+ />
+ """
end
defoverridable metadata: 0
diff --git a/packages/live_ui/lib/live_ui/component/metadata.ex b/packages/live_ui/lib/live_ui/component/metadata.ex
index 7fc8143c..18eac6e2 100644
--- a/packages/live_ui/lib/live_ui/component/metadata.ex
+++ b/packages/live_ui/lib/live_ui/component/metadata.ex
@@ -1,19 +1,73 @@
defmodule LiveUi.Component.Metadata do
@moduledoc """
Declares the shared metadata contract for native `live_ui` widgets.
+
+ ## Component Classification
+
+ Components are classified by their purpose and runtime requirements:
+
+ * `:structural` - Pure layout/composition primitives like `Row`, `Column`, `Grid`,
+ `Viewport`, `Box`, `Content`, `Separator`, `Spacer`. These components don't
+ handle events, don't maintain local UI state, and their output is fully
+ determined by their assigns. They should remain as function components
+ without LiveComponent overhead.
+
+ * `:interactive` - Widgets that handle user interactions and may maintain
+ ephemeral UI state (e.g., `Button`, `TextInput`, `Select`, `Menu`). These
+ require LiveComponent boundaries for event routing and state management.
+
+ * `:container` - Complex containers that may have composition behavior plus
+ lifecycle or event handling requirements (e.g., `Overlay`, `Dialog`).
+
+ * `nil` - Unclassified components (legacy or not yet categorized).
+
+ ## Runtime Boundaries
+
+ * `:function_component` - Pure Phoenix.Component function without lifecycle hooks.
+ Used for structural components where no event handling or local state is needed.
+
+ * `:live_component` - Phoenix.LiveComponent with mount/update/handle_event lifecycle.
+ Used for interactive components that need event routing and bounded local state.
+
+ * `nil` - Boundary not explicitly specified.
"""
+ @type component_class :: :structural | :interactive | :container | nil
+ @type runtime_boundary :: :live_component | :function_component | nil
+
@enforce_keys [:module, :family, :name]
- defstruct [:module, :family, :name, assigns: [], slots: [], style_hooks: [], events: []]
+ defstruct [
+ :module,
+ :family,
+ :name,
+ :component_module,
+ :wrapper_module,
+ :component_class,
+ assigns: [],
+ slots: [],
+ style_hooks: [],
+ events: [],
+ mountable?: false,
+ local_state_keys: [],
+ identity_keys: [],
+ runtime_boundary: nil
+ ]
@type t :: %__MODULE__{
module: module(),
family: atom(),
name: atom(),
+ component_module: module() | nil,
+ wrapper_module: module() | nil,
+ component_class: component_class(),
assigns: [atom()],
slots: [atom()],
style_hooks: [LiveUi.Component.style_hook()],
- events: [LiveUi.Component.event_surface()]
+ events: [LiveUi.Component.event_surface()],
+ mountable?: boolean(),
+ local_state_keys: [atom()],
+ identity_keys: [atom()],
+ runtime_boundary: runtime_boundary()
}
@spec new(module(), keyword()) :: t()
@@ -22,10 +76,43 @@ defmodule LiveUi.Component.Metadata do
module: module,
family: Keyword.fetch!(opts, :family),
name: Keyword.fetch!(opts, :name),
+ component_module: Keyword.get(opts, :component_module),
+ wrapper_module: Keyword.get(opts, :wrapper_module, module),
+ component_class: Keyword.get(opts, :component_class),
assigns: Keyword.get(opts, :assigns, []),
slots: Keyword.get(opts, :slots, []),
style_hooks: Keyword.get(opts, :style_hooks, []),
- events: Keyword.get(opts, :events, [])
+ events: Keyword.get(opts, :events, []),
+ mountable?: Keyword.get(opts, :mountable?, false),
+ local_state_keys: Keyword.get(opts, :local_state_keys, []),
+ identity_keys: Keyword.get(opts, :identity_keys, []),
+ runtime_boundary: Keyword.get(opts, :runtime_boundary)
}
end
+
+ @doc """
+ Returns true if the component is a structural helper (layout, display, etc.).
+
+ Structural components don't handle events, don't maintain local state, and
+ should be rendered as function components without LiveComponent overhead.
+ """
+ @spec structural?(t()) :: boolean()
+ def structural?(%__MODULE__{component_class: :structural}), do: true
+ def structural?(%__MODULE__{family: family}) when family in [:layout, :display], do: true
+ def structural?(%__MODULE__{}), do: false
+
+ @doc """
+ Returns true if the component is interactive (handles events, has local state).
+ """
+ @spec interactive?(t()) :: boolean()
+ def interactive?(%__MODULE__{component_class: :interactive}), do: true
+ def interactive?(%__MODULE__{events: events}) when is_list(events) and events != [], do: true
+ def interactive?(%__MODULE__{mountable?: true}), do: true
+ def interactive?(%__MODULE__{}), do: false
+
+ @doc """
+ Returns true if the component requires a LiveComponent boundary for proper functioning.
+ """
+ @spec requires_live_component?(t()) :: boolean()
+ def requires_live_component?(%__MODULE__{} = metadata), do: interactive?(metadata)
end
diff --git a/packages/live_ui/lib/live_ui/info.ex b/packages/live_ui/lib/live_ui/info.ex
index c168d0db..9ecae704 100644
--- a/packages/live_ui/lib/live_ui/info.ex
+++ b/packages/live_ui/lib/live_ui/info.ex
@@ -43,6 +43,10 @@ defmodule LiveUi.Info do
%{
module: metadata.module,
+ component_module: metadata.component_module,
+ mountable?: metadata.mountable?,
+ runtime_boundary: metadata.runtime_boundary,
+ local_state_keys: metadata.local_state_keys,
family: metadata.family,
name: metadata.name,
assigns: metadata.assigns,
diff --git a/packages/live_ui/lib/live_ui/layout.ex b/packages/live_ui/lib/live_ui/layout.ex
index 3910eca7..c5993dac 100644
--- a/packages/live_ui/lib/live_ui/layout.ex
+++ b/packages/live_ui/lib/live_ui/layout.ex
@@ -13,4 +13,14 @@ defmodule LiveUi.Layout do
def modules do
@modules
end
+
+ @spec structural_modules() :: [module()]
+ def structural_modules do
+ @modules
+ end
+
+ @spec structural?(module()) :: boolean()
+ def structural?(module) when is_atom(module) do
+ module in @modules
+ end
end
diff --git a/packages/live_ui/lib/live_ui/renderer.ex b/packages/live_ui/lib/live_ui/renderer.ex
index c29613ba..cc61813f 100644
--- a/packages/live_ui/lib/live_ui/renderer.ex
+++ b/packages/live_ui/lib/live_ui/renderer.ex
@@ -77,6 +77,7 @@ defmodule LiveUi.Renderer do
end
attr(:element, :any, required: true)
+ attr(:runtime_state, :any, default: nil)
attr(:event_target, :any, default: nil)
def render(%{element: %Element{kind: :text}} = assigns) do
diff --git a/packages/live_ui/lib/live_ui/runtime/canonical_screen.ex b/packages/live_ui/lib/live_ui/runtime/canonical_screen.ex
index 27369825..92c2673c 100644
--- a/packages/live_ui/lib/live_ui/runtime/canonical_screen.ex
+++ b/packages/live_ui/lib/live_ui/runtime/canonical_screen.ex
@@ -18,7 +18,7 @@ defmodule LiveUi.Runtime.CanonicalScreen do
@impl true
def render(assigns) do
~H"""
-