@cahyo-dimas/freeday 1.53.0 → 2.0.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 (145) hide show
  1. package/CHANGELOG.md +657 -535
  2. package/COMPONENTS.md +153 -135
  3. package/README.id.md +46 -45
  4. package/README.md +46 -43
  5. package/USAGE.md +39 -35
  6. package/adapters/blazor/FdyAppShell.razor +50 -0
  7. package/adapters/blazor/FdyAppShell.razor.cs +114 -0
  8. package/adapters/blazor/FdyCfl.razor.cs +1 -1
  9. package/adapters/blazor/FdyChart.razor +1 -1
  10. package/adapters/blazor/FdyChart.razor.cs +3 -3
  11. package/adapters/blazor/FdyChartSeries.cs +1 -1
  12. package/adapters/blazor/FdyCombo.razor.cs +2 -2
  13. package/adapters/blazor/FdyModal.razor +1 -1
  14. package/adapters/blazor/FdyTable.razor.cs +7 -7
  15. package/adapters/blazor/FdyTableFilter.razor +1 -1
  16. package/adapters/blazor/FdyTableFooter.razor +1 -1
  17. package/adapters/blazor/FdyTableFooter.razor.cs +4 -4
  18. package/adapters/blazor/Freeday.Blazor.csproj +1 -1
  19. package/adapters/blazor/FreedayComponentBase.cs +1 -1
  20. package/adapters/blazor/TableModel.cs +3 -3
  21. package/adapters/blazor/TableTypes.cs +3 -3
  22. package/adapters/blazor/freeday-blazor.js +5 -5
  23. package/adapters/core/app-shell.d.ts +30 -0
  24. package/adapters/core/app-shell.js +134 -0
  25. package/adapters/core/table-model.d.ts +2 -2
  26. package/adapters/core/table-model.js +8 -8
  27. package/adapters/react/components/FdyAppShell.tsx +159 -0
  28. package/adapters/react/components/FdyAutocomplete.tsx +1 -1
  29. package/adapters/react/components/FdyCascade.tsx +1 -1
  30. package/adapters/react/components/FdyCfl.tsx +11 -11
  31. package/adapters/react/components/FdyDateRange.tsx +2 -2
  32. package/adapters/react/components/FdyDatepicker.tsx +5 -5
  33. package/adapters/react/components/FdyDrawer.tsx +1 -1
  34. package/adapters/react/components/FdyModal.tsx +2 -2
  35. package/adapters/react/components/FdyTable.tsx +7 -7
  36. package/adapters/react/components/FdyTableFilter.tsx +1 -1
  37. package/adapters/react/components/FdyTableFooter.tsx +5 -5
  38. package/adapters/react/index.d.ts +2 -1
  39. package/adapters/react/index.js +1 -0
  40. package/adapters/react/useFreeday.js +3 -3
  41. package/adapters/vue/components/FdyAppShell.vue +160 -0
  42. package/adapters/vue/components/FdyAutocomplete.vue +1 -1
  43. package/adapters/vue/components/FdyCascade.vue +1 -1
  44. package/adapters/vue/components/FdyCfl.vue +8 -8
  45. package/adapters/vue/components/FdyChart.vue +2 -2
  46. package/adapters/vue/components/FdyCombo.vue +1 -1
  47. package/adapters/vue/components/FdyDateRange.vue +2 -2
  48. package/adapters/vue/components/FdyDatepicker.vue +5 -5
  49. package/adapters/vue/components/FdyDrawer.vue +3 -3
  50. package/adapters/vue/components/FdyModal.vue +3 -3
  51. package/adapters/vue/components/FdyTable.vue +7 -7
  52. package/adapters/vue/components/FdyTableFilter.vue +1 -1
  53. package/adapters/vue/components/FdyTableFooter.vue +5 -5
  54. package/adapters/vue/index.d.ts +3 -2
  55. package/adapters/vue/index.js +1 -0
  56. package/adapters/vue/useFreeday.js +3 -3
  57. package/dist/asset.d.ts +3 -3
  58. package/dist/freeday-app-shell.js +49 -9
  59. package/dist/freeday-autocomplete.js +2 -2
  60. package/dist/freeday-breakpoint.js +1 -1
  61. package/dist/freeday-carousel.js +5 -5
  62. package/dist/freeday-cascade.js +7 -7
  63. package/dist/freeday-cfl.js +9 -9
  64. package/dist/freeday-chart.js +11 -11
  65. package/dist/freeday-chip.js +3 -3
  66. package/dist/freeday-datepicker.js +11 -7
  67. package/dist/freeday-datetime.js +5 -5
  68. package/dist/freeday-drawer.js +1 -1
  69. package/dist/freeday-form.js +14 -14
  70. package/dist/freeday-mask.js +6 -6
  71. package/dist/freeday-menu.js +1 -1
  72. package/dist/freeday-number.js +3 -3
  73. package/dist/freeday-popover.js +3 -3
  74. package/dist/freeday-rating.js +2 -2
  75. package/dist/freeday-select.js +5 -5
  76. package/dist/freeday-slider.js +2 -2
  77. package/dist/freeday-stepper.js +7 -7
  78. package/dist/freeday-table.js +15 -15
  79. package/dist/freeday-tabs.js +1 -1
  80. package/dist/freeday-timepicker.js +2 -2
  81. package/dist/freeday-toast.js +8 -8
  82. package/dist/freeday-tree.js +3 -3
  83. package/dist/freeday-upload.js +25 -25
  84. package/dist/freeday.bundle.css +176 -176
  85. package/dist/freeday.css +170 -170
  86. package/dist/freeday.d.ts +2 -2
  87. package/dist/freeday.js +202 -158
  88. package/dist/freeday.tokens.css +5 -5
  89. package/docs/agent-onboarding.md +42 -42
  90. package/docs/getting-started.md +35 -35
  91. package/docs/integrations.md +18 -18
  92. package/docs/reference-screen.html +14 -14
  93. package/package.json +5 -4
  94. package/src/base.css +6 -6
  95. package/src/components/accordion.css +4 -4
  96. package/src/components/alert.css +1 -1
  97. package/src/components/app-shell.css +10 -10
  98. package/src/components/appbar.css +3 -3
  99. package/src/components/autocomplete.css +1 -1
  100. package/src/components/avatar.css +3 -3
  101. package/src/components/badge.css +5 -5
  102. package/src/components/breadcrumb.css +1 -1
  103. package/src/components/breakpoints.css +4 -4
  104. package/src/components/button.css +13 -13
  105. package/src/components/card.css +8 -8
  106. package/src/components/carousel.css +2 -2
  107. package/src/components/cascade.css +2 -2
  108. package/src/components/cfl.css +4 -4
  109. package/src/components/chart.css +3 -3
  110. package/src/components/chip.css +2 -2
  111. package/src/components/combo.css +3 -3
  112. package/src/components/composition.css +7 -7
  113. package/src/components/datepicker.css +6 -6
  114. package/src/components/datetimepicker.css +1 -1
  115. package/src/components/description-list.css +1 -1
  116. package/src/components/divider.css +1 -1
  117. package/src/components/drawer.css +1 -1
  118. package/src/components/file-upload.css +5 -5
  119. package/src/components/filterbar.css +7 -7
  120. package/src/components/form-grid.css +2 -2
  121. package/src/components/input-group.css +6 -6
  122. package/src/components/input.css +6 -6
  123. package/src/components/kbd.css +1 -1
  124. package/src/components/list.css +6 -6
  125. package/src/components/menu.css +3 -3
  126. package/src/components/modal.css +1 -1
  127. package/src/components/pagination.css +1 -1
  128. package/src/components/progress.css +1 -1
  129. package/src/components/rating.css +2 -2
  130. package/src/components/selection.css +4 -4
  131. package/src/components/skeleton.css +1 -1
  132. package/src/components/slider.css +1 -1
  133. package/src/components/spinner.css +1 -1
  134. package/src/components/states.css +1 -1
  135. package/src/components/stepper.css +2 -2
  136. package/src/components/table.css +15 -15
  137. package/src/components/tabs.css +3 -3
  138. package/src/components/timeline.css +1 -1
  139. package/src/components/timepicker.css +1 -1
  140. package/src/components/toast.css +3 -3
  141. package/src/components/tooltip.css +1 -1
  142. package/src/components/tree.css +2 -2
  143. package/tokens/breakpoints.d.ts +2 -2
  144. package/tokens/breakpoints.mjs +3 -3
  145. package/tokens/tokens.json +1 -1
package/COMPONENTS.md CHANGED
@@ -15,30 +15,31 @@ live docs.
15
15
  1. **Prefix is `fdy-`, pattern is compact BEM**: block `fdy-card`, element `fdy-card__title`,
16
16
  modifier `fdy-card--elevated`. A modifier is **always** written alongside its block class:
17
17
  `class="fdy-btn fdy-btn--ghost"`, never `fdy-btn--ghost` alone.
18
- **One documented exception:** `.fdy-input-group__addon--icon` is **standalone** it is a
18
+ **One documented exception:** `.fdy-input-group__addon--icon` is **standalone**. It is a
19
19
  borderless leading glyph, and adding the base `.fdy-input-group__addon` would give it the grey
