@moldable-ai/ui 0.2.20 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/dist/catalog/catalog-coverage.d.ts +15 -0
  2. package/dist/catalog/catalog-coverage.d.ts.map +1 -0
  3. package/dist/catalog/catalog-coverage.js +44 -0
  4. package/dist/catalog/catalog-manifest.d.ts.map +1 -1
  5. package/dist/catalog/catalog-manifest.js +35 -23
  6. package/dist/catalog/catalog-types.d.ts +17 -0
  7. package/dist/catalog/catalog-types.d.ts.map +1 -1
  8. package/dist/catalog/catalog-types.js +7 -0
  9. package/dist/catalog/component-catalog.d.ts.map +1 -1
  10. package/dist/catalog/component-catalog.js +16 -6
  11. package/dist/catalog/core-catalog-exclusions.d.ts +8 -0
  12. package/dist/catalog/core-catalog-exclusions.d.ts.map +1 -0
  13. package/dist/catalog/core-catalog-exclusions.js +155 -0
  14. package/dist/catalog/core-catalog.d.ts +0 -4
  15. package/dist/catalog/core-catalog.d.ts.map +1 -1
  16. package/dist/catalog/core-catalog.js +12 -98
  17. package/dist/catalog/core-collection-stories.d.ts +3 -0
  18. package/dist/catalog/core-collection-stories.d.ts.map +1 -0
  19. package/dist/catalog/core-collection-stories.js +42 -0
  20. package/dist/catalog/core-control-stories.d.ts +3 -0
  21. package/dist/catalog/core-control-stories.d.ts.map +1 -0
  22. package/dist/catalog/core-control-stories.js +97 -0
  23. package/dist/catalog/core-feedback-stories.d.ts +3 -0
  24. package/dist/catalog/core-feedback-stories.d.ts.map +1 -0
  25. package/dist/catalog/core-feedback-stories.js +50 -0
  26. package/dist/catalog/core-foundation-stories.d.ts +3 -0
  27. package/dist/catalog/core-foundation-stories.d.ts.map +1 -0
  28. package/dist/catalog/core-foundation-stories.js +53 -0
  29. package/dist/catalog/core-navigation-stories.d.ts +3 -0
  30. package/dist/catalog/core-navigation-stories.d.ts.map +1 -0
  31. package/dist/catalog/core-navigation-stories.js +25 -0
  32. package/dist/catalog/core-overlay-stories.d.ts +3 -0
  33. package/dist/catalog/core-overlay-stories.d.ts.map +1 -0
  34. package/dist/catalog/core-overlay-stories.js +20 -0
  35. package/dist/catalog/index.d.ts +3 -1
  36. package/dist/catalog/index.d.ts.map +1 -1
  37. package/dist/catalog/index.js +3 -1
  38. package/dist/components/app-error-boundary.d.ts.map +1 -1
  39. package/dist/components/app-error-boundary.js +3 -2
  40. package/dist/components/chat/chat-panel.d.ts +0 -1
  41. package/dist/components/chat/chat-panel.d.ts.map +1 -1
  42. package/dist/components/chat/chat-panel.js +7 -11
  43. package/dist/components/ui/alert-dialog.d.ts +10 -4
  44. package/dist/components/ui/alert-dialog.d.ts.map +1 -1
  45. package/dist/components/ui/alert-dialog.js +14 -6
  46. package/dist/components/ui/alert.d.ts +19 -3
  47. package/dist/components/ui/alert.d.ts.map +1 -1
  48. package/dist/components/ui/alert.js +25 -8
  49. package/dist/components/ui/app-frame.d.ts +9 -3
  50. package/dist/components/ui/app-frame.d.ts.map +1 -1
  51. package/dist/components/ui/app-frame.js +45 -6
  52. package/dist/components/ui/button.d.ts +6 -4
  53. package/dist/components/ui/button.d.ts.map +1 -1
  54. package/dist/components/ui/button.js +40 -8
  55. package/dist/components/ui/collapsible.d.ts +9 -1
  56. package/dist/components/ui/collapsible.d.ts.map +1 -1
  57. package/dist/components/ui/collapsible.js +13 -2
  58. package/dist/components/ui/collection-navigation.d.ts +8 -0
  59. package/dist/components/ui/collection-navigation.d.ts.map +1 -0
  60. package/dist/components/ui/collection-navigation.js +19 -0
  61. package/dist/components/ui/command.d.ts +5 -1
  62. package/dist/components/ui/command.d.ts.map +1 -1
  63. package/dist/components/ui/command.js +13 -1
  64. package/dist/components/ui/confirm-dialog.d.ts +19 -0
  65. package/dist/components/ui/confirm-dialog.d.ts.map +1 -0
  66. package/dist/components/ui/confirm-dialog.js +45 -0
  67. package/dist/components/ui/date-picker.d.ts +24 -0
  68. package/dist/components/ui/date-picker.d.ts.map +1 -0
  69. package/dist/components/ui/date-picker.js +92 -0
  70. package/dist/components/ui/dialog-primary-action.d.ts +4 -0
  71. package/dist/components/ui/dialog-primary-action.d.ts.map +1 -0
  72. package/dist/components/ui/dialog-primary-action.js +37 -0
  73. package/dist/components/ui/dialog.d.ts +8 -2
  74. package/dist/components/ui/dialog.d.ts.map +1 -1
  75. package/dist/components/ui/dialog.js +15 -5
  76. package/dist/components/ui/empty.d.ts +7 -2
  77. package/dist/components/ui/empty.d.ts.map +1 -1
  78. package/dist/components/ui/empty.js +20 -7
  79. package/dist/components/ui/error-boundary-view.d.ts +12 -0
  80. package/dist/components/ui/error-boundary-view.d.ts.map +1 -0
  81. package/dist/components/ui/error-boundary-view.js +8 -0
  82. package/dist/components/ui/grid.d.ts.map +1 -1
  83. package/dist/components/ui/grid.js +70 -46
  84. package/dist/components/ui/icon-button.d.ts.map +1 -1
  85. package/dist/components/ui/icon-button.js +1 -2
  86. package/dist/components/ui/index.d.ts +5 -0
  87. package/dist/components/ui/index.d.ts.map +1 -1
  88. package/dist/components/ui/index.js +5 -0
  89. package/dist/components/ui/input-group.d.ts +1 -1
  90. package/dist/components/ui/list.d.ts +5 -1
  91. package/dist/components/ui/list.d.ts.map +1 -1
  92. package/dist/components/ui/list.js +27 -16
  93. package/dist/components/ui/material.d.ts +34 -0
  94. package/dist/components/ui/material.d.ts.map +1 -0
  95. package/dist/components/ui/material.js +34 -0
  96. package/dist/components/ui/message-scroller-controller.d.ts +162 -0
  97. package/dist/components/ui/message-scroller-controller.d.ts.map +1 -0
  98. package/dist/components/ui/message-scroller-controller.js +888 -0
  99. package/dist/components/ui/message-scroller-policy.d.ts +74 -0
  100. package/dist/components/ui/message-scroller-policy.d.ts.map +1 -0
  101. package/dist/components/ui/message-scroller-policy.js +129 -0
  102. package/dist/components/ui/message-scroller-primitive.d.ts +55 -0
  103. package/dist/components/ui/message-scroller-primitive.d.ts.map +1 -0
  104. package/dist/components/ui/message-scroller-primitive.js +226 -0
  105. package/dist/components/ui/message-scroller.d.ts +2 -2
  106. package/dist/components/ui/message-scroller.d.ts.map +1 -1
  107. package/dist/components/ui/message-scroller.js +3 -15
  108. package/dist/components/ui/navigation-button-group.d.ts +16 -0
  109. package/dist/components/ui/navigation-button-group.d.ts.map +1 -0
  110. package/dist/components/ui/navigation-button-group.js +9 -0
  111. package/dist/components/ui/popover.d.ts +9 -2
  112. package/dist/components/ui/popover.d.ts.map +1 -1
  113. package/dist/components/ui/popover.js +13 -3
  114. package/dist/components/ui/scroll-area.d.ts +8 -2
  115. package/dist/components/ui/scroll-area.d.ts.map +1 -1
  116. package/dist/components/ui/scroll-area.js +6 -3
  117. package/dist/components/ui/search-field.d.ts.map +1 -1
  118. package/dist/components/ui/segmented-control.d.ts.map +1 -1
  119. package/dist/components/ui/segmented-control.js +6 -5
  120. package/dist/components/ui/sheet.d.ts +4 -2
  121. package/dist/components/ui/sheet.d.ts.map +1 -1
  122. package/dist/components/ui/sheet.js +12 -9
  123. package/dist/components/ui/sonner.d.ts +1 -1
  124. package/dist/components/ui/sonner.d.ts.map +1 -1
  125. package/dist/components/ui/spinner.d.ts.map +1 -1
  126. package/dist/components/ui/spinner.js +1 -1
  127. package/dist/components/ui/table.d.ts.map +1 -1
  128. package/dist/components/ui/table.js +7 -2
  129. package/dist/components/ui/toggle.d.ts +12 -4
  130. package/dist/components/ui/toggle.d.ts.map +1 -1
  131. package/dist/components/ui/toggle.js +13 -2
  132. package/dist/components/ui/toolbar.d.ts +9 -2
  133. package/dist/components/ui/toolbar.d.ts.map +1 -1
  134. package/dist/components/ui/toolbar.js +23 -4
  135. package/dist/components/ui/tooltip.d.ts +8 -3
  136. package/dist/components/ui/tooltip.d.ts.map +1 -1
  137. package/dist/components/ui/tooltip.js +11 -6
  138. package/dist/index.d.ts +6 -1
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +7 -1
  141. package/dist/lib/frame-lifecycle.d.ts +23 -0
  142. package/dist/lib/frame-lifecycle.d.ts.map +1 -1
  143. package/dist/lib/frame-lifecycle.js +30 -3
  144. package/dist/lib/host-date-time-picker.d.ts +36 -0
  145. package/dist/lib/host-date-time-picker.d.ts.map +1 -0
  146. package/dist/lib/host-date-time-picker.js +162 -0
  147. package/dist/lib/host-file-dialog.d.ts +66 -0
  148. package/dist/lib/host-file-dialog.d.ts.map +1 -0
  149. package/dist/lib/host-file-dialog.js +221 -0
  150. package/dist/lib/host-native-menu.d.ts +75 -0
  151. package/dist/lib/host-native-menu.d.ts.map +1 -0
  152. package/dist/lib/host-native-menu.js +187 -0
  153. package/dist/lib/host-services.d.ts +13 -0
  154. package/dist/lib/host-services.d.ts.map +1 -0
  155. package/dist/lib/host-services.js +161 -0
  156. package/dist/lib/host-share.d.ts +39 -0
  157. package/dist/lib/host-share.d.ts.map +1 -0
  158. package/dist/lib/host-share.js +161 -0
  159. package/dist/lib/theme.d.ts.map +1 -1
  160. package/dist/lib/theme.js +2 -1
  161. package/docs/README.md +4 -1
  162. package/docs/adaptive-materials.md +79 -0
  163. package/docs/app-anatomy.md +3 -1
  164. package/docs/component-catalog.md +26 -3
  165. package/docs/design-tokens.md +17 -4
  166. package/docs/foundations.md +5 -3
  167. package/docs/standalone-app-windows.md +8 -0
  168. package/docs/visual-regression.md +78 -0
  169. package/package.json +12 -4
  170. package/src/components/ui/alert-dialog.md +58 -0
  171. package/src/components/ui/alert.md +28 -6
  172. package/src/components/ui/app-frame.md +5 -0
  173. package/src/components/ui/button.md +10 -3
  174. package/src/components/ui/collapsible.md +52 -0
  175. package/src/components/ui/combobox.md +0 -1
  176. package/src/components/ui/command.md +54 -0
  177. package/src/components/ui/date-field.md +1 -1
  178. package/src/components/ui/date-picker.md +55 -0
  179. package/src/components/ui/dialog.md +59 -0
  180. package/src/components/ui/empty.md +8 -3
  181. package/src/components/ui/error-boundary-view.md +98 -0
  182. package/src/components/ui/grid.md +8 -4
  183. package/src/components/ui/icon-button.md +2 -2
  184. package/src/components/ui/list.md +35 -2
  185. package/src/components/ui/material.md +51 -0
  186. package/src/components/ui/navigation-button-group.md +46 -0
  187. package/src/components/ui/popover.md +31 -0
  188. package/src/components/ui/scroll-area.md +11 -7
  189. package/src/components/ui/search-field.md +1 -1
  190. package/src/components/ui/segmented-control.md +2 -5
  191. package/src/components/ui/sheet.md +30 -0
  192. package/src/components/ui/table.md +4 -2
  193. package/src/components/ui/toggle-button.md +48 -0
  194. package/src/components/ui/toolbar.md +5 -0
  195. package/src/components/ui/tooltip.md +31 -0
  196. package/src/styles/index.css +213 -0
