@starci/grammar 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +139 -8
  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 +86 -56
  5. package/dist/common/components-list-panel.css +13 -0
  6. package/dist/common/components-navigation.css +161 -32
  7. package/dist/common/components-overlays.css +44 -86
  8. package/dist/common/components-surface-controls.css +15 -0
  9. package/dist/common/registry.d.ts +22 -18
  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 +14 -14
  14. package/dist/common/renderers-forms.d.ts.map +1 -1
  15. package/dist/common/renderers-forms.js.map +1 -1
  16. package/dist/common/renderers-navigation.d.ts +11 -5
  17. package/dist/common/renderers-navigation.d.ts.map +1 -1
  18. package/dist/common/renderers-navigation.js +8 -2
  19. package/dist/common/renderers-navigation.js.map +1 -1
  20. package/dist/common/renderers.d.ts +2 -1
  21. package/dist/common/renderers.d.ts.map +1 -1
  22. package/dist/common/renderers.js +1 -0
  23. package/dist/common/renderers.js.map +1 -1
  24. package/dist/common/rule-catalog.generated.d.ts +1 -1
  25. package/dist/common/rule-catalog.generated.d.ts.map +1 -1
  26. package/dist/common/rule-catalog.generated.js +3 -2
  27. package/dist/common/rule-catalog.generated.js.map +1 -1
  28. package/dist/common/styles.css +61 -2
  29. package/dist/core/OtpInput.d.ts +3 -0
  30. package/dist/core/OtpInput.d.ts.map +1 -1
  31. package/dist/core/OtpInput.js +4 -2
  32. package/dist/core/OtpInput.js.map +1 -1
  33. package/dist/core/branch/AlertDialog/index.d.ts.map +1 -1
  34. package/dist/core/branch/AlertDialog/index.js +25 -20
  35. package/dist/core/branch/AlertDialog/index.js.map +1 -1
  36. package/dist/core/branch/ComboBox/index.d.ts +2 -2
  37. package/dist/core/branch/ComboBox/index.d.ts.map +1 -1
  38. package/dist/core/branch/ComboBox/index.js +3 -1
  39. package/dist/core/branch/ComboBox/index.js.map +1 -1
  40. package/dist/core/branch/DatePicker/index.d.ts +2 -2
  41. package/dist/core/branch/DatePicker/index.d.ts.map +1 -1
  42. package/dist/core/branch/DatePicker/index.js +3 -1
  43. package/dist/core/branch/DatePicker/index.js.map +1 -1
  44. package/dist/core/branch/DateRangePicker/index.d.ts +2 -2
  45. package/dist/core/branch/DateRangePicker/index.d.ts.map +1 -1
  46. package/dist/core/branch/DateRangePicker/index.js +3 -1
  47. package/dist/core/branch/DateRangePicker/index.js.map +1 -1
  48. package/dist/core/branch/Dialog/index.d.ts.map +1 -1
  49. package/dist/core/branch/Dialog/index.js +6 -2
  50. package/dist/core/branch/Dialog/index.js.map +1 -1
  51. package/dist/core/branch/Drawer/index.d.ts.map +1 -1
  52. package/dist/core/branch/Drawer/index.js +6 -2
  53. package/dist/core/branch/Drawer/index.js.map +1 -1
  54. package/dist/core/branch/DropdownMenu/index.d.ts +16 -4
  55. package/dist/core/branch/DropdownMenu/index.d.ts.map +1 -1
  56. package/dist/core/branch/DropdownMenu/index.js +18 -8
  57. package/dist/core/branch/DropdownMenu/index.js.map +1 -1
  58. package/dist/core/branch/ListBox/index.d.ts +20 -5
  59. package/dist/core/branch/ListBox/index.d.ts.map +1 -1
  60. package/dist/core/branch/ListBox/index.js +19 -9
  61. package/dist/core/branch/ListBox/index.js.map +1 -1
  62. package/dist/core/branch/Popover/index.d.ts.map +1 -1
  63. package/dist/core/branch/Popover/index.js +2 -1
  64. package/dist/core/branch/Popover/index.js.map +1 -1
  65. package/dist/core/branch/Rail/index.d.ts.map +1 -1
  66. package/dist/core/branch/Rail/index.js +4 -2
  67. package/dist/core/branch/Rail/index.js.map +1 -1
  68. package/dist/core/branch/Select/index.d.ts +6 -3
  69. package/dist/core/branch/Select/index.d.ts.map +1 -1
  70. package/dist/core/branch/Select/index.js +14 -21
  71. package/dist/core/branch/Select/index.js.map +1 -1
  72. package/dist/core/branch/SurfaceAccordionCard/index.d.ts.map +1 -1
  73. package/dist/core/branch/SurfaceAccordionCard/index.js +3 -1
  74. package/dist/core/branch/SurfaceAccordionCard/index.js.map +1 -1
  75. package/dist/core/branch/SurfaceCard/index.d.ts.map +1 -1
  76. package/dist/core/branch/SurfaceCard/index.js +3 -1
  77. package/dist/core/branch/SurfaceCard/index.js.map +1 -1
  78. package/dist/core/branch/SurfaceListCard/index.d.ts.map +1 -1
  79. package/dist/core/branch/SurfaceListCard/index.js +3 -1
  80. package/dist/core/branch/SurfaceListCard/index.js.map +1 -1
  81. package/dist/core/composite/AvatarButton/index.d.ts +22 -0
  82. package/dist/core/composite/AvatarButton/index.d.ts.map +1 -0
  83. package/dist/core/composite/AvatarButton/index.js +11 -0
  84. package/dist/core/composite/AvatarButton/index.js.map +1 -0
  85. package/dist/core/composite/AvatarGroup/index.d.ts +11 -3
  86. package/dist/core/composite/AvatarGroup/index.d.ts.map +1 -1
  87. package/dist/core/composite/AvatarGroup/index.js +9 -5
  88. package/dist/core/composite/AvatarGroup/index.js.map +1 -1
  89. package/dist/core/composite/CheckboxGroup/index.d.ts +2 -2
  90. package/dist/core/composite/CheckboxGroup/index.d.ts.map +1 -1
  91. package/dist/core/composite/CheckboxGroup/index.js +5 -1
  92. package/dist/core/composite/CheckboxGroup/index.js.map +1 -1
  93. package/dist/core/composite/IdentityLockup/index.d.ts +20 -0
  94. package/dist/core/composite/IdentityLockup/index.d.ts.map +1 -0
  95. package/dist/core/composite/IdentityLockup/index.js +11 -0
  96. package/dist/core/composite/IdentityLockup/index.js.map +1 -0
  97. package/dist/core/composite/RadioGroup/index.d.ts +2 -2
  98. package/dist/core/composite/RadioGroup/index.d.ts.map +1 -1
  99. package/dist/core/composite/RadioGroup/index.js +5 -1
  100. package/dist/core/composite/RadioGroup/index.js.map +1 -1
  101. package/dist/core/composite/Timeline/index.js +1 -1
  102. package/dist/core/composite/Timeline/index.js.map +1 -1
  103. package/dist/core/composition/ChatWorkspace/index.d.ts.map +1 -1
  104. package/dist/core/composition/ChatWorkspace/index.js +2 -1
  105. package/dist/core/composition/ChatWorkspace/index.js.map +1 -1
  106. package/dist/core/composition/Sidebar/index.d.ts +1 -1
  107. package/dist/core/composition/Sidebar/index.d.ts.map +1 -1
  108. package/dist/core/composition/Sidebar/index.js +10 -4
  109. package/dist/core/composition/Sidebar/index.js.map +1 -1
  110. package/dist/core/composition/WorkspaceShell/classNames.d.ts +1 -0
  111. package/dist/core/composition/WorkspaceShell/classNames.d.ts.map +1 -1
  112. package/dist/core/composition/WorkspaceShell/classNames.js +1 -0
  113. package/dist/core/composition/WorkspaceShell/classNames.js.map +1 -1
  114. package/dist/core/composition/WorkspaceShell/index.d.ts +30 -0
  115. package/dist/core/composition/WorkspaceShell/index.d.ts.map +1 -1
  116. package/dist/core/composition/WorkspaceShell/index.js +9 -4
  117. package/dist/core/composition/WorkspaceShell/index.js.map +1 -1
  118. package/dist/core/dna.d.ts +5 -0
  119. package/dist/core/dna.d.ts.map +1 -1
  120. package/dist/core/dna.js +5 -0
  121. package/dist/core/dna.js.map +1 -1
  122. package/dist/core/overlayScope.d.ts +16 -0
  123. package/dist/core/overlayScope.d.ts.map +1 -1
  124. package/dist/core/overlayScope.js +12 -0
  125. package/dist/core/overlayScope.js.map +1 -1
  126. package/dist/core/primitive/Avatar/index.d.ts +17 -6
  127. package/dist/core/primitive/Avatar/index.d.ts.map +1 -1
  128. package/dist/core/primitive/Avatar/index.js +21 -4
  129. package/dist/core/primitive/Avatar/index.js.map +1 -1
  130. package/dist/core/primitive/Checkbox/index.d.ts +2 -2
  131. package/dist/core/primitive/Checkbox/index.d.ts.map +1 -1
  132. package/dist/core/primitive/Checkbox/index.js +5 -1
  133. package/dist/core/primitive/Checkbox/index.js.map +1 -1
  134. package/dist/core/primitive/DateField/index.d.ts +2 -2
  135. package/dist/core/primitive/DateField/index.d.ts.map +1 -1
  136. package/dist/core/primitive/DateField/index.js +5 -1
  137. package/dist/core/primitive/DateField/index.js.map +1 -1
  138. package/dist/core/primitive/Field/index.d.ts +5 -2
  139. package/dist/core/primitive/Field/index.d.ts.map +1 -1
  140. package/dist/core/primitive/Field/index.js +2 -2
  141. package/dist/core/primitive/Field/index.js.map +1 -1
  142. package/dist/core/primitive/Input/index.d.ts +2 -1
  143. package/dist/core/primitive/Input/index.d.ts.map +1 -1
  144. package/dist/core/primitive/Input/index.js +4 -2
  145. package/dist/core/primitive/Input/index.js.map +1 -1
  146. package/dist/core/primitive/Kbd/index.d.ts +2 -0
  147. package/dist/core/primitive/Kbd/index.d.ts.map +1 -1
  148. package/dist/core/primitive/Kbd/index.js +5 -3
  149. package/dist/core/primitive/Kbd/index.js.map +1 -1
  150. package/dist/core/primitive/NumberField/index.d.ts +2 -2
  151. package/dist/core/primitive/NumberField/index.d.ts.map +1 -1
  152. package/dist/core/primitive/NumberField/index.js +5 -1
  153. package/dist/core/primitive/NumberField/index.js.map +1 -1
  154. package/dist/core/primitive/PlainPressable/index.d.ts +21 -0
  155. package/dist/core/primitive/PlainPressable/index.d.ts.map +1 -0
  156. package/dist/core/primitive/PlainPressable/index.js +16 -0
  157. package/dist/core/primitive/PlainPressable/index.js.map +1 -0
  158. package/dist/core/primitive/PressableField/index.d.ts +4 -1
  159. package/dist/core/primitive/PressableField/index.d.ts.map +1 -1
  160. package/dist/core/primitive/PressableField/index.js +5 -1
  161. package/dist/core/primitive/PressableField/index.js.map +1 -1
  162. package/dist/core/primitive/RelativeTime/clock.d.ts +5 -0
  163. package/dist/core/primitive/RelativeTime/clock.d.ts.map +1 -0
  164. package/dist/core/primitive/RelativeTime/clock.js +28 -0
  165. package/dist/core/primitive/RelativeTime/clock.js.map +1 -0
  166. package/dist/core/primitive/RelativeTime/index.d.ts +18 -0
  167. package/dist/core/primitive/RelativeTime/index.d.ts.map +1 -0
  168. package/dist/core/primitive/RelativeTime/index.js +45 -0
  169. package/dist/core/primitive/RelativeTime/index.js.map +1 -0
  170. package/dist/core/primitive/SearchField/index.d.ts +2 -2
  171. package/dist/core/primitive/SearchField/index.d.ts.map +1 -1
  172. package/dist/core/primitive/SearchField/index.js +5 -1
  173. package/dist/core/primitive/SearchField/index.js.map +1 -1
  174. package/dist/core/primitive/Switch/index.d.ts +18 -2
  175. package/dist/core/primitive/Switch/index.d.ts.map +1 -1
  176. package/dist/core/primitive/Switch/index.js +2 -1
  177. package/dist/core/primitive/Switch/index.js.map +1 -1
  178. package/dist/core/primitive/Textarea/index.d.ts +2 -2
  179. package/dist/core/primitive/Textarea/index.d.ts.map +1 -1
  180. package/dist/core/primitive/Textarea/index.js +3 -1
  181. package/dist/core/primitive/Textarea/index.js.map +1 -1
  182. package/dist/core/primitive/TimeField/index.d.ts +2 -2
  183. package/dist/core/primitive/TimeField/index.d.ts.map +1 -1
  184. package/dist/core/primitive/TimeField/index.js +5 -1
  185. package/dist/core/primitive/TimeField/index.js.map +1 -1
  186. package/dist/core/surfaceContext.d.ts +11 -0
  187. package/dist/core/surfaceContext.d.ts.map +1 -0
  188. package/dist/core/surfaceContext.js +14 -0
  189. package/dist/core/surfaceContext.js.map +1 -0
  190. package/dist/offset-pop/components-overlays.css +1 -1
  191. package/package.json +8 -3
