@starci/grammar 0.11.0 → 0.12.1

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 (200) hide show
  1. package/README.md +143 -12
  2. package/dist/.build-stamp.json +4 -4
  3. package/dist/common/components-avatar-group.css +65 -0
  4. package/dist/common/components-forms.css +58 -73
  5. package/dist/common/components-list-panel.css +13 -0
  6. package/dist/common/components-navigation.css +39 -38
  7. package/dist/common/components-overlays.css +16 -1
  8. package/dist/common/components-surface-controls.css +15 -0
  9. package/dist/common/registry.d.ts +21 -20
  10. package/dist/common/registry.d.ts.map +1 -1
  11. package/dist/common/registry.js +2 -2
  12. package/dist/common/registry.js.map +1 -1
  13. package/dist/common/renderers-forms.d.ts +12 -12
  14. package/dist/common/renderers-navigation.d.ts +7 -7
  15. package/dist/common/renderers-navigation.d.ts.map +1 -1
  16. package/dist/common/renderers-navigation.js.map +1 -1
  17. package/dist/common/renderers.d.ts +2 -1
  18. package/dist/common/renderers.d.ts.map +1 -1
  19. package/dist/common/renderers.js +1 -0
  20. package/dist/common/renderers.js.map +1 -1
  21. package/dist/common/rule-catalog.generated.d.ts +2 -1
  22. package/dist/common/rule-catalog.generated.d.ts.map +1 -1
  23. package/dist/common/rule-catalog.generated.js +12 -2
  24. package/dist/common/rule-catalog.generated.js.map +1 -1
  25. package/dist/common/styles.css +163 -50
  26. package/dist/core/OtpInput.d.ts +3 -0
  27. package/dist/core/OtpInput.d.ts.map +1 -1
  28. package/dist/core/OtpInput.js +4 -2
  29. package/dist/core/OtpInput.js.map +1 -1
  30. package/dist/core/branch/AlertDialog/index.d.ts.map +1 -1
  31. package/dist/core/branch/AlertDialog/index.js +25 -20
  32. package/dist/core/branch/AlertDialog/index.js.map +1 -1
  33. package/dist/core/branch/ComboBox/index.d.ts +2 -2
  34. package/dist/core/branch/ComboBox/index.d.ts.map +1 -1
  35. package/dist/core/branch/ComboBox/index.js +3 -1
  36. package/dist/core/branch/ComboBox/index.js.map +1 -1
  37. package/dist/core/branch/DatePicker/index.d.ts +2 -2
  38. package/dist/core/branch/DatePicker/index.d.ts.map +1 -1
  39. package/dist/core/branch/DatePicker/index.js +3 -1
  40. package/dist/core/branch/DatePicker/index.js.map +1 -1
  41. package/dist/core/branch/DateRangePicker/index.d.ts +2 -2
  42. package/dist/core/branch/DateRangePicker/index.d.ts.map +1 -1
  43. package/dist/core/branch/DateRangePicker/index.js +3 -1
  44. package/dist/core/branch/DateRangePicker/index.js.map +1 -1
  45. package/dist/core/branch/Dialog/index.d.ts.map +1 -1
  46. package/dist/core/branch/Dialog/index.js +6 -2
  47. package/dist/core/branch/Dialog/index.js.map +1 -1
  48. package/dist/core/branch/Drawer/index.d.ts.map +1 -1
  49. package/dist/core/branch/Drawer/index.js +6 -2
  50. package/dist/core/branch/Drawer/index.js.map +1 -1
  51. package/dist/core/branch/DropdownMenu/index.d.ts +12 -5
  52. package/dist/core/branch/DropdownMenu/index.d.ts.map +1 -1
  53. package/dist/core/branch/DropdownMenu/index.js +13 -6
  54. package/dist/core/branch/DropdownMenu/index.js.map +1 -1
  55. package/dist/core/branch/ListBox/index.d.ts +20 -5
  56. package/dist/core/branch/ListBox/index.d.ts.map +1 -1
  57. package/dist/core/branch/ListBox/index.js +19 -9
  58. package/dist/core/branch/ListBox/index.js.map +1 -1
  59. package/dist/core/branch/Popover/index.d.ts.map +1 -1
  60. package/dist/core/branch/Popover/index.js +2 -1
  61. package/dist/core/branch/Popover/index.js.map +1 -1
  62. package/dist/core/branch/Rail/index.d.ts.map +1 -1
  63. package/dist/core/branch/Rail/index.js +4 -2
  64. package/dist/core/branch/Rail/index.js.map +1 -1
  65. package/dist/core/branch/Select/index.d.ts +6 -3
  66. package/dist/core/branch/Select/index.d.ts.map +1 -1
  67. package/dist/core/branch/Select/index.js +14 -21
  68. package/dist/core/branch/Select/index.js.map +1 -1
  69. package/dist/core/branch/SurfaceAccordionCard/index.d.ts.map +1 -1
  70. package/dist/core/branch/SurfaceAccordionCard/index.js +3 -1
  71. package/dist/core/branch/SurfaceAccordionCard/index.js.map +1 -1
  72. package/dist/core/branch/SurfaceCard/index.d.ts.map +1 -1
  73. package/dist/core/branch/SurfaceCard/index.js +3 -1
  74. package/dist/core/branch/SurfaceCard/index.js.map +1 -1
  75. package/dist/core/branch/SurfaceListCard/index.d.ts.map +1 -1
  76. package/dist/core/branch/SurfaceListCard/index.js +3 -1
  77. package/dist/core/branch/SurfaceListCard/index.js.map +1 -1
  78. package/dist/core/branch/Tooltip/index.d.ts +13 -0
  79. package/dist/core/branch/Tooltip/index.d.ts.map +1 -1
  80. package/dist/core/branch/Tooltip/index.js +40 -1
  81. package/dist/core/branch/Tooltip/index.js.map +1 -1
  82. package/dist/core/composite/AvatarButton/index.d.ts +3 -1
  83. package/dist/core/composite/AvatarButton/index.d.ts.map +1 -1
  84. package/dist/core/composite/AvatarButton/index.js +1 -1
  85. package/dist/core/composite/AvatarButton/index.js.map +1 -1
  86. package/dist/core/composite/AvatarGroup/index.d.ts +11 -3
  87. package/dist/core/composite/AvatarGroup/index.d.ts.map +1 -1
  88. package/dist/core/composite/AvatarGroup/index.js +9 -5
  89. package/dist/core/composite/AvatarGroup/index.js.map +1 -1
  90. package/dist/core/composite/CheckboxGroup/index.d.ts +2 -2
  91. package/dist/core/composite/CheckboxGroup/index.d.ts.map +1 -1
  92. package/dist/core/composite/CheckboxGroup/index.js +5 -1
  93. package/dist/core/composite/CheckboxGroup/index.js.map +1 -1
  94. package/dist/core/composite/IdentityLockup/index.d.ts +4 -2
  95. package/dist/core/composite/IdentityLockup/index.d.ts.map +1 -1
  96. package/dist/core/composite/IdentityLockup/index.js +1 -1
  97. package/dist/core/composite/IdentityLockup/index.js.map +1 -1
  98. package/dist/core/composite/RadioGroup/index.d.ts +2 -2
  99. package/dist/core/composite/RadioGroup/index.d.ts.map +1 -1
  100. package/dist/core/composite/RadioGroup/index.js +5 -1
  101. package/dist/core/composite/RadioGroup/index.js.map +1 -1
  102. package/dist/core/composite/Timeline/index.js +1 -1
  103. package/dist/core/composite/Timeline/index.js.map +1 -1
  104. package/dist/core/composition/ChatWorkspace/index.d.ts.map +1 -1
  105. package/dist/core/composition/ChatWorkspace/index.js +2 -1
  106. package/dist/core/composition/ChatWorkspace/index.js.map +1 -1
  107. package/dist/core/composition/Sidebar/index.d.ts +9 -2
  108. package/dist/core/composition/Sidebar/index.d.ts.map +1 -1
  109. package/dist/core/composition/Sidebar/index.js +49 -25
  110. package/dist/core/composition/Sidebar/index.js.map +1 -1
  111. package/dist/core/composition/WorkspaceShell/classNames.d.ts +1 -0
  112. package/dist/core/composition/WorkspaceShell/classNames.d.ts.map +1 -1
  113. package/dist/core/composition/WorkspaceShell/classNames.js +1 -0
  114. package/dist/core/composition/WorkspaceShell/classNames.js.map +1 -1
  115. package/dist/core/composition/WorkspaceShell/index.d.ts +30 -0
  116. package/dist/core/composition/WorkspaceShell/index.d.ts.map +1 -1
  117. package/dist/core/composition/WorkspaceShell/index.js +10 -4
  118. package/dist/core/composition/WorkspaceShell/index.js.map +1 -1
  119. package/dist/core/dna.d.ts +5 -0
  120. package/dist/core/dna.d.ts.map +1 -1
  121. package/dist/core/dna.js +5 -0
  122. package/dist/core/dna.js.map +1 -1
  123. package/dist/core/overlayScope.d.ts +16 -0
  124. package/dist/core/overlayScope.d.ts.map +1 -1
  125. package/dist/core/overlayScope.js +12 -0
  126. package/dist/core/overlayScope.js.map +1 -1
  127. package/dist/core/primitive/Avatar/index.d.ts +11 -6
  128. package/dist/core/primitive/Avatar/index.d.ts.map +1 -1
  129. package/dist/core/primitive/Avatar/index.js +21 -4
  130. package/dist/core/primitive/Avatar/index.js.map +1 -1
  131. package/dist/core/primitive/Button/index.d.ts +1 -1
  132. package/dist/core/primitive/Button/index.d.ts.map +1 -1
  133. package/dist/core/primitive/Button/index.js +3 -3
  134. package/dist/core/primitive/Button/index.js.map +1 -1
  135. package/dist/core/primitive/Checkbox/index.d.ts +2 -2
  136. package/dist/core/primitive/Checkbox/index.d.ts.map +1 -1
  137. package/dist/core/primitive/Checkbox/index.js +5 -1
  138. package/dist/core/primitive/Checkbox/index.js.map +1 -1
  139. package/dist/core/primitive/DateField/index.d.ts +2 -2
  140. package/dist/core/primitive/DateField/index.d.ts.map +1 -1
  141. package/dist/core/primitive/DateField/index.js +5 -1
  142. package/dist/core/primitive/DateField/index.js.map +1 -1
  143. package/dist/core/primitive/Field/index.d.ts +3 -2
  144. package/dist/core/primitive/Field/index.d.ts.map +1 -1
  145. package/dist/core/primitive/Field/index.js.map +1 -1
  146. package/dist/core/primitive/IconButton/index.d.ts +2 -2
  147. package/dist/core/primitive/IconButton/index.d.ts.map +1 -1
  148. package/dist/core/primitive/IconButton/index.js +1 -1
  149. package/dist/core/primitive/IconButton/index.js.map +1 -1
  150. package/dist/core/primitive/Input/index.d.ts +4 -1
  151. package/dist/core/primitive/Input/index.d.ts.map +1 -1
  152. package/dist/core/primitive/Input/index.js +5 -3
  153. package/dist/core/primitive/Input/index.js.map +1 -1
  154. package/dist/core/primitive/NumberField/index.d.ts +2 -2
  155. package/dist/core/primitive/NumberField/index.d.ts.map +1 -1
  156. package/dist/core/primitive/NumberField/index.js +5 -1
  157. package/dist/core/primitive/NumberField/index.js.map +1 -1
  158. package/dist/core/primitive/PlainPressable/index.d.ts +3 -1
  159. package/dist/core/primitive/PlainPressable/index.d.ts.map +1 -1
  160. package/dist/core/primitive/PlainPressable/index.js +1 -1
  161. package/dist/core/primitive/PlainPressable/index.js.map +1 -1
  162. package/dist/core/primitive/PressableField/index.d.ts +4 -1
  163. package/dist/core/primitive/PressableField/index.d.ts.map +1 -1
  164. package/dist/core/primitive/PressableField/index.js +5 -1
  165. package/dist/core/primitive/PressableField/index.js.map +1 -1
  166. package/dist/core/primitive/RelativeTime/clock.d.ts +5 -0
  167. package/dist/core/primitive/RelativeTime/clock.d.ts.map +1 -0
  168. package/dist/core/primitive/RelativeTime/clock.js +28 -0
  169. package/dist/core/primitive/RelativeTime/clock.js.map +1 -0
  170. package/dist/core/primitive/RelativeTime/index.d.ts +18 -0
  171. package/dist/core/primitive/RelativeTime/index.d.ts.map +1 -0
  172. package/dist/core/primitive/RelativeTime/index.js +45 -0
  173. package/dist/core/primitive/RelativeTime/index.js.map +1 -0
  174. package/dist/core/primitive/SearchField/index.d.ts +4 -2
  175. package/dist/core/primitive/SearchField/index.d.ts.map +1 -1
  176. package/dist/core/primitive/SearchField/index.js +5 -1
  177. package/dist/core/primitive/SearchField/index.js.map +1 -1
  178. package/dist/core/primitive/Switch/index.d.ts +9 -4
  179. package/dist/core/primitive/Switch/index.d.ts.map +1 -1
  180. package/dist/core/primitive/Switch/index.js +1 -1
  181. package/dist/core/primitive/Switch/index.js.map +1 -1
  182. package/dist/core/primitive/TextAction/index.d.ts.map +1 -1
  183. package/dist/core/primitive/TextAction/index.js +3 -1
  184. package/dist/core/primitive/TextAction/index.js.map +1 -1
  185. package/dist/core/primitive/Textarea/index.d.ts +2 -2
  186. package/dist/core/primitive/Textarea/index.d.ts.map +1 -1
  187. package/dist/core/primitive/Textarea/index.js +3 -1
  188. package/dist/core/primitive/Textarea/index.js.map +1 -1
  189. package/dist/core/primitive/TimeField/index.d.ts +2 -2
  190. package/dist/core/primitive/TimeField/index.d.ts.map +1 -1
  191. package/dist/core/primitive/TimeField/index.js +5 -1
  192. package/dist/core/primitive/TimeField/index.js.map +1 -1
  193. package/dist/core/primitive/actionStyles.d.ts +1 -1
  194. package/dist/core/primitive/actionStyles.d.ts.map +1 -1
  195. package/dist/core/surfaceContext.d.ts +11 -0
  196. package/dist/core/surfaceContext.d.ts.map +1 -0
  197. package/dist/core/surfaceContext.js +14 -0
  198. package/dist/core/surfaceContext.js.map +1 -0
  199. package/dist/offset-pop/components-overlays.css +1 -1
  200. package/package.json +8 -3
