@starci/hfs 4.7.34 → 4.7.36
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.
- package/CHANGELOG.md +13 -0
- package/README.md +8 -11
- package/codemod/button-intent.mjs +356 -0
- package/lint/run.mjs +70 -29
- package/package.json +3 -2
- package/runtime/knowledge/grammars/index.yaml +111 -0
- package/runtime/knowledge/grammars/offset-pop/playbook.yaml +302 -0
- package/runtime/knowledge/grammars/starci/playbook.yaml +1190 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +22 -8
- package/runtime/knowledge/hfs/fe-stack.yaml +12 -4
- package/runtime/knowledge/hfs/rules.yaml +163 -83
- package/runtime/knowledge/hfs/slots.yaml +3 -3
- package/runtime/knowledge/ui/presentation/font.yaml +101 -0
- package/runtime/knowledge/ui/presentation/padding.yaml +217 -0
- package/runtime/modules/kernel/failure-codes.yaml +102 -20
- package/runtime/modules/kernel/removed-vocabulary.yaml +29 -0
- package/runtime/modules/kernel/roles.yaml +1 -1
- package/runtime/packages/cli/src/catalog.generated.mjs +119 -9
- package/runtime/scripts/gates/canon-pins.mjs +103 -0
- package/runtime/scripts/gates/gate.mjs +3 -36
- package/runtime/scripts/gates/output.mjs +40 -0
- package/runtime/scripts/gates/test-world-run.mjs +2 -4
- package/runtime/scripts/guards/release-definition.mjs +22 -3
- package/runtime/scripts/hfs/architecture/config.mjs +8 -8
- package/runtime/scripts/hfs/check.mjs +1 -1
- package/runtime/scripts/hfs/db-restamp-verb.mjs +2 -2
- package/runtime/scripts/hfs/declaration-shape.mjs +8 -2
- package/runtime/scripts/hfs/grammar-presentation.mjs +152 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +2 -2
- package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -0
- package/runtime/scripts/hfs/rules/fe-stack.mjs +44 -1
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/view.mjs +1 -0
- package/runtime/scripts/lib/canon-binding-text.mjs +129 -0
- package/runtime/scripts/lib/css-rules.mjs +18 -0
- package/runtime/scripts/lib/example-refs.mjs +1 -1
- package/runtime/scripts/lib/grammar-lint.mjs +48 -0
- package/runtime/scripts/lib/grammar-vendor-parity-jsx.mjs +360 -0
- package/runtime/scripts/lib/grammar-vendor-parity.mjs +110 -157
- package/runtime/scripts/lib/numeric-match.mjs +5 -0
- package/runtime/scripts/lib/release-version.mjs +119 -0
- package/runtime/scripts/lib/runtime-publication-tags.mjs +3 -46
- package/runtime/scripts/lib/source-package-imports.mjs +111 -0
- package/runtime/scripts/work/draw/draw-layer.mjs +5 -4
- package/scaffold/app.mjs +7 -5
- package/src/main.mjs +30 -15
- package/stack/config.mjs +2 -1
- package/stack/render.mjs +24 -3
- package/sync/index.mjs +8 -2
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +0 -1
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +0 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -1
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +0 -1
- package/templates/fe/tool-config/eslint.config.mjs +4 -1
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
schema: starci/knowledge-source@1
|
|
2
|
+
id: grammar.starci.playbook
|
|
3
|
+
title: StarCi Core — playbook
|
|
4
|
+
purpose: |
|
|
5
|
+
Which idioms a business shape asks for, and what a supplied reference is allowed to contribute to
|
|
6
|
+
that answer. The idioms themselves are in idioms.yaml; what the package publishes is in DNA.yaml.
|
|
7
|
+
A shape not listed here is not a house idiom. It may still be composed for the approved product
|
|
8
|
+
direction when existing Common semantics, family anatomy and product facts can express it; only
|
|
9
|
+
promotion into the family's reusable idiom set belongs to the family owner.
|
|
10
|
+
Shapes marked `since: "0.5.0"` compose the forms, overlay and navigation renderers Common added in
|
|
11
|
+
0.5.0. They name renderers directly (`composition`) rather than idioms, because no product source
|
|
12
|
+
has yet shown them twice; `guidance` holds the rules every Core app built on them follows.
|
|
13
|
+
appliesTo:
|
|
14
|
+
- grammar
|
|
15
|
+
family: starci
|
|
16
|
+
provenance:
|
|
17
|
+
publicEntry: "@starci/grammar/common"
|
|
18
|
+
package: "@starci/grammar"
|
|
19
|
+
version: "0.5.0"
|
|
20
|
+
captured: "2026-09-23"
|
|
21
|
+
limitations: |
|
|
22
|
+
Use this playbook only when this family is selected in the current .starciwork specification.
|
|
23
|
+
Examples below are historical references; verify actually installed exports (prefer
|
|
24
|
+
@starci/grammar/common as the current public entry where supported), renderers and CSS before
|
|
25
|
+
promising a capability. Reading steps below are reasoning within one op, not an auto-running op
|
|
26
|
+
chain. Do not invent Grammar component APIs absent from DNA/installed source.
|
|
27
|
+
shapes:
|
|
28
|
+
- id: purchase-decision-page
|
|
29
|
+
title: Purchase decision page — one offer, one price, one commitment (subscriptions)
|
|
30
|
+
referenceMayContribute: |
|
|
31
|
+
Keep: the order of regions (what is read before the price), which single decision dominates, and
|
|
32
|
+
what folds away instead of competing with it. Never crosses over: page layout, brand, palette,
|
|
33
|
+
type scale, component anatomy, or any control the reference draws that this Grammar does not
|
|
34
|
+
publish.
|
|
35
|
+
idiomSequence:
|
|
36
|
+
- idiom: joined-bands-flush-card
|
|
37
|
+
role: Explanatory column in primary of PrimaryRailLayout
|
|
38
|
+
- idiom: neutral-summary-band
|
|
39
|
+
role: A neutral band opens the card with its summary for its intro line
|
|
40
|
+
- idiom: generated-art-band
|
|
41
|
+
role: Generated art is a band, not a card for the journey asset
|
|
42
|
+
- idiom: title-supporting-line
|
|
43
|
+
role: Title and one supporting line for each included outcome
|
|
44
|
+
- idiom: one-highlighted-card
|
|
45
|
+
role: One highlighted card on the decision itself
|
|
46
|
+
- idiom: bottom-action-band
|
|
47
|
+
role: The card's one action closes the bottom band for the purchase
|
|
48
|
+
- idiom: pending-same-tree
|
|
49
|
+
role: Pending is the same tree, resting across the offer read
|
|
50
|
+
notes: |
|
|
51
|
+
Secondary explanation folds away; whether it folds into a disclosure is a
|
|
52
|
+
DIRECTION_CHOICE_REQUIRED (seen once). Decision card sits in rail.
|
|
53
|
+
|
|
54
|
+
- id: learner-dashboard
|
|
55
|
+
title: Learner dashboard — many peer sections, one intended next step
|
|
56
|
+
referenceMayContribute: |
|
|
57
|
+
Keep: which sections exist and their reading order, and which single section is the reader's
|
|
58
|
+
next action. Never crosses over: grid geometry, card chrome, densities, or a section that would
|
|
59
|
+
need a renderer this family does not publish.
|
|
60
|
+
idiomSequence:
|
|
61
|
+
- idiom: joined-bands-flush-card
|
|
62
|
+
role: Per section
|
|
63
|
+
- idiom: neutral-summary-band
|
|
64
|
+
role: Where the section states a measure
|
|
65
|
+
- idiom: title-supporting-line
|
|
66
|
+
role: Inside every row
|
|
67
|
+
- idiom: bottom-action-band
|
|
68
|
+
role: Where a section leads somewhere
|
|
69
|
+
- idiom: pending-same-tree
|
|
70
|
+
role: For every read, one slot per api through guidance state-recipes
|
|
71
|
+
- idiom: one-highlighted-card
|
|
72
|
+
role: Across the page exactly once, on the resume or next-step section
|
|
73
|
+
|
|
74
|
+
- id: sign-in
|
|
75
|
+
title: Sign-in — one surface, several journeys, one submit per step
|
|
76
|
+
referenceMayContribute: |
|
|
77
|
+
Keep: the order of the ways in (shortcut before form, or the reverse), which journeys share one
|
|
78
|
+
surface, and where the way to the other journey sits. Never crosses over: provider branding,
|
|
79
|
+
illustration, field chrome, or a social-login row this product does not have a provider for.
|
|
80
|
+
idiomSequence:
|
|
81
|
+
- idiom: title-supporting-line
|
|
82
|
+
role: Surface header (Heading + muted subtitle)
|
|
83
|
+
- idiom: single-column-form-stack
|
|
84
|
+
role: Per step, one Button variant="primary" type="submit" each
|
|
85
|
+
- idiom: pending-same-tree
|
|
86
|
+
role: Through isPending and isDisabled rather than a replaced tree
|
|
87
|
+
notes: |
|
|
88
|
+
The way onward as TextAction, last. Whether the provider shortcut precedes the form is seen
|
|
89
|
+
once; carry the reference's order and say so.
|
|
90
|
+
|
|
91
|
+
- id: account-settings-form
|
|
92
|
+
title: Settings or profile form — named groups, one submit, server errors in place
|
|
93
|
+
since: "0.5.0"
|
|
94
|
+
referenceMayContribute: |
|
|
95
|
+
Keep: which groups exist, their order, and which fields are required. Never crosses over: field
|
|
96
|
+
chrome, validation copy style, or a control the package does not publish.
|
|
97
|
+
composition:
|
|
98
|
+
- renderer: Form
|
|
99
|
+
role: |
|
|
100
|
+
The boundary: `label` (form landmark), `onSubmit(FormData)`, `validationErrors` keyed by field
|
|
101
|
+
`name` for server-side refusals, `isPending` while the submit is in flight
|
|
102
|
+
- renderer: Fieldset
|
|
103
|
+
role: One per named group (`legend`, optional `description`, cross-field `errorMessage`)
|
|
104
|
+
- renderer: "Select / ComboBox / Textarea / NumberField / SearchField / DateField / DatePicker"
|
|
105
|
+
role: |
|
|
106
|
+
Each control carries its own `label`, `description`, `errorMessage`, `isRequired`, `name` —
|
|
107
|
+
the shared Field contract; a placeholder is never the name
|
|
108
|
+
- renderer: "Checkbox / CheckboxGroup / RadioGroup / Switch / SegmentedControl / Slider"
|
|
109
|
+
role: |
|
|
110
|
+
Choices; a Switch is an immediate setting, a Checkbox a value submitted with the form
|
|
111
|
+
- renderer: Field
|
|
112
|
+
role: |
|
|
113
|
+
Only for a control Grammar does not publish (a third-party widget, OtpInput, a native input):
|
|
114
|
+
its render prop hands over the id and `aria-describedby`/`aria-invalid` so the control is
|
|
115
|
+
announced like a Grammar one
|
|
116
|
+
- renderer: Button
|
|
117
|
+
role: One `variant="primary" type="submit"` after the last group; `isPending` mirrors the Form's
|
|
118
|
+
notes: |
|
|
119
|
+
The base `Input` still speaks `hint`/`isError`; the 0.5.0 controls speak
|
|
120
|
+
`description`/`errorMessage`/`isInvalid`. Do not mix the two vocabularies in one form without
|
|
121
|
+
checking that the Form's `validationErrors` reaches the control. In Core, fields are white fills
|
|
122
|
+
with a transparent border (see family.yaml `fields-on-a-surface`): on a top-level SurfaceCard
|
|
123
|
+
verify the rendered field edge.
|
|
124
|
+
|
|
125
|
+
- id: destructive-confirmation
|
|
126
|
+
title: Destructive confirmation — one irreversible decision
|
|
127
|
+
since: "0.5.0"
|
|
128
|
+
composition:
|
|
129
|
+
- renderer: AlertDialog
|
|
130
|
+
role: |
|
|
131
|
+
`tone="negative"` (the default), `title` naming the consequence, `confirmLabel` and
|
|
132
|
+
`cancelLabel` in the product's words; `onConfirm` may return a promise, which keeps the confirm
|
|
133
|
+
pending and the dialog open until it settles
|
|
134
|
+
- renderer: Button
|
|
135
|
+
role: |
|
|
136
|
+
The trigger, inside the Core root: `intent="destructive"` at the entry step, which resolves to
|
|
137
|
+
`danger-soft` (CTA-6); the confirm inside the AlertDialog is the solid `danger` confirm, dismiss
|
|
138
|
+
comes first and takes initial focus
|
|
139
|
+
notes: |
|
|
140
|
+
Initial focus lands on the cancel action and an outside press never dismisses. A reversible
|
|
141
|
+
action is not a confirmation: act, then offer undo through a Toast `action`.
|
|
142
|
+
|
|
143
|
+
- id: secondary-task-sheet
|
|
144
|
+
title: Secondary task beside the page — filters, details, an edit
|
|
145
|
+
since: "0.5.0"
|
|
146
|
+
composition:
|
|
147
|
+
- renderer: Drawer
|
|
148
|
+
role: |
|
|
149
|
+
`placement="right"` at wide widths; `placement="bottom"` (grab handle shown by default) when
|
|
150
|
+
the task is thumb-driven on a phone. `footer` may take a render function receiving `close`
|
|
151
|
+
- renderer: Dialog
|
|
152
|
+
role: When the task must block the page and is short (`size` sm/md/lg/full)
|
|
153
|
+
notes: |
|
|
154
|
+
Both portal into the Core root and restore focus to their trigger. One overlay at a time; a
|
|
155
|
+
Popover or DropdownMenu opened from inside a Drawer stays inside the same root.
|
|
156
|
+
|
|
157
|
+
- id: app-shell-mobile-first
|
|
158
|
+
title: App shell — WorkspaceShell, navbar and sidebar drawer
|
|
159
|
+
since: "0.5.0"
|
|
160
|
+
ownership: FE-RULE-OWNERSHIP
|
|
161
|
+
knowledge: knowledge/ui/composition/app-shell.yaml
|
|
162
|
+
composition:
|
|
163
|
+
- renderer: CoreGrammarRoot
|
|
164
|
+
role: The single root around everything below
|
|
165
|
+
- renderer: WorkspaceShell
|
|
166
|
+
role: |
|
|
167
|
+
`viewport="fixed"` realizes FE-APP-SHELL-1: exactly 100dvh, a sticky navbar, optional sidebar,
|
|
168
|
+
and independently scrolling navigation and primary content. The document never scrolls.
|
|
169
|
+
Under viewport="fixed", navigation resolves navigationVisibility to wide and provides
|
|
170
|
+
navigationDrawer or a compactHeader containing a left Drawer with Sidebar presentation="drawer".
|
|
171
|
+
Both opener paths default visibility to wide; an explicit visibility also resolves to wide.
|
|
172
|
+
Under viewport="fixed",
|
|
173
|
+
navigationTrack defaults to intrinsic so a collapsed sidebar leaves no empty track.
|
|
174
|
+
- renderer: NavigationFeatureNav
|
|
175
|
+
role: |
|
|
176
|
+
The navbar may use this renderer or the WorkspaceShell header with TopBar. Its family CSS
|
|
177
|
+
supplies sticky top placement, a 4rem band, a solid surface and a full-width bottom separator.
|
|
178
|
+
- renderer: Sidebar
|
|
179
|
+
role: |
|
|
180
|
+
Desktop navigation uses the expanded or collapsed rail; the left mobile Drawer composes
|
|
181
|
+
`presentation="drawer"`. Its label, destinations and selectedKey come from the application.
|
|
182
|
+
- renderer: Toaster
|
|
183
|
+
role: Once, last child of the root
|
|
184
|
+
recipes:
|
|
185
|
+
- id: navbar-controls
|
|
186
|
+
implements: [FE-APP-SHELL-2]
|
|
187
|
+
compose: |
|
|
188
|
+
Every bar control uses `size="control"`, the 2.75rem shared height. Icon uses the family
|
|
189
|
+
size-5 token (20px) throughout the region. SearchField or its trigger shows Ctrl K and opens
|
|
190
|
+
on Ctrl/Cmd+K; inside a surface its variant resolves through the nesting context.
|
|
191
|
+
- id: workspace-identity
|
|
192
|
+
implements: [FE-APP-SHELL-2]
|
|
193
|
+
compose: |
|
|
194
|
+
IdentityLockup sits inside PlainPressable size="control". Its square logo mark has a
|
|
195
|
+
transparent background, no radius and object-fit contain; without an image it uses Avatar's
|
|
196
|
+
bundled DiceBear initials. The title
|
|
197
|
+
has a muted role subtitle and no caret. PlainPressable has no button shape or hover effect;
|
|
198
|
+
press uses scale(0.96), focus remains visible and reduced motion removes the zoom.
|
|
199
|
+
- id: navbar-right-cluster
|
|
200
|
+
implements: [FE-APP-SHELL-3]
|
|
201
|
+
compose: |
|
|
202
|
+
In order: a round tertiary IconButton with the Heroicons Language glyph opening a
|
|
203
|
+
single-select DropdownMenu, Switch size="control" with sun/moon size-5 thumb icons and
|
|
204
|
+
isLabelHidden, a bell IconButton with count badge opening Popover, then a circle-only
|
|
205
|
+
AvatarButton size="control" opening the account DropdownMenu. AvatarButton uses uploaded src
|
|
206
|
+
first and otherwise the bundled DiceBear initials style: the first and last words of name
|
|
207
|
+
supply the letters, and userId passed as seed determines colour, falling back to name when
|
|
208
|
+
seed is absent (owner:avatar-default-dicebear-initials). DropdownMenu preserves HeroUI's
|
|
209
|
+
anatomy and default selection indicator. The supported language labels use the "Vietnamese"
|
|
210
|
+
and "English" entries in modules/i18n/messages/app-shell.yaml; each language keeps its autonym.
|
|
211
|
+
- id: account-menu
|
|
212
|
+
implements: [FE-APP-SHELL-3]
|
|
213
|
+
compose: |
|
|
214
|
+
A workspace section shows identity marks and single selection. An account section shows an
|
|
215
|
+
icon, account name and description email · role. The sign-out item and every destructive item
|
|
216
|
+
use `tone="danger"`; the family prop realizes the universal danger semantics. Sections
|
|
217
|
+
receive automatic separators. Localized sign-out copy comes from
|
|
218
|
+
modules/i18n/messages/app-shell.yaml.
|
|
219
|
+
- id: mobile-navigation
|
|
220
|
+
implements: [FE-APP-SHELL-4]
|
|
221
|
+
compose: |
|
|
222
|
+
Below md the navbar holds identity, search trigger, bell and avatar; language and theme move
|
|
223
|
+
into the drawer. The compactHeader band directly below it is also 4rem, with an edge-to-edge
|
|
224
|
+
separator and one vertically centred round tertiary IconButton carrying the Heroicons
|
|
225
|
+
ellipsis at size-5. It opens Drawer placement="left" containing Sidebar
|
|
226
|
+
presentation="drawer" with every group plus language and theme. The drawer closes on item,
|
|
227
|
+
backdrop or Escape, traps focus and scrolls independently. The opener band hides at desktop.
|
|
228
|
+
Inline navigation is hidden below md with navigationVisibility="wide"; navigationDrawer or
|
|
229
|
+
the compactHeader containing the left Drawer and its drawer Sidebar supplies the opener.
|
|
230
|
+
The shell has neither tabs in the opener band nor a BottomNav.
|
|
231
|
+
- id: create-actions
|
|
232
|
+
implements: [FE-APP-SHELL-5]
|
|
233
|
+
compose: |
|
|
234
|
+
The create action stays on its page. The static matcher reads appShell.product from
|
|
235
|
+
modules/goal/source-phrases.yaml; the navbar never receives that page command.
|
|
236
|
+
- id: sidebar-rail
|
|
237
|
+
implements: [FE-APP-SHELL-6]
|
|
238
|
+
compose: |
|
|
239
|
+
Sidebar fills the height below the navbar, uses the surface background in both themes and
|
|
240
|
+
scrolls independently. Groups use translated headings such as Main, Operations and Settings
|
|
241
|
+
from modules/i18n/messages/app-shell.yaml. The expanded item has a size-5 icon, label and
|
|
242
|
+
Grammar current-page treatment. A keyboard-accessible toggle collapses it into icon-only
|
|
243
|
+
destinations with size-5 icons in 2.75rem targets, current-page highlighting, count badges
|
|
244
|
+
and hover/focus tooltips. Group order is stable, spacing replaces group headings and
|
|
245
|
+
settings-type groups stay at the end. The application persists collapse state per user.
|
|
246
|
+
- id: conversation-panel
|
|
247
|
+
implements: [FE-APP-SHELL-7]
|
|
248
|
+
compose: |
|
|
249
|
+
The panel has its title, a nested SearchField and a filter Tabs row, then one list sorted by
|
|
250
|
+
latest message. The approved reference categories are All, Groups, Customers and Unread
|
|
251
|
+
when those categories exist in the product; labels come from modules/i18n/messages/ui.yaml
|
|
252
|
+
and modules/i18n/messages/app-shell.yaml. ListBox panel alignment keeps rows and section
|
|
253
|
+
captions on the same content edge without padding wrappers. A row shows avatar, name,
|
|
254
|
+
trailing time, last message and unread badge; the open row is selected through HeroUI.
|
|
255
|
+
The name and preview each occupy one line with an ellipsis, and the title carries no group
|
|
256
|
+
glyph (owner:item-description-one-line). Pass emphasis="unread" for unread rows so Grammar
|
|
257
|
+
strengthens the label and preview; read rows keep the vendor label and muted preview.
|
|
258
|
+
Group identity uses AvatarGroup layout="grid" with the member/count arrangement declared
|
|
259
|
+
by presentation.groupIdentity.
|
|
260
|
+
notes: |
|
|
261
|
+
FE-RULE-OWNERSHIP in knowledge/patterns/fe/index.yaml separates universal app-shell semantics
|
|
262
|
+
from these family APIs, values, shapes and effects. @starci/eslint-grammar-starci owns their
|
|
263
|
+
code checks. Inspect installed Grammar exports and the running shell when proving the recipe.
|
|
264
|
+
|
|
265
|
+
readingSteps:
|
|
266
|
+
- id: business-shape-first
|
|
267
|
+
order: 1
|
|
268
|
+
title: Business shape first
|
|
269
|
+
instruction: |
|
|
270
|
+
Name the shape and the one outcome the surface exists to produce. The shape selects the idiom
|
|
271
|
+
sequence; nothing else does.
|
|
272
|
+
- id: then-reference
|
|
273
|
+
order: 2
|
|
274
|
+
title: Then the reference, if the request supplied one
|
|
275
|
+
instruction: |
|
|
276
|
+
A reference contributes region order, which decision dominates, and what folds away. It never
|
|
277
|
+
contributes layout, brand, palette, or component anatomy — those are already owned, by this
|
|
278
|
+
family and by knowledge/ui. A reference that can only be honoured by breaking an idiom is
|
|
279
|
+
evidence about the reference, not about StarCi.
|
|
280
|
+
- id: then-idioms
|
|
281
|
+
order: 3
|
|
282
|
+
title: Then the idioms
|
|
283
|
+
instruction: |
|
|
284
|
+
Composed in the order the shape's row gives, each one taken whole: an idiom is a relationship,
|
|
285
|
+
and half of it is a different shape.
|
|
286
|
+
- id: compare-dna
|
|
287
|
+
order: 4
|
|
288
|
+
title: Compare DNA with actually installed source
|
|
289
|
+
instruction: |
|
|
290
|
+
Confirm named renderers/props against DNA.yaml and installed exports (public entry
|
|
291
|
+
@starci/grammar/common where supported), and read the gaps in family.yaml before promising a
|
|
292
|
+
capability. A composition that needs a recorded gap is not a direction yet.
|
|
293
|
+
- id: resolve-product-composition
|
|
294
|
+
order: 5
|
|
295
|
+
title: Then resolve the remaining product composition
|
|
296
|
+
instruction: |
|
|
297
|
+
Reuse an established idiom where it fits; otherwise compose existing Common semantics and family
|
|
298
|
+
anatomy, extend an existing owner when evidence shows its declared gap, and add a new owner only
|
|
299
|
+
for an evidenced gap. A shape absent from this table and a shape seen once are evidence limits
|
|
300
|
+
on house style, not reasons to stop creative work already inside the approved product scope.
|
|
301
|
+
|
|
302
|
+
authoritySplit:
|
|
303
|
+
directionChoiceRequired: |
|
|
304
|
+
DIRECTION_CHOICE_REQUIRED is reserved for a material product choice whose acceptable options lead
|
|
305
|
+
to different outcomes and whose answer is absent from the request. A reference/idiom conflict is
|
|
306
|
+
resolved by the authority split: product facts and approved region order come from the request,
|
|
307
|
+
Common semantics come from knowledge/ui, and family anatomy and reusable style come from the
|
|
308
|
+
selected Grammar source. State assumptions for minor in-scope choices; when a material choice has
|
|
309
|
+
no answer, ask the user. Do not invent a fallback algorithm or run the next op.
|
|
310
|
+
imageryClarification:
|
|
311
|
+
date: "2026-09-05"
|
|
312
|
+
guidance: |
|
|
313
|
+
Imagery can highlight the key idea, explain content, strengthen recognition or create a relevant,
|
|
314
|
+
evocative focal point. Deliberate visual emphasis is a concrete role; an image need not be a
|
|
315
|
+
functional diagram to earn its place. Name the subject, the idea or claim it highlights, and the
|
|
316
|
+
intended order of attention in the selected .starciwork design node. Judge its scale, contrast and
|
|
317
|
+
crop on the rendered surface: it should support that hierarchy without obscuring the content or
|
|
318
|
+
competing with the primary action. Blank space alone is never a reason to add an image; filling
|
|
319
|
+
space or balancing density without a content or emphasis role does not qualify. Record no image
|
|
320
|
+
when imagery would not improve the composition.
|
|
321
|
+
limitation: |
|
|
322
|
+
Extending or adding an owner is not authorized by this playbook; it needs an op with explicit
|
|
323
|
+
library-edit scope. Historical image notes are design references, not decisions for the current
|
|
324
|
+
product.
|
|
325
|
+
guidance:
|
|
326
|
+
- id: nested-controls-and-panel-lists
|
|
327
|
+
ownership: FE-RULE-OWNERSHIP
|
|
328
|
+
implements: [ANATOMY-2, PADDING-0]
|
|
329
|
+
requirement: |
|
|
330
|
+
SurfaceCard (always painted), SurfaceListCard, a painted SurfaceAccordionCard, Sidebar, Dialog,
|
|
331
|
+
AlertDialog, Drawer and Popover establish the surface nesting context; Rail and an unpainted
|
|
332
|
+
SurfaceAccordionCard pass their parent's context through. Input, SearchField, Textarea,
|
|
333
|
+
NumberField, DateField, TimeField, Select, ComboBox, DatePicker, DateRangePicker, Checkbox,
|
|
334
|
+
CheckboxGroup, RadioGroup, OtpInput and PressableField resolve variant="secondary" inside it and
|
|
335
|
+
variant="primary" outside. Compose the surface and omit the field variant.
|
|
336
|
+
Switch and Slider have no layer variant; a Radio follows its RadioGroup. ChatWorkspace's
|
|
337
|
+
compact drawer also establishes the context. SurfaceCard and bounded SurfaceAccordionCard derive
|
|
338
|
+
data-grammar-surface-depth from the parent context. The top layer is raised and a nested layer is
|
|
339
|
+
subordinate; never raise both. The family DNA owns their surface paint.
|
|
340
|
+
FE_NESTED_VARIANT rejects a static field variant
|
|
341
|
+
anywhere in app code and primary Tabs inside a painted surface; FE_GRAMMAR_LAYOUT_ONLY rejects
|
|
342
|
+
application paint utilities, arbitrary paint properties and inline style paint.
|
|
343
|
+
DRAW_NESTED_VARIANT checks the corresponding rendered variant and
|
|
344
|
+
surface stamps.
|
|
345
|
+
ListBox resolves inset="panel" inside that context; callers may select the same value explicitly
|
|
346
|
+
elsewhere. Its rows and sections captions share the panel edge. SurfaceListCard owns its list
|
|
347
|
+
shell with no added inset; SurfaceCard composition="joined" owns flush bands. A grouping wrapper
|
|
348
|
+
adds no padding. An extra px-*, ps-* or pe-* around ListBox or a section caption stacks the inset
|
|
349
|
+
and fails FE_LIST_PANEL_PADDING.
|
|
350
|
+
rationale: |
|
|
351
|
+
ANATOMY-2 owns nested separation and PADDING-0 owns the single content edge. Their props and
|
|
352
|
+
variant values belong here under FE-RULE-OWNERSHIP in knowledge/patterns/fe/index.yaml; R318
|
|
353
|
+
executes the composition checks in the universal @starci/eslint-canon-fe because they hold for
|
|
354
|
+
every family that publishes the shared surface and field API.
|
|
355
|
+
- id: overlays-inside-the-root
|
|
356
|
+
requirement: |
|
|
357
|
+
Every overlay trigger (Dialog, AlertDialog, Drawer, Popover, DropdownMenu, and the Select,
|
|
358
|
+
ComboBox and DatePicker fields) must render inside `CoreGrammarRoot`. They portal into the nearest
|
|
359
|
+
`.grammar-common-root`; a trigger in a separate React tree or outside the root opens its overlay
|
|
360
|
+
in `<body>`, where no Core token, theme or forced-colours rule applies.
|
|
361
|
+
rationale: |
|
|
362
|
+
Core's DNA is scoped to `[data-grammar-family="core"]`; an overlay is only Core while it is a DOM
|
|
363
|
+
descendant of that scope.
|
|
364
|
+
- id: one-toaster-placed-clear-of-chrome
|
|
365
|
+
requirement: |
|
|
366
|
+
Mount exactly one `Toaster` (with `label` and `dismissLabel`) inside the root and add toasts with
|
|
367
|
+
`toastQueue.add({ title, description, tone, action })`. Keep the default `bottom-end` on a page
|
|
368
|
+
with no fixed bottom chrome; choose a placement clear of any page-owned fixed content. A toast
|
|
369
|
+
with an `action`, or `tone` negative/pending, persists until dismissed.
|
|
370
|
+
rationale: |
|
|
371
|
+
A second Toaster duplicates announcements; fixed content must not cover an action.
|
|
372
|
+
- id: forms-share-the-field-contract
|
|
373
|
+
requirement: |
|
|
374
|
+
Compose forms from the Field-contract controls inside `Form` (and `Fieldset` for named groups);
|
|
375
|
+
reach for `Field` only to give a non-Grammar control the same label/description/error anatomy.
|
|
376
|
+
Server refusals go in `Form validationErrors` by field `name`, not in a separate banner.
|
|
377
|
+
rationale: |
|
|
378
|
+
One contract means one error location a screen reader can find, and one set of
|
|
379
|
+
`data-grammar-field-*` hooks every family paints.
|
|
380
|
+
- id: mobile-chrome-by-width
|
|
381
|
+
requirement: |
|
|
382
|
+
Follow FE-APP-SHELL-4 in knowledge/ui/composition/app-shell.yaml for mobile navbar content,
|
|
383
|
+
the subnav trigger row and the Sidebar drawer.
|
|
384
|
+
rationale: |
|
|
385
|
+
The shell canon owns responsive navigation composition.
|
|
386
|
+
- id: state-recipes
|
|
387
|
+
title: stateRecipes — the one recipe for per-slot data status
|
|
388
|
+
requirement: |
|
|
389
|
+
A data status — loading, unauthorized, forbidden, error, empty — belongs to one slot, the data of one api, and
|
|
390
|
+
renders only through the product's one `SlotView` composite with the recipes below. A data status
|
|
391
|
+
is never drawn and never a shape: a surface's `XState` lists drawn shapes only
|
|
392
|
+
(`starci-fe/no-data-status-shape`), and a Block's `component.tsx` never branches on `isLoading`,
|
|
393
|
+
`isError` or `isForbidden` itself (`starci-grammar/slot-status-through-slotview`). Only a Block owns
|
|
394
|
+
slots; a page, layout or overlay has no data status. A block with several apis carries one slot
|
|
395
|
+
per api and each resolves its status independently. The block keeps its SurfaceCard and label in
|
|
396
|
+
every status; the recipe replaces only that slot's region. Every product built on this family
|
|
397
|
+
applies it.
|
|
398
|
+
rationale: |
|
|
399
|
+
One recipe per status keeps every block's loading, 403, failure and empty identical, and keeps
|
|
400
|
+
the owner's approved drawings free of states nobody drew.
|
|
401
|
+
reference:
|
|
402
|
+
example: examples/shape-slot
|
|
403
|
+
files:
|
|
404
|
+
- apps/shape-slot/src/components/composites/SlotView/index.tsx # SlotView: the recipe, in status order
|
|
405
|
+
- apps/shape-slot/src/modules/slot/index.ts # Slot, toSlot (403 is its own status), SlotLabels
|
|
406
|
+
- apps/shape-slot/src/hooks/slot/useSlotLabels.ts # shared forbidden/error/retry copy + the slot's empty sentence
|
|
407
|
+
codeExample: examples/index.yaml#connected-block
|
|
408
|
+
consolidates:
|
|
409
|
+
- idioms.yaml pending-same-tree
|
|
410
|
+
- knowledge/ui/composition/state.yaml STATE-2, STATE-3
|
|
411
|
+
- knowledge/ui/composition/feedback.yaml FEEDBACK-1, FEEDBACK-2
|
|
412
|
+
- DNA.yaml Skeleton, EmptyNotice, Alert
|
|
413
|
+
order: [loading, unauthorized, forbidden, error, empty, ready]
|
|
414
|
+
recipes:
|
|
415
|
+
- status: loading
|
|
416
|
+
uiRules: [STATE-2]
|
|
417
|
+
render: |
|
|
418
|
+
The ready tree, drawn from placeholder data of the slot's type, in the shape the unit's
|
|
419
|
+
`xDefaultState` names, with isSkeleton on every owner: same card, same bands, same row count
|
|
420
|
+
(idiom pending-same-tree). Never a spinner and never a different tree. The skeleton renders
|
|
421
|
+
inert geometry and states no value; an unknown number is a bar, never a rendered zero.
|
|
422
|
+
skeletonMap:
|
|
423
|
+
- part: Text line or Heading
|
|
424
|
+
render: "`isSkeleton` on the owner: one bar at that type-scale step's line height, as long as the placeholder text"
|
|
425
|
+
- part: Media (Image, MediaFrame viewport)
|
|
426
|
+
render: "`Skeleton shape=\"rect\"` at the frame's `ratio` (square, landscape, wide); `shape=\"circle\"` for an avatar. A block at the ratio, never a collapsed box"
|
|
427
|
+
- part: Button
|
|
428
|
+
render: |
|
|
429
|
+
A bar at the Button's height with the Button's pill corner (family.yaml button-pill):
|
|
430
|
+
`isSkeleton` where the installed Button publishes it, otherwise `Skeleton shape="rect"` in
|
|
431
|
+
the Button's box. It never fires (idiom pending-same-tree action-must-not-fire)
|
|
432
|
+
- part: A region that exists only on ready data (a disclosure of the loaded lines)
|
|
433
|
+
render: Absent while isSkeleton (STATE-3)
|
|
434
|
+
- status: unauthorized
|
|
435
|
+
uiRules: [STATE-3]
|
|
436
|
+
render: |
|
|
437
|
+
401 is its own status, never forbidden and never an error. The slot shows one sign-in prompt in
|
|
438
|
+
place: `EmptyNotice message={labels.unauthorized} actionLabel={labels.signIn}
|
|
439
|
+
actionVariant="primary" onAction={signIn}`, where `signIn` opens the product's sign-in route and
|
|
440
|
+
returns to this address. The prompt discloses none of the slot's data. A secondary slot hides, as
|
|
441
|
+
forbidden does, so a page whose reads all need a session shows the prompt once, in its primary slot.
|
|
442
|
+
- status: forbidden
|
|
443
|
+
uiRules: [STATE-3]
|
|
444
|
+
render: |
|
|
445
|
+
403 is its own status, never an error (`toSlot`). The slot's role decides: a secondary slot
|
|
446
|
+
uses `hide` — it unmounts with its wrapper and leaves nothing focusable; a primary slot uses
|
|
447
|
+
`notice` — `EmptyNotice message={labels.forbidden}` in place. A primary slot is never hidden
|
|
448
|
+
and a secondary slot never shows a notice.
|
|
449
|
+
- status: error
|
|
450
|
+
uiRules: [FEEDBACK-2]
|
|
451
|
+
render: |
|
|
452
|
+
Retry at the slot, re-running that slot's read only (the query's `mutate`), never a page
|
|
453
|
+
reload. Block form, where the slot is a region: `EmptyNotice message={labels.error}
|
|
454
|
+
actionLabel={labels.retry} actionVariant="secondary" onAction={retry}` with `isActionPending`
|
|
455
|
+
bound while the retry runs. Inline form, where the slot is one line inside a row or band too
|
|
456
|
+
small for a notice: `Alert tone="negative"` in the slot's place, carrying the retry action. A
|
|
457
|
+
command that is unsafe to repeat is not retried (FEEDBACK-2 case-4); a rejected field value
|
|
458
|
+
is a field error (FEEDBACK-1), never a slot status.
|
|
459
|
+
- status: empty
|
|
460
|
+
uiRules: [FEEDBACK-2, STATE-3]
|
|
461
|
+
render: |
|
|
462
|
+
`EmptyNotice message={labels.empty}` with the slot's own empty sentence; `actionLabel` and
|
|
463
|
+
`onAction` only when a real next step exists, otherwise both are omitted.
|
|
464
|
+
- status: ready
|
|
465
|
+
render: The slot's items through the same tree, isSkeleton false.
|
|
466
|
+
notFound: |
|
|
467
|
+
A route whose resource does not exist calls `notFound()` in its route adapter (FE-FUNCTION-7
|
|
468
|
+
case-2). 404 is never a page state, a shape or a slot status.
|
|
469
|
+
copy: |
|
|
470
|
+
unauthorized, signIn, forbidden, error and retry come from the shared `slot` message namespace; each slot adds its own
|
|
471
|
+
empty sentence. The connected half resolves them into `SlotLabels`; no status string is written
|
|
472
|
+
in a component.
|
|
473
|
+
|
|
474
|
+
presentation:
|
|
475
|
+
collectionItems:
|
|
476
|
+
ownership: FE-RULE-OWNERSHIP
|
|
477
|
+
requirement: |
|
|
478
|
+
ListBoxItem, DropdownMenuItem, SidebarItem and the ListOption shared by Select and ComboBox
|
|
479
|
+
render descriptions in HeroUI's Description slot. The family DNA's collectionItem.descriptionLines
|
|
480
|
+
defaults to one line with an ellipsis (owner:item-description-one-line); descriptionLines accepts
|
|
481
|
+
1, 2 or 3 for a bounded override through the packaged stylesheet's line clamp. The complete
|
|
482
|
+
description remains available to assistive technology. ListBoxItem emphasis="unread" uses
|
|
483
|
+
collectionItem.unreadWeight for both label and description; omit it for HeroUI's read treatment.
|
|
484
|
+
labelLeading remains an optional decorative title slot, separate from conversation identity.
|
|
485
|
+
glyphs:
|
|
486
|
+
ownership: FE-RULE-OWNERSHIP
|
|
487
|
+
implements: FE-ICON
|
|
488
|
+
source: knowledge/hfs/fe-stack.yaml
|
|
489
|
+
requirement: |
|
|
490
|
+
The glyph sources and cuts declared by the stack catalog are realized through the Icon leaf.
|
|
491
|
+
StarCi's Grammar lint checks that concrete boundary and its measured icon scale. Verify the
|
|
492
|
+
selected region's value against the family DNA and styles rather than inventing an app value.
|
|
493
|
+
A menu item's brand mark is passed as DropdownMenuItem.mark with iconSource omitted; the public
|
|
494
|
+
type makes them mutually exclusive and Grammar sizes the mark in the icon's leading slot.
|
|
495
|
+
groupIdentity:
|
|
496
|
+
ownership: FE-RULE-OWNERSHIP
|
|
497
|
+
implements: COLLECTION-3
|
|
498
|
+
requirement: |
|
|
499
|
+
Compose AvatarGroup layout="grid" at one Avatar size, including size="control". Two members
|
|
500
|
+
split the round mark; three show one member above two below; four occupy a 2 by 2 grid. Every cell
|
|
501
|
+
has a ring in the surface colour. At five or more members the first three remain visible and the
|
|
502
|
+
fourth cell shows the total member count, such as 5 or 30, through Avatar's shared fallback path.
|
|
503
|
+
The row layout keeps its +N count. overflowLabel receives the hidden-member count in both layouts
|
|
504
|
+
for accessible naming, while the grid digits show the total. A single person uses Avatar.
|
|
505
|
+
Pass an uploaded image as `src`; otherwise Grammar always draws the bundled DiceBear `initials`
|
|
506
|
+
style, with the first letter of the first word and the first letter of the last word of `name`
|
|
507
|
+
(one word supplies one letter). `seed`, the user or entity id, determines colour when present;
|
|
508
|
+
otherwise `name` does. The image clips to the Avatar shape, circle by default, and keeps text
|
|
509
|
+
contrast in both themes (owner:avatar-default-dicebear-initials).
|
|
510
|
+
Avatar, AvatarButton and each AvatarGroup member accept `kind="person" | "agent"`, default person.
|
|
511
|
+
An agent's name is free text and uses exactly the same initials rule, without role-suffix parsing.
|
|
512
|
+
`kind="agent"` adds a HeroUI Badge at the Avatar's bottom-right edge with a bot glyph, a surface-colour
|
|
513
|
+
ring and an accessible name containing AI (owner:agent-avatar-initials-bot-badge). The family hides
|
|
514
|
+
the badge below 32px, including the 16px to 24px cells in a 2 by 2 grid; the identity's accessible
|
|
515
|
+
name still includes AI. Apps neither declare nor import `@dicebear/*`. Apps supply member identity
|
|
516
|
+
data; the renderer owns the geometry.
|
|
517
|
+
padding:
|
|
518
|
+
ownership: FE-RULE-OWNERSHIP
|
|
519
|
+
knowledge: knowledge/ui/presentation/padding.yaml
|
|
520
|
+
ownerVocabulary:
|
|
521
|
+
owners:
|
|
522
|
+
- name: App
|
|
523
|
+
meaning: The boundary belongs to the application
|
|
524
|
+
applicationWrites: The class
|
|
525
|
+
- name: A component name
|
|
526
|
+
meaning: Common already applies the inset inside that component
|
|
527
|
+
applicationWrites: Nothing. Compose it
|
|
528
|
+
- name: —
|
|
529
|
+
meaning: Common exposes no public path for this boundary
|
|
530
|
+
applicationWrites: The class, recorded as a workaround
|
|
531
|
+
notes: |-
|
|
532
|
+
Each case names who owns the inset. The owner decides whether the application writes a class at all.
|
|
533
|
+
|
|
534
|
+
Writing a class where a component is the owner is `APP_REIMPLEMENTATION`. Reaching into a Grammar
|
|
535
|
+
component with a selector or a passed class to change its inset is `APP_OVERRIDE`.
|
|
536
|
+
scale:
|
|
537
|
+
steps:
|
|
538
|
+
- id: PADDING-0
|
|
539
|
+
class: p-0
|
|
540
|
+
value: "0"
|
|
541
|
+
- id: PADDING-1
|
|
542
|
+
class: p-1
|
|
543
|
+
value: .25rem
|
|
544
|
+
- id: PADDING-2
|
|
545
|
+
class: p-2
|
|
546
|
+
value: .5rem
|
|
547
|
+
- id: PADDING-3
|
|
548
|
+
class: p-3
|
|
549
|
+
value: .75rem
|
|
550
|
+
- id: PADDING-4
|
|
551
|
+
class: p-4
|
|
552
|
+
value: 1rem
|
|
553
|
+
- id: PADDING-5
|
|
554
|
+
class: p-6
|
|
555
|
+
value: 1.5rem
|
|
556
|
+
- id: PADDING-6
|
|
557
|
+
class: p-8
|
|
558
|
+
value: 2rem
|
|
559
|
+
notes: |-
|
|
560
|
+
`COMMON_SPACING_SCALE` is closed. The rule ID is the ordinal position on that scale. It is not the
|
|
561
|
+
Tailwind step number, and the two diverge from PADDING-5 onward.
|
|
562
|
+
|
|
563
|
+
Common publishes no padding token. The internal insets below are reached through the components that
|
|
564
|
+
own them, not through a variable the application may set.
|
|
565
|
+
The page inset is the one exception and is not on this scale: `--grammar-page-inset` resolves to
|
|
566
|
+
`clamp(1rem, 3vw, 2rem)`, a responsive value owned by `PageContainer`. An application never
|
|
567
|
+
reproduces it with a fixed class.
|
|
568
|
+
The rem values resolve to `0 / 4 / 8 / 12 / 16 / 24 / 32` CSS pixels only at a computed root of
|
|
569
|
+
`16px`. Runtime checks use `expectedPx = remFactor * observedRootFontPx`.
|
|
570
|
+
componentOwnership:
|
|
571
|
+
title: Padding Common already owns
|
|
572
|
+
notes: |-
|
|
573
|
+
Retained ownership reference, not proof of the current package/API. Verify actual installed source/props/tokens before use; correct outdated rows within selected maintenance scope using [source and maintenance](index.yaml#source-and-maintenance).
|
|
574
|
+
A part that carries one rule per side or per property names that CSS property after the part (`item padding-inline`); one part and property has one rule.
|
|
575
|
+
rows:
|
|
576
|
+
- component: ChatWorkspace
|
|
577
|
+
element: drawer body, hasRail, isCompactRail
|
|
578
|
+
rule: PADDING-0
|
|
579
|
+
- component: ChatWorkspace
|
|
580
|
+
element: overlay rail, hasRail, isCompactRail
|
|
581
|
+
rule: PADDING-4
|
|
582
|
+
- component: ChatWorkspace
|
|
583
|
+
element: overlay rail, hasRail, not isCompactRail
|
|
584
|
+
rule: PADDING-4
|
|
585
|
+
- component: ChatWorkspace
|
|
586
|
+
element: rail trigger boundary padding-block, hasRail, isCompactRail
|
|
587
|
+
rule: PADDING-2
|
|
588
|
+
- component: ChatWorkspace
|
|
589
|
+
element: rail trigger boundary padding-inline, hasRail, isCompactRail
|
|
590
|
+
rule: PADDING-4
|
|
591
|
+
- component: ChatWorkspace
|
|
592
|
+
element: rail trigger, hasRail, isCompactRail
|
|
593
|
+
rule: PADDING-3
|
|
594
|
+
- component: EmptyNotice
|
|
595
|
+
element: root
|
|
596
|
+
rule: PADDING-4
|
|
597
|
+
- component: HorizontalScrollRegion
|
|
598
|
+
element: root
|
|
599
|
+
rule: PADDING-1
|
|
600
|
+
- component: NavigationFeatureNav
|
|
601
|
+
element: primary
|
|
602
|
+
rule: PADDING-3
|
|
603
|
+
- component: OtpInput
|
|
604
|
+
element: root
|
|
605
|
+
rule: PADDING-1
|
|
606
|
+
- component: PressableField
|
|
607
|
+
element: root
|
|
608
|
+
rule: PADDING-3
|
|
609
|
+
- component: PressableField
|
|
610
|
+
element: shortcut, shortcut!=undefined
|
|
611
|
+
rule: PADDING-1
|
|
612
|
+
- component: Rail
|
|
613
|
+
element: body padding-inline, inset="content"
|
|
614
|
+
rule: PADDING-3
|
|
615
|
+
- component: Rail
|
|
616
|
+
element: body padding-block, inset="content"
|
|
617
|
+
rule: PADDING-5
|
|
618
|
+
- component: Sidebar
|
|
619
|
+
element: footer padding-inline padding-block-end, not collapsed, footer!=undefined
|
|
620
|
+
rule: PADDING-3
|
|
621
|
+
- component: Sidebar
|
|
622
|
+
element: footer padding-block-start, not collapsed, footer!=undefined
|
|
623
|
+
rule: PADDING-0
|
|
624
|
+
- component: Sidebar
|
|
625
|
+
element: header, not collapsed, header!=undefined
|
|
626
|
+
rule: PADDING-3
|
|
627
|
+
- component: Sidebar
|
|
628
|
+
element: list, collapsed
|
|
629
|
+
rule: PADDING-2
|
|
630
|
+
- component: Sidebar
|
|
631
|
+
element: list, not collapsed
|
|
632
|
+
rule: PADDING-3
|
|
633
|
+
- component: Sidebar
|
|
634
|
+
element: section label padding-block-end, group.label!=undefined, not collapsed
|
|
635
|
+
rule: PADDING-1
|
|
636
|
+
- component: Sidebar
|
|
637
|
+
element: section label padding-inline, group.label!=undefined, not collapsed
|
|
638
|
+
rule: PADDING-2
|
|
639
|
+
- component: Sidebar
|
|
640
|
+
element: section label padding-block-start, group.label!=undefined, not collapsed
|
|
641
|
+
rule: PADDING-3
|
|
642
|
+
- component: Sidebar
|
|
643
|
+
element: toggle, canToggle
|
|
644
|
+
rule: PADDING-2
|
|
645
|
+
- component: StaticStateRow
|
|
646
|
+
element: root
|
|
647
|
+
rule: PADDING-4
|
|
648
|
+
- component: ListRow
|
|
649
|
+
element: root (carries starci-core-static-row, @starci/grammar 0.9.1)
|
|
650
|
+
rule: PADDING-4
|
|
651
|
+
- component: Subnav
|
|
652
|
+
element: root
|
|
653
|
+
rule: PADDING-3
|
|
654
|
+
- component: SurfaceAccordionCard
|
|
655
|
+
element: accordion panel
|
|
656
|
+
rule: PADDING-0
|
|
657
|
+
- component: SurfaceAccordionCard
|
|
658
|
+
element: accordion trigger
|
|
659
|
+
rule: PADDING-4
|
|
660
|
+
- component: SurfaceAccordionCard
|
|
661
|
+
element: body padding-top
|
|
662
|
+
rule: PADDING-0
|
|
663
|
+
- component: SurfaceAccordionCard
|
|
664
|
+
element: body padding-bottom
|
|
665
|
+
rule: PADDING-3
|
|
666
|
+
- component: SurfaceAccordionCard
|
|
667
|
+
element: body padding-right
|
|
668
|
+
rule: PADDING-4
|
|
669
|
+
- component: SurfaceAccordionCard
|
|
670
|
+
element: body padding-left
|
|
671
|
+
rule: PADDING-5
|
|
672
|
+
- component: SurfaceCard
|
|
673
|
+
element: content, composition!="joined"
|
|
674
|
+
rule: PADDING-4
|
|
675
|
+
- component: SurfaceCard
|
|
676
|
+
element: content, composition="joined"
|
|
677
|
+
rule: PADDING-0
|
|
678
|
+
- component: SurfaceListCard
|
|
679
|
+
element: root
|
|
680
|
+
rule: PADDING-0
|
|
681
|
+
- component: Tabs
|
|
682
|
+
element: hero tabs tab
|
|
683
|
+
rule: PADDING-3
|
|
684
|
+
- component: Tabs
|
|
685
|
+
element: root, inset="page"
|
|
686
|
+
rule: PADDING-5
|
|
687
|
+
- component: TextAction
|
|
688
|
+
element: root padding-inline, appearance="choice"
|
|
689
|
+
rule: PADDING-2
|
|
690
|
+
- component: TextAction
|
|
691
|
+
element: root padding-inline, appearance="route"
|
|
692
|
+
rule: PADDING-3
|
|
693
|
+
- component: TextAction
|
|
694
|
+
element: root padding-inline, appearance="section"
|
|
695
|
+
rule: PADDING-3
|
|
696
|
+
- component: TextAction
|
|
697
|
+
element: root padding-block, appearance="tab"
|
|
698
|
+
rule: PADDING-3
|
|
699
|
+
- component: Tooltip
|
|
700
|
+
element: content padding-block
|
|
701
|
+
rule: PADDING-1
|
|
702
|
+
- component: Tooltip
|
|
703
|
+
element: content padding-inline
|
|
704
|
+
rule: PADDING-2
|
|
705
|
+
guidance:
|
|
706
|
+
- id: side-contact
|
|
707
|
+
title: Side contact
|
|
708
|
+
requirement: |-
|
|
709
|
+
A boundary does not always take one value on all four sides. When a surface is flush and its child
|
|
710
|
+
bands own their own inset, each side takes its value from what that side touches.
|
|
711
|
+
| What the side touches | Value | Rule |
|
|
712
|
+
| --- | --- | --- |
|
|
713
|
+
| The outer edge of the surface | `1rem` | PADDING-4 |
|
|
714
|
+
| A separator between bands | `.75rem` | PADDING-3 |
|
|
715
|
+
| Nothing, because a child owns the edge | `0` | PADDING-0 |
|
|
716
|
+
A separator already draws the boundary, so the side meeting it needs less clearance than a bare outer
|
|
717
|
+
edge. Inline sides of a band always meet the outer edge, so they stay at `1rem`.
|
|
718
|
+
For a stack of bands this resolves to `px-4` throughout, `pt-4` on the first band, `pb-4` on the last,
|
|
719
|
+
and `.75rem` on every side that meets a separator. A band between two separators is `px-4 py-3`.
|
|
720
|
+
Common owns the flush surface itself through `composition="joined"`, which sets the content inset to
|
|
721
|
+
`0`. It exposes no path for the band insets, so those remain an application workaround.
|
|
722
|
+
- id: composite-insets
|
|
723
|
+
title: Composite insets
|
|
724
|
+
requirement: |-
|
|
725
|
+
`PADDING-0` through `PADDING-6` are the scale: one rule, one value. From `PADDING-7` onward the rules
|
|
726
|
+
are named recipes whose sides deliberately take different values. The number keeps counting, but it
|
|
727
|
+
stops meaning a position on the scale, so a composite rule never appears in the scale table and never
|
|
728
|
+
introduces a value of its own. Every side of a composite resolves to a base rule.
|
|
729
|
+
Append the next number when a genuinely new asymmetric pattern appears. State the reason each side
|
|
730
|
+
differs; a recipe without one is a product decision, not a rule.
|
|
731
|
+
- id: axis-variants
|
|
732
|
+
title: Axis variants
|
|
733
|
+
requirement: |-
|
|
734
|
+
`px-*` and `py-*` are not separate rules. They apply an existing rule to one axis when the two axes
|
|
735
|
+
carry different boundary decisions.
|
|
736
|
+
| Axis | Class | Meaning |
|
|
737
|
+
| --- | --- | --- |
|
|
738
|
+
| Inline | `px-*` | The chosen rule applies to logical start and end only |
|
|
739
|
+
| Block | `py-*` | The chosen rule applies to block start and end only |
|
|
740
|
+
| One side | `pt-*`, `pb-*`, `ps-*`, `pe-*` | The chosen rule applies to that side only |
|
|
741
|
+
Per-side classes are what the side-contact rule needs, because the two block sides of one band often
|
|
742
|
+
take different values. `px-4 pt-4 pb-3` is PADDING-4 on three sides and PADDING-3 on the side meeting
|
|
743
|
+
a separator, and every side still names its own case.
|
|
744
|
+
On the same element the narrower class wins the sides it names rather than adding, so `p-4 pb-3`
|
|
745
|
+
resolves to `1rem` on three sides and `.75rem` at the bottom. Between nested elements padding does
|
|
746
|
+
add, which is why a wrapper introduced only to change one side produces an inset nobody intended.
|
|
747
|
+
recipes:
|
|
748
|
+
- rule: PADDING-0
|
|
749
|
+
title: "`p-0` / `0`"
|
|
750
|
+
cases:
|
|
751
|
+
- id: case-1
|
|
752
|
+
when: A container only groups children which each carry their own boundary
|
|
753
|
+
owner: "`App`"
|
|
754
|
+
render: '`<section className="p-0">`'
|
|
755
|
+
- id: case-2
|
|
756
|
+
when: Surface bands run flush to the card edge
|
|
757
|
+
owner: "`SurfaceCard`"
|
|
758
|
+
render: '`composition="joined"` already sets `0`'
|
|
759
|
+
- id: case-3
|
|
760
|
+
when: A Grammar list sits inside a panel or its rows own their own inset
|
|
761
|
+
owner: "`SurfaceListCard` / `ListBox`"
|
|
762
|
+
render: ListBox resolves inset="panel" in a surface nesting context, or callers select it explicitly. Its
|
|
763
|
+
row boxes align with the panel content edge, like the search field and tabs boxes, without
|
|
764
|
+
negative-margin bleed (owner:panel-list-box-edge). The list retains HeroUI p-1; items retain
|
|
765
|
+
px-2 py-1.5, rounded-2xl and mt-1 between items, with the same padding and radius for hover,
|
|
766
|
+
pressed and selected backgrounds. Section captions share the inset content edge. Padding
|
|
767
|
+
wrappers with px-*, ps-* or pe-* stack this inset and fail FE_LIST_PANEL_PADDING in the universal
|
|
768
|
+
FE canon (R318).
|
|
769
|
+
- rule: PADDING-1
|
|
770
|
+
title: "`p-1` / `.25rem`"
|
|
771
|
+
cases:
|
|
772
|
+
- id: case-1
|
|
773
|
+
when: A compact marker or key floating over an app-owned canvas
|
|
774
|
+
owner: "`App`"
|
|
775
|
+
render: '`<aside className="p-1">` with `<Text size="xs">`'
|
|
776
|
+
- id: case-2
|
|
777
|
+
when: A scroll region reserving room so focus rings are not clipped
|
|
778
|
+
owner: "`HorizontalScrollRegion`"
|
|
779
|
+
render: Compose the region; no padding
|
|
780
|
+
- id: case-3
|
|
781
|
+
when: A short hint attached to a control
|
|
782
|
+
owner: "`Tooltip`"
|
|
783
|
+
render: Compose the tooltip; no padding
|
|
784
|
+
- rule: PADDING-2
|
|
785
|
+
title: "`p-2` / `.5rem`"
|
|
786
|
+
cases:
|
|
787
|
+
- id: case-1
|
|
788
|
+
when: A small auxiliary strip whose four sides share one inset
|
|
789
|
+
owner: "`App`"
|
|
790
|
+
render: '`<aside className="p-2">` with `<Text size="sm">`'
|
|
791
|
+
- id: case-2
|
|
792
|
+
when: A compact bar of controls above app-owned content
|
|
793
|
+
owner: "`App`"
|
|
794
|
+
render: '`<div className="flex items-center gap-2 p-2">`'
|
|
795
|
+
- rule: PADDING-3
|
|
796
|
+
title: "`p-3` / `.75rem`"
|
|
797
|
+
cases:
|
|
798
|
+
- id: case-1
|
|
799
|
+
when: A nested app container that owns a boundary distinct from its parent's
|
|
800
|
+
owner: "`App`"
|
|
801
|
+
render: '`<aside className="p-3">` inside a `p-4` section'
|
|
802
|
+
- id: case-2
|
|
803
|
+
when: The inline inset of a tab strip or secondary navigation
|
|
804
|
+
owner: "`Tabs`, `Subnav`"
|
|
805
|
+
render: Compose the component; no padding
|
|
806
|
+
- id: case-3
|
|
807
|
+
when: A band side that meets a separator inside a flush surface
|
|
808
|
+
owner: "`—`"
|
|
809
|
+
render: '`<div className="px-4 py-3">` between two separators'
|
|
810
|
+
- id: case-4
|
|
811
|
+
when: The separator-facing side of a metric cell or list row
|
|
812
|
+
owner: '`DescriptionList` layout="stat-strip" (its divided cells, @starci/grammar 0.6.0), else `—`'
|
|
813
|
+
render: '`<div className="p-4 pb-3">`, or `pt-3` when the separator is above'
|
|
814
|
+
- rule: PADDING-4
|
|
815
|
+
title: "`p-4` / `1rem`"
|
|
816
|
+
cases:
|
|
817
|
+
- id: case-1
|
|
818
|
+
when: An app section that owns the full distance from its border to its content
|
|
819
|
+
owner: "`App`"
|
|
820
|
+
render: '`<section className="p-4">` with `<Heading level={2}>`'
|
|
821
|
+
- id: case-2
|
|
822
|
+
when: Content inside a card
|
|
823
|
+
owner: "`SurfaceCard`"
|
|
824
|
+
render: Compose the card; no padding
|
|
825
|
+
- id: case-3
|
|
826
|
+
when: A disclosure trigger and its expanded panel
|
|
827
|
+
owner: "`SurfaceAccordionCard`"
|
|
828
|
+
render: Compose the card; no padding
|
|
829
|
+
- id: case-4
|
|
830
|
+
when: An empty-state notice
|
|
831
|
+
owner: "`EmptyNotice`"
|
|
832
|
+
render: Compose the notice; no padding
|
|
833
|
+
- id: case-5
|
|
834
|
+
when: A fenced code block
|
|
835
|
+
owner: "`FencedCodeBlock`"
|
|
836
|
+
render: Compose the block; no padding
|
|
837
|
+
- id: case-6
|
|
838
|
+
when: A band side that meets the outer edge of a flush surface
|
|
839
|
+
owner: "`—`"
|
|
840
|
+
render: "`pt-4` on the first band, `pb-4` on the last"
|
|
841
|
+
- id: case-7
|
|
842
|
+
when: The inline sides of any band inside a flush surface
|
|
843
|
+
owner: "`—`"
|
|
844
|
+
render: "`px-4` on every band, unchanged by position"
|
|
845
|
+
- rule: PADDING-5
|
|
846
|
+
title: "`p-6` / `1.5rem`"
|
|
847
|
+
cases:
|
|
848
|
+
- id: case-1
|
|
849
|
+
when: A section whose four sides share one deliberately generous boundary decision
|
|
850
|
+
owner: "`App`"
|
|
851
|
+
render: '`<section className="p-6">`'
|
|
852
|
+
- id: case-2
|
|
853
|
+
when: A tab frame carrying the page-level inline inset
|
|
854
|
+
owner: "`Tabs`"
|
|
855
|
+
render: '`inset="page"` already sets `1.5rem`'
|
|
856
|
+
- rule: PADDING-6
|
|
857
|
+
title: "`p-8` / `2rem`"
|
|
858
|
+
cases:
|
|
859
|
+
- id: case-1
|
|
860
|
+
when: A page region that needs strong equal clearance on every side
|
|
861
|
+
owner: "`App`"
|
|
862
|
+
render: '`<section className="p-8">`'
|
|
863
|
+
- id: case-2
|
|
864
|
+
when: An empty or waiting region occupying most of the viewport
|
|
865
|
+
owner: "`App`"
|
|
866
|
+
render: '`<section className="p-8">` with `<EmptyNotice>`'
|
|
867
|
+
- rule: PADDING-7
|
|
868
|
+
title: Inset step at the wider breakpoint
|
|
869
|
+
cases:
|
|
870
|
+
- id: case-1
|
|
871
|
+
when: A decision surface whose inline inset opens up on a wider viewport
|
|
872
|
+
owner: "`—`"
|
|
873
|
+
render: '`<div className="px-4 sm:px-6">`'
|
|
874
|
+
- id: case-2
|
|
875
|
+
when: The same surface's outer-edge block sides, stepping with it
|
|
876
|
+
owner: "`—`"
|
|
877
|
+
render: '`<div className="px-4 pt-4 pb-4 sm:px-6 sm:pt-6 sm:pb-6">`'
|
|
878
|
+
- id: case-3
|
|
879
|
+
when: A separator-facing side inside that surface
|
|
880
|
+
owner: "`—`"
|
|
881
|
+
render: "`pt-3` stays `.75rem` at both widths"
|
|
882
|
+
- rule: PADDING-8
|
|
883
|
+
title: Subordinate content indented at the inline start
|
|
884
|
+
cases:
|
|
885
|
+
- id: case-1
|
|
886
|
+
when: A disclosure body that reads as the answer belonging to the trigger above it
|
|
887
|
+
owner: "`—`"
|
|
888
|
+
render: '`<div className="ps-6 pe-4 py-3">`'
|
|
889
|
+
- id: case-2
|
|
890
|
+
when: A nested group whose rows belong to a named parent row
|
|
891
|
+
owner: "`—`"
|
|
892
|
+
render: Same recipe, one step per level of nesting
|
|
893
|
+
- rule: PADDING-9
|
|
894
|
+
title: Block inset of a routed block at the wider breakpoint
|
|
895
|
+
cases:
|
|
896
|
+
- id: case-1
|
|
897
|
+
when: The `main` of a routed block that sits directly under the page shell
|
|
898
|
+
owner: "`App`"
|
|
899
|
+
render: '`<main className="py-6 sm:py-8">`'
|
|
900
|
+
font:
|
|
901
|
+
ownership: FE-RULE-OWNERSHIP
|
|
902
|
+
knowledge: knowledge/ui/presentation/font.yaml
|
|
903
|
+
ownerVocabulary:
|
|
904
|
+
owners:
|
|
905
|
+
- name: A component name
|
|
906
|
+
meaning: "`Text` or `Heading` renders this rank"
|
|
907
|
+
applicationWrites: The prop only
|
|
908
|
+
- name: —
|
|
909
|
+
meaning: The rank has no public prop
|
|
910
|
+
applicationWrites: Nothing. Report the gap
|
|
911
|
+
notes: |-
|
|
912
|
+
Typography belongs to Grammar. The owner cell names which component renders the rank.
|
|
913
|
+
|
|
914
|
+
There is no `App` owner in this file. An application typography class is `APP_OVERRIDE`, and a raw
|
|
915
|
+
`font-size` or `font-weight` that changes semantic rank is rejected even when the pixels match.
|
|
916
|
+
scale:
|
|
917
|
+
steps:
|
|
918
|
+
- id: FONT-1
|
|
919
|
+
- id: FONT-2
|
|
920
|
+
- id: FONT-3
|
|
921
|
+
- id: FONT-4
|
|
922
|
+
- id: FONT-5
|
|
923
|
+
- id: FONT-6
|
|
924
|
+
notes: |-
|
|
925
|
+
The type scale is closed and shared by `Text` and `Heading`. The rule ID is the ordinal position on
|
|
926
|
+
that scale, smallest first. It is not a Tailwind step and not a heading level.
|
|
927
|
+
|
|
928
|
+
`Text` and `Heading` are not interchangeable at the same size. A heading declares document structure;
|
|
929
|
+
a text line does not. Choosing `Heading` to obtain a size, or `Text` to avoid an outline entry, is a
|
|
930
|
+
structure error rather than a typography one.
|
|
931
|
+
Tracking is not an application decision. `Heading` applies `tracking-tight` at FONT-4 and FONT-6 only.
|
|
932
|
+
componentOwnership:
|
|
933
|
+
title: Font Common already owns
|
|
934
|
+
notes: Retained ownership reference, not proof of the current package/API. Verify actual installed
|
|
935
|
+
source/props/tokens before use; correct outdated rows within selected maintenance scope using [source
|
|
936
|
+
and maintenance](index.yaml#source-and-maintenance).
|
|
937
|
+
rows:
|
|
938
|
+
- component: ChatWorkspace
|
|
939
|
+
element: rail trigger, hasRail, isCompactRail
|
|
940
|
+
rule: FONT-2
|
|
941
|
+
- component: Heading
|
|
942
|
+
element: root, scale!="display", level=1
|
|
943
|
+
rule: FONT-4
|
|
944
|
+
- component: Heading
|
|
945
|
+
element: root, scale!="display", level=2
|
|
946
|
+
rule: FONT-3
|
|
947
|
+
- component: Heading
|
|
948
|
+
element: root, scale!="display", level=3
|
|
949
|
+
rule: FONT-2
|
|
950
|
+
- component: Heading
|
|
951
|
+
element: root, scale!="display", level=4
|
|
952
|
+
rule: FONT-1
|
|
953
|
+
- component: Heading
|
|
954
|
+
element: root, scale="display"
|
|
955
|
+
rule: FONT-6
|
|
956
|
+
- component: Input
|
|
957
|
+
element: reveal or copy label, action is present and its icon is absent
|
|
958
|
+
rule: FONT-1
|
|
959
|
+
- component: Label
|
|
960
|
+
element: root, depth!="nested"
|
|
961
|
+
rule: FONT-2
|
|
962
|
+
- component: Label
|
|
963
|
+
element: root, depth="nested"
|
|
964
|
+
rule: FONT-1
|
|
965
|
+
- component: PressableField
|
|
966
|
+
element: placeholder
|
|
967
|
+
rule: FONT-2
|
|
968
|
+
- component: PressableField
|
|
969
|
+
element: shortcut, shortcut!=undefined
|
|
970
|
+
rule: FONT-1
|
|
971
|
+
- component: Sidebar
|
|
972
|
+
element: item label, not collapsed
|
|
973
|
+
rule: FONT-2
|
|
974
|
+
- component: Sidebar
|
|
975
|
+
element: section label, group.label!=undefined, not collapsed
|
|
976
|
+
rule: FONT-1
|
|
977
|
+
- component: Text
|
|
978
|
+
element: root, size="md"
|
|
979
|
+
rule: FONT-3
|
|
980
|
+
- component: Text
|
|
981
|
+
element: root, size="metric-lead"
|
|
982
|
+
rule: FONT-5
|
|
983
|
+
- component: Text
|
|
984
|
+
element: root, size="sm"
|
|
985
|
+
rule: FONT-2
|
|
986
|
+
- component: Text
|
|
987
|
+
element: root, size="xs"
|
|
988
|
+
rule: FONT-1
|
|
989
|
+
- component: TextAction
|
|
990
|
+
element: root, size="md"
|
|
991
|
+
rule: FONT-3
|
|
992
|
+
- component: TextAction
|
|
993
|
+
element: root, size="sm"
|
|
994
|
+
rule: FONT-2
|
|
995
|
+
- component: TextAction
|
|
996
|
+
element: root, size="xs"
|
|
997
|
+
rule: FONT-1
|
|
998
|
+
- component: Tooltip
|
|
999
|
+
element: content
|
|
1000
|
+
rule: FONT-1
|
|
1001
|
+
- component: SurfaceListCard
|
|
1002
|
+
element: fact, label is visible and fact is present, depth="nested"
|
|
1003
|
+
rule: FONT-1
|
|
1004
|
+
- component: SurfaceListCard
|
|
1005
|
+
element: fact, label is visible and fact is present, depth!="nested"
|
|
1006
|
+
rule: FONT-2
|
|
1007
|
+
guidance:
|
|
1008
|
+
- id: weight
|
|
1009
|
+
title: Weight
|
|
1010
|
+
requirement: |-
|
|
1011
|
+
Weight is a second axis, not a position on the size scale. Its public set is closed.
|
|
1012
|
+
| Prop | Rendered | Use for |
|
|
1013
|
+
| --- | --- | --- |
|
|
1014
|
+
| `weight="normal"` | `font-normal`, the default | Reading copy and ordinary facts |
|
|
1015
|
+
| `weight="medium"` | `font-medium` | A compact label, or a title above its own description |
|
|
1016
|
+
| `weight="semibold"` | `font-semibold` | A fact that must be found first within its region |
|
|
1017
|
+
Weight refines a rank the application has already chosen. It never manufactures a new hierarchy
|
|
1018
|
+
level, and it never substitutes for a heading. `Heading` applies its own weight per level and takes
|
|
1019
|
+
no weight prop.
|
|
1020
|
+
Bold beyond `semibold` has no public prop. Treat an intent that needs it as a gap rather than writing
|
|
1021
|
+
a class.
|
|
1022
|
+
recipes:
|
|
1023
|
+
- rule: FONT-1
|
|
1024
|
+
title: "`text-xs` / `leading-4`"
|
|
1025
|
+
cases:
|
|
1026
|
+
- id: case-1
|
|
1027
|
+
when: Brief metadata that stays understandable at the smallest size
|
|
1028
|
+
owner: "`Text`"
|
|
1029
|
+
render: '`<Text size="xs">Optional</Text>`'
|
|
1030
|
+
- id: case-2
|
|
1031
|
+
when: The label of a nested region inside an already-titled section
|
|
1032
|
+
owner: "`Heading`"
|
|
1033
|
+
render: "`<Heading level={4}>Card details</Heading>`"
|
|
1034
|
+
remark: |-
|
|
1035
|
+
`Text size="xs"` always resolves to muted and cannot be promoted to another tone. `Heading level={4}`
|
|
1036
|
+
carries the same muted treatment.
|
|
1037
|
+
notThisRule: ordinary sentences, or any fact the reader must not miss. Use FONT-2 or FONT-3.
|
|
1038
|
+
- rule: FONT-2
|
|
1039
|
+
title: "`text-sm` / `leading-5`"
|
|
1040
|
+
cases:
|
|
1041
|
+
- id: case-1
|
|
1042
|
+
when: A complete line ranked below normal body copy, such as a timestamp or short fact
|
|
1043
|
+
owner: "`Text`"
|
|
1044
|
+
render: '`<Text size="sm">Updated 2 minutes ago</Text>`'
|
|
1045
|
+
- id: case-2
|
|
1046
|
+
when: A third-level region heading inside a section
|
|
1047
|
+
owner: "`Heading`"
|
|
1048
|
+
render: "`<Heading level={3}>Payment method</Heading>`"
|
|
1049
|
+
notThisRule: shrinking body copy so it fits a container that is too small. Fix the container.
|
|
1050
|
+
- rule: FONT-3
|
|
1051
|
+
title: "`text-base` / `leading-6`"
|
|
1052
|
+
cases:
|
|
1053
|
+
- id: case-1
|
|
1054
|
+
when: Ordinary explanatory copy with normal reading priority
|
|
1055
|
+
owner: "`Text`"
|
|
1056
|
+
render: "`<Text>Your subscription renews on 12 March.</Text>`"
|
|
1057
|
+
- id: case-2
|
|
1058
|
+
when: The heading of a section inside a page
|
|
1059
|
+
owner: "`Heading`"
|
|
1060
|
+
render: "`<Heading level={2}>Billing</Heading>`"
|
|
1061
|
+
- rule: FONT-4
|
|
1062
|
+
title: "`text-xl`"
|
|
1063
|
+
cases:
|
|
1064
|
+
- id: case-1
|
|
1065
|
+
when: The single heading that names the whole page or route
|
|
1066
|
+
owner: "`Heading`"
|
|
1067
|
+
render: "`<Heading level={1}>Account</Heading>`"
|
|
1068
|
+
notThisRule: a second `level={1}` on the same page. One document, one top-level heading.
|
|
1069
|
+
- rule: FONT-5
|
|
1070
|
+
title: "`text-3xl` / `leading-9`"
|
|
1071
|
+
cases:
|
|
1072
|
+
- id: case-1
|
|
1073
|
+
when: The single leading metric or short value that is the main fact of its region
|
|
1074
|
+
owner: "`Text`"
|
|
1075
|
+
render: '`<Text size="metric-lead" weight="semibold">84%</Text>`'
|
|
1076
|
+
notThisRule: sentences, decoration, or imitating a heading. It creates no heading semantics.
|
|
1077
|
+
- rule: FONT-6
|
|
1078
|
+
title: "`text-4xl` / `leading-tight`"
|
|
1079
|
+
cases:
|
|
1080
|
+
- id: case-1
|
|
1081
|
+
when: A page-root heading that must carry marketing or landing weight
|
|
1082
|
+
owner: "`Heading`"
|
|
1083
|
+
render: '`<Heading level={1} scale="display">Learn without limits</Heading>`'
|
|
1084
|
+
notThisRule: raising the visual size of a nested heading. `scale` changes the recipe, never the outline position.
|
|
1085
|
+
anatomyObservations:
|
|
1086
|
+
- id: prompt-shape-b2
|
|
1087
|
+
lane: ui
|
|
1088
|
+
kind: proven-template
|
|
1089
|
+
summary: |-
|
|
1090
|
+
Verified in the draw lab against the real SurfaceListCard render — the prompt shape that
|
|
1091
|
+
produced a direction both faithful and beautiful:
|
|
1092
|
+
1. Frame: flat product UI screenshot, size, light theme.
|
|
1093
|
+
2. SUBJECT: screen name + product name.
|
|
1094
|
+
3. FIXED ANATOMY: literal layout top-to-bottom; containment spelled as inside/outside the
|
|
1095
|
+
surface; known-confusion regions get the anti-pattern callout plus the correct structure.
|
|
1096
|
+
4. BRAND + TOKENS: background, surface, radius, shadow, exact hex for primary and where it
|
|
1097
|
+
may appear; nested controls use the secondary variant.
|
|
1098
|
+
5. CREATIVE LICENSE: an explicit "You MAY" list and an explicit "You MAY NOT" list.
|
|
1099
|
+
6. Text rule: every label spelled exactly, no lorem ipsum.
|
|
1100
|
+
Control runs: the same prompt without section 3's literal containment drew the section
|
|
1101
|
+
label inside the card (wrong vs the real render); the same prompt with unbounded freedom
|
|
1102
|
+
produced a beautiful screen for a different product.
|
|
1103
|
+
- id: surface-label-placement
|
|
1104
|
+
lane: ui
|
|
1105
|
+
kind: anatomy-detail
|
|
1106
|
+
summary: |-
|
|
1107
|
+
The two card components place their labels differently, and a prompt that treats them
|
|
1108
|
+
alike invents one of them. `SurfaceCard` paints one material box around label AND
|
|
1109
|
+
content — its label ("New task") sits inside the card at the top. `SurfaceListCard`
|
|
1110
|
+
renders its label and fact as siblings on the page background ABOVE the list shell —
|
|
1111
|
+
the card bounds only the rows, the footer sits below it.
|
|
1112
|
+
- id: list-separator-bleed
|
|
1113
|
+
lane: ui
|
|
1114
|
+
kind: anatomy-detail
|
|
1115
|
+
summary: |-
|
|
1116
|
+
List separators are edge-to-edge, not inset. The list shell ships `PADDING-0`; the
|
|
1117
|
+
divider is a `border-top` on the row element itself, so it spans the card's full width
|
|
1118
|
+
and touches both edges. Only the row's content is inset by the row's own padding.
|
|
1119
|
+
Rows opt into this shipped geometry via `starci-core-static-row` — an app-owned row
|
|
1120
|
+
without the class renders no separator at all.
|
|
1121
|
+
- id: list-row-anatomy
|
|
1122
|
+
lane: ui
|
|
1123
|
+
kind: anatomy-detail
|
|
1124
|
+
summary: |-
|
|
1125
|
+
An interactive row in a SurfaceListCard is the grammar `ListRow` (@starci/grammar 0.9.1), never an
|
|
1126
|
+
app-owned `<li>` and never a second `<ul>` nested in the card (the card already renders the list,
|
|
1127
|
+
so a nested list loses the row inset and the separator - a defect that leaves rows flush against
|
|
1128
|
+
the card edge). Its slots, in reading order: `leading`
|
|
1129
|
+
(an IconTile, an avatar, or a logo; a horizontal bank wordmark takes a fixed 64x40 frame rather
|
|
1130
|
+
than a square tile, which would shrink it to a sliver), `title`, `badges` (at most two,
|
|
1131
|
+
DISCIPLINE-1 case-3), one muted `description`, `actions` (one visible Button, then a DropdownMenu
|
|
1132
|
+
with destructive commands last behind an AlertDialog), and a `children` detail region under the
|
|
1133
|
+
copy for an Alert about the row or revealed values. It carries `starci-core-static-row`, so the
|
|
1134
|
+
inset, row gap and edge-to-edge separator are the static row's. Collapsed by default (the collapse
|
|
1135
|
+
law): mark and copy share the first line, actions and detail take their own; the surface container
|
|
1136
|
+
query at 28.001rem lays it out leading | copy | actions. Several accounts of one provider are one
|
|
1137
|
+
ListRow each, told apart by the leading mark, the name and the account's last digits; the card's
|
|
1138
|
+
`fact` carries the count and `labelEnd` the add action (0.9.1 renders both).
|
|
1139
|
+
- id: card-division-and-measure
|
|
1140
|
+
lane: ui
|
|
1141
|
+
kind: anatomy-detail
|
|
1142
|
+
summary: |-
|
|
1143
|
+
Grammar ships card division and card width — a direction that invents either is
|
|
1144
|
+
app-owned and must say so. `SurfaceCard composition="joined"` stacks children
|
|
1145
|
+
edge-to-edge inside one card with shipped separators (the reference
|
|
1146
|
+
joined-card pattern); it divides VERTICALLY only — a
|
|
1147
|
+
side-by-side split inside one card is not shipped anatomy. `SurfaceCard
|
|
1148
|
+
measure="form"/"formCompact"` is the owned width contract for compact forms: a
|
|
1149
|
+
direction drawing a form card full-width and filling the dead space is answering
|
|
1150
|
+
a question `measure` already answers better.
|
|
1151
|
+
- id: joined-band-treatment
|
|
1152
|
+
lane: ui
|
|
1153
|
+
kind: anatomy-detail
|
|
1154
|
+
summary: |-
|
|
1155
|
+
Emphasis inside a joined card is a flush band, not an inset panel. The reference
|
|
1156
|
+
dashboard's anatomy: children stack full-bleed inside the bounded
|
|
1157
|
+
surface (`joined` sets the content to gap:0/padding:0), adjacent bands separate
|
|
1158
|
+
with an edge-to-edge `border-t` hairline, and a band that needs emphasis carries
|
|
1159
|
+
`bg-surface-secondary` (neutral) or `bg-accent-soft` (accent tint) across the
|
|
1160
|
+
card's full width with its own padding (px-4/pt-4/pb-3 top, p-3 middle). A
|
|
1161
|
+
nested rounded box floating inside the card is not the pattern.
|
|
1162
|
+
- id: special-card-recipe
|
|
1163
|
+
lane: ui
|
|
1164
|
+
kind: anatomy-detail
|
|
1165
|
+
summary: |-
|
|
1166
|
+
The dashboard's "special card" formula is composition, not decoration: IMAGE × joined
|
|
1167
|
+
plus HIGHLIGHT × joined. Proof in source — the reference task card: a joined card whose first band
|
|
1168
|
+
is an accent-filled hero slab (`bg-accent`) carrying a decorative 3D illustration
|
|
1169
|
+
anchored to the trailing edge (`aria-hidden`, `pointer-events-none`, bottom/right
|
|
1170
|
+
absolute); separator; a reward band that is neutral by default and turns
|
|
1171
|
+
`bg-success-soft` ONLY on the claimed state ("only a proven claimed outcome receives
|
|
1172
|
+
success"); separator; a task-cell grid using `divide-y`/`divide-x` for edge-to-edge
|
|
1173
|
+
cell lines. the reference purchase card: joined card = intro band + full-bleed journey
|
|
1174
|
+
illustration band + benefit list, with MISSING-ASSET discipline — the illustration is
|
|
1175
|
+
withheld entirely under the dark scheme rather than repainted. Grammar ships the
|
|
1176
|
+
card-level "special" affordance itself: `SurfaceCard isHighlight` wraps the surface in
|
|
1177
|
+
an animated conic-gradient edge sweep (`starci-core-surface-highlight-sweep`, 3s spin,
|
|
1178
|
+
suppressed while `pending`). Direction prompts MAY therefore use: a tinted hero slab
|
|
1179
|
+
band with anchored decorative art, state-semantic tinted bands (accent-soft /
|
|
1180
|
+
surface-secondary / success-soft), and the card edge highlight — but a highlight or
|
|
1181
|
+
tint must always MEAN a state, never mere prettiness.
|
|
1182
|
+
- id: direction-scale-drift
|
|
1183
|
+
lane: ui
|
|
1184
|
+
kind: known-drift
|
|
1185
|
+
summary: |-
|
|
1186
|
+
Image models inflate type scale — surface labels ship at 0.875rem (0.75rem nested)
|
|
1187
|
+
but directions routinely draw them 1.3–1.5x larger. Scale is token truth, not
|
|
1188
|
+
direction truth: the image fixes containment and composition; the component's own
|
|
1189
|
+
CSS fixes size. A direction drawn with oversized labels is not falsified for it —
|
|
1190
|
+
but an implementation capture that reproduces the oversized size is.
|