package/README.md CHANGED
@@ -140,20 +140,77 @@ Each group below matches a Storybook group. Run `npm run storybook`: every story
140
140
  the family and theme chosen in the toolbar. It shows the default, disabled, invalid, pending and
141
141
  selected/current states where they apply. Overlay stories open by default.
142
142
 
143
- **Primitives (25).** Avatar, Badge, Button, CloseButton, Divider, GrammarRoot, Heading, Icon,
144
- IconButton, IconTile, Image, IncludedMark, Kbd, Label, LeadingNumber, Link, Meter, Progress,
145
- ProgressCircle, RankArtwork, Skeleton, Spinner, StateMark, Text, TextAction.
143
+ **Primitives (27).** Avatar, Badge, Button, CloseButton, Divider, GrammarRoot, Heading, Icon,
144
+ IconButton, IconTile, Image, IncludedMark, Kbd, Label, LeadingNumber, Link, Meter, PlainPressable,
145
+ Progress, ProgressCircle, RankArtwork, RelativeTime, Skeleton, Spinner, StateMark, Text, TextAction.
146
146
 
147
147
  **Forms (24).** ButtonGroup, Checkbox, CheckboxGroup, ComboBox, DateField, DatePicker,
148
148
  DateRangePicker, Field, Fieldset, FileDropzone, Form, Input, NumberField, OtpInput,