@@ -0,0 +1,79 @@
1
+ # Adaptive materials
2
+
3
+ Moldable uses restrained translucent material to separate controls and
4
+ navigation from the content they operate on. Material frames content; it does
5
+ not replace content surfaces.
6
+
7
+ ## Roles
8
+
9
+ Use `regular` material for normal toolbar and window chrome, grouped controls,
10
+ menus, and temporary control overlays. It provides enough frosting for text
11
+ and icons to remain stable as content moves behind it.
12
+
13
+ Use `clear` material only for compact controls floating over photos, maps,
14
+ video, or another richly colored canvas. Check the control against the
15
+ lightest, darkest, and busiest content it can cover. If contrast is not stable,
16
+ use `regular`.
17
+
18
+ Documents, calendars, tables, forms, cards, sheets, inspectors, and primary
19
+ working canvases remain opaque. Do not stack translucent content cards to
20
+ create hierarchy.
21
+
22
+ ## Components
23
+
24
+ `Material` applies one adaptive surface:
25
+
26
+ ```tsx
27
+ <Material asChild variant="clear" shape="circle" interactive>
28
+ <IconButton label="Center map" variant="ghost">
29
+ <Locate />
30
+ </IconButton>
31
+ </Material>
32
+ ```
33
+
34
+ `MaterialGroup` applies one sampled layer to a related cluster:
35
+
36
+ ```tsx
37
+ <MaterialGroup variant="regular" shape="capsule" spacing="compact">
38
+ <IconButton label="Previous" variant="ghost">
39
+ <ChevronLeft />
40
+ </IconButton>
41
+ <IconButton label="Next" variant="ghost">
42
+ <ChevronRight />
43
+ </IconButton>
44
+ </MaterialGroup>
45
+ ```
46
+
47
+ Children of a `MaterialGroup` should be ordinary transparent or ghost
48
+ controls. Do not apply another `Material` to every child. A single grouped
49
+ sampler produces clearer hierarchy and avoids accumulated blur.
50
+
51
+ `Toolbar`, `AppFrameTitlebar`, and `AppFrameStatusbar` expose a `material`
52
+ property. Their normal chrome uses `regular`; pass `material="none"` for an
53
+ opaque or transparent composition that supplies its own surface.
54
+
55
+ ## Fallback behavior
56
+
57
+ Every material starts from an opaque fallback. Translucent sampling is enabled
58
+ only when the renderer supports backdrop filtering.
59
+
60
+ - Reduced transparency uses opaque fills and removes blur and saturation.
61
+ - Increased contrast makes fills more opaque and strengthens borders.
62
+ - Forced colors use system surfaces and borders without blur or shadows.
63
+ - Reduced motion removes material press transforms.
64
+
65
+ The manual `data-transparency="reduced"`, `data-contrast="increased"`, and
66
+ `data-motion="reduced"` attributes mirror system preferences for deterministic
67
+ catalog and app testing.
68
+
69
+ Do not animate blur, saturation, or backdrop filters. They are expensive,
70
+ visually noisy, and difficult to make accessible. Limit the number of visible
71
+ sampled layers and avoid nesting them.
72
+
73
+ ## Host boundary
74
+
75
+ CSS material samples content inside the app renderer. It cannot sample the
76
+ desktop behind a native window. Native behind-window material, window
77
+ transparency, active-window appearance, and energy policy remain host-owned
78
+ capabilities. Apps should use the same semantic variants regardless of which
79
+ adapter renders them.
@@ -9,7 +9,9 @@ canvas independent of the desktop shell.
9
9
  ```tsx
10
10
  <AppFrame>
11
11
  <AppFrameTitlebar>
12
- <AppFrameToolbar>{/* title, view controls, primary actions */}</AppFrameToolbar>
12
+ <AppFrameToolbar>
13
+ {/* title, view controls, primary actions */}
14
+ </AppFrameToolbar>
13
15
  </AppFrameTitlebar>
14
16
  <AppFrameContent scrollable={false}>
15
17
  {/* app-owned canvas and panes */}
@@ -20,6 +20,7 @@ const manifest = createCatalogManifest({
20
20
  {
21
21
  id: 'primary-action',
22
22
  title: 'Primary action',
23
+ components: ['button'],
23
24
  interactions: ['rest', 'focus-visible', 'disabled', 'loading'],
24
25
  render: ({ interaction }) => (
25
26
  <Button
@@ -45,6 +46,8 @@ Every public interactive component should cover:
45
46
  - light and dark themes;
46
47
  - regular and compact density;
47
48
  - wide and narrow viewports;
49
+ - standard accessibility behavior plus reduced transparency, increased
50
+ contrast, and reduced motion where material or motion is part of the recipe;
48
51
  - rest, focus-visible, disabled, invalid, selected, loading, and empty states
49
52
  where those states apply;
50
53
  - keyboard behavior in unit tests.
@@ -54,6 +57,26 @@ lets a visual-regression runner capture one addressable case at a time. Keep
54
57
  catalog stories deterministic: fixed content, fixed dates, no network calls,
55
58
  and no animation unless motion itself is under review.
56
59
 
57
- The included `coreComponentCatalog` is a smoke foundation, not the complete
58
- acceptance bar. Add each new public component and its meaningful states as part
59
- of the same change that adds the component.
60
+ Accessibility environments are opt-in per story through
61
+ `accessibilityModes`. Standard cases preserve the existing case ID; a
62
+ nonstandard mode is appended as the final segment. The catalog maps those
63
+ modes to deterministic `data-transparency`, `data-contrast`, and `data-motion`
64
+ attributes.
65
+
66
+ ## Public component accountability
67
+
68
+ Every story declares the public visual component modules it exercises through
69
+ `components`. The core catalog test reads the barrel exports from
70
+ `components/ui/index.ts` and requires every module to be either:
71
+
72
+ - named by at least one deterministic story; or
73
+ - listed in `coreCatalogExclusions` with a concise reason.
74
+
75
+ Exclusions are visible debt, not permanent exemptions. Remove an exclusion in
76
+ the same change that adds representative stories. Unknown component ids, stale
77
+ exclusions, duplicate exclusions, and empty reasons fail the catalog test.
78
+
79
+ The core catalog covers window materials, semantic controls, collections,
80
+ navigation, representative popover material, and feedback recipes. Complex
81
+ portals and geometry-dependent components remain explicitly deferred until an
82
+ isolated browser fixture can make their states deterministic.
@@ -119,15 +119,28 @@ related objects visually consistent. `rounded-window` is host-informed and
119
119
  normally only useful in previews; the native host owns the actual window
120
120
  shape.
121
121
 
122
- Material tokens keep depth and translucency restrained:
122
+ Adaptive material has two semantic variants. Each includes fill, fallback,
123
+ border, highlight, blur, and saturation tokens:
123
124
 
124
- - `--material-blur-toolbar` and `--material-blur-overlay`
125
- - `--material-saturation`
126
- - `shadow-panel`, `shadow-overlay`, and `shadow-window`
125
+ - `--material-regular-*` supports normal toolbar and control chrome.
126
+ - `--material-clear-*` supports compact controls over media or a richly
127
+ colored canvas.
128
+ - `shadow-panel`, `shadow-overlay`, and `shadow-window` provide elevation
129
+ independently from translucency.
130
+
131
+ Use `Material` and `MaterialGroup` instead of assembling those tokens in app
132
+ markup. `MaterialGroup` owns one sampled layer for a related control cluster;
133
+ its children must not add nested material.
127
134
 
128
135
  Translucency must have an opaque fallback. The token system removes blur and
129
136
  uses opaque toolbar and overlay surfaces when the operating system requests
130
137
  reduced transparency or an ancestor has `data-transparency="reduced"`.
138
+ Unsupported renderers stay on the same opaque fallback. Increased contrast
139
+ makes fills more opaque and strengthens their borders.
140
+
141
+ Material is a control and navigation affordance. Primary canvases, documents,
142
+ tables, calendars, forms, cards, sheets, and inspectors remain opaque. See
143
+ [Adaptive materials](./adaptive-materials.md) for composition rules.
131
144
 
132
145
  ## Motion
133
146
 
@@ -37,9 +37,11 @@ Status color communicates status only; it is not decoration.
37
37
  Use semantic tokens only. Do not introduce raw gray, zinc, white, or black
38
38
  classes in app UI.
39
39
 
40
- Use material tokens for translucency and elevation. Materials distinguish
41
- canvas, toolbar, sidebar, raised, recessed, and overlay roles; they are not
42
- decorative glass effects.
40
+ Use adaptive material only for navigation and control layers. Regular material
41
+ supports toolbars and grouped controls; clear material is reserved for compact
42
+ controls over media. Content canvases and content-bearing surfaces stay
43
+ opaque. Group related controls under one sampler and never nest material
44
+ effects.
43
45
 
44
46
  ## Motion
45
47
 
@@ -41,6 +41,14 @@ the document root. Composite content insets then resolve automatically. The
41
41
  transport used to deliver those values belongs to the host integration layer,
42
42
  not individual apps.
43
43
 
44
+ Dedicated windows use the host titlebar by default. An app with a unified
45
+ toolbar can declare `{ "window": { "titlebar": "app" } }` in `moldable.json`.
46
+ In that mode the host removes its strip and advertises the titlebar height and
47
+ leading window-control inset. `AppFrameTitlebar` consumes those values, sends
48
+ `startWindowDrag` on a non-interactive pointerdown, and sends
49
+ `titlebarDoubleClick` on a non-interactive double-click. The latter toggles
50
+ maximize as a cross-platform approximation of the macOS titlebar zoom action.
51
+
44
52
  Window appearance roles are `--window-chrome-background`,
45
53
  `--window-chrome-foreground`, `--window-chrome-muted-foreground`,
46
54
  `--window-border`, and `--material-shadow-window`. The host may update the
@@ -0,0 +1,78 @@
1
+ # Visual and accessibility regression
2
+
3
+ The phase 1 runner exercises every authored story in the core component
4
+ catalog. It does not start the desktop shell or any Moldable-managed app.
5
+
6
+ ## What runs
7
+
8
+ `visual-host/` is a small Vite entry that renders one story at a time:
9
+
10
+ ```text
11
+ ?story=controls/button&theme=dark&width=360
12
+ ```
13
+
14
+ Unique bare story ids are also accepted. Optional
15
+ `transparency=reduced` and `contrast=increased` parameters set the same
16
+ manual accessibility attributes used by the catalog. The host bundles Inter
17
+ and Roboto Mono, waits for `document.fonts.ready`, and disables animations,
18
+ transitions, scrolling, and the caret under `data-motion="reduced"`.
19
+
20
+ The Playwright runner builds and serves that static host, blocks non-host
21
+ network requests, freezes time and randomness, and tests Chromium at:
22
+
23
+ - light and dark themes;
24
+ - 360 × 720 and 960 × 720 viewports;
25
+ - rest plus the browser-driven states declared by an exemplar story.
26
+
27
+ A story opts into browser-driven states with `liveStates` and marks exactly
28
+ one element:
29
+
30
+ ```tsx
31
+ <Button data-state-target="hover focus pressed">Continue</Button>
32
+ ```
33
+
34
+ Playwright performs real hover, focus, and held-pointer interactions on that
35
+ element. Button, segmented control, and toggle stories are the phase 1
36
+ exemplars.
37
+
38
+ Every captured case also receives an axe WCAG 2 A/AA and WCAG 2.1 A/AA scan.
39
+ Serious and critical violations fail. Per-story suppressions belong in
40
+ `visual-tests/visual-matrix.ts` and must include a reason. Global
41
+ suppressions are intentionally unsupported.
42
+
43
+ ## Local commands
44
+
45
+ Install the pinned Chromium build once:
46
+
47
+ ```bash
48
+ pnpm -C packages/ui exec playwright install chromium
49
+ ```
50
+
51
+ Run against committed baselines:
52
+
53
+ ```bash
54
+ pnpm -C packages/ui test:visual
55
+ ```
56
+
57
+ Create or intentionally update baselines after reviewing the UI change:
58
+
59
+ ```bash
60
+ pnpm -C packages/ui test:visual:update
61
+ ```
62
+
63
+ Baseline PNGs live in `visual-tests/baselines/`. Failures emit actual,
64
+ expected, diff, and trace artifacts in the gitignored `.visual-results/`
65
+ directory.
66
+
67
+ ## Determinism and skips
68
+
69
+ Stories must use fixed inputs and no live data, network, wall-clock timers, or
70
+ unseeded randomness. Add an unavoidable phase 1 skip to
71
+ `deterministicStorySkips` in `visual-tests/visual-matrix.ts` with a specific
72
+ reason. Skips are reported in the runner summary; silent conditional skips
73
+ are not allowed.
74
+
75
+ ## CI
76
+
77
+ The `ui-regression` CI job installs Chromium and its Linux dependencies,
78
+ builds only `@moldable-ai/ui`, then runs the combined visual and axe suite.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moldable-ai/ui",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "Shared UI components for Moldable applications",
5
5
  "author": "Desiderata LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -42,11 +42,15 @@
42
42
  ],
43
43
  "scripts": {
44
44
  "build": "tsc",
45
+ "build:visual-host": "vite build --config vite.visual.config.ts",
45
46
  "dev": "tsc --watch",
46
47
  "lint": "eslint . --ext .ts,.tsx --max-warnings 0",
47
48
  "format": "prettier --write . --ignore-path ../../.prettierignore",
48
- "check-types": "tsc --noEmit",
49
- "test": "vitest run"
49
+ "check-types": "tsc --noEmit && tsc --noEmit -p tsconfig.visual.json",
50
+ "test": "vitest run",
51
+ "test:visual": "pnpm build:visual-host && playwright test --config playwright.visual.config.ts",
52
+ "test:visual:update": "pnpm build:visual-host && playwright test --config playwright.visual.config.ts --update-snapshots",
53
+ "visual:serve": "vite preview --config vite.visual.config.ts --host 127.0.0.1 --port 4179 --strictPort"
50
54
  },
51
55
  "dependencies": {
52
56
  "@base-ui/react": "^1.1.0",
@@ -80,7 +84,6 @@
80
84
  "@radix-ui/react-toggle": "^1.1.10",
81
85
  "@radix-ui/react-toggle-group": "^1.1.11",
82
86
  "@radix-ui/react-tooltip": "^1.2.8",
83
- "@shadcn/react": "^0.2.1",
84
87
  "@tailwindcss/typography": "^0.5.16",
85
88
  "class-variance-authority": "^0.7.1",
86
89
  "clsx": "^2.1.1",
@@ -106,14 +109,19 @@
106
109
  "zod": "^4.3.5"
107
110
  },
108
111
  "devDependencies": {
112
+ "@axe-core/playwright": "^4.12.1",
113
+ "@fontsource/inter": "^5.3.0",
114
+ "@fontsource/roboto-mono": "^5.3.0",
109
115
  "@moldable-ai/eslint-config": "workspace:*",
110
116
  "@moldable-ai/prettier-config": "workspace:*",
111
117
  "@moldable-ai/typescript-config": "workspace:*",
118
+ "@playwright/test": "^1.62.0",
112
119
  "@types/node": "^24.0.3",
113
120
  "@types/react": "^19.1.6",
114
121
  "@types/react-dom": "^19.1.6",
115
122
  "jsdom": "^26.1.0",
116
123
  "typescript": "^5.8.3",
124
+ "vite": "^6.4.3",
117
125
  "vitest": "^4.1.8"
118
126
  },
119
127
  "peerDependencies": {
@@ -0,0 +1,58 @@
1
+ # Alert Dialog and Confirm Dialog
2
+
3
+ Use `AlertDialog` when the user must acknowledge a consequential choice. Use the
4
+ composed anatomy for custom content and `ConfirmDialog` for the common
5
+ promise-aware confirmation path.
6
+
7
+ ```tsx
8
+ <ConfirmDialog
9
+ trigger={<Button variant="destructive">Delete recording</Button>}
10
+ title="Delete recording?"
11
+ description="This removes the local audio and transcript."
12
+ confirmLabel="Delete"
13
+ pendingLabel="Deleting…"
14
+ variant="destructive"
15
+ onConfirm={() => deleteRecording()}
16
+ />
17
+ ```
18
+
19
+ `ConfirmDialog` prevents duplicate submission, disables cancellation while the
20
+ operation is pending, closes only after success, and keeps failures in the
21
+ dialog. Pass `getErrorMessage` when a safe, actionable domain error can be
22
+ shown. The default error deliberately avoids exposing internal details.
23
+
24
+ `AlertDialogContent` supports `sm`, `default`, and `lg` and stays within the
25
+ host safe area. Use `AlertDialogBody` when explanation or affected-item content
26
+ can grow. Keep the consequence visible while the action is pending.
27
+
28
+ For composed alert dialogs, put `loading` on `AlertDialogAction` while its
29
+ request is in flight. This forwards the standard `Button` busy state, blocks
30
+ duplicate activation, and preserves the action label width and accessible name.
31
+ Use `pending` with `pendingLabel` when maintaining the confirmation-specific
32
+ status-label pattern; `ConfirmDialog` manages that state automatically. Disable
33
+ the surrounding form only when its other controls must not change during the
34
+ request.
35
+
36
+ Mark one confirming action with `primary` to enable Command+Enter on macOS or
37
+ Control+Enter on other platforms. The shortcut does not run without a marked
38
+ action, while that action is loading or disabled, during key composition or
39
+ repeat, or when a focused control consumes the event with `preventDefault()`.
40
+ Plain Enter remains native and Escape remains the Radix cancel behavior.
41
+
42
+ ```tsx
43
+ <AlertDialogFooter>
44
+ <span className="text-muted-foreground mr-auto inline-flex items-center gap-1 text-xs">
45
+ Confirm <Kbd>⌘</Kbd>
46
+ <Kbd>Enter</Kbd>
47
+ </span>
48
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
49
+ <AlertDialogAction primary loading={isDeleting}>
50
+ Delete recording
51
+ </AlertDialogAction>
52
+ </AlertDialogFooter>
53
+ ```
54
+
55
+ Use concrete labels such as “Delete recording,” not “OK.” Put the safe,
56
+ reversible action first in keyboard order and the confirming action last. Do
57
+ not use an alert dialog for ordinary navigation, long forms, or success
58
+ messages.
@@ -2,7 +2,9 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- `Alert` presents an important inline message that belongs to the current surface. Use it for recoverable errors, permission or connection problems, and information the user should notice before continuing.
5
+ `Alert` presents a persistent inline callout that belongs to the current
6
+ surface. Use it for recoverable errors, permission or connection problems, and
7
+ information the user should notice before continuing.
6
8
 
7
9
  Use `AlertDialog` when the user must confirm an action. Use `Empty` for the normal absence of data. Use a toast for brief feedback that does not need to remain in the layout.
8
10
 
@@ -12,23 +14,42 @@ Use `AlertDialog` when the user must confirm an action. Use `Empty` for the norm
12
14
  import { CircleAlert } from 'lucide-react'
13
15
  import { Alert, AlertDescription, AlertTitle } from '@moldable-ai/ui'
14
16
 
15
- ;<Alert variant="destructive">
17
+ ;<Alert tone="danger">
16
18
  <CircleAlert />
17
19
  <AlertTitle>Could not save</AlertTitle>
18
20
  <AlertDescription>
19
21
  The file changed on disk. Reload it, then try again.
20
22
  </AlertDescription>
23
+ <AlertActions>
24
+ <Button size="sm">Reload</Button>
25
+ </AlertActions>
21
26
  </Alert>
22
27
  ```
