@viglet/viglet-design-system 2026.3.9 → 2026.3.11

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 (259) hide show
  1. package/README.md +528 -44
  2. package/consumers.json +186 -0
  3. package/dist/badge-locale-CJkHDlPg.cjs +1 -0
  4. package/dist/badge-locale-DxDp8Q3S.js +397 -0
  5. package/dist/bento/bento-actions-menu.d.ts +8 -0
  6. package/dist/bento/bento-actions-menu.d.ts.map +1 -1
  7. package/dist/bento/bento-back-to-top.d.ts +9 -1
  8. package/dist/bento/bento-back-to-top.d.ts.map +1 -1
  9. package/dist/bento/bento-command-palette.d.ts +56 -9
  10. package/dist/bento/bento-command-palette.d.ts.map +1 -1
  11. package/dist/bento/bento-data-table.d.ts +81 -0
  12. package/dist/bento/bento-data-table.d.ts.map +1 -0
  13. package/dist/bento/bento-diff.d.ts +41 -0
  14. package/dist/bento/bento-diff.d.ts.map +1 -0
  15. package/dist/bento/bento-empty-state.d.ts +8 -1
  16. package/dist/bento/bento-empty-state.d.ts.map +1 -1
  17. package/dist/bento/bento-entity-shell.d.ts.map +1 -1
  18. package/dist/bento/bento-filter-bar.d.ts +60 -0
  19. package/dist/bento/bento-filter-bar.d.ts.map +1 -0
  20. package/dist/bento/bento-form-section.d.ts +14 -1
  21. package/dist/bento/bento-form-section.d.ts.map +1 -1
  22. package/dist/bento/bento-inline-edit.d.ts.map +1 -1
  23. package/dist/bento/bento-nav-rail.d.ts +13 -13
  24. package/dist/bento/bento-nav-rail.d.ts.map +1 -1
  25. package/dist/bento/bento-nav.d.ts +10 -2
  26. package/dist/bento/bento-nav.d.ts.map +1 -1
  27. package/dist/bento/bento-palette-trigger.d.ts +18 -0
  28. package/dist/bento/bento-palette-trigger.d.ts.map +1 -0
  29. package/dist/bento/bento-section-chrome.d.ts +19 -39
  30. package/dist/bento/bento-section-chrome.d.ts.map +1 -1
  31. package/dist/bento/bento-shell-shortcuts.d.ts +67 -0
  32. package/dist/bento/bento-shell-shortcuts.d.ts.map +1 -0
  33. package/dist/bento/bento-shell.d.ts +53 -0
  34. package/dist/bento/bento-shell.d.ts.map +1 -0
  35. package/dist/bento/bento-shortcuts-dialog.d.ts +9 -7
  36. package/dist/bento/bento-shortcuts-dialog.d.ts.map +1 -1
  37. package/dist/bento/bento-status-marker.d.ts +5 -2
  38. package/dist/bento/bento-status-marker.d.ts.map +1 -1
  39. package/dist/bento/bento-tile.d.ts.map +1 -1
  40. package/dist/bento/bento-user-menu.d.ts +9 -8
  41. package/dist/bento/bento-user-menu.d.ts.map +1 -1
  42. package/dist/bento/bento-version-rail.d.ts +31 -0
  43. package/dist/bento/bento-version-rail.d.ts.map +1 -0
  44. package/dist/bento/index.d.ts +15 -6
  45. package/dist/bento/index.d.ts.map +1 -1
  46. package/dist/bento.cjs +5 -1
  47. package/dist/bento.css +70 -4
  48. package/dist/bento.es.js +1778 -701
  49. package/dist/catalogue.json +5283 -0
  50. package/dist/components/app-footer.d.ts +7 -0
  51. package/dist/components/app-footer.d.ts.map +1 -1
  52. package/dist/components/app-switcher.d.ts.map +1 -1
  53. package/dist/components/backend-status.d.ts +19 -11
  54. package/dist/components/backend-status.d.ts.map +1 -1
  55. package/dist/components/badge-colorful.d.ts +13 -0
  56. package/dist/components/badge-colorful.d.ts.map +1 -1
  57. package/dist/components/badge-locale.d.ts +7 -0
  58. package/dist/components/badge-locale.d.ts.map +1 -1
  59. package/dist/components/error-boundary.d.ts +15 -13
  60. package/dist/components/error-boundary.d.ts.map +1 -1
  61. package/dist/components/language-select.d.ts +8 -0
  62. package/dist/components/language-select.d.ts.map +1 -1
  63. package/dist/components/login/login.d.ts +27 -0
  64. package/dist/components/login/login.d.ts.map +1 -1
  65. package/dist/components/mode-toggle.d.ts +9 -0
  66. package/dist/components/mode-toggle.d.ts.map +1 -1
  67. package/dist/components/nav-secondary.d.ts +7 -0
  68. package/dist/components/nav-secondary.d.ts.map +1 -1
  69. package/dist/components/router/dialog.delete.d.ts +8 -0
  70. package/dist/components/router/dialog.delete.d.ts.map +1 -1
  71. package/dist/components/router/gradient-button-link.d.ts +6 -0
  72. package/dist/components/router/gradient-button-link.d.ts.map +1 -1
  73. package/dist/components/router/index.d.ts +0 -11
  74. package/dist/components/router/index.d.ts.map +1 -1
  75. package/dist/components/router/loading-provider.d.ts +4 -0
  76. package/dist/components/router/loading-provider.d.ts.map +1 -1
  77. package/dist/components/startup-first/startup-first.d.ts +29 -0
  78. package/dist/components/startup-first/startup-first.d.ts.map +1 -1
  79. package/dist/components/ui/accordion.d.ts +17 -0
  80. package/dist/components/ui/accordion.d.ts.map +1 -1
  81. package/dist/components/ui/avatar.d.ts +14 -0
  82. package/dist/components/ui/avatar.d.ts.map +1 -1
  83. package/dist/components/ui/badge.d.ts +6 -0
  84. package/dist/components/ui/badge.d.ts.map +1 -1
  85. package/dist/components/ui/breadcrumb.d.ts +25 -0
  86. package/dist/components/ui/breadcrumb.d.ts.map +1 -1
  87. package/dist/components/ui/button.d.ts +15 -1
  88. package/dist/components/ui/button.d.ts.map +1 -1
  89. package/dist/components/ui/card.d.ts +27 -0
  90. package/dist/components/ui/card.d.ts.map +1 -1
  91. package/dist/components/ui/checkbox.d.ts +4 -0
  92. package/dist/components/ui/checkbox.d.ts.map +1 -1
  93. package/dist/components/ui/dialog.d.ts +36 -0
  94. package/dist/components/ui/dialog.d.ts.map +1 -1
  95. package/dist/components/ui/drawer.d.ts +35 -0
  96. package/dist/components/ui/drawer.d.ts.map +1 -1
  97. package/dist/components/ui/dropdown-menu.d.ts +53 -0
  98. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  99. package/dist/components/ui/floating-formulas-bg.d.ts +8 -0
  100. package/dist/components/ui/floating-formulas-bg.d.ts.map +1 -1
  101. package/dist/components/ui/form-actions.d.ts +7 -0
  102. package/dist/components/ui/form-actions.d.ts.map +1 -1
  103. package/dist/components/ui/form-item-two-columns.d.ts +7 -0
  104. package/dist/components/ui/form-item-two-columns.d.ts.map +1 -1
  105. package/dist/components/ui/form.d.ts +25 -0
  106. package/dist/components/ui/form.d.ts.map +1 -1
  107. package/dist/components/ui/glass-card.d.ts +18 -14
  108. package/dist/components/ui/glass-card.d.ts.map +1 -1
  109. package/dist/components/ui/gradient-button.d.ts +15 -1
  110. package/dist/components/ui/gradient-button.d.ts.map +1 -1
  111. package/dist/components/ui/gradient-switch.d.ts +6 -0
  112. package/dist/components/ui/gradient-switch.d.ts.map +1 -1
  113. package/dist/components/ui/hover-card.d.ts +14 -0
  114. package/dist/components/ui/hover-card.d.ts.map +1 -1
  115. package/dist/components/ui/icon-picker-dialog.d.ts.map +1 -1
  116. package/dist/components/ui/index.d.ts +6 -4
  117. package/dist/components/ui/index.d.ts.map +1 -1
  118. package/dist/components/ui/input.d.ts +6 -0
  119. package/dist/components/ui/input.d.ts.map +1 -1
  120. package/dist/components/ui/label.d.ts +4 -0
  121. package/dist/components/ui/label.d.ts.map +1 -1
  122. package/dist/components/ui/navigation-menu.d.ts +41 -0
  123. package/dist/components/ui/navigation-menu.d.ts.map +1 -1
  124. package/dist/components/ui/pagination.d.ts +25 -0
  125. package/dist/components/ui/pagination.d.ts.map +1 -1
  126. package/dist/components/ui/popover.d.ts +24 -0
  127. package/dist/components/ui/popover.d.ts.map +1 -1
  128. package/dist/components/ui/progress.d.ts +4 -0
  129. package/dist/components/ui/progress.d.ts.map +1 -1
  130. package/dist/components/ui/pulse-ring.d.ts +9 -11
  131. package/dist/components/ui/pulse-ring.d.ts.map +1 -1
  132. package/dist/components/ui/resizable.d.ts +11 -0
  133. package/dist/components/ui/resizable.d.ts.map +1 -1
  134. package/dist/components/ui/section-card.d.ts +9 -0
  135. package/dist/components/ui/section-card.d.ts.map +1 -1
  136. package/dist/components/ui/select.d.ts +36 -0
  137. package/dist/components/ui/select.d.ts.map +1 -1
  138. package/dist/components/ui/separator.d.ts +4 -0
  139. package/dist/components/ui/separator.d.ts.map +1 -1
  140. package/dist/components/ui/sheet.d.ts +29 -0
  141. package/dist/components/ui/sheet.d.ts.map +1 -1
  142. package/dist/components/ui/sidebar.d.ts +83 -0
  143. package/dist/components/ui/sidebar.d.ts.map +1 -1
  144. package/dist/components/ui/skeleton.d.ts +4 -0
  145. package/dist/components/ui/skeleton.d.ts.map +1 -1
  146. package/dist/components/ui/slider.d.ts +4 -0
  147. package/dist/components/ui/slider.d.ts.map +1 -1
  148. package/dist/components/ui/sonner.d.ts +16 -0
  149. package/dist/components/ui/sonner.d.ts.map +1 -1
  150. package/dist/components/ui/stepper.d.ts +8 -0
  151. package/dist/components/ui/stepper.d.ts.map +1 -1
  152. package/dist/components/ui/sticky-save-bar.d.ts +7 -0
  153. package/dist/components/ui/sticky-save-bar.d.ts.map +1 -1
  154. package/dist/components/ui/switch.d.ts +4 -0
  155. package/dist/components/ui/switch.d.ts.map +1 -1
  156. package/dist/components/ui/table.d.ts +31 -0
  157. package/dist/components/ui/table.d.ts.map +1 -1
  158. package/dist/components/ui/tabs.d.ts +16 -0
  159. package/dist/components/ui/tabs.d.ts.map +1 -1
  160. package/dist/components/ui/textarea.d.ts +4 -0
  161. package/dist/components/ui/textarea.d.ts.map +1 -1
  162. package/dist/components/ui/toggle-group.d.ts +7 -0
  163. package/dist/components/ui/toggle-group.d.ts.map +1 -1
  164. package/dist/components/ui/toggle.d.ts +4 -0
  165. package/dist/components/ui/toggle.d.ts.map +1 -1
  166. package/dist/components/ui/tooltip.d.ts +18 -0
  167. package/dist/components/ui/tooltip.d.ts.map +1 -1
  168. package/dist/components/ui/user-avatar.d.ts +2 -1
  169. package/dist/components/ui/user-avatar.d.ts.map +1 -1
  170. package/dist/components/ui/viglet-assistant.d.ts +103 -0
  171. package/dist/components/ui/viglet-assistant.d.ts.map +1 -0
  172. package/dist/components/ui/viglet-avatar.d.ts +44 -0
  173. package/dist/components/ui/viglet-avatar.d.ts.map +1 -0
  174. package/dist/contexts/breadcrumb.context.d.ts +7 -0
  175. package/dist/contexts/breadcrumb.context.d.ts.map +1 -1
  176. package/dist/contexts/user.context.d.ts +8 -0
  177. package/dist/contexts/user.context.d.ts.map +1 -1
  178. package/dist/exports.json +359 -30
  179. package/dist/floating-formulas-bg.cjs +1 -1
  180. package/dist/floating-formulas-bg.es.js +56 -55
  181. package/dist/hooks/index.d.ts +1 -0
  182. package/dist/hooks/index.d.ts.map +1 -1
  183. package/dist/hooks/use-assistant-notifications.d.ts +15 -0
  184. package/dist/hooks/use-assistant-notifications.d.ts.map +1 -0
  185. package/dist/i18n/index.d.ts +168 -0
  186. package/dist/i18n/index.d.ts.map +1 -1
  187. package/dist/i18n/locales/en/assistant.json.d.ts +25 -0
  188. package/dist/i18n/locales/en/bento.json.d.ts +48 -2
  189. package/dist/i18n/locales/en/common.json.d.ts +6 -1
  190. package/dist/i18n/locales/en/language.json.d.ts +8 -0
  191. package/dist/i18n/locales/pt/assistant.json.d.ts +25 -0
  192. package/dist/i18n/locales/pt/bento.json.d.ts +48 -2
  193. package/dist/i18n/locales/pt/common.json.d.ts +6 -1
  194. package/dist/i18n/locales/pt/language.json.d.ts +8 -0
  195. package/dist/i18n.cjs +1 -1
  196. package/dist/i18n.es.js +572 -2
  197. package/dist/index.cjs +1 -1
  198. package/dist/index.d.ts +0 -1
  199. package/dist/index.d.ts.map +1 -1
  200. package/dist/index.es.js +2280 -733
  201. package/dist/lib/busy.d.ts +26 -0
  202. package/dist/lib/busy.d.ts.map +1 -0
  203. package/dist/lib/corner-slot.d.ts +15 -0
  204. package/dist/lib/corner-slot.d.ts.map +1 -0
  205. package/dist/{loading-provider-BWWhhfEn.js → loading-provider-ClDkFPuv.js} +84 -81
  206. package/dist/loading-provider-DGuCfsl9.cjs +1 -0
  207. package/dist/preset.css +7 -10
  208. package/dist/router.cjs +1 -1
  209. package/dist/router.es.js +2 -776
  210. package/dist/user.context-Cr3GAQ_r.js +762 -0
  211. package/dist/user.context-hbdvmXOn.cjs +1 -0
  212. package/dist/utils-Fk5jmzzb.js +41 -0
  213. package/dist/utils-Oagwe_oa.cjs +1 -0
  214. package/dist/viglet-design-system.css +1 -1
  215. package/dist/vite/boot-loader.d.ts +33 -2
  216. package/dist/vite/boot-loader.d.ts.map +1 -1
  217. package/dist/vite.cjs +16 -5
  218. package/dist/vite.es.js +27 -14
  219. package/docs/BENTO-AUTHORING.md +81 -11
  220. package/docs/reference/grounds.dc.html +9 -9
  221. package/package.json +16 -4
  222. package/scripts/consumer-entries.mjs +209 -0
  223. package/scripts/lib/contrast.mjs +301 -0
  224. package/scripts/mcp.mjs +300 -0
  225. package/scripts/page-lint.mjs +428 -0
  226. package/scripts/page-reference.mjs +54 -10
  227. package/dist/badge-locale-CoEEv7s5.js +0 -581
  228. package/dist/badge-locale-hQyWhAKe.cjs +0 -7
  229. package/dist/breadcrumb.context-KQut7j8U.js +0 -917
  230. package/dist/breadcrumb.context-Vwc3lAlf.cjs +0 -1
  231. package/dist/components/router/blank-slate.d.ts +0 -21
  232. package/dist/components/router/blank-slate.d.ts.map +0 -1
  233. package/dist/components/router/grid.list.d.ts +0 -39
  234. package/dist/components/router/grid.list.d.ts.map +0 -1
  235. package/dist/components/router/internal.sidebar.d.ts +0 -61
  236. package/dist/components/router/internal.sidebar.d.ts.map +0 -1
  237. package/dist/components/router/nav-main.d.ts +0 -25
  238. package/dist/components/router/nav-main.d.ts.map +0 -1
  239. package/dist/components/router/nav-user.d.ts +0 -24
  240. package/dist/components/router/nav-user.d.ts.map +0 -1
  241. package/dist/components/router/page-content.d.ts +0 -12
  242. package/dist/components/router/page-content.d.ts.map +0 -1
  243. package/dist/components/router/page-header.d.ts +0 -23
  244. package/dist/components/router/page-header.d.ts.map +0 -1
  245. package/dist/components/router/page.d.ts +0 -22
  246. package/dist/components/router/page.d.ts.map +0 -1
  247. package/dist/components/router/sticky.page.header.d.ts +0 -85
  248. package/dist/components/router/sticky.page.header.d.ts.map +0 -1
  249. package/dist/components/router/sub.page.d.ts +0 -50
  250. package/dist/components/router/sub.page.d.ts.map +0 -1
  251. package/dist/components/router/sub.page.header.d.ts +0 -37
  252. package/dist/components/router/sub.page.header.d.ts.map +0 -1
  253. package/dist/i18n-DD3rQOWi.js +0 -426
  254. package/dist/i18n-DrBq-qJJ.cjs +0 -1
  255. package/dist/lib/react-markers.d.ts +0 -17
  256. package/dist/lib/react-markers.d.ts.map +0 -1
  257. package/dist/loading-provider-CmeMqLGk.cjs +0 -1
  258. package/dist/user.context-B9vIBzY5.cjs +0 -1
  259. package/dist/user.context-D1PxP4D4.js +0 -512