149
149
  PressableField, RadioGroup, Rating, SearchField, SegmentedControl, Select, Slider, Switch,
150
150
  Textarea, TimeField. Every Field-contract control takes `label`, `description`, `errorMessage`,
151
151
  `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>`.
152
+ `Form` also take `isPending`. `OtpInput` names the code with `label` or an existing label's `labelledBy`.
153
153
 
154
154
  **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`.
155
+ Tooltip. Dialog, AlertDialog, Drawer, Popover and DropdownMenu support uncontrolled `defaultOpen`
156
+ and controlled `isOpen` / `onOpenChange` through the shared overlay contract.
157
+
158
+ `DropdownMenuItem` accepts either `iconSource` or `mark`, a `ReactNode` such as a logo; its public
159
+ type rejects both together. Grammar renders either in the same decorative leading slot, with the
160
+ same alignment and icon-size rule. Keep the visible item label as its accessible name and supply
161
+ the mark without visual slot CSS.
162
+
163
+ `RelativeTime` renders `<time dateTime>` for live relative copy such as "5 minutes ago" and a
164
+ countdown such as "in 5 minutes". Pass `dateTime` as a timezone-qualified ISO string and `locale`
165
+ as the active BCP 47 locale. `mode="relative"` is the default; `mode="countdown"` reduces the time
166
+ remaining and settles at the locale's "now" when the target expires. `size` (`xs`, `sm`, `md`) and
167
+ `tone` (`default`, `muted`, `accent`) use Common's text presentation.
168
+
169
+ ```tsx
170
+ <RelativeTime dateTime="2026-10-25T09:00:00Z" locale="en" />
171
+ <RelativeTime dateTime="2026-10-25T10:00:00+01:00" locale="en" mode="countdown" />
172
+ ```
173
+
174
+ The server and initial hydration render show `fallback`, or the canonical ISO instant when it is
175
+ omitted. After hydration, every instance reads the same shared ticker. The first subscriber starts
176
+ it and the last unsubscribe stops it; leaves own no clock or refresh interval. Supply translated
177
+ `fallback` copy when an ISO instant is unsuitable. Time changes are not a live announcement, so a
178
+ page of timestamps does not repeatedly interrupt assistive technology. Grammar owns native
179
+ `Intl.RelativeTimeFormat` for this primitive; other application date display follows FE-DATE.
180
+
181
+ **DropdownMenu entries.** `entries` is a list of items and sections; the app writes data and no markup, no separator, no
182
+ className and no size. An item takes `id`, `label`, `description`, `shortcut`, `href`, `isDisabled`, `submenu` and:
183
+
184
+ - `iconSource`: a glyph component (the app's Heroicons). Grammar draws it in HeroUI's leading position at the canon icon size,
185
+ 20px (`size-5`, `Icon` `leading`; the Heroicons rule R93 / FE-ICON). HeroUI's own docs example draws its sample glyph at
186
+ `size-4`, which is example content and not a component default, so 20px is StarCi's icon rule and not a vendor override.
187
+ - `leading`: a node instead of a glyph, e.g. an identity mark (`<Avatar size="sm" shape="rounded" isDecorative />`).
188
+ - `tone: "danger"`: HeroUI's `danger` variant, which colours the label and the glyph. Sign out, delete, remove, leave workspace
189
+ and revoke are destructive; the lint rule `starci-fe/destructive-menu-entry-needs-danger-tone` refuses such an entry without it.
190
+
191
+ A section takes `label` and optionally `selection` (`mode: "single" | "multiple"`, `selectedIds`, `onChange`). Between two
192
+ adjacent entries, when either is a section, Grammar always inserts HeroUI's own `Separator`: the app cannot forget one. The selection
193
+ 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
194
+ an item with a submenu).
195
+
196
+ ```tsx
197
+ <DropdownMenu
198
+ trigger={<AvatarButton name="Alex Morgan" />}
199
+ placement="bottom end"
200
+ entries={[
201
+ { kind: "section", id: "workspace", label: "Workspace", selection: { mode: "single", selectedIds, onChange },
202
+ items: [{ id: "hakuen", label: "Hakuen", description: "Owner", leading: <Avatar name="Hakuen" size="sm" shape="rounded" isDecorative /> }] },
203
+ { kind: "section", id: "account", items: [{ id: "profile", label: "Profile", description: "Name and photo", iconSource: UserIcon }] },
204
+ { kind: "section", id: "session", items: [{ id: "sign-out", label: "Sign out", tone: "danger", iconSource: ArrowRightStartOnRectangleIcon }] },
205
+ ]}
206
+ />
207
+ ```
208
+
209
+ **HeroUI parity.** Grammar is a wrapper of HeroUI, so a wrapper keeps HeroUI's anatomy and defaults. The vendor reference
210
+ (`knowledge/vendor/heroui/<component>.yaml` in the StarCi runtime) is measured from the installed `@heroui/react` and
211
+ `@heroui/styles`; the gate `GRAMMAR_VENDOR_PARITY` fails a wrapper that renders a part HeroUI does not have, omits one it
212
+ requires, restyles what the vendor decides, or replaces a vendor default, unless an owner-pinned override declares it. `ListBox`
213
+ and `Select` are HeroUI's own list-box items (a `Label`, a `Description`, the item indicator at the end of the row).
157
214
 
158
215
  **Navigation & Data (14).** Accordion, AvatarGroup, Breadcrumbs, Calendar, DataTable,
159
216
  DescriptionList, Disclosure, ListBox, Pagination, Stepper, Subnav, Tabs, TagGroup, Timeline.
@@ -188,6 +245,44 @@ CORS request; the renderer leaves this unset by default to preserve ordinary med
188
245
  The [HTML media and track standard](https://html.spec.whatwg.org/multipage/media.html#the-track-element)
189
246
  defines the native loading and caption behaviour (reviewed 2026-10-18).
190
247
 
248
+ `Switch` takes `size="md" | "lg"` (`lg` is the StarCi 4rem by 2.25rem pill with a 1.75rem round thumb) and
249
+ `thumbIcon` / `selectedThumbIcon` for a glyph inside the thumb, so a sun/moon theme switch is
250
+ `<Switch size="lg" label="Dark mode" thumbIcon={Sun} selectedThumbIcon={Moon} />`.
251
+
252
+ `Switch size="control"` and `Avatar` / `AvatarButton size="control"` share the control height (2.75rem, the
253
+ IconButton of a bar), so a language button, a theme switch, a bell and an avatar line up at one height; `Switch
254
+ isLabelHidden` keeps its label as the accessible name and draws nothing. `WorkspaceShell viewport="fixed"` is one
255
+ viewport tall (`100dvh`) with the header on top and the navigation and primary regions scrolling on their own, so an
256
+ app writes no viewport arithmetic; `navigationDrawer` adds the compact-width opener row (the bar's height, an
257
+ edge-to-edge divider, a round tertiary icon button) and its left Sidebar drawer. Pass `defaultOpen`
258
+ to let the Drawer own its state; no state hook or callback is needed in the layout. The shell
259
+ reuses the shared overlay contract, so optional `isOpen` / `onOpenChange` remain available for
260
+ coordinated client interactions. The opener is the Drawer's own trigger, which owns activation,
261
+ dismissal and focus return.
262
+
263
+ `PlainPressable` is a pressable that does not look like a button: no padding, background, border or
264
+ shape in any state, no hover effect, a zoom-out on press, a focus ring, and no zoom under
265
+ `prefers-reduced-motion`. `AvatarButton` is a circle-only avatar built on it (an account-menu
266
+ trigger) and `IdentityLockup` a mark beside a title and a muted subtitle with no caret (a workspace
267
+ 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
268
+ corner) and `Avatar` takes `shape="square" | "rounded"`.
269
+
270
+ Personal avatars are circles. Pass the user or identity id as `seed` on `Avatar`, `AvatarButton`
271
+ or each `AvatarGroup` member. Grammar renders the fixed Lorelei DiceBear style chosen by its DNA,
272
+ memoized for that seed with identical server and client output. An uploaded `src` wins; with
273
+ neither `src` nor `seed`, the existing initials fallback remains. Images have `alt=""`; the avatar
274
+ shell carries its name, or `isDecorative` hides it beside a visible name. `AvatarButton` names the
275
+ button and keeps its inner avatar decorative. Apps install and import Grammar; its pinned
276
+ DiceBear runtime dependencies and avatar drawing stay inside the package.
277
+ The package requires Node 22 or newer, as does the pinned DiceBear engine.
278
+
279
+ ```tsx
280
+ <Avatar name={user.name} seed={user.id} isDecorative />
281
+ <AvatarButton name={user.name} seed={user.id} src={user.avatarUrl} />
282
+ <AvatarGroup label="Team" items={members.map(user => ({ id: user.id, name: user.name, seed: user.id }))}
283
+ overflowLabel={count => `${count} more members`} />
284
+ ```
285
+
191
286
  **Surfaces (14).** EmptyNotice, FencedCodeBlock, HorizontalScrollRegion, MarkdownArticle,
192
287
  MarkdownTableFrame, MediaFrame, Rail, SectionHeader, StaticStateRow, SurfaceAccordionCard,
193
288
  SurfaceCard, SurfaceCopyGroup, SurfaceListCard, VerticalScrollRegion.
@@ -202,6 +297,42 @@ with TopBar, BottomNav and a Toast.
202
297
  0.5.0 groups. `defineGrammarFamily` builds a new family over the registry. It may replace
203
298
  renderers with prop-compatible versions and add extensions whose names don't collide with Common's.
204
299
 
300
+ ### Surface controls, panel lists and group identity
301
+
302
+ SurfaceCard (including `frame="frameless"`), SurfaceListCard, SurfaceAccordionCard, Rail, Sidebar
303
+ and the content of Drawer, Dialog, AlertDialog and Popover supply the nesting context of ANATOMY-2.
304
+ Omit a field's `variant`: Input, SearchField, Textarea, NumberField, DateField, TimeField, Select,
305
+ ComboBox, DatePicker, DateRangePicker, Checkbox, CheckboxGroup, RadioGroup, PressableField and OtpInput use `secondary`
306
+ inside these surfaces and `primary` outside. Overlay triggers keep their surrounding context.
307
+ The FE canon's `FE_NESTED_VARIANT` rejects an explicit primary field inside a surface and raw
308
+ `bg-surface` paint in application classNames.
309
+
310
+ ListBox defaults to `inset="panel"` inside the same context. The list container extends into the
311
+ panel gutter so row content, loading content and section captions align with the header and search
312
+ field without padding wrappers (PADDING-0). Items keep HeroUI's default padding and selection-indicator
313
+ space; row backgrounds extend into the gutter. Outside a surface, the vendor list inset stays;
314
+ `inset="panel"` also selects the alignment explicitly. Pass flat `items` or named `sections`
315
+ (`ListBoxSection`), with unique item ids across the whole list. `FE_LIST_PANEL_PADDING` rejects
316
+ horizontal padding wrappers around a list or its section caption. Sidebar's expanded and collapsed
317
+ rows also retain HeroUI item padding.
318
+
319
+ ```tsx
320
+ <SurfaceCard ariaLabel="Conversations">
321
+ <Heading level={2}>Conversations</Heading>
322
+ <SearchField label="Find a conversation" />
323
+ <ListBox label="Conversations" sections={[
324
+ { id: "recent", label: "Recent", items: [{ id: "team", label: "Design team" }] },
325
+ ]} />
326
+ </SurfaceCard>
327
+ ```
328
+
329
+ For a conversation or group identity, use `AvatarGroup layout="grid"` (COLLECTION-3, the Zalo
330
+ reference). It is one round Avatar-sized mark at `sm`, `md`, `lg` or `control`: two members split
331
+ the circle, three show one above two, and four use a 2 by 2 grid with a hairline gap. With more
332
+ members, the fourth cell shows `+N` for everyone beyond the first three. Each member uses Avatar's
333
+ uploaded image, `seed` and initials fallback, and accepts the same custom `fallback`. A single
334
+ person uses Avatar. The default `layout="row"` keeps the overlapping member row.
335
+
205
336
  ## Theming
206
337
 
207
338
  - **Tokens.** Each family writes its DNA as CSS custom properties on its root: `--starci-core-*`,
@@ -213,8 +344,8 @@ renderers with prop-compatible versions and add extensions whose names don't col
213
344
  `--starci-core-surface-tertiary`; HeroUI's `--surface-tertiary` is left to Common and is never
214
345
  re-bound by the family. To adjust a family, override its tokens on the
215
346
  root, for example `<CoreGrammarRoot style={{ "--starci-core-accent": "#2f6bff" }}>`. The
216
- `*_TOKEN_NAMES` / `*_TOKEN_DEFAULTS` exports are the supported list. Values are validated by the
217
- family specs, not at runtime.
347
+ `*_TOKEN_NAMES` / `*_TOKEN_DEFAULTS` exports are the supported list. The DNA generator measures
348
+ their packaged values; callers own their token overrides.
218
349
  - **Spacing.** Every family uses the Common 4px scale (`COMMON_SPACING_SCALE`,
219
350
  `COMMON_SPACING_TOKENS`), and Common's `--grammar-*-gap` / `--grammar-page-inset` variables.
220
351
  - **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.10.0",
4
+ "version": "0.12.0",
5
5
  "algorithm": "starci/grammar-source-digest@1",
6
- "sourceDigest": "sha256:521af9d2573c0cb6415b238e8df1c985d9ac2d6962938a12050e0f6076c8d659",
7
- "distDigest": "sha256:c2f06707823826a97dd0f7deda9f4e0b3ee151c5c745a66082cda5cd966383cc",
8
- "builtAt": "2026-10-18T15:47:18.150Z"
6
+ "sourceDigest": "sha256:fd2b712405d87230cd1736101068d562483cd3c0b8b4ef4d629bb9944354842f",
7
+ "distDigest": "sha256:1a7c5eb6dcb25a1ef5bfba338d3c374fce07a3d1f534fe8f830b66e2ca1893b3",
8
+ "builtAt": "2026-10-25T11:48:36.769Z"
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"] {
@@ -160,6 +163,8 @@
160
163
  .starci-core-field-skeleton-control[data-grammar-field-skeleton="choice"] > .starci-core-skeleton { border-radius: 0.25rem; }
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; }
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; }
163
168
 
164
169
  .starci-core-field-skeleton-choice-label {
165
170
  color: var(--foreground, CanvasText);
@@ -177,7 +182,6 @@
177
182
  }
178
183
  }
179
184
 
180
- .starci-core-select-trigger,
181
185
  .starci-core-combo-box-group,
182
186
  .starci-core-search-field-group,
183
187
  .starci-core-number-field-group,
@@ -186,7 +190,6 @@
186
190
  min-width: 0;
187
191
  }
188
192
 
189
- .starci-core-select-value,
190
193
  .starci-core-combo-box-input,
191
194
  .starci-core-search-field-input,
192
195
  .starci-core-number-field-input {
@@ -202,7 +205,6 @@
202
205
  * inside inherits the corner (no square right side behind the trigger) and stretches to the group's height.
203
206
  */
204
207
  .starci-core-input-field,
205
- .starci-core-select-trigger,
206
208
  .starci-core-combo-box-group,
207
209
  .starci-core-search-field-group,
208
210
  .starci-core-number-field-group,
@@ -235,11 +237,6 @@
235
237
  }
236
238
  }
237
239
 
238
- .starci-core-select-trigger {
239
- align-items: center;
240
- padding-inline: 0.75rem 1.75rem;
241
- }
242
-
243
240
  .starci-core-combo-box-group {
244
241
  align-items: stretch;
245
242
  }
@@ -257,40 +254,6 @@
257
254
  padding-inline-end: 0.5rem;
258
255
  }
259
256
 
260
- /*
261
- * The selected value of a Select stays ONE line and keeps the field height: it ellipsizes (the trigger carries the
262
- * full text as its title) and shows only the option's label, never its description. Popover options still wrap.
263
- */
264
- .starci-core-select-value {
265
- flex: 1 1 0;
266
- overflow: hidden;
267
- overflow-wrap: normal;
268
- text-overflow: ellipsis;
269
- white-space: nowrap;
270
- }
271
-
272
- .starci-core-select-value-text {
273
- display: block;
274
- min-width: 0;
275
- overflow: hidden;
276
- text-overflow: ellipsis;
277
- }
278
-
279
- .starci-core-select-value .starci-core-list-option-copy {
280
- display: block;
281
- overflow: hidden;
282
- text-overflow: ellipsis;
283
- }
284
-
285
- .starci-core-select-value .starci-core-list-option-label {
286
- overflow-wrap: normal;
287
- }
288
-
289
- .starci-core-select-value .starci-core-list-option-description,
290
- .starci-core-select-value [data-slot="list-box-item-indicator"] {
291
- display: none;
292
- }
293
-
294
257
  .starci-core-date-input {
295
258
  min-width: 0;
296
259
  flex: 1;
@@ -302,22 +265,14 @@
302
265
  color: var(--muted, GrayText);
303
266
  }
304
267
 
305
- /* --- List popover (Select, ComboBox) --- */
306
- .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] {
307
272
  display: flex;
308
273
  min-width: 0;
309
274
  flex: 1;
310
275
  flex-direction: column;
311
- gap: 0.25rem;
312
- }
313
-
314
- .starci-core-list-option-label {
315
- overflow-wrap: anywhere;
316
- }
317
-
318
- .starci-core-list-option-description {
319
- color: var(--muted, GrayText);
320
- font-size: 0.75rem;
321
276
  }
322
277
 
323
278
  .starci-core-list-empty {
@@ -354,6 +309,81 @@
354
309
  gap: 0.75rem 1rem;
355
310
  }
356
311
 
312
+ /*
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.
320
+ */
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;
338
+ border-radius: 9999px !important;
339
+ }
340
+
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;
344
+ border-radius: 9999px !important;
345
+ margin-inline-start: 0.25rem !important;
346
+ align-items: center;
347
+ justify-content: center;
348
+ }
349
+
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;
352
+ }
353
+
354
+ /* The description and error align under the label, past the wider track. */
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);
357
+ }
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
+ */
363
+ .starci-core-switch-icon {
364
+ display: inline-flex;
365
+ flex: none;
366
+ inline-size: 1.25rem;
367
+ block-size: 1.25rem;
368
+ align-items: center;
369
+ justify-content: center;
370
+ }
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
+
382
+ /* A swapping pair shows the glyph of the current state only; a single glyph always shows. */
383
+ [data-component="Switch"][data-grammar-switch-icons="swap"] [data-grammar-switch-icon="on"] { display: none; }
384
+ [data-component="Switch"][data-grammar-switch-icons="swap"]:is([data-selected="true"], [aria-checked="true"]) [data-grammar-switch-icon="on"] { display: inline-flex; }
385
+ [data-component="Switch"][data-grammar-switch-icons="swap"]:is([data-selected="true"], [aria-checked="true"]) [data-grammar-switch-icon="off"] { display: none; }
386
+
357
387
  /* --- Slider --- */
358
388
  .starci-core-slider {
359
389
  width: 100%;
@@ -530,7 +560,7 @@
530
560
  }
531
561
 
532
562
  /* --- Read-only reads as settled, not as unavailable --- */
533
- [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) {
534
564
  background: var(--surface-secondary, var(--field-background, Canvas));
535
565
  }
536
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
+ }