23
28
 
24
- An alert can include a compact retry or setup action in its description, but the message should still stand on its own. Place the action close to the cause rather than at the top of the whole app.
29
+ Dismissible informational callouts may use `AlertDismiss`. Dismissal state is
30
+ owned by the caller:
31
+
32
+ ```tsx
33
+ <Alert tone="info">
34
+ <InfoIcon />
35
+ <AlertTitle>Local-only data</AlertTitle>
36
+ <AlertDescription>This workspace has not been synced.</AlertDescription>
37
+ <AlertDismiss label="Dismiss local-only notice" onClick={dismiss} />
38
+ </Alert>
39
+ ```
25
40
 
26
41
  ## API summary
27
42
 
28
- - `Alert`: a `div` with `role="alert"`.
29
- - `variant`: `default` or `destructive`; defaults to `default`.
43
+ - `tone`: `neutral`, `info`, `success`, `warning`, or `danger`.
44
+ - `density`: `compact` or `default`.
45
+ - Neutral through warning tones default to `role="status"`; danger defaults to
46
+ `role="alert"`. The native `role` prop can override that choice.
47
+ - `variant="default"` and `variant="destructive"` remain compatibility aliases.
30
48
  - `AlertTitle`: the short, emphasized heading.
31
49
  - `AlertDescription`: muted supporting content; supports nested paragraphs and controls.