20
20
  fill and divider of a `Rp` / `%` prefix, which is not what a search icon is. Written
21
21
  `class="fdy-input-group__addon--icon"`, alone. It is the only one; everywhere else the rule holds.
22
22
  2. **Never invent a class.** If it is not in this file, it does not exist. Freeday has no
23
23
  `fdy-grid`, no `fdy-flex`, no `fdy-mt-4`, no `fdy-primary`.
24
- 3. **Never write a raw hex or px value** in app CSS. Use the tokens `var(--color-primary)`,
24
+ 3. **Never write a raw hex or px value** in app CSS. Use the tokens: `var(--color-primary)`,
25
25
  `var(--space-4)`, `var(--radius-md)`, `var(--shadow-1)`, `var(--dur-2)`. Spacing is a 4px scale
26
26
  (`--space-0`…`--space-24`); a loose value breaks density switching.
27
27
  4. **Freeday owns components + tokens, not layout.** The only layout it ships is the shell
28
28
  (`.fdy-app`), the composition primitives below, and `.fdy-hidden`/`.fdy-visually-hidden`.
29
29
  Grids, stacks and one-off gaps come from your own layout layer (a utility framework run
30
30
  **utilities-only, preflight off**, with its theme defined in terms of `var(--space-N)`).
31
- 5. **`aria-invalid="true"` is the canonical error state** on form controls it drives the visual
31
+ 5. **`aria-invalid="true"` is the canonical error state** on form controls. It drives the visual
32
32
  state *and* the screen-reader state. Pair it with `aria-describedby` → the `id` of a
33
33
  `.fdy-help.fdy-help--error`. The `--error` class modifiers exist only for the case where the
34
34
  invalid state does not belong to the control itself.
35
35
  6. **Interactive components need their enhancer script.** Static ones (button, card, badge, table,
36
36
  alert, breadcrumb, timeline, accordion, tree-without-cascade…) are CSS-only.
37
- 7. **On Vue, React or Blazor, ten components have a typed wrapper use it.** `FdyCombo` ·
37
+ 7. **On Vue, React or Blazor, eleven components have a typed wrapper. Use it.** `FdyCombo` ·
38
38
  `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` ·
39
39
  `FdyTable` · `FdyModal` · `FdyDrawer`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
40
40
  `Freeday.Blazor` RCL. Each is flagged at its own section below. Hand-writing their raw markup in
41
- those stacks *looks* right the enhancer initialises once and the first render is correct — then
41
+ those stacks *looks* right at first, because the enhancer initialises once and the first render is
42
+ correct. Then
42
43
  breaks quietly: later-rendered DOM is never hydrated and the widget's state sits in the DOM
43
44
  instead of your framework's. Everything else is the same markup everywhere; hydrate the other
44
45
  interactive components with `useFreeday` (Vue/React) or `FreedayBlazor.initAll` (Blazor).
@@ -47,11 +48,11 @@ live docs.
47
48
 
48
49
  `.fdy-visually-hidden` is `position:absolute`, and `clip` hides **painting**, not **layout**. An
49
50
  absolutely positioned box resolves against its nearest *positioned* ancestor, and `overflow` clips
50
- only what is contained that way so in an unpositioned scroller, a hidden label parks at its static
51
+ only what is contained that way, so in an unpositioned scroller, a hidden label parks at its static
51
52
  position (possibly thousands of px to the right) and drags the **whole document** sideways. It is
52
53
  invisible in the DOM and immune to `overflow-x: hidden` on every wrapper.
53
54
 
54
- Every kit container that clips or scrolls therefore declares `position: relative` —
55
+ Every kit container that clips or scrolls therefore declares `position: relative`:
55
56
  `.fdy-table-scroll`, `.fdy-table-wrap`, `.fdy-list`, `.fdy-card`, `.fdy-tabs__list`,
56
57
  `.fdy-carousel__viewport`, `.fdy-accordion` (the rest are already inside a positioned ancestor).
57
58
  Two consequences for you:
@@ -66,7 +67,7 @@ Two consequences for you:
66
67
 
67
68
  Every enhancer is zero-dependency, auto-initialises once on `DOMContentLoaded`, and is idempotent:
68
69
  re-hydrate SPA-rendered DOM with `window.Freeday<X>.initAll(root)`. **`root` may be the widget
69
- itself or an ancestor of it** both work, so a framework ref placed directly on the enhanced
70
+ itself or an ancestor of it**. Both work, so a framework ref placed directly on the enhanced
70
71
  element is fine. Events are bubbling
71
72
  `CustomEvent`s; the payload is in `event.detail`.
72
73
 
@@ -114,6 +115,8 @@ is a no-op.
114
115
  # Shell & composition
115
116
 
116
117
  ## App shell — `.fdy-app`