package/README.md CHANGED
@@ -17,7 +17,7 @@ npm install @starci/grammar @heroui/react react react-dom
17
17
  npm install ./.claude/packages/grammar
18
18
  ```
19
19
 
20
- The package needs Node.js 20 or newer. Its peer dependencies are React 18+, `@xyflow/react` 12 (for `FlowGraph`) and `@heroui/react`
20
+ The package needs Node.js 22 or newer. Its peer dependencies are React 18+, `@xyflow/react` 12 (for `FlowGraph`) and `@heroui/react`
21
21
  3.2+. Load the HeroUI stylesheet once in the app, as HeroUI's own setup describes. Then import
22
22
  exactly one Grammar family stylesheet. Each family sheet already imports Common anatomy first.
23
23
 
@@ -36,6 +36,18 @@ its root from one place. Heritage does not re-export Common. It exports `heritag
36
36
 
37
37
  ## Usage
38
38
 
39
+ ### App shell
40
+
41
+ The [app-shell canon](../../knowledge/ui/composition/app-shell.yaml) owns `FE-APP-SHELL-1` through `FE-APP-SHELL-7`.
42
+ The `Compositions/App shell` stories show the fixed shell, grouped sidebar, icon rail and mobile drawer.
43
+
44
+ - `size="control"` is available on `Button`, `IconButton`, `TextAction`, `PlainPressable`, `IdentityLockup`, `Input` and `SearchField`, alongside `Switch`, `Avatar` and `AvatarButton`.
45
+ - `SidebarItem.badge`, `badgeLabel` and `badgeMax` show and announce unread counts in both sidebar presentations. `SidebarGroup.placement="end"` places settings groups after the main groups and pushes them to the bottom.
46
+ - The collapsed Sidebar uses Grammar's internal `AnchoredTooltip` on hover and focus. Its native popover reaches the top layer without leaving the family DOM scope or changing HeroUI's collection anatomy. `onAction` runs for every activated destination, including the selected item, so a drawer can close consistently.
47
+ - A fixed `WorkspaceShell` frames its `compactHeader` slot as the mobile opener band; pass a client `Drawer` with an ellipsis `IconButton` trigger. The `navigationDrawer` form owns the same band when it supplies the opener.
48
+ - `AvatarButton seed` passes the stable identity id to Grammar's shared `Avatar`, which draws the deterministic local DiceBear image; an uploaded `src` takes precedence.
49
+ - `DropdownMenuItem.mark` accepts a decorative identity mark and is mutually exclusive with `iconSource`. A destructive item uses `tone="danger"`; the existing `negative` tone has the same vendor presentation.
50
+
39
51
  ### Common (neutral structure only)