50
+ - `AlertActions`: a compact action row.
51
+ - `AlertDismiss`: an icon action with a required accessible `label`; the caller
52
+ owns dismissal.
32
53
  - All parts accept the native props for their rendered element and a `className`.
33
54
  - A direct icon child on `Alert` receives the component’s grid placement and sizing.
34
55
 
@@ -44,9 +65,10 @@ An alert can include a compact retry or setup action in its description, but the
44
65
 
45
66
  - Do not use an alert as decorative introductory copy.
46
67
  - Do not show stack traces, raw exceptions, or secret values.
47
- - Do not use `destructive` for neutral notices.
68
+ - Do not use `danger` for neutral notices.
48
69
  - Do not rely on the icon or color alone for meaning.
49
70
  - Do not use `Alert` as a confirmation surface.
71
+ - Do not use an alert for brief success feedback that should be a toast.
50
72
 
51
73
  ## Moldable considerations
52
74
 
@@ -21,6 +21,9 @@ surface.
21
21
  - Let the host render real window controls. Never draw imitation traffic lights.
22
22
  - Make the app's primary canvas the visual focus; titlebars and status bars are
23
23
  supporting structure.
24
+ - Titlebars and status bars use `material="regular"` by default. Use
25
+ `material="clear"` only when compact chrome overlays a rich canvas, or
26
+ `material="none"` when the composition supplies an opaque surface.
24
27
  - The app frame does not make the outer window draggable. A decorated native
