@evinvest/uikit 0.29.4 → 0.31.0

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 (204) hide show
  1. package/README.md +294 -90
  2. package/dist/{chunk-3IPB4QQJ.js → chunk-35SPIU4U.js} +10 -6
  3. package/dist/chunk-35SPIU4U.js.map +1 -0
  4. package/dist/components/account-list.js +128 -0
  5. package/dist/components/account-list.js.map +1 -0
  6. package/dist/components/account-menu-profile.js +114 -0
  7. package/dist/components/account-menu-profile.js.map +1 -0
  8. package/dist/components/account-menu.js +50 -23
  9. package/dist/components/account-menu.js.map +1 -1
  10. package/dist/components/account-settings-dialog.js +84 -0
  11. package/dist/components/account-settings-dialog.js.map +1 -0
  12. package/dist/components/alert-dialog.js +3 -2
  13. package/dist/components/alert-dialog.js.map +1 -1
  14. package/dist/components/animated-number-columns.js +105 -0
  15. package/dist/components/animated-number-columns.js.map +1 -0
  16. package/dist/components/animated-number.js +157 -0
  17. package/dist/components/animated-number.js.map +1 -0
  18. package/dist/components/app-shell.js +9 -0
  19. package/dist/components/app-shell.js.map +1 -1
  20. package/dist/components/aside.js +140 -0
  21. package/dist/components/aside.js.map +1 -0
  22. package/dist/components/badge.js +1 -1
  23. package/dist/components/badge.js.map +1 -1
  24. package/dist/components/button.js +3 -11
  25. package/dist/components/button.js.map +1 -1
  26. package/dist/components/chat-composer.js +141 -0
  27. package/dist/components/chat-composer.js.map +1 -0
  28. package/dist/components/chat-message.js +195 -0
  29. package/dist/components/chat-message.js.map +1 -0
  30. package/dist/components/chat-panel.js +137 -0
  31. package/dist/components/chat-panel.js.map +1 -0
  32. package/dist/components/chat-stream-text.js +69 -0
  33. package/dist/components/chat-stream-text.js.map +1 -0
  34. package/dist/components/chat-typing.js +26 -0
  35. package/dist/components/chat-typing.js.map +1 -0
  36. package/dist/components/command-palette.js +119 -0
  37. package/dist/components/command-palette.js.map +1 -0
  38. package/dist/components/command.js +3 -1
  39. package/dist/components/command.js.map +1 -1
  40. package/dist/components/confirm-dialog.js +123 -0
  41. package/dist/components/confirm-dialog.js.map +1 -0
  42. package/dist/components/context-menu.js +53 -5
  43. package/dist/components/context-menu.js.map +1 -1
  44. package/dist/components/dialog.js +3 -2
  45. package/dist/components/dialog.js.map +1 -1
  46. package/dist/components/drawer.js +3 -1
  47. package/dist/components/drawer.js.map +1 -1
  48. package/dist/components/dropdown-menu.js +64 -5
  49. package/dist/components/dropdown-menu.js.map +1 -1
  50. package/dist/components/form-row.js +51 -0
  51. package/dist/components/form-row.js.map +1 -0
  52. package/dist/components/header-controls.js +149 -0
  53. package/dist/components/header-controls.js.map +1 -0
  54. package/dist/components/icon-button.js +35 -0
  55. package/dist/components/icon-button.js.map +1 -0
  56. package/dist/components/key-value.js +68 -0
  57. package/dist/components/key-value.js.map +1 -0
  58. package/dist/components/menubar.js +39 -5
  59. package/dist/components/menubar.js.map +1 -1
  60. package/dist/components/native-select.js +3 -4
  61. package/dist/components/native-select.js.map +1 -1
  62. package/dist/components/nav-collapse.js +142 -0
  63. package/dist/components/nav-collapse.js.map +1 -0
  64. package/dist/components/nav-model.js.map +1 -1
  65. package/dist/components/navigation-menu.js +4 -5
  66. package/dist/components/navigation-menu.js.map +1 -1
  67. package/dist/components/page-frame.js +2 -5
  68. package/dist/components/page-frame.js.map +1 -1
  69. package/dist/components/popover-form.js +60 -0
  70. package/dist/components/popover-form.js.map +1 -0
  71. package/dist/components/resource-error.js +1 -1
  72. package/dist/components/resource-error.js.map +1 -1
  73. package/dist/components/section-nav.js +73 -20
  74. package/dist/components/section-nav.js.map +1 -1
  75. package/dist/components/select.js +16 -6
  76. package/dist/components/select.js.map +1 -1
  77. package/dist/components/settings.js +94 -0
  78. package/dist/components/settings.js.map +1 -0
  79. package/dist/components/sheet.js +3 -2
  80. package/dist/components/sheet.js.map +1 -1
  81. package/dist/components/shell-nav.js +80 -38
  82. package/dist/components/shell-nav.js.map +1 -1
  83. package/dist/components/smart-header.js +53 -0
  84. package/dist/components/smart-header.js.map +1 -0
  85. package/dist/components/status-item.js +42 -0
  86. package/dist/components/status-item.js.map +1 -0
  87. package/dist/components/system-banner.js +22 -12
  88. package/dist/components/system-banner.js.map +1 -1
  89. package/dist/components/top-bar.js +4 -3
  90. package/dist/components/top-bar.js.map +1 -1
  91. package/dist/components/ui-icons.js +77 -0
  92. package/dist/components/ui-icons.js.map +1 -0
  93. package/dist/components/use-count-tween.js +42 -0
  94. package/dist/components/use-count-tween.js.map +1 -0
  95. package/dist/components/workspace-tab.js +140 -0
  96. package/dist/components/workspace-tab.js.map +1 -0
  97. package/dist/generated/accent.js +2 -2
  98. package/dist/generated/accent.js.map +1 -1
  99. package/dist/generated/account-list.js +35 -0
  100. package/dist/generated/account-list.js.map +1 -0
  101. package/dist/generated/account-settings-dialog.js +27 -0
  102. package/dist/generated/account-settings-dialog.js.map +1 -0
  103. package/dist/generated/alert-dialog.js +1 -1
  104. package/dist/generated/alert-dialog.js.map +1 -1
  105. package/dist/generated/alert.js +2 -2
  106. package/dist/generated/alert.js.map +1 -1
  107. package/dist/generated/aside.js +36 -0
  108. package/dist/generated/aside.js.map +1 -0
  109. package/dist/generated/badge.js +7 -6
  110. package/dist/generated/badge.js.map +1 -1
  111. package/dist/generated/band.js +1 -1
  112. package/dist/generated/band.js.map +1 -1
  113. package/dist/generated/breadcrumb.js +2 -2
  114. package/dist/generated/breadcrumb.js.map +1 -1
  115. package/dist/generated/button-group.js +2 -2
  116. package/dist/generated/button-group.js.map +1 -1
  117. package/dist/generated/button.js +4 -4
  118. package/dist/generated/button.js.map +1 -1
  119. package/dist/generated/card.js +6 -6
  120. package/dist/generated/card.js.map +1 -1
  121. package/dist/generated/chat-composer.js +21 -0
  122. package/dist/generated/chat-composer.js.map +1 -0
  123. package/dist/generated/chat-message.js +86 -0
  124. package/dist/generated/chat-message.js.map +1 -0
  125. package/dist/generated/chat-typing.js +9 -0
  126. package/dist/generated/chat-typing.js.map +1 -0
  127. package/dist/generated/command-palette.js +17 -0
  128. package/dist/generated/command-palette.js.map +1 -0
  129. package/dist/generated/command.js +3 -3
  130. package/dist/generated/command.js.map +1 -1
  131. package/dist/generated/confirm-dialog.js +28 -0
  132. package/dist/generated/confirm-dialog.js.map +1 -0
  133. package/dist/generated/context-menu.js +1 -1
  134. package/dist/generated/context-menu.js.map +1 -1
  135. package/dist/generated/date-time-picker.js +1 -1
  136. package/dist/generated/date-time-picker.js.map +1 -1
  137. package/dist/generated/dialog.js +1 -1
  138. package/dist/generated/dialog.js.map +1 -1
  139. package/dist/generated/drawer.js +1 -1
  140. package/dist/generated/drawer.js.map +1 -1
  141. package/dist/generated/dropdown-menu.js +1 -1
  142. package/dist/generated/dropdown-menu.js.map +1 -1
  143. package/dist/generated/form-row.js +17 -0
  144. package/dist/generated/form-row.js.map +1 -0
  145. package/dist/generated/header-controls.js +56 -0
  146. package/dist/generated/header-controls.js.map +1 -0
  147. package/dist/generated/icon-button.js +13 -0
  148. package/dist/generated/icon-button.js.map +1 -0
  149. package/dist/generated/key-value.js +15 -0
  150. package/dist/generated/key-value.js.map +1 -0
  151. package/dist/generated/menubar.js +1 -1
  152. package/dist/generated/menubar.js.map +1 -1
  153. package/dist/generated/navigation-menu.js +1 -1
  154. package/dist/generated/navigation-menu.js.map +1 -1
  155. package/dist/generated/popover-form.js +13 -0
  156. package/dist/generated/popover-form.js.map +1 -0
  157. package/dist/generated/section-label.js +10 -0
  158. package/dist/generated/section-label.js.map +1 -0
  159. package/dist/generated/select.js +15 -3
  160. package/dist/generated/select.js.map +1 -1
  161. package/dist/generated/settings.js +36 -0
  162. package/dist/generated/settings.js.map +1 -0
  163. package/dist/generated/sheet.js +1 -1
  164. package/dist/generated/sheet.js.map +1 -1
  165. package/dist/generated/slider.js +1 -1
  166. package/dist/generated/slider.js.map +1 -1
  167. package/dist/generated/smart-header.js +11 -0
  168. package/dist/generated/smart-header.js.map +1 -0
  169. package/dist/generated/sonner.js +4 -4
  170. package/dist/generated/sonner.js.map +1 -1
  171. package/dist/generated/status-item.js +17 -0
  172. package/dist/generated/status-item.js.map +1 -0
  173. package/dist/generated/system-banner.js +33 -0
  174. package/dist/generated/system-banner.js.map +1 -0
  175. package/dist/generated/top-bar.js +9 -0
  176. package/dist/generated/top-bar.js.map +1 -0
  177. package/dist/generated/workspace-tab.js +17 -0
  178. package/dist/generated/workspace-tab.js.map +1 -0
  179. package/dist/index.d.ts +852 -156
  180. package/dist/index.js +21 -1
  181. package/dist/lib/cn.js +8 -1
  182. package/dist/lib/cn.js.map +1 -1
  183. package/dist/lib/number-parts.js +90 -0
  184. package/dist/lib/number-parts.js.map +1 -0
  185. package/dist/palette-cli.js +1 -1
  186. package/dist/palette.d.ts +6 -4
  187. package/dist/palette.js +3 -1
  188. package/dist/primitives/dismissable-layer.js +76 -4
  189. package/dist/primitives/dismissable-layer.js.map +1 -1
  190. package/dist/primitives/focus-scope.js +20 -5
  191. package/dist/primitives/focus-scope.js.map +1 -1
  192. package/dist/primitives/use-floating.js +16 -1
  193. package/dist/primitives/use-floating.js.map +1 -1
  194. package/dist/primitives/use-reduced-motion.js +20 -0
  195. package/dist/primitives/use-reduced-motion.js.map +1 -0
  196. package/package.json +1 -1
  197. package/styles/ev.css +281 -49
  198. package/styles/theme.css +335 -39
  199. package/styles/tokens.css +643 -103
  200. package/dist/chunk-3IPB4QQJ.js.map +0 -1
  201. package/dist/components/terminal.js +0 -206
  202. package/dist/components/terminal.js.map +0 -1
  203. package/dist/generated/terminal.js +0 -70
  204. package/dist/generated/terminal.js.map +0 -1