package/README.md CHANGED
@@ -1,25 +1,31 @@
1
1
  # Viglet Design System
2
2
 
3
3
  Shared component library, design tokens, hooks, utilities, and i18n for the
4
- seven applications that install it: the Turing, Dumont and Shio consoles,
5
- the Cloud Console, the Cloud Home, Schools, and the Roadkeep GUI.
6
- `consumers.json` is the list, and it is what the guards read.
4
+ nine applications that install it: the Turing, Dumont and Shio consoles,
5
+ the Cloud Console, the Cloud Home, Schools, the Roadkeep GUI,
6
+ the Openviglet Website and Viglet Docs. `consumers.json` is the list, and it is
7
+ what the guards read.
7
8
 
8
9
  **[Browse the component catalogue](https://openviglet.github.io/viglet-design-system/)** — every
9
10
  component, its variants and its props, rebuilt on each commit to `2026.3`. Look
10
11
  there before writing a component: the catalogue is the answer to "does this
11
12
  already exist".
12
13
 
13
- The six applications that install this package are the consumers it holds itself
14
+ The nine applications that install this package are the consumers it holds itself
14
15
  to, declared in [`consumers.json`](consumers.json) rather than remembered: each
15
- entry names its framework, its chrome, the accent it renders with and the
16
- subpaths it takes, and CI refuses prose that names a subset as though it were
17
- the whole set. An eighth is one entry there, and the checks widen with it.
16
+ entry names its framework, its chrome, the accent it renders with, where its
17
+ source is and the subpaths it takes, and CI refuses prose that names a subset as
18
+ though it were the whole set. A tenth is one entry there, and the checks widen
19
+ with it. The file ships in the package, and a consumer holds its own entry to its
20
+ source with [`viglet-ds-consumer-entries`](#keeping-your-entry-in-the-register-true).
18
21
 
19
22
  Three are Vite consoles (Turing, Shio, Dumont), one is a Vite platform home
20
- (the Cloud Home), one is a Vite desktop shell (the Roadkeep GUI), and two are
21
- Next applications (the Cloud Console and Schools). The split matters: a Next
22
- consumer takes no `./router` and no `./vite`.
23
+ (the Cloud Home), one is a Vite desktop shell (the Roadkeep GUI), one is a Vite
24
+ marketing site (the Openviglet Website), two are Next applications
25
+ (the Cloud Console and Schools), and one is Docusaurus (Viglet Docs). The split
26
+ matters: a
27
+ Next consumer takes no `./router` and no `./vite`, and Viglet Docs takes no root
28
+ entry at all — only `./preset` and the background.
23
29
 
24
30
  ## Installation
25
31
 
@@ -27,30 +33,41 @@ consumer takes no `./router` and no `./vite`.
27
33
  pnpm add @viglet/viglet-design-system
28
34
  ```
29
35
 
36
+ **Declare `react-hook-form` yourself.** It is a required peer, not something this
37
+ package brings: the root entry exports `Form`, and that module re-exports
38
+ `FormProvider`, `useFormContext` and `useFormState` straight from it. A second
39
+ copy in your tree is a second React context, and `useFormContext` then returns
40
+ null inside a form that looks correctly wired. Declaring it is what keeps the
41
+ install to one copy — `npm ls react-hook-form` should say `deduped`.
42
+
30
43
  ## Trying a change in a product before publishing
31
44
 
32
45
  A change here is a change to shared chrome, so the question is always what it
33
- does to Shio, Turing, Dumont, the Cloud Console, the Cloud Home, Schools and the
34
- Roadkeep GUI — and the answer should not require a publish. `use:local` walks a
35
- products root, so it reaches the first three; the other four are checked out
36
- elsewhere and `npm pack` into them is the loop until that changes (VDS73). From this checkout:
46
+ does to Shio, Turing, Dumont, the Cloud Console, the Cloud Home, Schools,
47
+ the Roadkeep GUI, the Openviglet Website and Viglet Docs — and the answer should
48
+ not require a publish. The last two take the package by caret range rather than
49
+ pinning it, so they also receive each release the moment they install one. From
50
+ this checkout:
37
51
 
38
52
  ```bash
39
- pnpm use:local # build, then push dist into every 2026.3 product on disk
53
+ pnpm use:local # build, then push dist into every consumer checked out here
40
54
  pnpm use:local --list # show what it would write to, and stop
41
55
  ```
42
56
 
43
- It finds the consumers by reading their `package.json`, and finds where each one
44
- keeps the installed copy by following the link its own package manager made, so
45
- neither a version bump nor a switch of package manager breaks the loop. Nothing
46
- in the product's manifest or lockfile changes: `pnpm install` in the product puts
47
- the published build back.
57
+ It finds each consumer at the `checkout` its entry in `consumers.json` declares,
58
+ relative to this repository, with `{line}` standing for this checkout's version
59
+ line, so a 2026.2 worktree pushes into the 2026.2 products. A declared checkout
60
+ that is not there is named, not skipped in silence, and finding none at all is an
61
+ error. It finds where each consumer keeps the installed copy by following the
62
+ link its own package manager made, so neither a version bump nor a switch of
63
+ package manager breaks the loop. Nothing in the product's manifest or lockfile
64
+ changes: `pnpm install` in the product puts the published build back.
48
65
 
49
- Pass directories to narrow it, and `--no-build` to reuse the `dist` already on
50
- disk:
66
+ Pass consumer ids or directories to narrow it, and `--no-build` to reuse the
67
+ `dist` already on disk:
51
68
 
52
69
  ```bash
53
- pnpm use:local ../shio/2026.3/shio-react --no-build
70
+ pnpm use:local shio --no-build
54
71
  ```
55
72
 
56
73
  One thing it refuses to do: push over a tree whose dependencies are behind. It
@@ -74,8 +91,8 @@ viglet-ds-check-duplicates src
74
91
  It fails, naming the import that replaces each local copy:
75
92
 
76
93
  ```
77
- src/components/page-header.tsx:18 declares PageHeader
78
- replace it with: import { PageHeader } from "@viglet/viglet-design-system/router"
94
+ src/components/bento-hero.tsx:18 declares BentoHero
95
+ replace it with: import { BentoHero } from "@viglet/viglet-design-system/bento"
79
96
  ```
80
97
 
81
98
  One-line re-export shims are the sanctioned pattern and are skipped. When a
@@ -98,6 +115,114 @@ worked through, and drop it once the count is zero. `--json` prints the findings
98
115
  for a bot to read; `--manifest <path>` checks against an export list other than
99
116
  the installed one.
100
117
 
118
+ ## Linting a page against the shell
119
+
120
+ Two rules in [docs/BENTO-AUTHORING.md](docs/BENTO-AUTHORING.md) pass every other
121
+ gate when broken: a page that sets its own width, gutter or vertical rhythm, and a
122
+ stylesheet that sets `--vg-primary` directly instead of through its
123
+ `--vg-primary-*-base` inputs. `viglet-ds-page-lint` checks both. Point it at the
124
+ directories whose pages render inside `BentoShell`:
125
+
126
+ ```bash
127
+ viglet-ds-page-lint src
128
+ ```
129
+
130
+ ```
131
+ src/app/home.page.tsx:125 page.column the page's outermost element sets max-w-7xl px-6 pb-12
132
+ remove them: BentoShell sets the column, and column="narrow" or "wide" names the other widths
133
+ src/index.css:14 primary.direct sets --vg-primary
134
+ claim it through the --vg-primary-base and --vg-primary-foreground-base inputs, light and -dark, at :root
135
+ ```
136
+
137
+ A page is a file ending in `.page.tsx` or `.page.jsx`, and the lint checks the
138
+ outermost element its default export returns. `--page-pattern <regex>` names your
139
+ own convention. It reads pages with the TypeScript compiler, so `typescript` must
140
+ be installed in the product. A deliberate exception takes a reason, in the file:
141
+
142
+ ```ts
143
+ // viglet-ds-allow-page-column -- a kiosk rendered outside the shell
144
+ ```
145
+
146
+ `viglet-ds-allow-primary -- <reason>` does the same for the second rule.
147
+ `--warn` and `--json` work as they do for `viglet-ds-check-duplicates`.
148
+
149
+ Pass the stylesheet where you re-key the palette to also measure it:
150
+
151
+ ```bash
152
+ viglet-ds-page-lint src --contrast src/index.css
153
+ ```
154
+
155
+ It lays your stylesheet over the preset the way the two cascade and measures every
156
+ token pair your stylesheet changes on both grounds: each `--vg-X` with its
157
+ `--vg-X-foreground`, text and muted text on the page, the accented label on the
158
+ page, and the white label on the accent fill. A pair under 4.5:1 on either ground
159
+ is a finding at the line that declares the token, with both grounds' ratios:
160
+
161
+ ```
162
+ src/index.css:41 contrast.pair --vg-accent-fg on --vg-background is 3.59:1 on the light ground and 7.53:1 on the dark ground; 4.5:1 needed
163
+ ```
164
+
165
+ It reads `oklch()`, hex, `rgb()`, `hsl()`, `var()` chains and
166
+ `color-mix(in oklab, …)`. Anything else is reported as unread rather than
167
+ guessed. `viglet-ds-allow-contrast -- <reason>` in the stylesheet keeps a pair
168
+ you have signed off.
169
+
170
+ ## Keeping your entry in the register true
171
+
172
+ `consumers.json` records the subpaths each product takes, and this package's
173
+ guards read it. `viglet-ds-consumer-entries` measures what your source actually imports and
174
+ compares it with your entry there, in both directions. Run it from the product's
175
+ package root, in its own test step:
176
+
177
+ ```bash
178
+ viglet-ds-consumer-entries
179
+ ```
180
+
181
+ It finds your entry by the `name` in your `package.json` (`--consumer <id>`
182
+ names it outright), scans the `sourceRoots` that entry declares, and fails on a
183
+ subpath you import and do not declare, or one you declare and import nowhere:
184
+
185
+ ```
186
+ consumer-entries: shio's source and its entry in consumers.json disagree
187
+
188
+ imported, and not declared:
189
+ ./router first at src/components/dialog.delete.tsx:17
190
+ ```
191
+
192
+ Tests, stories and comments inside the roots are not counted. A failure means
193
+ the entry here is wrong, so the fix is a change to `consumers.json` in this
194
+ repository. `--json` prints the measurement; `--root <dir>` measures a checkout
195
+ other than the current directory.
196
+
197
+ ## Using it from Claude Code
198
+
199
+ This repository publishes a Claude Code plugin, `viglet-ds`, for a product that
200
+ installs the package. It serves the component catalogue over MCP, loads a skill
201
+ that points a session at it, adds `/viglet-ds-check` to run the CLIs above
202
+ against the working tree, and denies a write that declares a component the
203
+ package already exports, naming the import to use instead.
204
+
205
+ The catalogue server is a bin of its own, `viglet-ds-mcp`, so any MCP client can
206
+ start it from a product's install. It has two tools: `find_component` ranks the
207
+ components that do a job, and `read_component` returns one component's purpose,
208
+ props, the contract sections that govern it and an import to start from. The
209
+ authoring contract, the boundary and the token reference are resources
210
+ (`viglet-ds://authoring`, `viglet-ds://boundary`, `viglet-ds://tokens`). The data
211
+ is `dist/catalogue.json`, generated at build time from the declarations you
212
+ compile against and also exported as `./catalogue.json`. A component's purpose is
213
+ its own doc comment, and every exported component has one: the build refuses a
214
+ component that arrives without it.
215
+
216
+ ```
217
+ /plugin marketplace add openviglet/viglet-design-system
218
+ /plugin install viglet-ds@viglet-design-system
219
+ ```
220
+
221
+ With the plugin enabled in a repository's `.claude/settings.json`,
222
+ `viglet-ds-page-reference` writes only the artboards and leaves the skill to the
223
+ plugin. [claude-plugin/README.md](claude-plugin/README.md) has the settings to
224
+ commit and the package range the plugin supports.
225
+
101
226
  ## Setup
102
227
 
103
228
  ### 1. Import styles
@@ -147,6 +272,12 @@ one the catalogue shows.
147
272
 
148
273
  ### 2. Initialize i18n
149
274
 
275
+ `/i18n` is the only way in. The root entry used to re-export these three, which
276
+ meant every consumer of `.` resolved `i18next` and
277
+ `i18next-browser-languagedetector` and carried the locale bundle whether or not
278
+ it ever asked for a translation; since 2026.3 it does not, and a product that
279
+ imported them from `.` changes the specifier and nothing else.
280
+
150
281
  ```ts
151
282
  import { initVigI18n } from "@viglet/viglet-design-system/i18n";
152
283
 
@@ -166,6 +297,31 @@ import i18n from "i18next";
166
297
  registerVigTranslations(i18n);
167
298
  ```
168
299
 
300
+ **Both merge key by key, and your key always wins.** Shipping your own `common`
301
+ does not cost you the package's — `common.save` stays yours and `common.next`
302
+ still resolves to the word this package ships. `registerVigTranslations` never
303
+ replaces a key the host already has, so calling it twice is safe.
304
+
305
+ **`initVigI18n` takes any language, not only the two this package ships.** Pass
306
+ `es`, `fr` or anything else and it arrives whole; `en` and `pt` still merge leaf
307
+ by leaf with yours. The package's own strings exist in `en` and `pt` only, so
308
+ `fallbackLng` stays `en` and a screen asking for a string the package ships
309
+ reads English under a third language until you translate it yourself.
310
+
311
+ This used to merge a namespace at a time: a product with its own `common`
312
+ replaced the package's outright, and every string the package asked for under
313
+ that namespace fell back to its English default — in a Portuguese product,
314
+ silently. If you worked around it by renaming a namespace or copying the
315
+ package's keys into yours, you can stop.
316
+
317
+ **One string lands before any of this.** The boot loader from
318
+ `@viglet/viglet-design-system/vite` is HTML written at build time and paints
319
+ before a bundle runs, so it cannot ask i18next for anything. Its status region is
320
+ named `"Loading <title>"` unless you say otherwise — pass `loadingLabel` for the
321
+ phrase in your language, or `loadingLabels` (a language tag to a phrase) for a
322
+ product that ships several, which the loader's inline script picks from
323
+ `navigator.language`.
324
+
169
325
  ### 3. Setup providers
170
326
 
171
327
  ```tsx
@@ -260,6 +416,14 @@ import {
260
416
  } from "@viglet/viglet-design-system";
261
417
  ```
262
418
 
419
+ `Button` and `GradientButton` take `loading` for an action in flight. The button
420
+ shows a spinner, sets `aria-busy`, ignores a second press (including a form's
421
+ submit) and keeps keyboard focus. Do not pass `disabled` for that: a disabled
422
+ button drops the focus of the control just pressed, just as the result is
423
+ announced. For a control that is unavailable for now, such as a Save with nothing
424
+ to save, pass `aria-disabled="true"`. It stays focusable and ignores activation.
425
+ Keep `disabled` for a control that is genuinely unavailable.
426
+
263
427
  ### App Components
264
428
 
265
429
  ```tsx
@@ -274,16 +438,25 @@ import {
274
438
  ```
275
439
 
276
440
  A page's chrome — its header, its list, its sidebar — comes from the
277
- [bento layer](#the-bento-layer) rather than from here. Read the next section
278
- before reaching for `PageHeader`, `GridList` or `InternalSidebar`.
441
+ [bento layer](#the-bento-layer). It is the only page vocabulary this package
442
+ ships; if you are upgrading from a release that still had `PageHeader`,
443
+ `GridList` or `InternalSidebar`, read the next section.
279
444
 
280
- ### Two eras, and which one a new page should use
445
+ ### Moving off the console era
281
446
 
282
- This package exports two complete page vocabularies. The **console era** is what the consoles ship on today; the **[bento layer](#the-bento-layer)** is the current one, and Turing has cut over to it. Every console-era export is marked `@deprecated` in its own types, so an editor strikes it through and offers the swap.
447
+ This package used to export two complete page vocabularies. The **console era**
448
+ is gone: `pnpm chrome:census` counts, per consumer checked out beside this
449
+ repository, the source files taking a console-era component — directly or
450
+ through a re-export shim — and it reads zero across all nine, so the eleven
451
+ components below were removed. The **[bento layer](#the-bento-layer)** is what
452
+ remains, and with one chrome left there is nothing to choose between: the
453
+ `SectionCardChromeProvider` that used to declare which one a page rendered in
454
+ went with them.
283
455
 
284
- Nothing is being removed. No product has started cutting over, and a removal will get its own roadmap line rather than arriving as a side effect of this notice — so existing pages keep working and need no rush.
456
+ Each row is a swap rather than a rename: the replacement takes its own props,
457
+ and the note says where the two differ.
285
458
 
286
- | Console era | Use instead | |
459
+ | Removed | Use instead | |
287
460
  |---|---|---|
288
461
  | `PageHeader` | `BentoHero` | Same title/subtitle/icon, plus a tone — the chip follows the product accent. |
289
462
  | `SubPageHeader` | `BentoHero` | With `BentoBackLink` for the back target and `BentoActionsMenu` for the menu. |
@@ -294,9 +467,166 @@ Nothing is being removed. No product has started cutting over, and a removal wil
294
467
  | `NavMain` | `BentoNavRail` | |
295
468
  | `NavUser` | `BentoUserMenu` | Routes and feature flags arrive as props. |
296
469
  | `SubPage` | `BentoEntityShell` | Renders through a render prop, so the shell never learns what an entity is. |
297
- | `Page`, `PageContent` | — | No single replacement, deliberately: the shell is where a product is itself. Compose `BentoNavRail`, `BentoUserMenu` and `BentoCommandPalette`. |
470
+ | `Page`, `PageContent` | `BentoShell` | Takes the rail, the header's two edges and the page; it owns `main` and the reading column, so a page sets no width. |
471
+
472
+ `DialogDelete`, `LoadProvider` and `GradientButtonLink` were never console-era — the bento layer uses them itself, and they are the whole of `./router` now.
473
+
474
+ ### The mascot
475
+
476
+ `VigletAvatar` is the Viglet mascot: a small sun that shows what the system is
477
+ doing. It is one `<canvas>` and no new dependency — the faceted core is drawn
478
+ directly, so a product gets the mascot by installing this package and nothing
479
+ else.
480
+
481
+ ```tsx
482
+ import { VigletAvatar } from "@viglet/viglet-design-system";
483
+
484
+ <VigletAvatar state="working" size={128} />;
485
+ ```
486
+
487
+ The five states are named for the toast kinds, so a product that already reports
488
+ through `toast` has nothing to translate:
489
+
490
+ | `state` | When | What it does |
491
+ |---|---|---|
492
+ | `idle` | nothing is happening | a slow, low glow |
493
+ | `working` | a request is in flight | an orbit with a travelling arc |
494
+ | `success` | it finished | a swell, then a pulse outward |
495
+ | `error` | it failed | the light goes out — it does not turn red |
496
+ | `attention` | something arrived | a ring rises from below |
497
+
498
+ Four further props shape it rather than drive it: `compact` pulls the camera in
499
+ and drops the pool of light, for a collapsed dock; `unread` holds a slow orbit
500
+ while an answer waits; `activity` is a counter you bump to make the mascot react
501
+ to something smaller than a state change, like a keystroke; and `seed` fixes the
502
+ field of rising embers. The same props always draw the same sparks, so a product
503
+ that wants a different field per visit passes a changing `seed` itself.
504
+
505
+ The mascot is decorative (`aria-hidden`) and says nothing a screen reader can
506
+ use — give the surface that wraps it the accessible name. It draws one frame and
507
+ starts no animation loop for a reader whose system asks for reduced motion, and
508
+ `paused` does the same for a product with its own motion switch. When nothing is
509
+ happening it stops drawing altogether rather than holding a loop open.
510
+
511
+ ### The assistant dock
512
+
513
+ `VigletAssistant` is the surface the mascot lives in. Collapsed it is a status
514
+ light with the system's last sentence typed beside it; open it is a panel with a
515
+ transcript and a composer.
516
+
517
+ ```tsx
518
+ import { VigletAssistant } from "@viglet/viglet-design-system";
519
+
520
+ <VigletAssistant
521
+ state={state}
522
+ caption="/q3-results published at 14:02."
523
+ messages={messages}
524
+ busy={busy}
525
+ onSend={(text) => ask(text)}
526
+ />;
527
+ ```
528
+
529
+ **The dock knows no backend.** `messages`, `busy` and `onSend` are the whole
530
+ contract — the endpoint, the prompt and the key stay in your product. Leaving
531
+ `onSend` off is how you turn the chat off: the composer is not rendered at all
532
+ and the dock becomes a place the system reports from, which is what a product
533
+ that only wants notification feedback should mount.
534
+
535
+ A message may carry one `action`, so an answer can offer something to do with
536
+ it — the label is your copy, already translated:
298
537
 
299
- `DialogDelete`, `LoadProvider` and `GradientButtonLink` are **not** console-era — the bento layer uses them itself, and they are not deprecated.
538
+ ```tsx
539
+ messages={[{
540
+ role: "assistant",
541
+ text: "Lead with the number.",
542
+ action: { label: "Use this title", onSelect: () => setTitle(suggested) },
543
+ }]}
544
+ ```
545
+
546
+ **What the system reports is a `report`, not an assistant message.** A publish,
547
+ a failure or an arrival has a `tone` (one of the mascot's five states), a time,
548
+ a `read` flag and up to three `actions`, and it renders apart from the
549
+ conversation. It never opens a composer, so a dock with no `onSend` lists reports
550
+ on their own. You own the list, as you own the messages:
551
+
552
+ ```tsx
553
+ <VigletAssistant
554
+ messages={reports}
555
+ onRead={(id) => markRead(id)}
556
+ onDismiss={(id) => dismiss(id)}
557
+ />
558
+
559
+ const reports = [{
560
+ id: "publish-q3",
561
+ role: "report",
562
+ tone: "error",
563
+ text: "Could not publish /q3-results.",
564
+ at: failedAt,
565
+ actions: [
566
+ { label: "Retry", onSelect: retry },
567
+ { label: "Open the page", onSelect: openPage },
568
+ ],
569
+ }];
570
+ ```
571
+
572
+ Collapsed, the orb counts the unread reports, and the caption is the newest one
573
+ unless you pass `caption`. The mascot's state follows that report's tone unless
574
+ you pass `state`. Pass `unread` a number to count something else. `onRead` fires
575
+ for each unread report when the dock opens or closes, since it was on screen,
576
+ and a dismiss button appears on each report once you pass `onDismiss`. A report
577
+ is announced once, when it arrives, and not again when the dock opens or
578
+ re-renders.
579
+
580
+ It pins itself to the bottom-right of the viewport; pass `inline` to render it
581
+ in flow and place it yourself. In a bento product, pass it to `BentoShell` as
582
+ `dock` instead: the shell holds the corner, stacks `BentoBackToTop` above the
583
+ dock, and keeps either from covering the other. `open` / `onOpenChange` make it controlled,
584
+ Escape collapses it, and the caption is typed for everyone while a screen reader
585
+ is handed the whole sentence at once.
586
+
587
+ Its own words — the state names, the two buttons, the placeholder — come from
588
+ the `assistant.*` keys this package ships in `en` and `pt`. See
589
+ [Initialize i18n](#2-initialize-i18n); everything else it says is your copy.
590
+
591
+ #### Letting notifications drive it
592
+
593
+ You already report outcomes through `toast`. `useAssistantNotifications` reads
594
+ the Toaster back, so the mascot moves without a second call beside each one:
595
+
596
+ ```tsx
597
+ import {
598
+ Toaster,
599
+ VigletAssistant,
600
+ useAssistantNotifications,
601
+ } from "@viglet/viglet-design-system";
602
+
603
+ function Chrome() {
604
+ const { state, caption, activity } = useAssistantNotifications();
605
+
606
+ return (
607
+ <>
608
+ <Toaster />
609
+ <VigletAssistant state={state} caption={caption} activity={activity} />
610
+ </>
611
+ );
612
+ }
613
+ ```
614
+
615
+ It **wraps nothing**. Call sonner from anywhere in your app — from a module that
616
+ has never heard of this hook — and the mascot still moves.
617
+
618
+ | Toast | State |
619
+ |---|---|
620
+ | `toast.loading` | `working` |
621
+ | `toast.success` | `success` |
622
+ | `toast.error` | `error` |
623
+ | `toast.warning`, `toast.info` | `attention` |
624
+ | nothing on screen | `idle` |
625
+
626
+ With several up, work in progress outranks an outcome and a failure outranks a
627
+ success. `caption` is the toast's own title when it is a string — a toast
628
+ rendered as JSX has no sentence to lift, so you get `null` rather than a guess.
629
+ The hook is opt-in: pass `state` yourself and nothing here interferes.
300
630
 
301
631
  ### Hooks
302
632
 
@@ -306,6 +636,7 @@ import {
306
636
  useDateLocale,
307
637
  useGridAdapter,
308
638
  useSubPageBreadcrumb,
639
+ useAssistantNotifications,
309
640
  useTheme,
310
641
  useBreadcrumb,
311
642
  useCurrentUser,
@@ -357,19 +688,29 @@ import type {
357
688
 
358
689
  ## What's Included
359
690
 
360
- ### UI Primitives (39 components)
691
+ ### UI Primitives (48 components)
692
+
693
+ Accordion, Avatar, Badge, Breadcrumb, Button, Card, Checkbox, Dialog, Drawer, DropdownMenu, FloatingFormulasBg, Form, FormActions, FormItemTwoColumns, GlassCard, GradientButton, GradientSwitch, HoverCard, IconPickerDialog, Input, Label, NavigationMenu, Pagination, Popover, Progress, PulseRing, Resizable, SectionCard, Select, Separator, Sheet, Sidebar, Skeleton, Slider, Stepper, StickySaveBar, Switch, Table, Tabs, Textarea, Toaster (sonner), Toggle, ToggleGroup, Tooltip, UserAvatar, UserMenu, VigletAssistant ([the dock](#the-assistant-dock)), VigletAvatar ([the mascot](#the-mascot))
694
+
695
+ ### App Components (19 components)
361
696
 
362
- Accordion, Avatar, Badge, Breadcrumb, Button, Card, Checkbox, Dialog, Drawer, DropdownMenu, Form, FormActions, FormItemTwoColumns, GradientButton, GradientSwitch, HoverCard, Input, Label, NavigationMenu, Pagination, Popover, Progress, Resizable, SectionCard, Select, Separator, Sheet, Sidebar, Skeleton, Slider, Sonner (Toaster), Stepper, Switch, Table, Tabs, Textarea, Toggle, ToggleGroup, Tooltip
697
+ AppFooter, AppSwitcher, BackendStatusBanner, BackendStatusProvider, BadgeColorful, BadgeLocale, DialogDelete, ErrorBoundary, GradientButtonLink, LanguageSelect, LanguageSwitcher, LoadProvider, Login, ModeToggle, ModeToggleSidebar, NavSecondary, StartupFirst, ThemeProvider, VigletAppSwitcher
363
698
 
364
- ### App Components (23 components)
699
+ `DialogDelete`, `GradientButtonLink` and `LoadProvider` are imported from `@viglet/viglet-design-system/router`; every other name above comes from the package root. `Login` and `StartupFirst` are compounds — each is a root with named parts on it, the way `Card` is.
365
700
 
366
- AppFooter, BadgeColorful, BadgeLocale, DialogDelete, GradientButtonLink, LanguageSelect, LoadProvider, ModeToggle, NavSecondary, ThemeProvider
701
+ ### Bento Layer (31 components)
367
702
 
368
- Console-era, `@deprecated`, still exported — see [the swap table](#two-eras-and-which-one-a-new-page-should-use): BlankSlate, GridList, InternalSidebar, NavMain, NavUser, Page, PageContent, PageHeader, StickyPageHeader, SubPage, SubPageHeader
703
+ AdaptiveSectionCard, BentoActionsMenu, BentoBackLink, BentoBackToTop, BentoCommandPalette, BentoCountTile, BentoDataTable, BentoDiff, BentoEmptyState, BentoEntityShell, BentoEntityTile, BentoFilterBar, BentoFormHero, BentoFormSection, BentoHero, BentoHeroIconPicker, BentoInlineEdit, BentoListPage, BentoNavRail, BentoPaletteTrigger, BentoPanel, BentoSaveBar, BentoScrollSaveBar, BentoSection, BentoShell, BentoShortcutsDialog, BentoStatusMarker, BentoTile, BentoTileGrid, BentoUserMenu, BentoVersionRail
704
+
705
+ All from `@viglet/viglet-design-system/bento`, with `@viglet/viglet-design-system/bento.css` beside it. [The bento layer](#the-bento-layer) is what each one is for and how a page composes them; this is the inventory.
706
+
707
+ Removed with the console era — see [where each one went](#moving-off-the-console-era): BlankSlate, GridList, InternalSidebar, NavMain, NavUser, Page, PageContent, PageHeader, StickyPageHeader, SubPage, SubPageHeader
369
708
 
370
709
  ### Hooks
371
710
 
372
- `useIsMobile`, `useDateLocale`, `useGridAdapter`, `useSubPageBreadcrumb`
711
+ `useIsMobile`, `useDateLocale`, `useGridAdapter`, `useSubPageBreadcrumb`, `useAssistantNotifications`, `useSidebarOptional`
712
+
713
+ `useSidebarOptional()` is `useSidebar()` for a component that may not have a `SidebarProvider` above it — it returns `undefined` instead of throwing, which is what a Module Federation remote needs when the host owns the sidebar and the remote only renders into it.
373
714
 
374
715
  `useGridAdapter(data, config)` tracks each extractor in `config`, not the object holding them, so a `url` builder that closes over a route param or a locale re-maps the rows when it changes. Pass the config inline if you like — keep the extractors themselves stable (a field name, a module-level function, a `useCallback`) and the memo holds.
375
716
 
@@ -419,7 +760,7 @@ This is separate from a component's **colour palette**. `SectionCard` (`blue | v
419
760
 
420
761
  ### i18n
421
762
 
422
- Base translations (EN/PT) for common UI strings: buttons, form labels, dialog text, navigation, theme.
763
+ Base translations (EN/PT) for common UI strings: buttons, form labels, dialog text, navigation, theme, and the assistant dock.
423
764
 
424
765
  ## Tech Stack
425
766
 
@@ -454,6 +795,7 @@ of it:
454
795
  ```ts
455
796
  import "@viglet/viglet-design-system/bento.css";
456
797
  import {
798
+ BentoShell,
457
799
  BentoEntityShell,
458
800
  BentoListPage,
459
801
  BentoFormHero,
@@ -466,11 +808,144 @@ The stylesheet is a separate import from the components, so a consumer taking
466
808
  only the layout maths does not pull CSS it never renders. It reads the preset's
467
809
  tokens, so import the preset too.
468
810
 
811
+ `BentoShell` is the page frame. It reserves the gutter for the rail you pass,
812
+ lays out the header's leading and trailing edges, holds the corner (the
813
+ `VigletAssistant` you pass as `dock`, with `BentoBackToTop` stacked above it;
814
+ `backToTop={false}` drops the button), and owns `main`: the width, the gutters
815
+ and the vertical rhythm. A page
816
+ inside it sets none of those. Choose the width by name with `column`: `default`,
817
+ `narrow` for a single-question form, `wide` for a table, or `full` for a tool
818
+ that fills the viewport. Each width is a custom property
819
+ (`--bento-column-default`, `--bento-column-narrow`, `--bento-column-wide`) that
820
+ you can redefine at `:root`.
821
+
822
+ ```tsx
823
+ <BentoShell
824
+ column={isForm ? "narrow" : "default"}
825
+ rail={<BentoNavRail groups={groups} homeRoute={ROUTES.HOME} />}
826
+ headerStart={<ProductMark />}
827
+ headerEnd={<BentoUserMenu accountRoute={ROUTES.ACCOUNT} logoutUrl={ROUTES.LOGOUT} />}
828
+ >
829
+ <Outlet />
830
+ </BentoShell>
831
+ ```
832
+
833
+ `BentoDataTable` is the list a curator sorts, range-selects and acts on at scale.
834
+ It mounts only the rows in view, so ten thousand cost what twenty do. Pass `rows`,
835
+ `getRowId`, `columns` (a header, a `cell`, and a `sortValue` to make it sortable),
836
+ and optionally `rowActions` for a per-row menu and `selectionActions` for the bar
837
+ that appears over a selection. The arrows move between rows, Shift with an arrow
838
+ or a click extends a selection, and Enter calls `onRowOpen`. The column picker
839
+ reports `onLayoutChange`, and you store the layout, as you do for `BentoListPage`.
840
+
841
+ ```tsx
842
+ <BentoDataTable
843
+ rows={posts}
844
+ getRowId={(p) => p.id}
845
+ getRowLabel={(p) => p.title}
846
+ label={t("posts")}
847
+ columns={[
848
+ { id: "title", header: t("title"), cell: (p) => p.title, sortValue: (p) => p.title, hideable: false },
849
+ { id: "updated", header: t("updated"), cell: (p) => format(p.updated), sortValue: (p) => p.updated },
850
+ ]}
851
+ rowActions={[{ id: "trash", label: t("trash"), icon: IconTrash, tone: "destructive", onSelect: trash }]}
852
+ selectionActions={[{ id: "trash", label: t("trash"), icon: IconTrash, onSelect: trash }]}
853
+ onRowOpen={(p) => navigate(p.url)}
854
+ />
855
+ ```
856
+
857
+ `AdaptiveSectionCard` renders the console's compound form markup — a `Header`
858
+ child and a `Content` child — as a frosted bento section, so a form written that
859
+ way moves without being rewritten. That compound API is the whole difference
860
+ from `BentoFormSection`, which takes the same icon, title and description as
861
+ props. A section whose header it cannot read keeps the frosted surface and all
862
+ of its children; a frosted box that is not part of a form is `BentoPanel`.
863
+
864
+ **`SectionCardChromeProvider`, `useSectionChrome`, the `SectionChrome` type and
865
+ the `defaultOpen` prop were removed in this release.** They picked between the
866
+ console card and the frosted section, and every consumer finished its cutover —
867
+ so every remaining call site passed `chrome="bento"`, the value it would have got
868
+ anyway. Delete the provider and the hook; the sections beneath render as they
869
+ already did. A `useSectionChrome()` call that drove a product's own branch can be
870
+ replaced by the bento arm of that branch.
871
+
872
+ **Give every `BentoActionsMenu` item an `id`**, a stable name for the verb such as
873
+ `"post.delete"`, and the same for `BentoEntityShell`'s `extraActions`. It is
874
+ rendered as `data-action-id` and means nothing to the package, but it lets a
875
+ product check its console verbs against its agent's, since a menu item is
876
+ otherwise an anonymous `onSelect`. An item without one warns outside production
877
+ in this release and stops type-checking in the next.
878
+
879
+ `BentoFilterBar` is the query row above a list: a search field, the facets you
880
+ declare (a `choice` of options, single or `multiple`, or a `date` range), the
881
+ active filters as removable chips, and a clear-all. It holds no state. You pass
882
+ `value` and get one `onChange` per action, typing included once it pauses, so the
883
+ value can live in the URL and a filtered view is a link. Pass it to a
884
+ `BentoDataTable` as `selectionScope` too, and a selection clears when the filters
885
+ change the rows:
886
+
887
+ ```tsx
888
+ <BentoFilterBar
889
+ value={filters}
890
+ onChange={setFilters}
891
+ facets={[
892
+ { id: "state", kind: "choice", label: t("state"), multiple: true, options: states },
893
+ { id: "updated", kind: "date", label: t("updated") },
894
+ ]}
895
+ />
896
+ <BentoDataTable rows={filtered} selectionScope={JSON.stringify(filters)} … />
897
+ ```
898
+
899
+ `BentoDiff` compares two versions of structured content field by field, for
900
+ revision history, a review of what an agent changed, or a translation against its
901
+ source. Pass `before`, `after` and the `fields` to compare. A field is `text`
902
+ (compared by word), `rich` (HTML, compared on the blocks it renders, so markup
903
+ alone is no change) or `value` (compared whole). Unchanged fields collapse behind
904
+ a count. A side that is `null` renders the other whole, as a created or deleted
905
+ item. Every change is named in text as well as coloured. `BentoVersionRail` lists
906
+ the revisions (author, person or agent, time) as a listbox a reader picks two
907
+ from, by keyboard or pointer. Neither fetches anything.
908
+
909
+ `BentoCommandPalette` takes a second group whose items the product supplies per
910
+ query, beside the nav items it matches itself. Pass `onQueryChange` to hear the
911
+ query as typed and `group` to answer it:
912
+
913
+ ```tsx
914
+ <BentoCommandPalette
915
+ open={open}
916
+ onOpenChange={setOpen}
917
+ items={navItems}
918
+ onQueryChange={setQuery}
919
+ group={{ label: t("documents"), items: hits, pending, onSelect: openDocument }}
920
+ />
921
+ ```
922
+
923
+ Those items are rendered in the order given and never re-ranked — your search
924
+ already ranked them by relevance it understands, and a substring match here
925
+ would only undo that. Set `pending` while the query is in flight so the dialog
926
+ says so rather than showing an empty list that is not empty yet.
927
+
928
+ Bind the shell's keys with `useBentoShellShortcuts` rather than a keydown listener
929
+ of your own. ⌘K on macOS, Ctrl+K elsewhere, or `/` calls `onPalette`, and `?`
930
+ calls `onShortcuts`. A bare key typed into an input, a textarea, a select or an
931
+ editable node is left to the field. `BentoPaletteTrigger` is the header button,
932
+ with the same chord as its hint, and `BentoShortcutsDialog` lists the same set
933
+ (`BENTO_SHELL_SHORTCUTS`), so the guide shows exactly the keys that work:
934
+
935
+ ```tsx
936
+ useBentoShellShortcuts({
937
+ onPalette: () => setPaletteOpen((open) => !open),
938
+ onShortcuts: () => setShortcutsOpen((open) => !open),
939
+ });
940
+
941
+ <BentoShell headerStart={<><ProductMark /><BentoPaletteTrigger onClick={() => setPaletteOpen(true)} /></>}>
942
+ ```
943
+
469
944
  "Carries none of it" is measured rather than claimed. `pnpm run build` ends by
470
945
  bundling three fixtures through this package's own `exports` map, and fails if a
471
946
  bento module, a bento class name or a run of `bento.css`'s selectors reaches a
472
947
  consumer that imported only the root entry. `size-budget.json` records what each
473
- entry costs: today the whole root entry is 87 KB gzipped and `./bento` 22 KB,
948
+ entry costs: today the whole root entry is 91 KB gzipped and `./bento` 31 KB,
474
949
  measured with everything the build externalises left out, so the numbers
475
950
  describe this package rather than its dependencies. The preset's
476
951
  `--vg-bento-tone-*` tokens are deliberately not counted as the layer: they ship
@@ -484,7 +959,7 @@ correct rather than a leak. The subpath is for a consumer who wants only that
484
959
  background.
485
960
 
486
961
  Every subpath in the table above is also **imported, required and type-checked
487
- as you would use it** on each build: one fixture pulls all fifteen through the
962
+ as you would use it** on each build: one fixture pulls all seventeen through the
488
963
  real `exports` map, a CommonJS probe requires the seven that offer it, and a
489
964
  generated TypeScript probe imports a value from each typed entry and uses it —
490
965
  so a `types` field resolving to the wrong declarations fails here rather than
@@ -502,6 +977,14 @@ do, and npm cannot mark a peer required for one entry point and optional for
502
977
  another. `pnpm run build` fails if any other entry starts importing it, so the
503
978
  split above stays true rather than becoming folklore.
504
979
 
980
+ **`react-hook-form` is a required peer, and `react-router-dom` is the only
981
+ optional one.** It was a plain dependency until VDS77, which is the one case
982
+ where that is wrong: every other library this package shares state through is a
983
+ peer, and this is the one carrying a React context a duplicate would split. The
984
+ build already left it for the consumer to supply, so the manifest was promising
985
+ something the bundle did not deliver; `scripts/externals.test.ts` now fails if
986
+ anything externalised as a peer is declared a dependency again.
987
+
505
988
  [docs/BENTO-AUTHORING.md](docs/BENTO-AUTHORING.md) is the contract for writing a
506
989
  bento page: the three page shapes, what the package will not hold for you, and
507
990
  the colour, i18n, accessibility and layout rules that keep two consoles looking
@@ -521,8 +1004,9 @@ The catalogue proves a component; these prove the arrangement.
521
1004
  ## Contributing
522
1005
 
523
1006
  The package manager here is pnpm, matching the Shio, Turing, Dumont, Cloud Home
524
- and Schools workspaces. The Cloud Console and the Roadkeep GUI install with npm,
525
- which is a fact about those repositories and not about this one.
1007
+ and Schools workspaces. The Cloud Console, the Roadkeep GUI,
1008
+ the Openviglet Website and Viglet Docs install with npm, which is a fact about
1009
+ those repositories and not about this one.
526
1010
 
527
1011
  ```bash
528
1012
  pnpm install