40
52
 
41
53
  ```tsx
@@ -140,20 +152,77 @@ Each group below matches a Storybook group. Run `npm run storybook`: every story
140
152
  the family and theme chosen in the toolbar. It shows the default, disabled, invalid, pending and
141
153
  selected/current states where they apply. Overlay stories open by default.
142
154
 
143
- **Primitives (26).** Avatar, Badge, Button, CloseButton, Divider, GrammarRoot, Heading, Icon,
155
+ **Primitives (27).** Avatar, Badge, Button, CloseButton, Divider, GrammarRoot, Heading, Icon,
144
156
  IconButton, IconTile, Image, IncludedMark, Kbd, Label, LeadingNumber, Link, Meter, PlainPressable,
145
- Progress, ProgressCircle, RankArtwork, Skeleton, Spinner, StateMark, Text, TextAction.
157
+ Progress, ProgressCircle, RankArtwork, RelativeTime, Skeleton, Spinner, StateMark, Text, TextAction.
146
158
 
147
159
  **Forms (24).** ButtonGroup, Checkbox, CheckboxGroup, ComboBox, DateField, DatePicker,
148
160
  DateRangePicker, Field, Fieldset, FileDropzone, Form, Input, NumberField, OtpInput,
149
161
  PressableField, RadioGroup, Rating, SearchField, SegmentedControl, Select, Slider, Switch,
150
162
  Textarea, TimeField. Every Field-contract control takes `label`, `description`, `errorMessage`,
151
163
  `isRequired`, `isDisabled`, `isReadOnly` and `isInvalid`. `Select`, `ComboBox`, `SearchField` and
152
- `Form` also take `isPending`. `OtpInput` has no label prop, so name it with a `<label for>`.
164
+ `Form` also take `isPending`. `OtpInput` names the code with `label` or an existing label's `labelledBy`.
153
165
 
154
166
  **Overlays (9).** Alert, AlertDialog, Dialog, Drawer, DropdownMenu, Popover, Toast, Toaster,
155
- Tooltip. Dialog, AlertDialog, Drawer, Popover and DropdownMenu are controlled through
156
- `isOpen`, `defaultOpen` and `onOpenChange`.
167
+ Tooltip. Dialog, AlertDialog, Drawer, Popover and DropdownMenu support uncontrolled `defaultOpen`
168
+ and controlled `isOpen` / `onOpenChange` through the shared overlay contract.
169
+
170
+ `DropdownMenuItem` accepts either `iconSource` or `mark`, a `ReactNode` such as a logo; its public
171
+ type rejects both together. Grammar renders either in the same decorative leading slot, with the
172
+ same alignment and icon-size rule. Keep the visible item label as its accessible name and supply
173
+ the mark without visual slot CSS.
174
+
175
+ `RelativeTime` renders `<time dateTime>` for live relative copy such as "5 minutes ago" and a
176
+ countdown such as "in 5 minutes". Pass `dateTime` as a timezone-qualified ISO string and `locale`
177
+ as the active BCP 47 locale. `mode="relative"` is the default; `mode="countdown"` reduces the time
178
+ remaining and settles at the locale's "now" when the target expires. `size` (`xs`, `sm`, `md`) and
179
+ `tone` (`default`, `muted`, `accent`) use Common's text presentation.
180
+
181
+ ```tsx
182
+ <RelativeTime dateTime="2026-10-25T09:00:00Z" locale="en" />
183
+ <RelativeTime dateTime="2026-10-25T10:00:00+01:00" locale="en" mode="countdown" />
184
+ ```
185
+
186
+ The server and initial hydration render show `fallback`, or the canonical ISO instant when it is
187
+ omitted. After hydration, every instance reads the same shared ticker. The first subscriber starts
188
+ it and the last unsubscribe stops it; leaves own no clock or refresh interval. Supply translated
189
+ `fallback` copy when an ISO instant is unsuitable. Time changes are not a live announcement, so a
190
+ page of timestamps does not repeatedly interrupt assistive technology. Grammar owns native
191
+ `Intl.RelativeTimeFormat` for this primitive; other application date display follows FE-DATE.
192
+
193
+ **DropdownMenu entries.** `entries` is a list of items and sections; the app writes data and no markup, no separator, no
194
+ className and no size. An item takes `id`, `label`, `description`, `shortcut`, `href`, `isDisabled`, `submenu` and:
195
+
196
+ - `iconSource`: a glyph component (the app's Heroicons). Grammar draws it in HeroUI's leading position at the canon icon size,
197
+ 20px (`size-5`, `Icon` `leading`; the Heroicons rule R93 / FE-ICON). HeroUI's own docs example draws its sample glyph at
198
+ `size-4`, which is example content and not a component default, so 20px is StarCi's icon rule and not a vendor override.
199
+ - `mark`: a decorative node instead of `iconSource`, e.g. an identity mark (`<Avatar size="sm" shape="rounded" isDecorative />`). The public type rejects both props together.
200
+ - `tone: "danger"`: HeroUI's `danger` variant, which colours the label and the glyph. Sign out, delete, remove, leave workspace
201
+ and revoke are destructive; the lint rule `starci-fe/destructive-menu-entry-needs-danger-tone` refuses such an entry without it.
202
+
203
+ A section takes `label` and optionally `selection` (`mode: "single" | "multiple"`, `selectedIds`, `onChange`). Between two
204
+ adjacent entries, when either is a section, Grammar always inserts HeroUI's own `Separator`: the app cannot forget one. The selection
205
+ mark is HeroUI's `Dropdown.ItemIndicator` in its default placement (the check sits at the start of the item, and at the end only for
206
+ an item with a submenu).
207
+
208
+ ```tsx
209
+ <DropdownMenu
210
+ trigger={<AvatarButton name="Alex Morgan" />}
211
+ placement="bottom end"
212
+ entries={[
213
+ { kind: "section", id: "workspace", label: "Workspace", selection: { mode: "single", selectedIds, onChange },
214
+ items: [{ id: "hakuen", label: "Hakuen", description: "Owner", mark: <Avatar name="Hakuen" size="sm" shape="rounded" isDecorative /> }] },
215
+ { kind: "section", id: "account", items: [{ id: "profile", label: "Profile", description: "Name and photo", iconSource: UserIcon }] },
216
+ { kind: "section", id: "session", items: [{ id: "sign-out", label: "Sign out", tone: "danger", iconSource: ArrowRightStartOnRectangleIcon }] },
217
+ ]}
218
+ />
219
+ ```
220
+
221
+ **HeroUI parity.** Grammar is a wrapper of HeroUI, so a wrapper keeps HeroUI's anatomy and defaults. The vendor reference
222
+ (`knowledge/vendor/heroui/<component>.yaml` in the StarCi runtime) is measured from the installed `@heroui/react` and
223
+ `@heroui/styles`; the gate `GRAMMAR_VENDOR_PARITY` fails a wrapper that renders a part HeroUI does not have, omits one it
224
+ requires, restyles what the vendor decides, or replaces a vendor default, unless an owner-pinned override declares it. `ListBox`
225
+ and `Select` are HeroUI's own list-box items (a `Label`, a `Description`, the item indicator at the end of the row).
157
226
 
158
227
  **Navigation & Data (14).** Accordion, AvatarGroup, Breadcrumbs, Calendar, DataTable,
159
228
  DescriptionList, Disclosure, ListBox, Pagination, Stepper, Subnav, Tabs, TagGroup, Timeline.
@@ -192,27 +261,89 @@ defines the native loading and caption behaviour (reviewed 2026-10-18).
192
261
  `thumbIcon` / `selectedThumbIcon` for a glyph inside the thumb, so a sun/moon theme switch is
193
262
  `<Switch size="lg" label="Dark mode" thumbIcon={Sun} selectedThumbIcon={Moon} />`.
194
263
 
264
+ `Switch size="control"` and `Avatar` / `AvatarButton size="control"` share the control height (2.75rem, the
265
+ IconButton of a bar), so a language button, a theme switch, a bell and an avatar line up at one height; `Switch
266
+ isLabelHidden` keeps its label as the accessible name and draws nothing. `WorkspaceShell viewport="fixed"` is one
267
+ viewport tall (`100dvh`) with the header on top and the navigation and primary regions scrolling on their own, so an
268
+ app writes no viewport arithmetic; `navigationDrawer` adds the compact-width opener row (the bar's height, an
269
+ edge-to-edge divider, a round tertiary icon button) and its left Sidebar drawer. Pass `defaultOpen`
270
+ to let the Drawer own its state; no state hook or callback is needed in the layout. The shell
271
+ reuses the shared overlay contract, so optional `isOpen` / `onOpenChange` remain available for
272
+ coordinated client interactions. The opener is the Drawer's own trigger, which owns activation,
273
+ dismissal and focus return.
274
+
195
275
  `PlainPressable` is a pressable that does not look like a button: no padding, background, border or
196
276
  shape in any state, no hover effect, a zoom-out on press, a focus ring, and no zoom under
197
277
  `prefers-reduced-motion`. `AvatarButton` is a circle-only avatar built on it (an account-menu
198
278
  trigger) and `IdentityLockup` a mark beside a title and a muted subtitle with no caret (a workspace
199
- switcher trigger). All three work as the `trigger` of `DropdownMenu` and `Popover`. `IdentityLockup`'s mark is a transparent, uncropped square by default (`markShape="rounded"` opts in to a
279
+ switcher trigger). All three work as the `trigger` of `DropdownMenu` and `Popover`. `IdentityLockup`'s mark and monogram fallback are transparent, with foreground-colored initials; the mark is an uncropped square by default (`markShape="rounded"` opts in to a
200
280
  corner) and `Avatar` takes `shape="square" | "rounded"`.
201
281
 
282
+ Personal avatars are circles. Pass the user or identity id as `seed` on `Avatar`, `AvatarButton`
283
+ or each `AvatarGroup` member. Grammar renders the fixed Lorelei DiceBear style chosen by its DNA,
284
+ memoized for that seed with identical server and client output. An uploaded `src` wins; with
285
+ neither `src` nor `seed`, the existing initials fallback remains. Images have `alt=""`; the avatar
286
+ shell carries its name, or `isDecorative` hides it beside a visible name. `AvatarButton` names the
287
+ button and keeps its inner avatar decorative. Apps install and import Grammar; its pinned
288
+ DiceBear runtime dependencies and avatar drawing stay inside the package.
289
+ The package requires Node 22 or newer, as does the pinned DiceBear engine.
290
+
291
+ ```tsx
292
+ <Avatar name={user.name} seed={user.id} isDecorative />
293
+ <AvatarButton name={user.name} seed={user.id} src={user.avatarUrl} />
294
+ <AvatarGroup label="Team" items={members.map(user => ({ id: user.id, name: user.name, seed: user.id }))}
295
+ overflowLabel={count => `${count} more members`} />
296
+ ```
297
+
202
298
  **Surfaces (14).** EmptyNotice, FencedCodeBlock, HorizontalScrollRegion, MarkdownArticle,
203
299
  MarkdownTableFrame, MediaFrame, Rail, SectionHeader, StaticStateRow, SurfaceAccordionCard,
204
300
  SurfaceCard, SurfaceCopyGroup, SurfaceListCard, VerticalScrollRegion.
205
301
 
206
302
  **Compositions (9).** BottomNav, ChatWorkspace, Footer, NavigationFeatureNav, PageContainer,
207
- PrimaryRailLayout, Sidebar, TopBar, WorkspaceShell. Storybook also pins three page recipes to
208
- each family: a settings form, a data list with a filter drawer and pagination, and a mobile home
209
- with TopBar, BottomNav and a Toast.
303
+ PrimaryRailLayout, Sidebar, TopBar, WorkspaceShell. Storybook pins settings and data-list content
304
+ recipes to each family; the mobile app recipes reuse the fixed App shell with its left navigation drawer.
210
305
 
211
306
  `COMMON_GRAMMAR_COMPONENTS` is the frozen registry of the published renderers.
212
307
  `COMMON_FORMS_COMPONENTS`, `COMMON_OVERLAYS_COMPONENTS` and `COMMON_NAVIGATION_COMPONENTS` are the
213
308
  0.5.0 groups. `defineGrammarFamily` builds a new family over the registry. It may replace
214
309
  renderers with prop-compatible versions and add extensions whose names don't collide with Common's.
215
310
 
311
+ ### Surface controls, panel lists and group identity
312
+
313
+ SurfaceCard (including `frame="frameless"`), SurfaceListCard, SurfaceAccordionCard, Rail, Sidebar
314
+ and the content of Drawer, Dialog, AlertDialog and Popover supply the nesting context of ANATOMY-2.
315
+ Omit a field's `variant`: Input, SearchField, Textarea, NumberField, DateField, TimeField, Select,
316
+ ComboBox, DatePicker, DateRangePicker, Checkbox, CheckboxGroup, RadioGroup, PressableField and OtpInput use `secondary`
317
+ inside these surfaces and `primary` outside. Overlay triggers keep their surrounding context.
318
+ The FE canon's `FE_NESTED_VARIANT` rejects an explicit primary field inside a surface and raw
319
+ `bg-surface` paint in application classNames.
320
+
321
+ ListBox defaults to `inset="panel"` inside the same context. The list container extends into the
322
+ panel gutter so row content, loading content and section captions align with the header and search
323
+ field without padding wrappers (PADDING-0). Items keep HeroUI's default padding and selection-indicator
324
+ space; row backgrounds extend into the gutter. Outside a surface, the vendor list inset stays;
325
+ `inset="panel"` also selects the alignment explicitly. Pass flat `items` or named `sections`
326
+ (`ListBoxSection`), with unique item ids across the whole list. `FE_LIST_PANEL_PADDING` rejects
327
+ horizontal padding wrappers around a list or its section caption. Sidebar's expanded and collapsed
328
+ rows also retain HeroUI item padding.
329
+
330
+ ```tsx
331
+ <SurfaceCard ariaLabel="Conversations">
332
+ <Heading level={2}>Conversations</Heading>
333
+ <SearchField label="Find a conversation" />
334
+ <ListBox label="Conversations" sections={[
335
+ { id: "recent", label: "Recent", items: [{ id: "team", label: "Design team" }] },
336
+ ]} />
337
+ </SurfaceCard>
338
+ ```
339
+
340
+ For a conversation or group identity, use `AvatarGroup layout="grid"` (COLLECTION-3, the Zalo
341
+ reference). It is one round Avatar-sized mark at `sm`, `md`, `lg` or `control`: two members split
342
+ the circle, three show one above two, and four use a 2 by 2 grid with a hairline gap. With more
343
+ members, the fourth cell shows `+N` for everyone beyond the first three. Each member uses Avatar's
344
+ uploaded image, `seed` and initials fallback, and accepts the same custom `fallback`. A single
345
+ person uses Avatar. The default `layout="row"` keeps the overlapping member row.
346
+
216
347
  ## Theming
217
348
 
218
349
  - **Tokens.** Each family writes its DNA as CSS custom properties on its root: `--starci-core-*`,
@@ -224,8 +355,8 @@ renderers with prop-compatible versions and add extensions whose names don't col
224
355
  `--starci-core-surface-tertiary`; HeroUI's `--surface-tertiary` is left to Common and is never
225
356
  re-bound by the family. To adjust a family, override its tokens on the
226
357
  root, for example `<CoreGrammarRoot style={{ "--starci-core-accent": "#2f6bff" }}>`. The
227
- `*_TOKEN_NAMES` / `*_TOKEN_DEFAULTS` exports are the supported list. Values are validated by the
228
- family specs, not at runtime.
358
+ `*_TOKEN_NAMES` / `*_TOKEN_DEFAULTS` exports are the supported list. The DNA generator measures
359
+ their packaged values; callers own their token overrides.
229
360
  - **Spacing.** Every family uses the Common 4px scale (`COMMON_SPACING_SCALE`,
230
361
  `COMMON_SPACING_TOKENS`), and Common's `--grammar-*-gap` / `--grammar-page-inset` variables.
231
362
  - **Dark.** `theme` on any root takes `"light"`, `"dark"` or `"system"` (the default), which sets
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schema": "starci/grammar-build-stamp@1",
3
3
  "package": "@starci/grammar",
4
- "version": "0.11.0",
4
+ "version": "0.12.1",
5
5
  "algorithm": "starci/grammar-source-digest@1",
6
- "sourceDigest": "sha256:9cb2cc563d7e0a1acdcc56a70dbc7c9944be34777c34cb8c6fcb1b5a247aba66",
7
- "distDigest": "sha256:5a334f07de9b640740b93c311007b8bdcaec035154a457f2cd3721e4386ed919",
8
- "builtAt": "2026-10-25T07:24:32.396Z"
6
+ "sourceDigest": "sha256:f6fce286c3f714d14372fa19725d4d9b92b8ed98d49342f022224250b09a1e14",
7
+ "distDigest": "sha256:53e4a14158e106244c9d13a98bdde5ad226f689c070e7170ecd71d58bb7a0120",
8
+ "builtAt": "2026-10-25T12:13:44.249Z"
9
9
  }
@@ -0,0 +1,65 @@
1
+ @layer starci-grammar-common {
2
+ /* The vendor Avatar shell supplies the exact single-avatar dimensions, including the control size. */
3
+ .starci-core-avatar-group[data-grammar-avatar-layout="grid"] {
4
+ --starci-avatar-group-text-size: calc(var(--avatar-size, 2.5rem) / 4);
5
+ overflow: hidden;
6
+ border-radius: 50%;
7
+ }
8
+
9
+ .starci-core-avatar-group-grid {
10
+ display: grid;
11
+ grid-template-columns: repeat(2, minmax(0, 1fr));
12
+ grid-template-rows: repeat(2, minmax(0, 1fr));
13
+ gap: 1px;
14
+ inline-size: 100%;
15
+ block-size: 100%;
16
+ background: var(--surface, Canvas);
17
+ }
18
+
19
+ .starci-core-avatar-group[data-grammar-avatar-cells="1"] > .starci-core-avatar-group-grid {
20
+ grid-template-columns: minmax(0, 1fr);
21
+ grid-template-rows: minmax(0, 1fr);
22
+ }
23
+
24
+ .starci-core-avatar-group[data-grammar-avatar-cells="2"] > .starci-core-avatar-group-grid {
25
+ grid-template-rows: minmax(0, 1fr);
26
+ }
27
+
28
+ /* Zalo's three-member identity has one full-width member above the two remaining members. */
29
+ .starci-core-avatar-group[data-grammar-avatar-cells="3"] .starci-core-avatar-group-member:first-child {
30
+ grid-column: 1 / -1;
31
+ }
32
+
33
+ .starci-core-avatar-group-grid > .starci-core-avatar-group-member {
34
+ inline-size: 100% !important;
35
+ block-size: 100% !important;
36
+ min-inline-size: 0;
37
+ min-block-size: 0;
38
+ border-radius: 0 !important;
39
+ font-size: inherit;
40
+ }
41
+
42
+ .starci-core-avatar-group-member > * {
43
+ border-radius: 0 !important;
44
+ }
45
+
46
+ .starci-core-avatar-group:not([data-grammar-avatar-cells="1"]) .starci-core-avatar-group-member [data-grammar-avatar-fallback] {
47
+ font-size: var(--starci-avatar-group-text-size) !important;
48
+ }
49
+
50
+ .starci-core-avatar-group-member img {
51
+ object-fit: cover;
52
+ }
53
+
54
+ .starci-core-avatar-group-count {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ min-inline-size: 0;
59
+ background: var(--surface-secondary, Canvas);
60
+ color: var(--foreground, CanvasText);
61
+ font-size: var(--starci-avatar-group-text-size);
62
+ font-weight: 600;
63
+ line-height: 1;
64
+ }
65
+ }
@@ -145,7 +145,10 @@
145
145
  border-radius: var(--field-radius, calc(var(--radius, 0.5rem) * 1.5));
146
146
  }
147
147
 
148
- .starci-core-field-skeleton-control:is([data-grammar-field-skeleton="select"], [data-grammar-field-skeleton="input"]) { block-size: 2.5rem; }
148
+ .starci-core-field-skeleton-control:is([data-grammar-field-skeleton="input"]) { block-size: 2.5rem; }
149
+
150
+ /* The select trigger is HeroUI's own (min-h-9). */
151
+ .starci-core-field-skeleton-control[data-grammar-field-skeleton="select"] { block-size: 2.25rem; }
149
152
 
150
153
  /* Joined segments: one block, a segment-width (5rem) per option, never wider than the field; `fill` takes the whole row. */
151
154
  .starci-core-field-skeleton-control[data-grammar-field-skeleton="segmented"] {
@@ -161,6 +164,7 @@
161
164
  .starci-core-field-skeleton-control[data-grammar-field-skeleton="switch"] { flex: none; block-size: 1.25rem; inline-size: 2rem; }
162
165
  .starci-core-field-skeleton-control[data-grammar-field-skeleton="switch"] > .starci-core-skeleton { border-radius: 999px; }
163
166
  .starci-core-field-skeleton-control[data-grammar-field-skeleton="switch"][data-grammar-switch-size="lg"] { block-size: 2.25rem; inline-size: 4rem; }
167
+ .starci-core-field-skeleton-control[data-grammar-field-skeleton="switch"][data-grammar-switch-size="control"] { block-size: var(--starci-core-control-min-size, 2.75rem); inline-size: 4.875rem; }
164
168
 
165
169
  .starci-core-field-skeleton-choice-label {
166
170
  color: var(--foreground, CanvasText);
@@ -178,7 +182,6 @@
178
182
  }
179
183
  }
180
184
 
181
- .starci-core-select-trigger,
182
185
  .starci-core-combo-box-group,
183
186
  .starci-core-search-field-group,
184
187
  .starci-core-number-field-group,
@@ -187,7 +190,6 @@
187
190
  min-width: 0;
188
191
  }
189
192
 
190
- .starci-core-select-value,
191
193
  .starci-core-combo-box-input,
192
194
  .starci-core-search-field-input,
193
195
  .starci-core-number-field-input {
@@ -203,7 +205,6 @@
203
205
  * inside inherits the corner (no square right side behind the trigger) and stretches to the group's height.
204
206
  */
205
207
  .starci-core-input-field,
206
- .starci-core-select-trigger,
207
208
  .starci-core-combo-box-group,
208
209
  .starci-core-search-field-group,
209
210
  .starci-core-number-field-group,
@@ -236,11 +237,6 @@
236
237
  }
237
238
  }
238
239
 
239
- .starci-core-select-trigger {
240
- align-items: center;
241
- padding-inline: 0.75rem 1.75rem;
242
- }
243
-
244
240
  .starci-core-combo-box-group {
245
241
  align-items: stretch;
246
242
  }
@@ -258,40 +254,6 @@
258
254
  padding-inline-end: 0.5rem;
259
255
  }
260
256
 
261
- /*
262
- * The selected value of a Select stays ONE line and keeps the field height: it ellipsizes (the trigger carries the
263
- * full text as its title) and shows only the option's label, never its description. Popover options still wrap.
264
- */
265
- .starci-core-select-value {
266
- flex: 1 1 0;
267
- overflow: hidden;
268
- overflow-wrap: normal;
269
- text-overflow: ellipsis;
270
- white-space: nowrap;
271
- }
272
-
273
- .starci-core-select-value-text {
274
- display: block;
275
- min-width: 0;
276
- overflow: hidden;
277
- text-overflow: ellipsis;
278
- }
279
-
280
- .starci-core-select-value .starci-core-list-option-copy {
281
- display: block;
282
- overflow: hidden;
283
- text-overflow: ellipsis;
284
- }
285
-
286
- .starci-core-select-value .starci-core-list-option-label {
287
- overflow-wrap: normal;
288
- }
289
-
290
- .starci-core-select-value .starci-core-list-option-description,
291
- .starci-core-select-value [data-slot="list-box-item-indicator"] {
292
- display: none;
293
- }
294
-
295
257
  .starci-core-date-input {
296
258
  min-width: 0;
297
259
  flex: 1;
@@ -303,22 +265,14 @@
303
265
  color: var(--muted, GrayText);
304
266
  }
305
267
 
306
- /* --- List popover (Select, ComboBox) --- */
307
- .starci-core-list-option-copy {
268
+ /* --- List popover (Select, ComboBox) ---
269
+ * An option is HeroUI's own: a `Label`, with a `Description` under it inside one column when it has one, then the
270
+ * item indicator. The column is the only Grammar element; it lays out and paints nothing. */
271
+ [data-grammar-option-copy] {
308
272
  display: flex;
309
273
  min-width: 0;
310
274
  flex: 1;
311
275
  flex-direction: column;
312
- gap: 0.25rem;
313
- }
314
-
315
- .starci-core-list-option-label {
316
- overflow-wrap: anywhere;
317
- }
318
-
319
- .starci-core-list-option-description {
320
- color: var(--muted, GrayText);
321
- font-size: 0.75rem;
322
276
  }
323
277
 
324
278
  .starci-core-list-empty {
@@ -356,44 +310,75 @@
356
310
  }
357
311
 
358
312
  /*
359
- * --- Switch size "lg": the StarCi look ---
360
- * A 4rem by 2.25rem fully round track, a 1.75rem round thumb inset 0.25rem, an optional glyph centred in the thumb.
361
- * Keyed on the stamps the component emits (`data-grammar-switch-size`, `data-grammar-switch-thumb`, `data-grammar-switch-icon`),
362
- * never on vendor class names. The geometry is `!important` because consumers import this sheet on either side of
363
- * `@heroui/styles`, so layer rank cannot decide against the vendor `.switch--*` size rules. The on position is a margin
364
- * (as HeroUI's own thumb), the percentage resolving against the track: 100% - thumb - inset.
313
+ * --- Switch sizes "lg" (the StarCi look) and "control" ---
314
+ * `lg`: a 4rem by 2.25rem fully round track, a thumb the track height less 0.5rem inset 0.25rem, an optional glyph centred in the
315
+ * thumb. `control`: the same shape at the shared control height (2.75rem), so a theme switch lines up with the IconButtons and the
316
+ * avatar of a bar. Keyed on the stamps the component emits (`data-grammar-switch-size`, `data-grammar-switch-thumb`,
317
+ * `data-grammar-switch-icon`), never on vendor class names. The geometry is `!important` because consumers import this sheet on
318
+ * either side of `@heroui/styles`, so layer rank cannot decide against the vendor `.switch--*` size rules. The on position is a
319
+ * margin (as HeroUI's own thumb), the percentage resolving against the track: 100% - thumb - inset.
365
320
  */
366
- [data-component="Switch"][data-grammar-switch-size="lg"] [data-grammar-choice-control="switch"] {
367
- block-size: 2.25rem !important;
368
- inline-size: 4rem !important;
321
+ [data-component="Switch"][data-grammar-switch-size="lg"] {
322
+ --grammar-switch-height: 2.25rem;
323
+ --grammar-switch-width: 4rem;
324
+ }
325
+
326
+ [data-component="Switch"][data-grammar-switch-size="control"] {
327
+ --grammar-switch-height: var(--starci-core-control-min-size, 2.75rem);
328
+ --grammar-switch-width: 4.875rem;
329
+ }
330
+
331
+ [data-component="Switch"]:is([data-grammar-switch-size="lg"], [data-grammar-switch-size="control"]) {
332
+ --grammar-switch-thumb: calc(var(--grammar-switch-height) - 0.5rem);
333
+ }
334
+
335
+ [data-component="Switch"]:is([data-grammar-switch-size="lg"], [data-grammar-switch-size="control"]) [data-grammar-choice-control="switch"] {
336
+ block-size: var(--grammar-switch-height) !important;
337
+ inline-size: var(--grammar-switch-width) !important;
369
338
  border-radius: 9999px !important;
370
339
  }
371
340
 
372
- [data-component="Switch"][data-grammar-switch-size="lg"] [data-grammar-switch-thumb] {
373
- block-size: 1.75rem !important;
374
- inline-size: 1.75rem !important;
341
+ [data-component="Switch"]:is([data-grammar-switch-size="lg"], [data-grammar-switch-size="control"]) [data-grammar-switch-thumb] {
342
+ block-size: var(--grammar-switch-thumb) !important;
343
+ inline-size: var(--grammar-switch-thumb) !important;
375
344
  border-radius: 9999px !important;
376
345
  margin-inline-start: 0.25rem !important;
377
346
  align-items: center;
378
347
  justify-content: center;
379
348
  }
380
349
 
381
- [data-component="Switch"][data-grammar-switch-size="lg"]:is([data-selected="true"], [aria-checked="true"]) [data-grammar-switch-thumb] {
382
- margin-inline-start: calc(100% - 1.75rem - 0.25rem) !important;
350
+ [data-component="Switch"]:is([data-grammar-switch-size="lg"], [data-grammar-switch-size="control"]):is([data-selected="true"], [aria-checked="true"]) [data-grammar-switch-thumb] {
351
+ margin-inline-start: calc(100% - var(--grammar-switch-thumb) - 0.25rem) !important;
383
352
  }
384
353
 
385
354
  /* The description and error align under the label, past the wider track. */
386
- [data-component="Switch"][data-grammar-switch-size="lg"] > [data-slot="description"],
387
- [data-component="Switch"][data-grammar-switch-size="lg"] > [data-slot="field-error"] {
388
- padding-inline-start: calc(4rem + 0.75rem);
355
+ [data-component="Switch"]:is([data-grammar-switch-size="lg"], [data-grammar-switch-size="control"]) > :is([data-slot="description"], [data-slot="field-error"]) {
356
+ padding-inline-start: calc(var(--grammar-switch-width) + 0.75rem);
389
357
  }
390
358
 
359
+ /*
360
+ * The thumb glyph box is the canon icon size, not whatever the vendor's thumb container leaves: 20px (size-5, `Icon` `leading`) on `lg`
361
+ * and `control`, 16px (`chip`) on `md`, whose thumb is 1rem tall. The glyph fills its box, centred in the thumb.
362
+ */
391
363
  .starci-core-switch-icon {
392
364
  display: inline-flex;
365
+ flex: none;
366
+ inline-size: 1.25rem;
367
+ block-size: 1.25rem;
393
368
  align-items: center;
394
369
  justify-content: center;
395
370
  }
396
371
 
372
+ [data-component="Switch"][data-grammar-switch-size="md"] .starci-core-switch-icon {
373
+ inline-size: 1rem;
374
+ block-size: 1rem;
375
+ }
376
+
377
+ .starci-core-switch-icon > .starci-core-icon {
378
+ inline-size: 100%;
379
+ block-size: 100%;
380
+ }
381
+
397
382
  /* A swapping pair shows the glyph of the current state only; a single glyph always shows. */
398
383
  [data-component="Switch"][data-grammar-switch-icons="swap"] [data-grammar-switch-icon="on"] { display: none; }
399
384
  [data-component="Switch"][data-grammar-switch-icons="swap"]:is([data-selected="true"], [aria-checked="true"]) [data-grammar-switch-icon="on"] { display: inline-flex; }
@@ -575,7 +560,7 @@
575
560
  }
576
561
 
577
562
  /* --- Read-only reads as settled, not as unavailable --- */
578
- [data-grammar-field="true"][data-grammar-readonly="true"] :where(.starci-core-select-trigger, .starci-core-combo-box-group, .starci-core-search-field-group, .starci-core-number-field-group, .starci-core-date-group, .starci-core-textarea-control) {
563
+ [data-grammar-field="true"][data-grammar-readonly="true"] :where(.starci-core-combo-box-group, .starci-core-search-field-group, .starci-core-number-field-group, .starci-core-date-group, .starci-core-textarea-control) {
579
564
  background: var(--surface-secondary, var(--field-background, Canvas));
580
565
  }
581
566
 
@@ -0,0 +1,13 @@
1
+ /* PADDING-0: align panel content through the container, retaining HeroUI item and header padding. */
2
+ @layer starci-grammar-common {
3
+ .grammar-common-root .starci-core-list-box[data-grammar-list-inset="panel"] {
4
+ /* Row backgrounds extend into the gutter; vendor padding returns their content to the panel edge. */
5
+ inline-size: calc(100% + 1rem);
6
+ margin-inline: -0.5rem;
7
+ padding-inline: 0 !important;
8
+ }
9
+
10
+ .grammar-common-root .starci-core-list-box[data-grammar-list-inset="panel"] .starci-core-list-box-empty {
11
+ padding-inline: 0.5rem;
12
+ }
13
+ }