118
+ > **Typed wrapper: `<FdyAppShell>`**. Vue (`v-model:navOpen`) · React (`navOpen`/`onNavOpenChange`) · Blazor (`@bind-NavOpen`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
119
+
117
120
  The frame every application lives in. Do not hand-roll one from flexbox: the responsive sidebar,
118
121
  off-canvas drawer and backdrop are built in.
119
122
 
@@ -121,25 +124,38 @@ off-canvas drawer and backdrop are built in.
121
124
  `__content` `__topbar` `__navtoggle` `__title` `__main` `__backdrop`
122
125
  - Modifiers: `--nav-open` (mobile drawer open, ≤720px) · `--nav-collapsed` (collapse to zero width,
123
126
  ≥721px) · `--static` (embed the shell in a page instead of filling the viewport)
124
- - Also: `.fdy-skip` the skip-to-content link, first child of the shell.
125
- - **Behaviour: `freeday-app-shell.js`.** Opt in with `data-fdy-app` on the root the markup below
127
+ - Also: `.fdy-skip`, the skip-to-content link, first child of the shell.
128
+ - **Behaviour: `freeday-app-shell.js`.** Opt in with `data-fdy-app` on the root. The markup below
126
129
  is unchanged. It owns the toggle in both modes plus everything an overlay needs that hand-rolling
127
130
  reliably forgets: Escape, backdrop click, closing when a `.fdy-nav__item` is followed, focus moved
128
131
  into the panel on open and returned to `__navtoggle` on close, `inert` on `__content` while the
129
132
  overlay is up, and a Tab trap inside the panel. `aria-expanded` on `__navtoggle` answers "is the
130
133
  nav showing?" in both modes, so you never read the state classes yourself.
131
- - **A hidden nav is not a tabbable nav.** `__sidebar` is `inert` whenever the nav is not visible
134
+ - **A hidden nav is not a tabbable nav.** `__sidebar` is `inert` whenever the nav is not visible:
132
135
  collapsed at ≥721px (`width:0`) or off-canvas at ≤720px (`translateX(-100%)`). Both hide it from
133
136
  the eye; neither hides it from the keyboard, so without this a nav nobody can see still swallows
134
137
  Tab on the way to the page.
135
138
  - **Crossing the breakpoint is part of the contract**: open the overlay, then widen the window, and
136
139
  `--nav-open` is dropped and `inert` removed. Leaving them set is how a page becomes permanently
137
140
  unreachable.
141
+ - **`navOpen` is one idea, in both modes.** The typed wrappers expose "is the nav visible?" and the
142
+ kit maps it: above the breakpoint a hidden nav is `--nav-collapsed`, below it a visible nav is
143
+ `--nav-open`. Leave the prop unbound (`undefined` in Vue/React, `null` in Blazor) and the shell
144
+ starts from the viewport: a column on a wide screen, hidden on a narrow one. That default is why
145
+ the prop is optional: a caller cannot express it as a single initial value before it knows the
146
+ viewport.
147
+ - **Binding your own state to the vanilla shell:** it emits a bubbling `fdy-app-nav` CustomEvent
148
+ (`detail {visible}`) on every real change, including the ones a viewport change causes, since a
149
+ bound value that stayed `true` while the nav went off-canvas would be describing a panel nobody
150
+ can see. Drive it with `FreedayAppShell.setVisible(root, visible)` rather than the class names;
151
+ setting what is already set announces nothing, so a bound host cannot loop on its own echo.
152
+ `FreedayAppShell.isVisible(root)` reads it back.
138
153
  - Skip the enhancer and nothing breaks: the classes still mean what they always meant and the
139
- behaviour is yours to write. `FreedayAppShell.init(root)` for markup mounted later.
154
+ behaviour is yours to write. `FreedayAppShell.init(root)` for markup mounted later. Hand it the
155
+ shell root itself, which is what the Blazor bridge does after its first render.
140
156
 
141
157
  **The nesting is fixed, not free-form:** `.fdy-app` is a flex **row** of `[__sidebar | __content]`,
142
- and `__content` is the column holding `__topbar` + `__main` it exists to give the sticky topbar a
158
+ and `__content` is the column holding `__topbar` + `__main`, which exists to give the sticky topbar a
143
159
  tall containing block. The brand goes in the **sidebar** (sized to match the topbar height), and
144
160
  `__main` already carries the page padding, so don't wrap your screen in another padded box.
145
161
 
@@ -172,15 +188,15 @@ tall containing block. The brand goes in the **sidebar** (sized to match the top
172
188
  Vertical section rhythm (`--space-8` between children). Assemble every screen from these; they
173
189
  carry the doctrine in markup.
174
190
 
175
- - `.fdy-page` the screen body
176
- - `.fdy-page__header` `.fdy-page__heading` (eyebrow + page title + `.fdy-page__desc`) on the
191
+ - `.fdy-page` is the screen body
192
+ - `.fdy-page__header` holds `.fdy-page__heading` (eyebrow + page title + `.fdy-page__desc`) on the
177
193
  left, `.fdy-page__actions` (**one** primary button) on the right
178
- - `.fdy-page-section` a region: `.fdy-page-section__head` + body, `--space-4` rhythm
179
- - `.fdy-toolbar` a horizontal control row; `__spacer` (flex filler), `__search` (capped width).
194
+ - `.fdy-page-section` is a region: `.fdy-page-section__head` + body, `--space-4` rhythm
195
+ - `.fdy-toolbar` is a horizontal control row; `__spacer` (flex filler), `__search` (capped width).
180
196
  It is `align-items:center`, so it is for **bare** controls. A `.fdy-field` with a *visible* label
181
- sits half a label-height low in it labelled fields belong in `.fdy-filterbar`
197
+ sits half a label-height low in it, so labelled fields belong in `.fdy-filterbar`
182
198
  (`align-items:flex-end`). In a toolbar, hide the label with `.fdy-visually-hidden`.
183
- - `.fdy-stats` a KPI grid of `.fdy-stat` (`__label` `__value` `__meta`). Deliberately **not**
199
+ - `.fdy-stats` is a KPI grid of `.fdy-stat` (`__label` `__value` `__meta`). Deliberately **not**
184
200
  cards. `.fdy-stats--boxed` for one shared surface with dividers.
185
201
 
186
202
  ```html
@@ -215,7 +231,7 @@ carry the doctrine in markup.
215
231
  ```
216
232
 
217
233
  ## Type roles & text
218
- One role per level of hierarchy never re-use a card title for a page title.
234
+ One role per level of hierarchy. Never re-use a card title for a page title.
219
235
 
220
236
  | Class | Element | Use |
221
237
  |---|---|---|
@@ -231,11 +247,11 @@ One role per level of hierarchy — never re-use a card title for a page title.
231
247
 
232
248
  ## Utilities
233
249
  `.fdy-hidden` · `.fdy-visually-hidden` (screen-reader-only) · `.fdy-list-reset` (strip native
234
- bullets/indent from a semantic list needed when your utility framework's preflight is off) ·
250
+ bullets/indent from a semantic list, needed when your utility framework's preflight is off) ·
235
251
  `.fdy-divider` (+`--vertical`) · `.fdy-kbd`.
236
252
 
237
253
  Responsive display: `.fdy-hide-below-sm|md|lg` · `.fdy-hide-above-sm|md|lg`.
238
- Breakpoints (min-width): `sm` 600 · `md` 960 · `lg` 1280 · `xl` 1920 importable as
254
+ Breakpoints (min-width): `sm` 600 · `md` 960 · `lg` 1280 · `xl` 1920, importable as
239
255
  `import { breakpoints } from '@cahyo-dimas/freeday/breakpoints'`.
240
256
 
241
257
  **`breakpoints.nav` (721) is separate and is the one the shell uses.** `.fdy-app` switches the
@@ -243,7 +259,7 @@ sidebar from off-canvas drawer to static column at 721px, *not* at `md`. Any `ma
243
259
  utility variant that has to agree with the shell must use `nav`; using `md` leaves 721–959px broken
244
260
  (sidebar already static while your script still treats it as an overlay).
245
261
 
246
- Density: `data-density="compact"` works on `<html>` **or any wrapper** the selector is a bare
262
+ Density: `data-density="compact"` works on `<html>` **or any wrapper**, because the selector is a bare
247
263
  `[data-density="compact"]` over inheriting custom properties, so one screen can be dense while the
248
264
  rest of the app is not. `data-density="comfortable"` is a real rule too, so the reverse also works:
249
265
  a compact root with one region opted back out (shared chrome that must match a sibling product).
@@ -253,13 +269,13 @@ a compact root with one region opted back out (shared chrome that must match a s
253
269
  # Actions
254
270
 
255
271
  ## Button — `.fdy-btn`
256
- **The base class is already the primary action** there is no `--primary`. Use it once per screen.
272
+ **The base class is already the primary action.** There is no `--primary`. Use it once per screen.
257
273
 
258
274
  - Modifiers: `--ghost` `--danger` `--text` `--icon` (square, icon-only) `--sm` `--lg`
259
275
  - Parts: `.fdy-btn__icon` (wraps the `<svg>`) · `.fdy-btn-group` (segmented row, `role="group"` +
260
276
  `aria-label`)
261
277
  - **Toggle / segmented:** `aria-pressed="true"` gives a real pressed state (soft primary fill on
262
- `--ghost`/`--text`, an inset-sunk gradient on the solid button). No extra class `aria-pressed`
278
+ `--ghost`/`--text`, an inset-sunk gradient on the solid button). No extra class: `aria-pressed`
263
279
  is already the right attribute, and it is what turns `.fdy-btn-group` from a joined row into a
264
280
  complete segmented control.
265
281
  - A11y: `--icon` **requires** `aria-label`. Use `<button type="button">` unless it submits.
@@ -273,7 +289,7 @@ a compact root with one region opted back out (shared chrome that must match a s
273
289
  ```
274
290
 
275
291
  **Quiet destructive:** combine `--danger` with `--ghost` or `--text`. The ground stays quiet and only
276
- the ink turns red `.fdy-menu__item--danger` has always worked this way. Use it whenever Delete
292
+ the ink turns red. `.fdy-menu__item--danger` has always worked this way. Use it whenever Delete
277
293
  shares a screen with the one primary action; a solid Delete beside Save is a second primary in all
278
294
  but name.
279
295
 
@@ -315,7 +331,7 @@ native control, otherwise a `<div>` + explicitly associated label.
315
331
  - `.fdy-label` (+`--required`) · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`)
316
332
 
317
333
  **Grouped controls are a `<fieldset>`, not a new block.** Put `.fdy-field` on the fieldset and
318
- `.fdy-label` on the legend the kit already resets the UA border/padding and supplies the spacing:
334
+ `.fdy-label` on the legend. The kit already resets the UA border/padding and supplies the spacing:
319
335
 
320
336
  ```html
321
337
  <fieldset class="fdy-field">
@@ -327,7 +343,7 @@ native control, otherwise a `<div>` + explicitly associated label.
327
343
 
328
344
  **`.fdy-label--required` marks the label, not the accessibility tree.** The control already carries
329
345
  `required`; the asterisk is painted through `::after` with CSS alt text, so a screen reader never
330
- reads "star" after the label something a `<span>` you have to remember to mark `aria-hidden`
346
+ reads "star" after the label, which a `<span>` you have to remember to mark `aria-hidden`
331
347
  cannot guarantee.
332
348
  - `[readonly]` is styled on input/textarea: full contrast, focusable, copyable.
333
349
 
@@ -345,8 +361,8 @@ cannot guarantee.
345
361
  </label>
346
362
  ```
347
363
 
348
- **Which `type` does `.fdy-input` cover?** Every text-like type `text` `email` `password` `tel`
349
- `url` `search` `number` themes identically. Two carry a native widget the kit does *not* override:
364
+ **Which `type` does `.fdy-input` cover?** Every text-like type (`text` `email` `password` `tel`
365
+ `url` `search` `number`) themes identically. Two carry a native widget the kit does *not* override:
350
366
 
351
367
  | Type | What the UA still draws | What to do |
352
368
  |---|---|---|
@@ -371,7 +387,7 @@ Prefix/suffix addons around an input: text (`Rp`, `%`), a decorative icon
371
387
 
372
388
  ## Number field — `[data-fdy-number]`
373
389
  A number input with its increment/decrement affordance back, after `.fdy-input` removed the
374
- browser's own. **Not a new block** it is an `.fdy-input-group` with two `__btn`s, so it inherits
390
+ browser's own. **Not a new block.** It is an `.fdy-input-group` with two `__btn`s, so it inherits
375
391
  the shared border, focus ring and error promotion. Needs `freeday-number.js`.
376
392
 
377
393
  ```html
@@ -385,13 +401,13 @@ the shared border, focus ring and error promotion. Needs `freeday-number.js`.
385
401
  ```
386
402
 
387
403
  - **No custom event.** Stepping fires native bubbling `input` + `change` on the input, so `v-model`,
388
- `onChange` and `@bind` work with no adapter the input stays the source of truth.
404
+ `onChange` and `@bind` work with no adapter, because the input stays the source of truth.
389
405
  - `min` / `max` / `step` live on the **input**; the buttons never do the arithmetic themselves
390
406
  (`stepUp()`/`stepDown()` clamp for free) and go `disabled` at a bound, on a `disabled`/`readonly`
391
407
  field, and when `step="any"` (a stepper cannot express "no defined increment").
392
408
  - The buttons are **not tab stops** (`tabindex="-1"`): the input is already focusable and ↑/↓
393
409
  already step it, so extra stops would cost every keyboard user and buy nothing. Keep the
394
- `aria-label` pointer and browse-mode users still get a named control.
410
+ `aria-label`, so pointer and browse-mode users still get a named control.
395
411
  - `type="button"` is required. Inside a `<form>`, a bare `<button>` submits it.
396
412
 
397
413
  ## Checkbox · radio · switch
@@ -404,7 +420,7 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
404
420
  ```
405
421
 
406
422
  ## Select / combobox — `.fdy-combo`
407
- > **Typed wrapper: `<FdyCombo>`** Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
423
+ > **Typed wrapper: `<FdyCombo>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
408
424
 
409
425
  Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
410
426
 
@@ -429,12 +445,12 @@ Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
429
445
 
430
446
  **The selected tick is CSS.** `.fdy-combo__check` is an empty box in the markup; the glyph is painted
431
447
  by the stylesheet on `[aria-selected="true"]` with alt text, so it never enters the accessibility
432
- tree. That keeps an option's accessible name **identical whether or not it is selected** the state
448
+ tree. That keeps an option's accessible name **identical whether or not it is selected**. The state
433
449
  is carried by `aria-selected` alone, announced once, and `getByRole('option', { name: 'August' })`
434
450
  keeps matching after selection. Do not put a glyph in that span.
435
451
 
436
452
  ## Autocomplete — `.fdy-autocomplete`
437
- > **Typed wrapper: `<FdyAutocomplete>`** Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
453
+ > **Typed wrapper: `<FdyAutocomplete>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
438
454
 
439
455
  Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
440
456
 
@@ -443,7 +459,7 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
443
459
  `aria-controls` → the listbox `id`, `autocomplete="off"`.
444
460
 
445
461
  ## Cascade select — `.fdy-cascade`
446
- > **Typed wrapper: `<FdyCascade>`** Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
462
+ > **Typed wrapper: `<FdyCascade>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
447
463
 
448
464
  Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
449
465
  child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
@@ -464,7 +480,7 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
464
480
  ```
465
481
 
466
482
  ## Choose-from-list (CFL) — `data-fdy-cfl`
467
- > **Typed wrapper: `<FdyCfl>`** Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
483
+ > **Typed wrapper: `<FdyCfl>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
468
484
 
469
485
  A read-only field backed by master data: the button opens a searchable dialog, the picked row fills
470
486
  the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
@@ -475,7 +491,7 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
475
491
  - Inside: `.fdy-cfl__search` `__results` `__row` `__check` `__empty` `__count` `__actions`
476
492
  - Hooks: `data-fdy-cfl-search` (the search input), `data-fdy-cfl-empty`, `data-fdy-cfl-count`,
477
493
  `data-fdy-cfl-confirm` (multi only), `data-fdy-cfl-trigger`, `data-fdy-cfl-open`
478
- - Rows are server state in a real app drive them from a controlled fetch, not a global store.
494
+ - Rows are server state. In a real app, drive them from a controlled fetch rather than a global store.
479
495
  - **Multi-select in the typed wrappers** (Vue · React · Blazor): `multiple` ticks rows and commits
480
496
  them together on **Confirm** instead of committing the row that was clicked. Vue and React widen
481
497
  their model to an array (`Row[] | null`); Blazor takes a second pair, `Values` /
@@ -522,32 +538,32 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
522
538
  ```
523
539
 
524
540
  **`clearable` makes the value removable.** Without it a choose-from-list can be set but never unset,
525
- which breaks every *optional* foreign key the value type already allows null, only the component
541
+ which breaks every *optional* foreign key. The value type already allows null; only the component
526
542
  could not produce it. With it, a clear button appears beside the trigger whenever a row is picked
527
543
  and the field is editable; it emits `null` (Vue `update:modelValue`/`change`, React `onChange`,
528
544
  Blazor `ValueChanged`) and returns focus to the trigger. It is a second `.fdy-input-group__btn`, not
529
545
  a new class.
530
546
 
531
547
  ## Date picker — `data-fdy-datepicker`
532
- > **Typed wrapper: `<FdyDatepicker>`** Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
548
+ > **Typed wrapper: `<FdyDatepicker>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
533
549
 
534
550
  Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author an empty `<div>`**
535
- the enhancer builds everything.
551
+ The enhancer builds everything.
536
552
 
537
553
  - `.fdy-datepicker` (+`--error`) · `__trigger` `__value` (+`--placeholder`) `__icon` `__clear`
538
554
  `__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` (+`--months` `--years`)
539
555
  `__dow` `__day` `__month` `__year`
540
556
  - Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
541
557
  get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start.
542
- **Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor) use it in those stacks.
558
+ **Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor). Use it in those stacks.
543
559
  **Getting to a distant date.** The title is a **button**, and it drills up one level each press:
544
560
  day grid → 12-cell month grid (`.fdy-cal__grid--months` of `.fdy-cal__month`) → 12-cell year grid
545
- (`.fdy-cal__grid--years` of `.fdy-cal__year`). The arrows follow the level shown months, then
561
+ (`.fdy-cal__grid--years` of `.fdy-cal__year`). The arrows follow the level shown: months, then
546
562
  years, then pages of twelve years. Picking a year drops to that year's months, picking a month
547
563
  drops to its days: every level above the day grid is **navigation**, so nothing is committed until a
548
564
  day is selected. From the year grid the title steps back down to months.
549
565
 
550
- Year pages are **aligned**, not centred on the year in view 2016–2027, then 2028–2039 so the
566
+ Year pages are **aligned**, not centred on the year in view (2016–2027, then 2028–2039), so the
551
567
  pages tile and a given year always sits in the same place. From August 2026 to March 2022 that is 7
552
568
  clicks; walking month by month is 53. To 1998 it is 8; walking is 336.
553
569
 
@@ -590,15 +606,15 @@ Click/Enter opens the file dialog; drop works too. Needs `freeday-upload.js`.
590
606
  - List: `.fdy-filelist` (+`--grid`) of `.fdy-file` (+`--success`, `--error`) · `__icon` `__meta`
591
607
  `__name` `__sub` `__progress` `__remove`
592
608
  - Attributes: `data-max-size` (bytes), `data-filelist="#id"`, `data-fdy-upload-simulate` (demo only
593
- the kit fakes a transfer to `done()`; never set it in an app)
609
+ the kit fakes a transfer to `done()`; never set it in an app)
594
610
  - A11y: the dropzone is `role="button" tabindex="0"` + `aria-label`.
595
611
 
596
- **Both events fire on the dropzone** `fdy-upload-add` *and* `fdy-upload-remove`. One listener, one
612
+ **Both events fire on the dropzone**: `fdy-upload-add` *and* `fdy-upload-remove`. One listener, one
597
613
  element. (The file list may be anywhere in the document; nothing is dispatched on the row, because a
598
614
  row in a sibling list would never bubble through the zone.)
599
615
 
600
616
  **The row is yours to drive.** `fdy-upload-add` carries `detail.row`, a small state machine over the
601
- rendered `.fdy-file`. A dropped file **rests** it shows its size and nothing else until you say a
617
+ rendered `.fdy-file`. A dropped file **rests**, showing its size and nothing else, until you say a
602
618
  transfer started; the kit never claims one it is not performing.
603
619
 
604
620
  | `detail.row` | State it renders |
@@ -612,15 +628,15 @@ transfer started; the kit never claims one it is not performing.
612
628
  | `.ready()` | back to **rest** (e.g. after a failed attempt the user will retry) |
613
629
  | `.el` | the row element |
614
630
 
615
- **If your request outlives the transfer, drive `waiting()`.** Server-side work after the last byte
616
- OCR, extraction, virus scanning, transcoding is not uploading, and `setProgress(100)` left standing
631
+ **If your request outlives the transfer, drive `waiting()`.** Server-side work after the last byte (OCR,
632
+ extraction, virus scanning, transcoding) is not uploading, and `setProgress(100)` left standing
617
633
  is read as a hang. `waiting()` is the state for it: the label says what the server is doing, and the
618
634
  bar stops claiming a percentage it no longer has.
619
635
 
620
636
  ```js
621
637
  zone.addEventListener('fdy-upload-add', (e) => {
622
638
  if (e.detail.rejected) return; // the kit already rendered the reason
623
- const { file, row } = e.detail; // row is at rest nothing is in flight yet
639
+ const { file, row } = e.detail; // row is at rest; nothing is in flight yet
624
640
  submitBtn.onclick = async () => {
625
641
  row.uploading();
626
642
  await send(file, (pct) => row.setProgress(pct));
@@ -633,7 +649,7 @@ zone.addEventListener('fdy-upload-remove', (e) => forget(e.detail.file)); // s
633
649
  ```
634
650
 
635
651
  **Bring your own row:** omit the file list entirely (no `data-filelist`, and no `.fdy-filelist`
636
- sibling) and the enhancer renders nothing while still dispatching `fdy-upload-add` `detail.row`
652
+ sibling) and the enhancer renders nothing while still dispatching `fdy-upload-add`, where `detail.row`
637
653
  still works, its element simply isn't attached. Note the fallback when `data-filelist` is absent is
638
654
  `parentNode.querySelector('.fdy-filelist')`, so a bare dropzone will adopt a list that happens to
639
655
  share its parent; give the dropzone its own container if you mean "no list".
@@ -660,7 +676,7 @@ A consistent filter row of `.fdy-field`s with a width rhythm (`--w-sm` · defaul
660
676
  `--w-xl` · `--w-2xl`), one `--w-grow` field (usually search) absorbing the slack, and
661
677
  `.fdy-filterbar__actions` pinned at the end. A control with **no label of its own** (a
662
678
  `.fdy-check`, `.fdy-switch`, `.fdy-radio`, or a lone `.fdy-btn`) is given `--control-h` so it
663
- centres on the input line instead of hanging below it the bar aligns `flex-end` to line up
679
+ centres on the input line instead of hanging below it. The bar aligns `flex-end` to line up
664
680
  labelled fields, which would otherwise drop a 20px checkbox below a 32px input's centre.
665
681
  `--actions-inline` keeps the actions on the control
666
682
  line. Zero JS. Composes with `.fdy-table-toolbar` on a shared baseline.
@@ -690,7 +706,7 @@ Linear multi-step flow: marker → check, one panel at a time, back/next. Needs
690
706
 
691
707
  **`__btn` is optional.** For a **read-only indicator** put `__marker` + `__label` straight in the
692
708
  `<li>`; for a **navigable** stepper wrap them in `<button class="fdy-step__btn">` (add `disabled`
693
- for a step that is not reachable yet). Both render identically the marker carries its own lift
709
+ for a step that is not reachable yet). Both render identically; the marker carries its own lift
694
710
  over the connector line, so omitting `__btn` cannot make the connector draw through the numbers.
695
711
 
696
712
  ```html
@@ -717,23 +733,23 @@ Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-tab
717
733
  </div>
718
734
  ```
719
735
 
720
- **Frozen axes `.fdy-table--sticky`.** For a grid read against two axes at once (a rate
736
+ **Frozen axes, `.fdy-table--sticky`.** For a grid read against two axes at once (a rate
721
737
  matrix, a timetable) or a wide one whose identity columns must stay visible. `.fdy-table--sticky`
722
738
  is the enabler: it separates the borders, because a collapsed border belongs to the table rather
723
739
  than the cell and scrolls out from under whatever you froze. Then ask for the axes you have:
724
740
 
725
- - **Header row** add `.fdy-table--sticky-head`, and put `.fdy-table-scroll--frozen` on the
741
+ - **Header row:** add `.fdy-table--sticky-head`, and put `.fdy-table-scroll--frozen` on the
726
742
  wrapper (it scrolls both ways and is the scrollport the freeze resolves against; size it with
727
743
  `--fdy-table-frozen-h`, default `30rem`). It is a separate modifier because a table that only
728
744
  freezes columns scrolls with the **page**, and `top:0` against the page sticks the header under
729
- the viewport edge over the app's own top bar.
730
- - **Columns** put `.fdy-table__freeze` on **every cell of the column, header included**, and give
745
+ the viewport edge, over the app's own top bar.
746
+ - **Columns:** put `.fdy-table__freeze` on **every cell of the column, header included**, and give
731
747
  each frozen column its own `--fdy-freeze-left`: `0` for the first, then the summed widths of the
732
748
  frozen columns to its left. Mark the last one `.fdy-table__freeze--edge` so the reader can see
733
749
  where the frozen block ends. One frozen column needs no variable at all.
734
750
 
735
751
  The offsets are the caller's job because they depend on rendered column widths, which CSS cannot
736
- sum measure them once after render (and on resize) and set the variable.
752
+ sum, so measure them once after render (and on resize) and set the variable.
737
753
 
738
754
  ```html
739
755
  <div class="fdy-table-scroll fdy-table-scroll--frozen" style="--fdy-table-frozen-h:26rem">
@@ -754,7 +770,7 @@ sum — measure them once after render (and on resize) and set the variable.
754
770
  ```
755
771
 
756
772
  ## Data table — `.fdy-datatable`
757
- > **Typed wrapper: `<FdyTable>`** controlled: `columns` + `rows`, with sort/filter/page events (`update:pageIndex` · `onPageIndexChange` · `PageIndexChanged`) and `process` for driving a card list off the same processed set. The markup below is the raw enhancer path.
773
+ > **Typed wrapper: `<FdyTable>`**. controlled: `columns` + `rows`, with sort/filter/page events (`update:pageIndex` · `onPageIndexChange` · `PageIndexChanged`) and `process` for driving a card list off the same processed set. The markup below is the raw enhancer path.
758
774
 
759
775
  The interactive table: global search, sort, per-column filters, row selection + bulk bar,
760
776
  pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` + `data-fdy-table`
@@ -773,20 +789,24 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
773
789
  detail), `.fdy-table__state` (in-table empty/loading row)
774
790
  - Footer: `.fdy-table-footer` · `__info` (`data-fdy-table-info`) + optional `__size` (a label and a
775
791
  rows-per-page control) + `<nav class="fdy-pagination" data-fdy-table-pagination>`. The typed
776
- wrappers render `FdyCombo` there **never a native `<select>`**, whose open list is an OS menu no
792
+ wrappers render `FdyCombo` there, **never a native `<select>`**, whose open list is an OS menu no
777
793
  stylesheet reaches. In the raw path the control is app-authored markup (its options *are* the
778
794
  offer) carrying `data-fdy-table-page-size`; the enhancer only wires it, and listens for both
779
795
  `change` and the `fdy-change` a `.fdy-combo` emits, so either kind works.
780
796
  - Sort values: put the raw value in `data-sort-value` when the cell text is formatted.
781
- - **Language caveat:** every user-visible string the **vanilla enhancers** write is Indonesian by
782
- default the table's footer and bulk count (`Menampilkan 1–5 dari 7`, `N dipilih`), its pager and
783
- filter UI (`Sebelumnya`, `Berikutnya`, `Filter kolom`, `Berisi teks`, `Reset`, `Tutup`), and the
784
- same elsewhere (`Format tidak valid.`, `Tampilkan kata sandi`, `Menunggu server…`). The
785
- Vue/React/Blazor components are English throughout, so **an English app should use the typed
786
- wrapper**.
787
- - **Overriding an enhancer's strings.** Each enhancer keeps its strings in one `TEXT` table and
797
+ - **Language: English throughout, on every path.** The vanilla enhancers wrote Indonesian until
798
+ 2.0.0 while the typed wrappers wrote English, which meant an app mixing the two paths read as
799
+ two products. A Blazor app mixes them by construction, since `FreedayBlazor.initAll` runs the
800
+ enhancers. The defaults are now English everywhere, and the suite enforces it rather than
801
+ stating it.
802
+ **Dates are the exception, and deliberately so:** the datepicker formats through `Intl` using the
803
+ page's `<html lang>`, falling back to `en`. An Indonesian app writes `lang="id"` and gets
804
+ Indonesian month and weekday names with no configuration at all, which is a better hatch than anything
805
+ the kit could invent, because it is the platform's.
806
+ - **Overriding an enhancer's strings.** This is also the migration path for an Indonesian app
807
+ upgrading past 2.0.0. Each enhancer keeps its strings in one `TEXT` table and
788
808
  reads them through `textOf()`, so any of them can be replaced per element with
789
- **`data-fdy-text-<key>`**, the key kebab-cased no forking, and no rendering the nodes yourself.
809
+ **`data-fdy-text-<key>`**, the key kebab-cased. No forking, and no rendering the nodes yourself.
790
810
  (HTML lowercases attribute names, so a camelCase key like `filterText` is written
791
811
  `data-fdy-text-filter-text`; the run-together spelling still resolves.):
792
812
 
@@ -805,25 +825,25 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
805
825
  arrives overridable or not at all.
806
826
 
807
827
  **Testing note:** a column's filter button and the dialog it opens deliberately share one
808
- accessible name (`Filter <column>`) a dialog named after its trigger is the normal pattern. In a
828
+ accessible name (`Filter <column>`), since a dialog named after its trigger is the normal pattern. In a
809
829
  Playwright/Testing-Library suite that means `getByLabel('Filter Name')` resolves to two elements;
810
830
  reach for `getByRole('button', { name: 'Filter Name' })` instead.
811
831
 
812
832
  **Rows per page.** Pass `pageSizes` (Vue/React `:page-sizes="[10, 20, 50]"`, Blazor
813
833
  `PageSizes="…"`) and the footer grows a rows-per-page control between the range and the pager.
814
- Omit it for none unchanged default. Picking a size keeps the reader on the row they were looking
834
+ Omit it for none, which is the unchanged default. Picking a size keeps the reader on the row they were looking
815
835
  at rather than dropping them on page 1.
816
836
 
817
- - **server mode** reported through `update:page` / `onPageChange` / `PageChanged`, the same event
837
+ - **server mode:** reported through `update:page` / `onPageChange` / `PageChanged`, the same event
818
838
  as a page click, carrying the new `size`. Read `size` to tell the two apart.
819
- - **client mode** the table applies it itself, so the control works with nothing wired, and also
839
+ - **client mode:** the table applies it itself, so the control works with nothing wired, and also
820
840
  emits `update:pageSize` / `onPageSizeChange` / `PageSizeChanged` for a caller that wants to
821
841
  persist the choice. Changing the `pageSize` prop wins back.
822
842
 
823
- A footer with a size control stays visible on a single page otherwise picking "100" on a
843
+ A footer with a size control stays visible on a single page. Otherwise picking "100" on a
824
844
  ninety-row list would remove the only way back to twenty.
825
845
 
826
- **Typed wrapper: `<FdyTableFooter>`** the footer alone (`page` in, `update:page` /
846
+ **Typed wrapper: `<FdyTableFooter>`.** The footer alone (`page` in, `update:page` /
827
847
  `onPageChange` / `PageChanged` out), for the one shape that cannot use the table's own: a
828
848
  **responsive** list, where a `.fdy-datatable` at `lg` and a `.fdy-list` below it are two renderings
829
849
  of one page of rows. A footer inside the table is inside the half a phone hides, so those screens
@@ -832,17 +852,17 @@ render it once, outside both, with `pager={false}` on the table.
832
852
  **Who draws the pager.** The table renders its own footer (range + pager) whenever there is more
833
853
  than one page. Two ways to take it over:
834
854
 
835
- - **client mode** pass `pageIndex` (with `pageSize`, without `page`) and drive it yourself;
836
- - **either mode** pass `pager={false}` (Vue `:pager="false"`, Blazor `Pager="false"`) and render
855
+ - **client mode:** pass `pageIndex` (with `pageSize`, without `page`) and drive it yourself;
856
+ - **either mode:** pass `pager={false}` (Vue `:pager="false"`, Blazor `Pager="false"`) and render
837
857
  your own control. In server mode this is the only way: the app already owns the page there, and
838
- was still being handed a second control. The typical shape is a responsive list a table at `lg`,
839
- `.fdy-list` below it where one pager has to serve both views so they cannot disagree.
858
+ was still being handed a second control. The typical shape is a responsive list (a table at `lg`,
859
+ `.fdy-list` below it) where one pager has to serve both views so they cannot disagree.
840
860
 
841
861
  Do not hide the footer with CSS. `display:none` works, but it reaches into a component's internals
842
862
  and breaks the moment the class changes.
843
863
 
844
864
  ## Pagination — `.fdy-pagination`
845
- The block class on the `<nav>` is a **structural hook only** it carries no rule of its own; the
865
+ The block class on the `<nav>` is a **structural hook only**. It carries no rule of its own; the
846
866
  `__list` / `__link` / `__ellipsis` elements do all the styling, and the data table targets
847
867
  `data-fdy-table-pagination`. Keep it on the wrapper anyway, for consistency with the rest of the kit.
848
868
 
@@ -857,7 +877,7 @@ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__ic
857
877
  replaces.
858
878
 
859
879
  ## Charts — `data-fdy-chart`
860
- > **Typed wrapper: `<FdyChart>`** data props in all three (`type` + `values` / `series`); it repaints on data change, so `FreedayChart.update(el)` is only for the raw path below.
880
+ > **Typed wrapper: `<FdyChart>`**. data props in all three (`type` + `values` / `series`); it repaints on data change, so `FreedayChart.update(el)` is only for the raw path below.
861
881
 
862
882
  Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`; call
863
883
  `FreedayChart.update(el)` after changing data (or use `FdyChart` in Vue/React/Blazor).
@@ -866,27 +886,27 @@ Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`
866
886
  - Roots: `.fdy-sparkline` · `.fdy-bars` · `.fdy-donut` · `.fdy-chart-xy` (built for line/area/bar)
867
887
  - Data: `data-values="1,2,3"` (single series) or `data-series='[{"label":"A","values":[…]}]'`
868
888
  (multi), `data-labels="Jan,Feb"`, `data-fdy-stacked`, `data-fdy-center` (donut)
869
- - Colour: `data-fdy-color="primary"` or `data-fdy-colors="success,warning,danger"` semantic token
889
+ - Colour: `data-fdy-color="primary"` or `data-fdy-colors="success,warning,danger"` for semantic token
870
890
  names **or** `chart-1`…`chart-8` slots to pin a category's colour. Multi-series defaults to the
871
891
  validated categorical palette `--chart-1`…`--chart-8`.
872
892
  - Format: `data-fdy-format="number|percent|currency"`; legend `data-fdy-legend` (`none` to drop);
873
893
  axes `data-fdy-axes`
874
- - Sizing is already set by the kit override **these**, never a `height` on the chart root (a
894
+ - Sizing is already set by the kit. Override **these**, never a `height` on the chart root (a
875
895
  root height fights `aspect-ratio` instead of setting it): `.fdy-chart-xy__plot` is `width:100%`
876
896
  + `aspect-ratio:16/9` + `min-height:8rem`; `.fdy-bars` is a fixed `height:9rem`; `.fdy-sparkline`
877
897
  is an inline-block `8rem × 2.25rem`.
878
898
  - Axis type: `--fdy-chart-tick-size` (default `var(--text-xs)`) sets the y-tick and x-label size on
879
- `.fdy-chart-xy`. It is a real CSS size a cartesian chart's `viewBox` is measured to its plot, so
880
- one user unit is one pixel and the renderer reads it back to size the y-gutter and to decide how
899
+ `.fdy-chart-xy`. It is a real CSS size: a cartesian chart's `viewBox` is measured to its plot, so
900
+ one user unit is one pixel, and the renderer reads it back to size the y-gutter and to decide how
881
901
  many x-labels fit. Set the token rather than `font-size` on `__tick`/`__xlabel`, or the gutter and
882
902
  the autoskip will still be computed from the old size.
883
903
  - A11y: every chart is `role="img"` + an **author-supplied** `aria-label`, and that label is the
884
- *entire* text alternative write one that carries the numbers, not just the shape. The renderer
885
- marks everything it draws `aria-hidden="true"` legend, bar values and labels, donut centre, the
886
- SVG so nothing inside the chart competes with your label or repeats it. `role="img"` alone would
904
+ *entire* text alternative, so write one that carries the numbers rather than just the shape. The renderer
905
+ marks everything it draws `aria-hidden="true"` (legend, bar values and labels, donut centre, the
906
+ SVG), so nothing inside the chart competes with your label or repeats it. `role="img"` alone would
887
907
  **not** have done that: it is Children Presentational per ARIA, but browsers keep the subtree in
888
908
  the accessibility tree anyway, so the kit hides it rather than relying on the spec. That hiding
889
- needs a name to work from a chart with **no** `aria-label`/`aria-labelledby` keeps its contents
909
+ needs a name to work from: a chart with **no** `aria-label`/`aria-labelledby` keeps its contents
890
910
  exposed, because an image with neither a name nor any text is worse than one that leaks its
891
911
  legend. Any `<table>`/text placed inside the element is **pre-render only** and is replaced on
892
912
  render. Good: `aria-label="Monthly posting trend: Posted 62% → 88%, Draft 24% → 16%"`.
@@ -896,7 +916,7 @@ Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`
896
916
  `__center` `__hit`; `.fdy-sparkline__line` `__area` `__dot`.
897
917
 
898
918
  ## Tree view — `.fdy-tree`
899
- Native `<details>` hierarchy zero JS for expand/collapse. `.fdy-tree__branch` on `<details>`,
919
+ Native `<details>` hierarchy, with zero JS for expand/collapse. `.fdy-tree__branch` on `<details>`,
900
920
  `.fdy-tree__leaf` on a leaf `<li>`, plus `__chevron` and `__icon` svgs inside `<summary>`.
901
921
  `.fdy-tree--checkbox` + `data-fdy-tree` (needs `freeday-tree.js`) adds cascading selection:
902
922
  `.fdy-tree__check` on each `<input class="fdy-checkbox">`; checking a branch checks its children,
@@ -904,19 +924,19 @@ a partial set makes the branch indeterminate. Checking never toggles expansion.
904
924
 
905
925
  ## List (flat rows) — `.fdy-list`
906
926
  The **flat** row container: one bordered surface, hairline dividers, **no shadow**. This is what a
907
- responsive `.fdy-datatable` should become below `md` not a stack of `.fdy-card`s, which carries
927
+ responsive `.fdy-datatable` should become below `md`, rather than a stack of `.fdy-card`s, which carries
908
928
  `--shadow-lift` and turns ten rows into ten floating objects.
909
929
 
910
930
  - `.fdy-list` on a `<ul>`/`<ol>` (list-style is reset for you) or a `<div>`
911
- - `.fdy-list__row` one row; `--interactive` for hover feedback, `--button` when the row **is** the
931
+ - `.fdy-list__row` is one row; `--interactive` for hover feedback, `--button` when the row **is** the
912
932
  control (render it as a real `<button>`/`<a>`; the UA box is reset without losing the list surface)
913
933
  - Row internals: `.fdy-list__main` (truncating stack) → `.fdy-list__title` + `.fdy-list__meta`, and
914
934
  `.fdy-list__aside` pinned right
915
- - **Disabled** `disabled` on the `<button>` (or `aria-disabled="true"` when the row is an `<a>`/
935
+ - **Disabled:** `disabled` on the `<button>` (or `aria-disabled="true"` when the row is an `<a>`/
916
936
  `<div>`) dims the row and withdraws the hover tint and the pointer cursor, same as every other
917
937
  control in the kit. Do not hand-roll it: an undimmed row that still lights up under the pointer
918
938
  reads as clickable while it is refusing input.
919
- - Not `.fdy-list-reset` that utility only strips UA bullets/indent from a semantic list.
939
+ - Not `.fdy-list-reset`, which only strips UA bullets/indent from a semantic list.
920
940
 
921
941
  ```html
922
942
  <ul class="fdy-list">
@@ -967,11 +987,11 @@ Every field is optional; it returns the toast element. Classes (rendered for you
967
987
  `__body` `__title` `__text` `__close`.
968
988
 
969
989
  **Above modals, and not by z-index.** The region is a `popover` and re-enters the **top layer** as
970
- each toast lands, because `.fdy-modal` is a native `<dialog>` see the note under Modal below. If
990
+ each toast lands, because `.fdy-modal` is a native `<dialog>`. See the note under Modal below. If
971
991
  a toast of yours is hidden behind something, a bigger `z-index` is not the fix; check whether that
972
992
  something is in the top layer too. Where the Popover API is unavailable (before Chrome 114 /
973
993
  Safari 17 / Firefox 125) the region falls back to a fixed block at `z-index: 200`, which cannot
974
- clear an open modal the message is still announced to a screen reader.
994
+ clear an open modal, the message is still announced to a screen reader.
975
995
 
976
996
  ## Tooltip — `.fdy-tooltip`
977
997
  Hover/focus only, never the sole carrier of information. Wrap in `.fdy-tooltip-wrap`; the trigger
@@ -991,7 +1011,7 @@ Size-matched placeholders so nothing shifts when data lands: `--title` `--text`
991
1011
  # Navigation
992
1012
 
993
1013
  ## Nav (menu) — `.fdy-nav`
994
- Navigation links **vertical by default** (the app shell sidebar), horizontal with `--horizontal`. Items are `<a class="fdy-nav__item">` with
1014
+ Navigation links, **vertical by default** (the app shell sidebar), horizontal with `--horizontal`. Items are `<a class="fdy-nav__item">` with
995
1015
  `__icon` / `__label` / `__badge`; the current one gets `aria-current="page"`.
996
1016
  `--flat` drops the surface. Nested groups are native `<details>`:
997
1017
 
@@ -1015,17 +1035,17 @@ Navigation links — **vertical by default** (the app shell sidebar), horizontal
1015
1035
  </header>
1016
1036
  ```
1017
1037
 
1018
- > **Which one for navigation?** A nav link is a link marked `aria-current="page"` never
1038
+ > **Which one for navigation?** A nav link is a link marked `aria-current="page"`, never
1019
1039
  > `aria-selected` (invalid ARIA on an anchor) and never `role="tab"`. Use `.fdy-nav--horizontal`
1020
1040
  > for an application's **primary** navigation. For **routed sub-navigation** that should look like
1021
1041
  > tabs (`/settings/profile` · `/settings/billing`), put `.fdy-tabs__list` / `.fdy-tabs__tab` on
1022
- > plain `<a>`s those classes honour `aria-current="page"` too and do **not** add the tab roles
1042
+ > plain `<a>`s, since those classes honour `aria-current="page"` too, and do **not** add the tab roles
1023
1043
  > or `freeday-tabs.js`, which promise a keyboard contract routes do not have.
1024
1044
 
1025
1045
  ## App bar — `.fdy-appbar`
1026
1046
  Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--sticky`
1027
1047
  `--elevated` `--dense` `--primary` (on-colour controls). Parts `__brand` `__spacer` `__actions`.
1028
- It ships **no link class of its own** put `<nav class="fdy-nav fdy-nav--horizontal">` in it
1048
+ It ships **no link class of its own**. Put `<nav class="fdy-nav fdy-nav--horizontal">` in it
1029
1049
  for primary navigation (see Nav above).
1030
1050
 
1031
1051
  ## Breadcrumb — `.fdy-breadcrumb`
@@ -1036,7 +1056,7 @@ for primary navigation (see Nav above).
1036
1056
  ## Tabs — `.fdy-tabs`
1037
1057
  APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
1038
1058
  `.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
1039
- so the same look serves routed sub-navigation built from plain links see the note under Nav.
1059
+ so the same look serves routed sub-navigation built from plain links. See the note under Nav.
1040
1060
 
1041
1061
  ```html
1042
1062
  <div class="fdy-tabs" data-fdy-tabs>
@@ -1054,13 +1074,13 @@ so the same look serves routed sub-navigation built from plain links — see the
1054
1074
  # Overlays
1055
1075
 
1056
1076
  ## Modal — `.fdy-modal`
1057
- > **Typed wrapper: `<FdyModal>`** Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
1077
+ > **Typed wrapper: `<FdyModal>`**. Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
1058
1078
 
1059
1079
  Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
1060
1080
  `--wide`; `--cfl` for the choose-from-list dialog.
1061
1081
 
1062
1082
  **It is in the TOP LAYER.** `showModal()` paints the dialog above every stacking context on the
1063
- page, so no `z-index` anywhere can put anything over it only another top-layer element (a
1083
+ page, so no `z-index` anywhere can put anything over it. Only another top-layer element (a
1064
1084
  `popover`, or another modal) can. It also makes the rest of the document inert, which means
1065
1085
  `elementFromPoint` outside the dialog returns the dialog: do not use a hit test to check what is
1066
1086
  painted on top of a modal, because it will tell you the dialog is, even when it visibly is not. Parts `__header` `__title` `__body` `__footer`
@@ -1068,34 +1088,34 @@ painted on top of a modal, because it will tell you the dialog is, even when it
1068
1088
  `data-close` on any button that should close it.
1069
1089
 
1070
1090
  **Stacking two overlays is supported.** Modal and drawer are both `<dialog>` opened with
1071
- `showModal()`, so both enter the top layer and paint in the order they were opened a confirm
1091
+ `showModal()`, so both enter the top layer and paint in the order they were opened. A confirm
1072
1092
  modal opened from inside an open drawer paints **above** it, and neither needs a `z-index` for
1073
1093
  that. Escape closes the topmost first, so cancelling returns to the still-open drawer; a second
1074
1094
  Escape then closes the drawer. Closing them out of order is fine too: closing the drawer while the
1075
1095
  modal is up leaves the modal open, painted and dismissible.
1076
1096
 
1077
1097
  One caveat, and it is the browser's rather than the kit's: **the second overlay's Escape can only
1078
- be refused if both were opened by a real gesture.** A `showModal()` that runs from script a timer,
1079
- or after an `await` that outlived the click still closes only the topmost, but its `cancel` event
1098
+ be refused if both were opened by a real gesture.** A `showModal()` that runs from script (a timer,
1099
+ or after an `await` that outlived the click) still closes only the topmost, but its `cancel` event
1080
1100
  arrives **non-cancelable**, so `preventDefault()` on it is ignored and an "unsaved changes, stay
1081
1101
  open" guard silently stops working. Routing Escape through app state instead, which is what the
1082
1102
  typed wrappers do, is unaffected: the dialog closes and your state follows it.
1083
1103
 
1084
1104
  *Correction to 1.51.1, which said such a dialog has its close watcher grouped with the one below so
1085
1105
  that a single Escape closes both.* That happens only on a page that has received **no user input at
1086
- all** a test harness, never an app; six seconds after one click the overlays are still
1106
+ all**: a test harness, never an app. Six seconds after one click the overlays are still
1087
1107
  independent. Open overlays with real clicks in a test, or a stacking assertion fails for a reason
1088
1108
  that is not the kit. Both rules measured on Chromium 133 and 151, and pinned as a matrix in
1089
1109
  `browser/overlay-stack.mjs`.
1090
1110
 
1091
1111
  ## Drawer — `.fdy-drawer`
1092
- > **Typed wrapper: `<FdyDrawer>`** Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
1112
+ > **Typed wrapper: `<FdyDrawer>`**. Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
1093
1113
 
1094
- Temporary side panel on native `<dialog>` left by default, `--right` to flip. Parts `__header`
1114
+ Temporary side panel on native `<dialog>`, left by default, `--right` to flip. Parts `__header`
1095
1115
  `__title` `__body` `__footer` `__close`. Open it from any
1096
1116
  `<button data-fdy-drawer="<dialog id>">`. Needs `freeday-drawer.js`.
1097
1117
 
1098
- Opened with `showModal()`, so it is in the **top layer** on the same terms as the modal including
1118
+ Opened with `showModal()`, so it is in the **top layer** on the same terms as the modal, including
1099
1119
  opening a modal over an open drawer, which is supported and described under
1100
1120
  [Modal → *Stacking two overlays*](#modal--fdy-modal). It slides in on `transform` and never animates
1101
1121
  opacity: wait on its position, not on `opacity`, if you need to know when it has arrived.
@@ -1117,25 +1137,25 @@ is `role="group" aria-roledescription="slide"`.
1117
1137
  ## Card — `.fdy-card`
1118
1138
  Parts `__body` `__title` `__desc` `__footer`. Modifiers:
1119
1139
 
1120
- - `--elevated` raise it (use sparingly; most surfaces are flat)
1121
- - `--interactive` cursor + hover-lift for a card that *has* a click handler. **Presentational
1122
- only:** it is the one affordance in the kit whose correctness lives outside it the CSS cannot
1140
+ - `--elevated` raises it (use sparingly; most surfaces are flat)
1141
+ - `--interactive` gives cursor + hover-lift to a card that *has* a click handler. **Presentational
1142
+ only:** it is the one affordance in the kit whose correctness lives outside it, because the CSS cannot
1123
1143
  know whether a handler exists, so the modifier without a control (a card that looks clickable and
1124
1144
  swallows every click) and a control without the modifier both fail silently. Pair it with a real
1125
- control normally the stretched target below.
1126
- - `--button` for a card that **is** the control: render it as `<button>` and add this to reset
1145
+ control, normally the stretched target below.
1146
+ - `--button` is for a card that **is** the control: render it as `<button>` and add this to reset
1127
1147
  the UA button box **without** losing the card surface/border
1128
- - `.fdy-card--button` never replaces keyboard semantics a clickable card must be a real
1148
+ - `.fdy-card--button` never replaces keyboard semantics. A clickable card must be a real
1129
1149
  `<button>` or `<a>`.
1130
- - **Disabled** `disabled` (or `aria-disabled="true"`) dims the card and withdraws the pointer
1150
+ - **Disabled:** `disabled` (or `aria-disabled="true"`) dims the card and withdraws the pointer
1131
1151
  cursor and the `--interactive` hover-lift.
1132
1152
  - The card is `position:relative`, so a badge or ribbon you absolutely position inside it anchors
1133
- to the card. That is also what keeps hidden labels inside it from escaping see *Containment*.
1153
+ to the card. That is also what keeps hidden labels inside it from escaping. See *Containment*.
1134
1154
 
1135
1155
  ### One card, one primary action, one escape hatch
1136
1156
 
1137
1157
  A card with **two** actions cannot be `--button`: interactive content nested in a `<button>` is
1138
- invalid HTML. Give the primary control `.fdy-btn--stretch` its hit area covers the whole card,
1158
+ invalid HTML. Give the primary control `.fdy-btn--stretch`: its hit area covers the whole card,
1139
1159
  while it stays a real `<button>`/`<a>` with real keyboard semantics.
1140
1160
 
1141
1161
  ```html
@@ -1152,34 +1172,32 @@ while it stays a real `<button>`/`<a>` with real keyboard semantics.
1152
1172
  ```
1153
1173
 
1154
1174
  - **The escape hatch needs nothing.** Every focusable element in a card that holds a stretched
1155
- target is raised above the overlay automatically forgetting a `z-index` here fails silently, and
1175
+ target is raised above the overlay automatically. Forgetting a `z-index` here fails silently, and
1156
1176
  a silent failure is what this pattern exists to remove. The `position` that raising needs is only a
1157
- **default** (zero-specificity), so a control you pin yourself a corner dismiss, a favourite star
1177
+ **default** (zero-specificity), so a control you pin yourself (a corner dismiss, a favourite star)
1158
1178
  keeps its own `position:absolute` and is raised anyway.
1159
1179
  - **Name the stretched button for what the whole card does** ("Open workspace Alpha", not "Open").
1160
1180
  Its label is the accessible name of the entire hit area.
1161
- - **Do not add `.fdy-btn--stretch` to more than one control per card** the last one wins the
1181
+ - **Do not add `.fdy-btn--stretch` to more than one control per card.** The last one wins the
1162
1182
  overlap, and which one that is depends on source order.
1163
1183
  - Text under the overlay is no longer selectable; that is the cost of the pattern, not a bug.
1164
1184
  - **Cards and list rows.** The overlay anchors to the nearest *positioned* ancestor, and both hosts
1165
1185
  supply one: `.fdy-card` always, `.fdy-list__row` when it contains a stretched target. Everything
1166
- above applies to a row unchanged same markup, same automatic raise. In any *other* container,
1186
+ above applies to a row unchanged: same markup, same automatic raise. In any *other* container,
1167
1187
  make the element `position:relative` yourself, or the overlay resolves against whatever is
1168
1188
  positioned further up (in a list that used to be `.fdy-list` itself, so every row's target covered
1169
- the whole list and the last one in the DOM won a click on row one opened row two).
1189
+ the whole list and the last one in the DOM won, so a click on row one opened row two).
1170
1190
  A row whose *whole* surface is one control is still better as `.fdy-list__row--button`.
1171
1191
  - `.fdy-btn` nudges itself on `:hover`/`:active`, and a transformed element becomes the containing
1172
- block for its own absolutely positioned descendants so a hand-rolled `::after{inset:0}` overlay
1192
+ block for its own absolutely positioned descendants, so a hand-rolled `::after{inset:0}` overlay
1173
1193
  **re-anchors to the button mid-gesture** and the click never lands. `--stretch` neutralises those
1174
1194
  transforms and moves the press feedback to the card. This is why the pattern is shipped rather
1175
1195
  than documented.
1176
1196
 
1177
1197
  ## Badge — `.fdy-badge`
1178
- Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only
1179
- the text carries the meaning.
1198
+ Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only: the text carries the meaning.
1180
1199
 
1181
- `--tone-1`…`--tone-8` for a status vocabulary larger than the semantic palette. Semantics first
1182
- a state that IS good, bad or waiting takes `--success` / `--danger` / `--warning`; these are for
1200
+ `--tone-1`…`--tone-8` for a status vocabulary larger than the semantic palette. Semantics first: a state that IS good, bad or waiting takes `--success` / `--danger` / `--warning`; these are for
1183
1201
  the rest, so two states a workflow distinguishes never share a look. Map each status to a FIXED
1184
1202
  tone, never to a hash of the string: a status is a closed set, and a hash reassigns colours the
1185
1203
  moment the set grows.
@@ -1192,19 +1210,19 @@ Put the count in the wrapper's `aria-label` and mark the overlay `aria-hidden="t
1192
1210
  ## Avatar — `.fdy-avatar`
1193
1211
  Initials or image. Sizes `--sm` / default / `--lg`; `.fdy-avatar-group` stacks them (label the
1194
1212
  group). `--tone-1`…`--tone-8` are decorative tints (WCAG AA in both themes) to distinguish
1195
- same-initial avatars hash the index off the **full** name, not the initials.
1213
+ same-initial avatars. Hash the index off the **full** name, not the initials.
1196
1214
 
1197
1215
  Sizes: `--xs` 1.5rem · *(base)* 2.5rem · `--sm` 2rem · `--lg` 3.5rem. Use **`--xs` inside a control**
1198
- `.fdy-btn--sm` is itself 2rem tall, so an `--sm` avatar fills a small button edge to edge and the
1216
+ because `.fdy-btn--sm` is itself 2rem tall, so an `--sm` avatar fills a small button edge to edge and the
1199
1217
  ghost border cuts across the circle.
1200
1218
 
1201
1219
  ## Chip — `.fdy-chip`
1202
1220
  Three roles inside a `.fdy-chips` row:
1203
1221
 
1204
- - **static / removable** `<span class="fdy-chip">`, `--primary` for an active filter, with
1222
+ - **static / removable:** `<span class="fdy-chip">`, `--primary` for an active filter, with
1205
1223
  `.fdy-chip__remove` (`aria-label` says what it removes)
1206
- - **choice** `<button class="fdy-chip fdy-chip--choice" aria-pressed>` toggle
1207
- - **filter** `--filter`, same but with a `.fdy-chip__check` mark
1224
+ - **choice:** `<button class="fdy-chip fdy-chip--choice" aria-pressed>` toggle
1225
+ - **filter:** `--filter`, same but with a `.fdy-chip__check` mark
1208
1226
 
1209
1227
  Interactive chips are managed by `freeday-chip.js`: wrap the row in `data-fdy-chips` (+
1210
1228
  `data-single` for pick-one, `data-label` for the group name), give each `data-value`.
@@ -1218,5 +1236,5 @@ by the accompanying text, not the marker colour alone.
1218
1236
  ---
1219
1237
 
1220
1238
  *Anything not listed here is not part of the public surface. If a screen needs a primitive that
1221
- isn't in this file, it probably belongs in the kit open an issue instead of re-inventing it
1239
+ isn't in this file, it probably belongs in the kit. Open an issue instead of re-inventing it
1222
1240
  locally.*