package/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # @evinvest/uikit
2
2
 
3
3
  A **dep-light** React UI kit with shadcn-style semantics — the TypeScript mirror
4
- of the `uikit` feature of the [`ev_lib`](https://github.com/EV-invest/lib) Rust crate
5
- (`ev_lib::uikit`, Dioxus). Both ports are driven from the same canonical class
6
- strings and the same design tokens, so a component looks and behaves the same
7
- whether it is rendered by React or by Dioxus.
4
+ of the UI kit of the [`ev_lib`](https://github.com/EV-invest/lib) Rust crate,
5
+ which ships it for Dioxus (`ev_lib::uikit_dioxus`) and Leptos
6
+ (`ev_lib::uikit_leptos`) over one framework-free core. All three are driven from
7
+ the same canonical class strings and the same design tokens, so a component
8
+ looks and behaves the same whether it is rendered by React, Dioxus or Leptos.
8
9
 
9
10
  "Dep-light" means: **no `@radix-ui/*`, no `class-variance-authority`, no
10
11
  `lucide-react`, no charting / carousel / date-picker / OTP / toast libraries.**
@@ -59,14 +60,23 @@ your Tailwind v4 entrypoint — this is the load-bearing part of the kit:
59
60
 
60
61
  | group | names |
61
62
  |---|---|
62
- | scope | `:root` · `[data-brand="…"]` — a theme root; `.light` · `.dark` — a polarity beneath it |
63
- | surfaces | `background` `card` `popover` `muted` `hover` |
64
- | ink | `ink` `ink-mid` `ink-soft` — hierarchy, loudest first |
65
- | lines | `border` `input` `ring` |
66
- | roles | `brand` `primary` `secondary` `positive` `accent-trace` `accent-debug` `accent-info` `accent-warn` `accent-error`, each with `on-*` where it gets filled; `primary-ink` — the primary role as ink on a surface (`#128377` fill under a white `on-primary`, `#2a9d8f` ink); `accent-error-ink` — the error role as text on its own tint (derived, a palette may pin it) |
67
- | scalars | `radius` `control-radius` `control-py` `display-scale` `band-py` `page-max` `page-px` `shell-rail-w` `shell-tab-bar-h` `shadow-*` `font-*` |
63
+ | scope | `:root` · `[data-brand="…"]` — a theme root; `.light` · `.dark` — a polarity beneath it; `[data-theme="oldschool"]` — EV's navy look |
64
+ | surfaces | `background` (page) · `card` (surface) · `secondary` (raised) · `popover` (overlay) · `muted` (inset, input fill) |
65
+ | states | `hover` · `active` · `selected` — translucent tints; `scrim` — behind a modal |
66
+ | ink | `ink` · `ink-mid` · `ink-soft` — hierarchy, loudest first; `ink-faint` — disabled only |
67
+ | lines | `border-subtle` (row rule) · `border` (hairline, 14 %) · `border-strong` (state and overlay edges) · `input` (control edge, ≥ 3:1) · `ring` |
68
+ | roles | `brand` `primary` `secondary` `positive` `accent-trace` `accent-debug` `accent-info` `accent-warn` `accent-error`, each with `on-*` where it gets filled; `primary-ink` — the primary role as ink on a surface; `primary-hover` · `primary-border` — the primary button's states |
69
+ | status inks | `positive-ink` · `accent-info-ink` — a quiet status role as text on a surface (derived, a palette may pin them); warn and error text wears `accent-warn` · `accent-error` itself, held to 3:1 |
70
+ | faces | `button-danger-*` (`bg` `hover` `border` `ink`) — the destructive `Button`; `chip-{neutral,primary,positive,info,warn,error}-{bg,border}` — the `Badge`; `button-primary-*` — no kit component paints it; derived for a brand, pinned per theme by EV |
71
+ | type | `text-caption` 11/14 · `body-s` 12/16 · `body` 13/18 · `body-l` 14/20 · `h3` 14/20 · `h2` 24/30 · `h1` 32/38 · `figure-m` 20/24 · `figure-l` 28/32 (size and line height each) |
72
+ | scalars | `radius` (panel, 6 px) · `control-radius` (4 px) · `chip-radius` (4 px) · `depth-card` `depth-elevated` `depth-overlay` · `control-py` `display-scale` `band-py` `page-max` `page-px` `shell-rail-w` `shell-tab-bar-h` · `font-*` |
68
73
  | motion | `ev-ease-out` `ev-ease-in-out` `ev-dur-fast` `ev-dur-base` `ev-dur-slow` `ev-rise` `ev-stagger` `ev-stagger-section` |
69
- | charts | `chart-1` … `chart-5` |
74
+ | charts | `chart-1` … `chart-6` |
75
+
76
+ A palette owes **29 names**: the colours that are not derived, which is every
77
+ role and surface above plus `chart-1` … `chart-6`. The states, lines, inks,
78
+ status inks, faces and chips are derived from them with formulas, so a brand
79
+ file that pins none of them still renders.
70
80
 
71
81
  Custom properties inherit, so a scope class on a `<section>` re-themes its
72
82
  subtree — `bg-card text-ink` is correct on both polarities with no prop. That is
@@ -80,14 +90,38 @@ polarity — so `on-{role}` exists exactly there.
80
90
 
81
91
  A fill and an ink are different jobs, and `primary` is the one role that
82
92
  names them separately: `primary` is the fill (`bg-primary text-on-primary` on
83
- a button, a checked switch or checkbox — `#128377`, white reads on it at
84
- 4.63:1 and it clears 3:1 against `background`, `secondary`, `card` and
85
- `popover`) and `primary-ink` is the same hue as ink (`text-primary-ink` on a
86
- link or eyebrow, a radio dot, a slider range, `border-primary-ink` on a checked
87
- outline, the focus `ring` — `#2a9d8f`, 5.85:1 on `background`, 4.72:1 on
88
- `card`). Every other role holds its own label and reads as ink with one value.
89
- A consumer sheet must define `--primary-ink`; see
90
- [`docs/spec/accents.md`](../../docs/spec/accents.md).
93
+ a primary button, a checked switch or checkbox — `#138377`, white reads on it at
94
+ 4.62:1 and it clears 3:1 against every plane) and `primary-ink` is the same hue
95
+ as ink (`text-primary-ink` on a link or eyebrow, a radio dot, a slider range,
96
+ the focus `ring` — `#35a395` in Dark, `#116f65` in Light). The status roles
97
+ follow the same rule with their `-ink` values. A consumer sheet must define
98
+ `--primary-ink`; see [`docs/spec/accents.md`](../../docs/spec/accents.md).
99
+
100
+ ### Themes
101
+
102
+ EV ships three, each a zero-specificity rule that pins every EV token:
103
+
104
+ | theme | how to switch | look |
105
+ |---|---|---|
106
+ | **Dark** (default) | nothing: no class, no attribute | near-black neutrals, warm ink, one teal accent, flat |
107
+ | **Light** | `class="light"` on `<html>` or on any wrapper | warm paper planes, same shapes |
108
+ | **Oldschool** | `data-theme="oldschool"` on `<html>` or on any wrapper | the navy look: radius 8 / control 6 / pill chips, tinted chips, real shadows |
109
+
110
+ Dark and Light are flat: depth is surface steps and hairlines, and the
111
+ `--depth-*` tiers behind `shadow-card`, `shadow-elevated` and `shadow-overlay`
112
+ are a zero shadow. Oldschool gives them back.
113
+
114
+ Scoping works the way the polarity classes always did. A `.light` or
115
+ `[data-theme="oldschool"]` wrapper inside a Dark page takes its own full token
116
+ set; a `.light` or `.dark` inside an Oldschool region also takes Oldschool.
117
+ EV's rules do not match inside a `[data-brand]` scope or on its element, so a
118
+ brand's own `.light` and `.dark` never pick EV's values up. Keep `data-theme`
119
+ and `data-brand` on separate elements. For a side-by-side view give each scoped
120
+ wrapper its own `PortalProvider` container so overlays stay inside the scope.
121
+
122
+ The planes sit only 1.04–1.14:1 apart on purpose, so what separates them is the
123
+ `--border` hairline (measured 1.37–1.46:1 on Dark, 1.35:1 on Light). Do not lift
124
+ a plane to chase the ratio; see [`docs/spec/accents.md`](../../docs/spec/accents.md).
91
125
 
92
126
  Accents are decorative and **ordered by significance**, quiet to loud. Pick a
93
127
  rung by how loud the thing should be, never by what it means; see
@@ -105,29 +139,29 @@ It has two halves, each also shipped as a flat sheet of its own —
105
139
  `styles/theme.css` (the contract) and `styles/ev.css` (EV's palette):
106
140
 
107
141
  - **the contract** — the `@theme inline` mapping, the geometry, and the tokens
108
- *derived* from a palette (`hover`, `ink-mid`, `ink-soft`, `border`, `input`
109
- from `ink`; `ring` from `primary-ink`; `accent-error-ink` from `accent-error`
110
- and `ink`). The derived ones are redeclared on
111
- `:root`, `[data-brand]`, `.light` and `.dark` at zero specificity, so a brand
112
- scope gets its own borders, hover and focus ring rather than inheriting the
113
- root's, and a palette may still pin any of them outright. The geometry
114
- (`radius`, `page-px`, `band-py`, …) and its responsive scale-ups restart on
115
- every `[data-brand]` the same way.
116
- - **EV's palette** — a single dark palette on `:where(:root, [data-brand="ev"])`.
117
- It binds neither polarity class, so a `.dark` section inside another brand
118
- never picks EV's values up, and `<Section polarity="light">` under EV is a
119
- no-op (EV is single-polarity).
142
+ *derived* from a palette (`hover` `active` `selected` `border-*` `ink-mid`
143
+ `ink-soft` `ink-faint` `input` from `ink`; `scrim`; `ring` from `primary-ink`;
144
+ `primary-hover` `primary-border`; the status inks; the button faces and
145
+ chips). The derived ones are redeclared on `:root`, `[data-brand]`, `.light`,
146
+ `.dark` and `[data-theme]` at zero specificity, so a brand scope gets its own
147
+ borders, hover and focus ring rather than inheriting the root's, and a
148
+ palette may still pin any of them outright. The geometry (`radius`,
149
+ `page-px`, `band-py`, …) and its responsive scale-ups restart on every
150
+ `[data-brand]` the same way.
151
+ - **EV's palette** — Dark on `:root`, `[data-brand="ev"]` and `.dark`; Light on
152
+ `.light`; Oldschool on `[data-theme="oldschool"]`. None of them matches inside
153
+ another brand's scope.
120
154
 
121
155
  ### Brand palettes
122
156
 
123
- A second brand is a palette against the same names, scoped to
157
+ A second brand is a palette against the same names (29 of them, `chart-6` among them), scoped to
124
158
  `[data-brand="<slug>"]`; light is its default and `.light` / `.dark` beneath it
125
159
  pick a polarity, so a `<Section polarity="dark">` inside the brand takes the
126
160
  brand's dark values. The package ships a generator for it, reading the
127
161
  `brand.toml` shape (`[colors.light]` and `[colors.dark]`, keyed by token name;
128
162
  optional top-level `mark` and `mark-aspect` become `--brand-mark` and
129
163
  `--brand-aspect`; the family names in `[fonts]` — `sans` (or aquafix's `text`),
130
- `display`, `mono` — become the [font parameters](#fonts); other keys and tables
164
+ `serif`, `mono` — become the [font parameters](#fonts); other keys and tables
131
165
  are ignored). Each polarity block also declares `--scheme: light | dark`, which
132
166
  the platform-drawn parts of the kit read (a `NativeSelect`'s popup):
133
167
 
@@ -192,13 +226,16 @@ under a brand under another brand may match either.
192
226
 
193
227
  ### Fonts
194
228
 
195
- The families are parameters, one per role; unset, each is EV's:
229
+ The families are parameters, one per role, and not part of the 29 colour names:
230
+ a palette is complete without them, and the generator only carries what a
231
+ brand file's `[fonts]` declares. Unset, each is EV's, and Oldschool carries the
232
+ families the navy look shipped with:
196
233
 
197
- | parameter | utilities | default (EV) |
198
- |---|---|---|
199
- | `--brand-font-sans` | `font-sans`, the page's base family | `--font-inter`, else `"Inter"` |
200
- | `--brand-font-display` | `font-display`, `font-serif` | `--font-playfair`, else `"Playfair Display"` |
201
- | `--brand-font-mono` | `font-mono` | `--font-inter`, else `"Inter"` |
234
+ | parameter | utilities | default (EV) | Oldschool |
235
+ |---|---|---|---|
236
+ | `--brand-font-sans` | `font-sans`, the page's base family | `--font-inter`, else `"Inter"` | same |
237
+ | `--brand-font-serif` | `font-serif` | `--font-inter-tight`, else `"Inter Tight"` | `--font-playfair`, else `"Playfair Display"` |
238
+ | `--brand-font-mono` | `font-mono` | `--font-jetbrains-mono`, else `"JetBrains Mono"` | `--font-inter`, else `"Inter"` |
202
239
 
203
240
  Declare them on the brand scope (the palette generator writes them from
204
241
  `[fonts]`) and load the files yourself — `next/font`, or your own `@font-face`;
@@ -206,10 +243,13 @@ the kit ships no font files:
206
243
 
207
244
  ```css
208
245
  [data-brand="aquafix"] {
209
- --brand-font-display: "Archivo";
246
+ --brand-font-serif: "Archivo";
210
247
  }
211
248
  ```
212
249
 
250
+ The serif role is the second face, for headings, figures, wordmarks, italic
251
+ accents and the avatar initial; it need not have serifs.
252
+
213
253
  The utilities resolve the chain on the element, so a scope re-fonts everything
214
254
  under it that carries one. The page's base family resolves once, on `<html>`: a
215
255
  brand scoped lower than `<html>` puts `font-sans` on its own element so plain
@@ -305,12 +345,12 @@ through hydration. Three parts of the kit make that hold:
305
345
  ```
306
346
 
307
347
  **Focus.** A control that paints a fill — `Button` `primary` / `secondary` /
308
- `destructive` or any accent fill, a filled `Badge`, `Switch`, `Checkbox` —
309
- shows focus as the solid `ring` standing 2px off it, so both edges of the
310
- indicator are against the surface: the ring reads on every surface at ≥ 3:1
311
- and so does the fill, on every palette and polarity `test/tokens.test.ts`
348
+ `destructive` or any accent fill, a `Badge` link, `Switch`, `Checkbox`,
349
+ `Toggle` — shows focus as the solid `ring` standing 2px off it, so both edges of
350
+ the indicator are against the surface: the ring reads on every surface at
351
+ ≥ 3:1 and so does the fill, on every palette and polarity `test/tokens.test.ts`
312
352
  measures. Controls that sit on the surface (`outline`, `ghost`, inputs) keep
313
- the `ring` border plus its halo.
353
+ the `ring` border plus its halo; list rows draw an inset ring.
314
354
 
315
355
  ## App shell
316
356
 
@@ -318,7 +358,8 @@ The frame a signed-in app sits in — a rail beside the page on a wide screen, a
318
358
  tab bar under it on a phone, one page inset and title scale, and the motion
319
359
  between screens. It knows nothing about routes, roles or data: the nav is a
320
360
  list you build, visibility is a list you filter, and the router comes in as two
321
- props. TS-only (see [Limitations](#limitations)).
361
+ props. TS-only (see [Limitations](#limitations)), except `TopBar`,
362
+ `SystemBanner` and `SectionLabel`, which have Dioxus twins.
322
363
 
323
364
  ```tsx
324
365
  // app/(app)/layout.tsx — the layout stays a Server Component; the nav is a client leaf
@@ -393,6 +434,15 @@ export function Nav() {
393
434
  middle click does not. One marker per group slides between rows on a CSS
394
435
  `transform` transition — it is placed by measurement, so until hydration the
395
436
  marked row carries the fill itself.
437
+ *Collapsible sections* are opt-in: set `collapsible` on a group (or on
438
+ `ShellNav` for every labelled group; a group's own value wins) and its label
439
+ becomes a header with a chevron. `defaultOpen`, controlled `open` /
440
+ `onOpenChange`, and `href` (the label links to a landing page, only the
441
+ chevron toggles) go on the group; the group holding the current route opens
442
+ when the route moves into it. Height runs on a `0fr → 1fr` grid track with a
443
+ 180 ms fade and chevron turn (`--ev-dur-fast`, `--ev-ease-in-out`), none under
444
+ reduced motion; closed rows are `inert`. `labels` also takes `expand` /
445
+ `collapse` (the chevron reads "Expand Guides"). `SectionNav` takes the same.
396
446
  - **`BottomTabBar`** — `items` (five fit a phone), the same router props,
397
447
  callbacks and optimistic mark, `labels` `{ nav, badge(n) }`. One rule slides by
398
448
  whole tab widths and fades on a route no tab claims; a numeric `badge` sits
@@ -415,8 +465,8 @@ export function Nav() {
415
465
  onValueChange?, linkComponent?, labels: { nav } }` — a page's own section
416
466
  rail (settings panes); items are buttons, or links when they have an `href`.
417
467
  - **`ResourceError`** `{ message, variant: "inline" (onRetry?, retrying?) |
418
- "alert" (title?), labels: { retry } }` and **`SystemBanner`** `{ tone: "warn" |
419
- "info", title?, icon?, onDismiss?, labels: { dismiss } }` — presentation
468
+ "alert" (title?), labels: { retry } }` and **`SystemBanner`** `{ tone: "neutral" |
469
+ "primary" | "info" | "warn", title?, icon?, onDismiss?, labels: { dismiss } }` — presentation
420
470
  only; what failed and whether a dismissal sticks are yours.
421
471
  - **`Settled`** `{ loading, skeleton, children }` — the skeleton → content
422
472
  handover: the content fades and rises in after a skeleton was shown, and cuts
@@ -463,6 +513,57 @@ The timing is tokens on every theme root, for your own transitions too
463
513
  `--ev-rise` (8px), `--ev-stagger` (35ms), `--ev-stagger-section` (50ms). The
464
514
  shell's geometry is `--shell-rail-w` (248px) and `--shell-tab-bar-h` (64px).
465
515
 
516
+
517
+ ### Animated numbers
518
+
519
+ `AnimatedNumber` shows a number that moves to its new value. It needs no
520
+ animation library: the digits are CSS transforms and transitions, and the
521
+ count-up is `requestAnimationFrame`.
522
+
523
+ ```tsx
524
+ <AnimatedNumber value={balance} format={{ style: "currency", currency: "USD" }} />
525
+ <AnimatedNumber value={change} format={{ style: "percent", signDisplay: "exceptZero" }} />
526
+ <AnimatedNumber value={volume} format={{ notation: "compact" }} mode="count" animateOnMount />
527
+ ```
528
+
529
+ | prop | |
530
+ |---|---|
531
+ | `value` | the number |
532
+ | `format`, `locales` | passed to `Intl.NumberFormat` |
533
+ | `mode` | `roll` (default): digit columns spin. `count`: the number is tweened and re-formatted each frame |
534
+ | `trend` | roll only. `1` every column spins up, `-1` down, `0` each column takes its shorter way. Default: the way the value moved |
535
+ | `duration` | ms per change: spin 750 (roll), whole tween 800 (count) |
536
+ | `prefix`, `suffix` | React nodes beside the figure, not animated |
537
+ | `live` | announce each new value politely; off by default |
538
+ | `animateOnMount`, `from` | animate the first render from `from` (default 0) |
539
+
540
+ It inherits font, size and colour, so it fits `text-figure-l font-serif
541
+ font-light` as well as a `text-body` table cell. `tabular-nums` is on.
542
+ `useCountTween(value, { duration, easing, from, animateOnMount })` returns the
543
+ in-flight number for a chart or a label of your own. `HeaderBalance` takes
544
+ `amount` (USD unless `format` says otherwise); any `figure`, `value` or other
545
+ `ReactNode` slot takes an `<AnimatedNumber />` as is.
546
+
547
+ **How roll works.** `formatToParts` splits the value. Integer digits are keyed
548
+ by place value from the right, fraction digits by position, so `9 -> 10` keeps
549
+ the existing column and only a new one enters. Symbols (currency, sign, group,
550
+ decimal, percent, compact suffix) are keyed by type. A digit is a clipped
551
+ one-line window over a strip moved with `translateY` on a spring-shaped
552
+ `linear()` easing (~4 % overshoot), with soft top and bottom edges. The strip
553
+ is unbounded, so a forced direction wraps (9 to 0 going up). Entering and leaving
554
+ parts animate width (`0fr` to `1fr`) and opacity in 350 ms, so their neighbours
555
+ slide.
556
+
557
+ **Accessibility.** The final formatted value is in the DOM once, as
558
+ visually hidden text; the moving glyphs are `aria-hidden` and not selectable,
559
+ so a copy gives the formatted text.
560
+
561
+ **Reduced motion.** Under `prefers-reduced-motion: reduce` a new value swaps in
562
+ at once: no spin, no width animation, no tween frames.
563
+
564
+ **SSR.** The server prints the final formatted value, also with `animateOnMount`,
565
+ so hydration does not jump; the count-up starts after hydration.
566
+
466
567
  ## Usage
467
568
 
468
569
  ```tsx
@@ -473,7 +574,7 @@ export function Example() {
473
574
  <Card>
474
575
  <CardHeader>
475
576
  <CardTitle>Portfolio</CardTitle>
476
- <Badge variant="success">+12.4%</Badge>
577
+ <Badge variant="positive">+12.4%</Badge>
477
578
  </CardHeader>
478
579
  <CardContent>
479
580
  <Button onClick={() => {}}>Invest</Button>
@@ -584,9 +685,11 @@ hold focus, so a bare list is pointer-only. The live demo is `CommandDemo` in
584
685
 
585
686
  The Rust crate is the source of truth; this package preserves its _semantics_
586
687
  while reading like idiomatic React. Canonical class strings are identical per
587
- element across the two ports.
688
+ element across all ports. The two Rust ports render the same HTML (pinned by
689
+ `rust/tests/parity.rs`) and share every row below; where Leptos idiom differs
690
+ the cell says so.
588
691
 
589
- | Concept | Rust (`ev_lib::uikit`) | TS (`@evinvest/uikit`) |
692
+ | Concept | Rust (`ev_lib::uikit_dioxus` / `ev_lib::uikit_leptos`) | TS (`@evinvest/uikit`) |
590
693
  | --- | --- | --- |
591
694
  | class fusion | `cn!` macro (`tailwind_fuse`) | `cn` (`clsx` + `tailwind-merge`) |
592
695
  | variants | `enum` + `fn class()` match | `as const` map keyed by variant |
@@ -595,8 +698,17 @@ element across the two ports.
595
698
  | keyboard nav | `use_roving_focus` | `useRovingFocus` |
596
699
  | overlay placement | inline `position:fixed` + `data-side` | `Portal` + `useFloating` |
597
700
  | dismiss / focus trap | full-screen backdrop / native order | `useDismissableLayer` / `useFocusScope` |
598
-
599
- ### Component inventory (all 75 bricks)
701
+ | brand webfonts | `Fonts`: Dioxus bundles the faces through `asset!`; Leptos has no asset pipeline, so `Fonts base="/fonts"` points the same `@font-face` rules at `base` and the consumer serves `FONT_FILES` there | none — load the files yourself ([Fonts](#fonts)) |
702
+ | optional slot present | an explicit prop, never read off `children`: `FieldSeparator { content: true, "or" }` renders the copy pill; `FormMessage { error, .. }` renders only while `error` (the same flag as `FormControl`'s) | `!!children` / `if (!children) return null` |
703
+ | option label | `SelectItem { value, text_value, .. }`: the text type-ahead matches, an explicit prop in both ports — never read off `children` | `SelectItem`'s `textValue`, else the text of its children |
704
+ | toasts | `ToasterProvider` + `use_toaster()` → `ToasterHandle` (`.toast` `.positive` `.error` `.info` `.warn` `.show` `.dismiss`), same in both ports | global `toast(...)` / `toast.dismiss` |
705
+ | children present | `SettingsSection` and `SystemBanner`: Dioxus checks that the children render something, Leptos that children were passed | `children !== undefined` |
706
+ | `SystemBanner` glyph | `icon` replaces the tone's glyph; Dioxus drops it with an empty `icon: rsx! {}`, Leptos with `icon=None` | `icon={null}` |
707
+ | `KeyValue` label | Dioxus: an `Element`; Leptos: `KeyValueLabel::{Text, View}` (`.into()` from a string), since only text names the copy button | `label` (`ReactNode`) |
708
+ | time-varying props | Dioxus: plain values; Leptos: signals — `HeaderBalance`, `NotificationBell`, `ModelChip`, `ChatComposer` `streaming` / `disabled`, `ConfirmDialog` `pending` / `error`, `ChatAttachment` `progress`, `AsidePanel` `open` | props |
709
+ | Ctrl+K hook | `use_command_palette_shortcut(on_trigger, enabled)`: `enabled` is a `bool` in Dioxus, a `Signal<bool>` in Leptos | `useCommandPaletteShortcut` |
710
+
711
+ ### Component inventory (all bricks)
600
712
 
601
713
  - **Tier A — static (23):** badge, button, button-group, card, input, textarea,
602
714
  label, field, separator, skeleton, spinner, kbd, table, container, alert,
@@ -614,27 +726,99 @@ element across the two ports.
614
726
  / `Forbidden` / `ServerError` presets (404 / 403 / 500). The kit ships no
615
727
  artwork: `Logo` masks whatever `--brand-mark` / `--brand-aspect` the consumer
616
728
  declares beside the palette, and the status pages use it for their mark.
617
- - **App shell — TS-only (12):** app-shell, shell-nav, bottom-tab-bar,
618
- nav-badge (`NavBadge` / `NavDot`), top-bar, account-menu, mobile-app-bar, page-frame (`PageFrame` /
619
- `PageHeading` / `SectionLabel`), section-nav, resource-error, system-banner,
620
- settled — see [App shell](#app-shell).
621
- - **Features — composed screens (1):** terminal — a trading terminal over an
622
- investment product's shares (`Terminal` grid placing each `TerminalPane` by
623
- `TerminalArea`; `TerminalTicker` / `TickerStat`; `OrderBook` with
624
- `OrderBookHead` / `OrderBookRow` / `OrderBookSpread`; `TradesTapeRow`;
625
- `OrderForm` / `OrderFormRow` / `OrderFormSubmit`; `OpenOrdersEmpty`). Column
626
- captions and every figure are the consumer's strings — the kit knows no
627
- locale and no number format.
729
+ - **App shell — TS-only (10):** app-shell, shell-nav, bottom-tab-bar,
730
+ nav-badge (`NavBadge` / `NavDot`), account-menu, mobile-app-bar, page-frame
731
+ (`PageFrame` / `PageHeading`), section-nav, resource-error, settled — see
732
+ [App shell](#app-shell). `TopBar`, `SystemBanner` and `SectionLabel` have
733
+ Dioxus twins (next item).
734
+ - **Composed parts — Rust and TS:** the class strings live once in
735
+ `rust/classes/src/<name>.rs`, and `nix run .#gen` writes them to
736
+ `ts/uikit/src/generated/<name>.ts`.
737
+ - *elements:* `IconButton` (required `aria-label`), `StatusItem` / `StatusDot`,
738
+ `WorkspaceTabs` / `WorkspaceTab`, `KeyValue` (copy button), `FormRow`,
739
+ `SystemBanner`, `SectionLabel`;
740
+ - *settings:* `SettingsLayout` / `SettingsSection` / `SettingsRow`,
741
+ `AccountSettingsDialog`, `ConfirmDialog` (async confirm), `AccountList` /
742
+ `AccountListItem`, `PopoverForm`, `CommandPalette` and
743
+ `useCommandPaletteShortcut` (Rust: `use_command_palette_shortcut`);
744
+ - *header and asides:* `TopBar`, `SmartHeader` (`default`, `collapsed`,
745
+ `banner`, `search`), `HeaderBalance`, `HeaderSearch`, `NotificationBell`,
746
+ `AsideHeader`, `AsideRail`, `AsidePanel` (`docked` or `overlay`);
747
+ - *AI chat:* `ChatMessage`, `ChatBubble` (`streaming` shows a caret),
748
+ `ChatMarker`, `ChatAttachment`, `ChatComposer`, `ModelChip` and `ChatTyping`
749
+ (the "thinking" indicator).
750
+ - **Composed parts — TS-only:** `AnimatedNumber` / `useCountTween`,
751
+ `ChatPanel`, `ChatDock`, `ChatStreamText` (pass the whole growing string; only
752
+ the new words fade in), the `AppShell` `aside` slot, and the profile form on
753
+ `AccountMenu`. How the Rust twins differ is in [Limitations](#limitations).
754
+
755
+ ### Variant vocabulary
756
+
757
+ A variant names the state ([`docs/spec/variants.md`](../../docs/spec/variants.md)).
758
+
759
+ | component | values |
760
+ |---|---|
761
+ | `Button` | `primary` (the filled call to action, default) · `secondary` · `outline` · `ghost` · `destructive` · `link` |
762
+ | `Badge` | `neutral` (default) · `primary` · `positive` · `info` · `warn` · `error` |
763
+ | `Alert` | `neutral` (default) · `destructive` · `success` · `info` |
764
+ | menu item (`DropdownMenuItem`, `ContextMenuItem`, `MenubarItem`) | `neutral` · `destructive` |
765
+ | `SystemBanner` | `neutral` · `primary` · `info` (default) · `warn` |
628
766
 
629
767
  The canonical variant set is the **superset** of the original cabinet (Rust) and
630
- landing (TS) sources — e.g. `Badge` keeps cabinet's `success` variant, and
631
- `Button` keeps landing's square icon shape at every size, as the separate `icon`
632
- boolean it always was in Rust (see [docs/spec/variants.md](../../docs/spec/variants.md)).
768
+ landing (TS) sources: `Button` keeps landing's square icon shape at every size,
769
+ as the separate `icon` boolean it always was in Rust. A `destructive` button
770
+ wears the `button-danger-*` face: an outline in Dark and Light, a fill in
771
+ Oldschool.
633
772
 
634
773
  `size="touch"` is the 44 px pointer-target floor (WCAG 2.5.5) for touch-first
635
774
  screens: `min-h-11` with `lg`'s padding, and a 44 × 44 square with `icon`. It is
636
775
  a button step only — `Input` and `SelectTrigger` already clear it at `lg`.
637
776
 
777
+ ## Upgrading from 0.30
778
+
779
+ **Renames and removals**
780
+
781
+ | 0.30 | now |
782
+ |---|---|
783
+ | `Badge variant="outline"` · `"secondary"` | `neutral` |
784
+ | `Badge variant="destructive"` · `"success"` | `error` · `positive` (new: `primary`, `info`, `warn`) |
785
+ | `SystemBanner` tones `warn`, `info` | the same, plus `neutral` and `primary`; `info` draws its icon in `accent-info-ink` |
786
+ | `accent-error-ink` (`text-accent-error-ink`, a brand file's `accent-error-ink`) | `accent-error`; destructive menu rows read in it, and `evinvest-palette` refuses the key |
787
+ | `Terminal`, `TerminalPane*`, `TerminalChart`, `TerminalTicker`, `TickerStat`, `OrderBook*`, `TradesTapeRow`, `OrderForm*`, `OpenOrdersEmpty` and their types | removed from the TS kit (the Rust terminal stays) |
788
+
789
+ **Silent changes: the compiler does not flag these.**
790
+
791
+ - A `Badge` with no `variant` was filled `primary` and is now `neutral`:
792
+ `rg -n '<Badge(\s|>)' --glob '*.tsx' | rg -v 'variant='`.
793
+ - A `destructive` `Button` wears the theme's `button-danger-*` face, an outline
794
+ in Dark and Light; an `outline` one is raised (`secondary` fill and a hairline).
795
+
796
+ **Tokens and themes**
797
+
798
+ - EV's navy palette is opt-in: `data-theme="oldschool"`. Dark is the default
799
+ and is a different colour set; `.light` is Light.
800
+ - Font defaults are Inter (sans), Inter Tight (serif role) and JetBrains Mono
801
+ (mono). `data-theme="oldschool"` sets Playfair Display and Inter for the serif
802
+ and mono roles, as the navy look shipped. Load the families a theme uses
803
+ (`--font-inter-tight`, `--font-jetbrains-mono`, or `--font-playfair` for
804
+ Oldschool); the kit ships no font files.
805
+ - The font role `display` is `serif`: `--brand-font-display` / `font-display`
806
+ become `--brand-font-serif` / `font-serif` (see [Fonts](#fonts)).
807
+ - A brand file must add `chart-6` (29 names now); `evinvest-palette` names it.
808
+ - Panel radius is 6 px at every width (the `sm` bump to 8 px is gone), controls
809
+ and chips 4 px, and the depth tiers are flat. **Aquafix, vifnet and any other
810
+ brand take these and lose their card shadow** unless their scope overrides
811
+ `--radius`, `--control-radius`, `--chip-radius` and `--depth-card`,
812
+ `--depth-elevated`, `--depth-overlay`. Check each brand before releasing.
813
+
814
+ **Layers across bundles**
815
+
816
+ - The shared layer stack (Escape order, outside clicks, the one shared scrim)
817
+ keeps its global key `@1`, so a micro-frontend bundling uikit 0.30 and a newer
818
+ host still share one stack. A 0.30 copy never notifies, so a newer modal's
819
+ shared scrim may not update when a 0.30 layer opens or closes under it; move
820
+ hosts and the MFEs they mount together to get the full behaviour.
821
+
638
822
  ## Limitations
639
823
 
640
824
  Reproducing everything dep-light means some behaviour is intentionally reduced,
@@ -668,18 +852,15 @@ measuring needs host-only `web-sys`). Known gaps:
668
852
  (`modal`, `pointerOutside`) and `DismissableLayerScope`; mark a floating
669
853
  region that should dismiss nothing with `LAYER_IGNORE_ATTR`.
670
854
  - **overlay layers (Rust):** `Select`, `Popover`, `Dialog` and `DropdownMenu`
671
- share one dismissable-layer stack per `VirtualDom`
672
- (`primitives::use_dismissable_layer`): Escape and a click on a backdrop act
855
+ share one dismissable-layer stack per `VirtualDom` in Dioxus, per root owner
856
+ (each island its own) in Leptos (`primitives::use_dismissable_layer`). The
857
+ stack follows renders, not handlers: a layer that closes itself stays on top
858
+ until it re-renders closed. Escape and a click on a backdrop act
673
859
  on the top layer only, so a `Select` inside a `Dialog` closes first. There is
674
860
  no document listener — an Escape counts only once it reaches an open layer's
675
861
  `onkeydown` — and `AlertDialog`, `Sheet`, `Drawer`, `ContextMenu`, `Menubar`,
676
862
  `NavigationMenu`, `HoverCard`, `Tooltip`, `InfoTip`, `DateTimePicker`,
677
863
  `CommandDialog` and `DropdownMenuSub` are not on the stack yet.
678
- - **terminal:** no plotting engine and no state. `TerminalChart` is a sized
679
- `relative` host that hands its element out (`ref` in TS, `id` in Rust) for a
680
- consumer-chosen charting library to mount into; `OrderForm` only prevents the
681
- submit's default — inputs, validation and the order itself are the
682
- consumer's. The open-orders pane reuses `Table` and `Tabs`.
683
864
  - **calendar:** single month, single-date selection (no range/multi-month, no
684
865
  dropdown captions). The grid is always six fixed weeks of 36px cells, so its
685
866
  size never changes with the month or the locale's weekday labels, and it
@@ -691,7 +872,7 @@ measuring needs host-only `web-sys`). Known gaps:
691
872
  locale whose short form contains whitespace (vi's "Thứ 2" → "T2", he's
692
873
  "יום ב׳" → "ב׳") as long as narrow still yields seven distinct labels. Rust has no `Intl`, so its captions
693
874
  stay English. Rust does manual date math; TS uses the built-in `Date`.
694
- - **date-time-picker:** the kit's own bricks only — an outline trigger, a
875
+ - **date-time-picker:** the kit's own bricks only — a raised trigger, a
695
876
  `Popover` with the `Calendar` and two numeric 24-hour hours/minutes fields (no
696
877
  native `datetime-local` / `time` input, so the browser's locale popup never
697
878
  appears). Its ARIA and button strings are `labels` overrides with English
@@ -707,19 +888,19 @@ measuring needs host-only `web-sys`). Known gaps:
707
888
  focus, via the shared `data-stack` CSS in `tokens.css`. The enter is a CSS
708
889
  keyframe (plays on insertion), the exit slides out on `data-state="closed"` and
709
890
  unmounts on `transitionend` (no host timer). React measures each toast's height
710
- for exact expanded spacing; Dioxus can't (layout measuring needs host-only
711
- `web-sys`), so it assumes a constant height — collapsed pile exact, expanded
712
- list uniform. Both **auto-dismiss** (default 4000ms) and **pause while the stack
891
+ for exact expanded spacing; Rust measures nothing, so it assumes a constant
892
+ height — collapsed pile exact, expanded list uniform. Both **auto-dismiss** (default 4000ms) and **pause while the stack
713
893
  is hovered/focused**, and support **persistent** toasts; React does it with a
714
- `setTimeout` (`duration: Infinity` to persist), Dioxus with a host-timer-free
894
+ `setTimeout` (`duration: Infinity` to persist), both Rust ports with a host-timer-free
715
895
  CSS "life" animation whose `animationend` closes the toast (`use_toaster().show(
716
896
  msg, variant, None)` to persist). Swipe-to-dismiss (horizontal drag, past 45px
717
897
  or a fast flick) is TS-only pointer physics.
718
898
  - **form:** react-hook-form is dropped — these are presentational + ARIA-id
719
899
  wiring; consumers own validation/state. Rust `FormControl` can't inject ids
720
900
  onto an arbitrary child (no `Slot`), so it publishes them as a
721
- `FormControlContext` that `Input`/`Textarea` consume; wrap a bare element and
722
- you wire them yourself.
901
+ `FormControlContext` signal that `Input`/`Textarea` consume; wrap a bare
902
+ element and you wire them yourself. Rust `FormMessage` renders on its `error`
903
+ flag, not on having children.
723
904
  - **carousel:** pointer-drag physics are TS-only (prev/next + keyboard in
724
905
  Rust). Embla momentum is not reproduced.
725
906
  - **drawer:** the Vaul-style enter/exit motion is shared by both ports via
@@ -739,12 +920,34 @@ measuring needs host-only `web-sys`). Known gaps:
739
920
  The lock-up is composed from `mark` + `brand` (display family) + `tagline`,
740
921
  or passed whole as `lockup`. Brand-coloured text reads `primary-ink`.
741
922
  - **TS-only for now:** `NativeSelect`, the `Field` id hand-off,
742
- `SelectValue` labels, `ListRows` / `ListRow` (a table's phone form), and the whole [app shell](#app-shell) (`AppShell`,
743
- `ShellNav`, `BottomTabBar`, `NavBadge` / `NavDot`, `TopBar`, `AccountMenu`, `MobileAppBar`, `PageFrame` /
744
- `PageHeading` / `SectionLabel`, `SectionNav`, `ResourceError`, `SystemBanner`,
745
- `Settled`) — no Dioxus twin. Its CSS (the entrances, the markers, the motion
746
- and shell tokens) ships in the shared sheet, so a Rust app can use the
747
- `data-enter` contract today.
923
+ `SelectValue` labels, `ListRows` / `ListRow` (a table's phone form), the
924
+ rest of the [app shell](#app-shell) (`AppShell`, `ShellNav`, `BottomTabBar`,
925
+ `NavBadge` / `NavDot`, `AccountMenu` and its profile form, `MobileAppBar`,
926
+ `PageFrame` / `PageHeading`, `SectionNav`, `ResourceError`, `Settled`) and
927
+ the parts that need a browser API Rust does not wrap: `AnimatedNumber` /
928
+ `useCountTween` (a frame loop and `Intl`), `ChatStreamText` (word diffing and
929
+ timers), and `ChatPanel` / `ChatDock` (`ResizeObserver`, scroll pinning).
930
+ Collapsible `ShellNav` / `SectionNav` groups are TS-only too. Their CSS (the
931
+ entrances, the markers, the motion and shell tokens) ships in the shared
932
+ sheet, so a Rust app can use the `data-enter` contract today.
933
+ - **Rust twins of the composed parts:** they keep the TS markup and class strings, with
934
+ these differences. Overlays keep the limits of the older Rust overlays (inline
935
+ `position: fixed`, no portal, no focus trap, not on the layer stack).
936
+ `HeaderBalance` takes a formatted `String` (no rolling digits, no `Settled`).
937
+ `ConfirmDialog` has `pending` and `error` props in place of a promise, and a
938
+ `pending` dialog ignores Esc, the scrim and the buttons. `KeyValue` takes a
939
+ `copy` callback; with the `wasm` feature and no callback it writes through
940
+ `web-sys`. `CommandPalette` is controllable (`open`, `default_open`,
941
+ `on_open_change`), and `use_command_palette_shortcut` listens for Ctrl or Cmd
942
+ K by key code on the `wasm` feature only, so it does nothing in an SSR build.
943
+ `AsidePanel` has no `matchMedia`: with `AsideCollapse::Md` it renders the
944
+ docked or sheet form and the drawer form together, and breakpoint classes show
945
+ one, so its children mount twice. Dialogs, popovers and menus do not name
946
+ themselves: pass `aria_labelledby` and the title `id`, or `aria_label`. The
947
+ TS tests that stub `matchMedia`, or that need `SectionNav` or `AppShell`, are
948
+ ported as markup checks or not at all. `dioxus-ssr` runs no effects, so the
949
+ clipboard write, the Ctrl+K listener and focus moves are checked in the
950
+ gallery only.
748
951
  - **app shell:** `ShellNav`'s marker is measured (one forced layout read per
749
952
  navigation); the tab bar's rule is arithmetic and assumes equal tabs. A
750
953
  `data-enter` nested inside another still entering composes both rises (the
@@ -769,7 +972,8 @@ npm run build # tsup → dist/ (ESM + d.ts)
769
972
  The Rust counterpart is verified from the repo root:
770
973
 
771
974
  ```sh
772
- cargo test -p ev_lib --features uikit
773
- cargo clippy -p ev_lib --features uikit --all-targets -- -D warnings
774
- cargo check -p ev_lib --features "uikit wasm" --target wasm32-unknown-unknown
975
+ cargo test -p ev_lib --features "uikit_dioxus uikit_leptos"
976
+ cargo clippy -p ev_lib --features "uikit_dioxus uikit_leptos" --all-targets -- -D warnings
977
+ cargo check -p ev_lib --features "uikit_dioxus wasm" --target wasm32-unknown-unknown
978
+ cargo check -p ev_lib --features "uikit_leptos wasm" --target wasm32-unknown-unknown
775
979
  ```
@@ -2,7 +2,8 @@
2
2
  function references(value) {
3
3
  return [...value.matchAll(/var\(--([A-Za-z0-9-]+)/g)].flatMap((m) => m[1] ? [m[1]] : []);
4
4
  }
5
- var DERIVED_SCOPE = ":where(:root, .dark, .light, [data-brand])";
5
+ var DERIVED_SCOPE = ":where(:root, .dark, .light, [data-brand], [data-theme])";
6
+ var FACES_SCOPE = ":where([data-theme], [data-brand], .light, .dark, :root)";
6
7
  function readRules(css) {
7
8
  const rules = [];
8
9
  const src = css.replace(/\/\*[\s\S]*?\*\//g, "");
@@ -48,10 +49,12 @@ function readContract(css) {
48
49
  }
49
50
  const derivedRule = rules.find((r) => r.selector === DERIVED_SCOPE);
50
51
  if (!derivedRule) throw new Error(`token sheet has no \`${DERIVED_SCOPE}\` rule \u2014 not the kit's contract`);
51
- const derived = [...derivedRule.declarations.keys()];
52
+ const facesRule = rules.find((r) => r.selector === FACES_SCOPE);
53
+ const formulas = new Map([...derivedRule.declarations, ...facesRule?.declarations ?? []]);
54
+ const derived = [...formulas.keys()];
52
55
  const stray = derived.filter((d) => !painted.includes(d));
53
56
  if (stray.length > 0) throw new Error(`derived tokens with no @theme mapping: ${stray.join(", ")}`);
54
- const derivedFrom = Object.fromEntries([...derivedRule.declarations].map(([name, value]) => [name, references(value)]));
57
+ const derivedFrom = Object.fromEntries([...formulas].map(([name, value]) => [name, references(value)]));
55
58
  return { required: painted.filter((p) => !derived.includes(p)), derived, derivedFrom };
56
59
  }
57
60
 
@@ -144,8 +147,8 @@ function splitTopLevel(inner) {
144
147
 
145
148
  // src/palette/brand.ts
146
149
  var POLARITIES = ["light", "dark"];
147
- var FONT_ROLES = ["sans", "display", "mono"];
148
- var FONT_KEYS = { sans: "sans", text: "sans", display: "display", mono: "mono" };
150
+ var FONT_ROLES = ["sans", "serif", "mono"];
151
+ var FONT_KEYS = { sans: "sans", text: "sans", serif: "serif", mono: "mono" };
149
152
  var PaletteError = class extends Error {
150
153
  constructor(problems) {
151
154
  super(`invalid brand palette:
@@ -293,6 +296,7 @@ ${extras.join("")}}
293
296
 
294
297
  export {
295
298
  DERIVED_SCOPE,
299
+ FACES_SCOPE,
296
300
  readRules,
297
301
  readContract,
298
302
  parseToml,
@@ -303,4 +307,4 @@ export {
303
307
  validateBrand,
304
308
  renderPalette
305
309
  };
306
- //# sourceMappingURL=chunk-3IPB4QQJ.js.map
310
+ //# sourceMappingURL=chunk-35SPIU4U.js.map