25
28
  titlebar or host-owned drag region must provide that behavior.
26
29
  - The host owns the `--window-*` inset variables. The component only consumes
@@ -31,3 +34,5 @@ surface.
31
34
  tests. Runtime apps should normally use `mode="auto"`; a future host context
32
35
  may set `data-moldable-display-mode` before render when a window still uses an
33
36
  app iframe.
37
+ - Material samples only renderer content. Native behind-window material remains
38
+ host-owned.
@@ -25,9 +25,15 @@ import { RefreshCw } from 'lucide-react'
25
25
  >
26
26
  <RefreshCw />
27
27
  </Button>
28
+
29
+ <Button type="submit" loading={isSaving}>
30
+ Save changes
31
+ </Button>
28
32
  ```
29
33
 
30
- Keep toolbar actions compact (`xs`, `sm`, `icon-xs`, or `icon-sm`). Reserve `default` or `lg` for the primary action in a form, setup state, or focused control dock. While an async action is running, disable the button, preserve its width, and show a small spinner beside a short status label.
34
+ Keep toolbar actions compact (`xs`, `sm`, `icon-xs`, or `icon-sm`). Reserve `default` or `lg` for the primary action in a form, setup state, or focused control dock. Set `loading` while that button's async action is in flight. The button blocks pointer and keyboard activation, exposes `aria-busy`, keeps its accessible label mounted, and centers a width-stable spinner.
35
+
36
+ Use `loading` for the action that started the request. Disable the whole form only when editing during the request would be unsafe or would invalidate the submitted payload; do not disable unrelated controls merely to communicate progress.
31
37
 
32
38
  For links that should look like buttons:
33
39
 
@@ -42,13 +48,14 @@ For links that should look like buttons:
42
48
  - `variant`: `default`, `destructive`, `outline`, `secondary`, `ghost`, or `link`. Defaults to `default`.
43
49
  - `size`: `default`, `xs`, `sm`, `lg`, `icon`, `icon-xs`, `icon-sm`, or `icon-lg`. Defaults to `default`.
44
50
  - `asChild`: renders a Radix `Slot` so the child owns the element and semantics.
51
+ - `loading`: blocks activation, applies disabled behavior and `aria-busy`, and overlays the shared `Spinner` without unmounting or resizing the label. Defaults to `false`.
45
52
  - Accepts native button props, including `type`, `disabled`, `aria-*`, and event handlers.
46
53
  - `buttonVariants` is exported for cases that need the same visual treatment on another component.
47
54
 
48
55
  ## Do
49
56
 
50
57
  - Set `type="button"` unless the control intentionally submits a form.
51
- - Add `cursor-pointer`; use the real `disabled` attribute for unavailable or busy actions.
58
+ - Add `cursor-pointer`; use the real `disabled` attribute for unavailable actions and `loading` for an in-flight action.
52
59
  - Give icon-only buttons an `aria-label` and a tooltip.
53
60
  - Use a specific Lucide icon that names the action.
54
61
  - Put one visually primary action in a local action group.
@@ -60,7 +67,7 @@ For links that should look like buttons:
60
67
  - Do not rely on color alone to explain a destructive or selected action.
61
68
  - Do not use raw color classes, decorative gradients, or oversized call-to-action styling.
62
69
  - Do not use `Sparkles` as a generic AI marker.
63
- - Do not leave a busy button clickable or allow repeated submission.
70
+ - Do not use `disabled` alone as the progress signal; use `loading` so progress is announced and the label width stays stable.
64
71
  - Do not hide essential actions behind hover-only icon buttons.
65
72
 
66
73
  ## Moldable considerations
@@ -0,0 +1,52 @@
1
+ # Collapsible
2
+
3
+ `Collapsible` exposes the unstyled Radix disclosure primitive.
4
+ `CollapsibleSection` adds a consistent section trigger, state-aware chevron,
5
+ border, and content spacing.
6
+
7
+ ```tsx
8
+ <CollapsibleSection defaultOpen>
9
+ <CollapsibleSectionTrigger>Advanced options</CollapsibleSectionTrigger>
10
+ <CollapsibleSectionContent>
11
+ Controls that are useful less often.
12
+ </CollapsibleSectionContent>
13
+ </CollapsibleSection>
14
+ ```
15
+
16
+ ## When to use
17
+
18
+ Use a collapsible section for one independently disclosed region. Use
19
+ `Accordion` when several peer sections need coordinated one-at-a-time or
20
+ multi-section behavior.
21
+
22
+ ## API
23
+
24
+ The base `Collapsible`, `CollapsibleTrigger`, and `CollapsibleContent` expose
25
+ the Radix props directly.
26
+
27
+ The section helpers add:
28
+
29
+ - `CollapsibleSection`: bordered section root.
30
+ - `CollapsibleSectionTrigger`: full-width trigger with a chevron driven by the
31
+ root `data-state`.
32
+ - `CollapsibleSectionContent`: standard inset content.
33
+ - `motion`: `default` or `none`.
34
+ - `contentClassName`: styles the inset inner content without changing the
35
+ animated viewport.
36
+
37
+ ## Keyboard and accessibility
38
+
39
+ The trigger is a button with `aria-expanded` and `aria-controls` managed by
40
+ Radix. Space and Enter toggle it. The chevron is decorative.
41
+
42
+ ## Motion
43
+
44
+ Motion is brief and uses the shared reduced-motion behavior. Set
45
+ `motion="none"` for content where movement would distract or when a parent owns
46
+ the transition.
47
+
48
+ ## Don't
49
+
50
+ - Do not put another interactive control inside the trigger.
51
+ - Do not hide required validation errors or primary actions by default.
52
+ - Do not use a disclosure solely to make a sparse layout look busier.
@@ -40,4 +40,3 @@ state, clear/trigger controls, and the chips family for multi-value input.
40
40
  - Label the input visibly or with an accessible name.
41
41
  - Do not deep-import the implementation; `Combobox` is exported from the
42
42
  package root.
43
-
@@ -0,0 +1,54 @@
1
+ # Command
2
+
3
+ `Command` renders searchable, keyboard-driven actions and local choices.
4
+
5
+ ## When to use
6
+
7
+ Use `CommandDialog` for an app-local command palette or picker with enough
8
+ options to benefit from search. Use the item anatomy helpers when a result has
9
+ supporting text or metadata.
10
+
11
+ ```tsx
12
+ <CommandItem value="Open project">
13
+ <CommandItemIcon>
14
+ <FolderIcon />
15
+ </CommandItemIcon>
16
+ <CommandItemContent>
17
+ <CommandItemTitle>Open project</CommandItemTitle>
18
+ <CommandItemDescription>Choose a local folder</CommandItemDescription>
19
+ </CommandItemContent>
20
+ <CommandShortcut>⌘O</CommandShortcut>
21
+ </CommandItem>
22
+ ```
23
+
24
+ ## Anatomy
25
+
26
+ - `Command`, `CommandInput`, and `CommandList` own filtering and navigation.
27
+ - `CommandGroup`, `CommandEmpty`, and `CommandSeparator` organize results.
28
+ - `CommandItem` is the interactive cmdk option.
29
+ - `CommandItemIcon`, `CommandItemContent`, `CommandItemTitle`, and
30
+ `CommandItemDescription` provide consistent row alignment.
31
+ - `CommandShortcut` displays a trailing shortcut hint; it does not install the
32
+ shortcut.
33
+
34
+ Use `NotificationDot`, `Status`, or `Spinner` as a trailing child when a result
35
+ needs an indicator. Keep those states semantic instead of adding decoration to
36
+ the row anatomy.
37
+
38
+ ## Keyboard and accessibility
39
+
40
+ cmdk owns arrow-key navigation, selection, and filtering. Give icon-only
41
+ information an accessible text equivalent. `CommandItemIcon` is decorative by
42
+ default because the adjacent title should name the command.
43
+
44
+ ## Platform behavior
45
+
46
+ This is a portable renderer component. An app-local palette does not replace
47
+ the host command surface. Register host-visible commands through the Moldable
48
+ command API and use this component only for commands owned by the current app.
49
+
50
+ ## Don't
51
+
52
+ - Do not create a second interactive row inside `CommandItem`.
53
+ - Do not install keyboard shortcuts from `CommandShortcut`.
54
+ - Do not use a command palette for a small fixed set that fits in a menu.
@@ -9,7 +9,7 @@
9
9
  ```tsx
10
10
  import { DateField } from '@moldable-ai/ui'
11
11
 
12
- <DateField
12
+ ;<DateField
13
13
  aria-label="Due date"
14
14
  value={dueDate}
15
15
  min="2026-01-01"