@cahyo-dimas/freeday 1.54.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 (143) hide show
  1. package/CHANGELOG.md +625 -545
  2. package/COMPONENTS.md +141 -138
  3. package/README.id.md +46 -45
  4. package/README.md +46 -43
  5. package/USAGE.md +34 -34
  6. package/adapters/blazor/FdyAppShell.razor +2 -2
  7. package/adapters/blazor/FdyAppShell.razor.cs +6 -6
  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 +1 -1
  24. package/adapters/core/app-shell.js +6 -6
  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 +5 -5
  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 +1 -1
  39. package/adapters/react/useFreeday.js +3 -3
  40. package/adapters/vue/components/FdyAppShell.vue +5 -5
  41. package/adapters/vue/components/FdyAutocomplete.vue +1 -1
  42. package/adapters/vue/components/FdyCascade.vue +1 -1
  43. package/adapters/vue/components/FdyCfl.vue +8 -8
  44. package/adapters/vue/components/FdyChart.vue +2 -2
  45. package/adapters/vue/components/FdyCombo.vue +1 -1
  46. package/adapters/vue/components/FdyDateRange.vue +2 -2
  47. package/adapters/vue/components/FdyDatepicker.vue +5 -5
  48. package/adapters/vue/components/FdyDrawer.vue +3 -3
  49. package/adapters/vue/components/FdyModal.vue +3 -3
  50. package/adapters/vue/components/FdyTable.vue +7 -7
  51. package/adapters/vue/components/FdyTableFilter.vue +1 -1
  52. package/adapters/vue/components/FdyTableFooter.vue +5 -5
  53. package/adapters/vue/index.d.ts +2 -2
  54. package/adapters/vue/useFreeday.js +3 -3
  55. package/dist/asset.d.ts +3 -3
  56. package/dist/freeday-app-shell.js +9 -9
  57. package/dist/freeday-autocomplete.js +2 -2
  58. package/dist/freeday-breakpoint.js +1 -1
  59. package/dist/freeday-carousel.js +5 -5
  60. package/dist/freeday-cascade.js +7 -7
  61. package/dist/freeday-cfl.js +9 -9
  62. package/dist/freeday-chart.js +11 -11
  63. package/dist/freeday-chip.js +3 -3
  64. package/dist/freeday-datepicker.js +11 -7
  65. package/dist/freeday-datetime.js +5 -5
  66. package/dist/freeday-drawer.js +1 -1
  67. package/dist/freeday-form.js +14 -14
  68. package/dist/freeday-mask.js +6 -6
  69. package/dist/freeday-menu.js +1 -1
  70. package/dist/freeday-number.js +3 -3
  71. package/dist/freeday-popover.js +3 -3
  72. package/dist/freeday-rating.js +2 -2
  73. package/dist/freeday-select.js +5 -5
  74. package/dist/freeday-slider.js +2 -2
  75. package/dist/freeday-stepper.js +7 -7
  76. package/dist/freeday-table.js +15 -15
  77. package/dist/freeday-tabs.js +1 -1
  78. package/dist/freeday-timepicker.js +2 -2
  79. package/dist/freeday-toast.js +8 -8
  80. package/dist/freeday-tree.js +3 -3
  81. package/dist/freeday-upload.js +25 -25
  82. package/dist/freeday.bundle.css +176 -176
  83. package/dist/freeday.css +170 -170
  84. package/dist/freeday.d.ts +2 -2
  85. package/dist/freeday.js +162 -158
  86. package/dist/freeday.tokens.css +5 -5
  87. package/docs/agent-onboarding.md +41 -41
  88. package/docs/getting-started.md +35 -35
  89. package/docs/integrations.md +18 -18
  90. package/docs/reference-screen.html +14 -14
  91. package/package.json +4 -3
  92. package/src/base.css +6 -6
  93. package/src/components/accordion.css +4 -4
  94. package/src/components/alert.css +1 -1
  95. package/src/components/app-shell.css +10 -10
  96. package/src/components/appbar.css +3 -3
  97. package/src/components/autocomplete.css +1 -1
  98. package/src/components/avatar.css +3 -3
  99. package/src/components/badge.css +5 -5
  100. package/src/components/breadcrumb.css +1 -1
  101. package/src/components/breakpoints.css +4 -4
  102. package/src/components/button.css +13 -13
  103. package/src/components/card.css +8 -8
  104. package/src/components/carousel.css +2 -2
  105. package/src/components/cascade.css +2 -2
  106. package/src/components/cfl.css +4 -4
  107. package/src/components/chart.css +3 -3
  108. package/src/components/chip.css +2 -2
  109. package/src/components/combo.css +3 -3
  110. package/src/components/composition.css +7 -7
  111. package/src/components/datepicker.css +6 -6
  112. package/src/components/datetimepicker.css +1 -1
  113. package/src/components/description-list.css +1 -1
  114. package/src/components/divider.css +1 -1
  115. package/src/components/drawer.css +1 -1
  116. package/src/components/file-upload.css +5 -5
  117. package/src/components/filterbar.css +7 -7
  118. package/src/components/form-grid.css +2 -2
  119. package/src/components/input-group.css +6 -6
  120. package/src/components/input.css +6 -6
  121. package/src/components/kbd.css +1 -1
  122. package/src/components/list.css +6 -6
  123. package/src/components/menu.css +3 -3
  124. package/src/components/modal.css +1 -1
  125. package/src/components/pagination.css +1 -1
  126. package/src/components/progress.css +1 -1
  127. package/src/components/rating.css +2 -2
  128. package/src/components/selection.css +4 -4
  129. package/src/components/skeleton.css +1 -1
  130. package/src/components/slider.css +1 -1
  131. package/src/components/spinner.css +1 -1
  132. package/src/components/states.css +1 -1
  133. package/src/components/stepper.css +2 -2
  134. package/src/components/table.css +15 -15
  135. package/src/components/tabs.css +3 -3
  136. package/src/components/timeline.css +1 -1
  137. package/src/components/timepicker.css +1 -1
  138. package/src/components/toast.css +3 -3
  139. package/src/components/tooltip.css +1 -1
  140. package/src/components/tree.css +2 -2
  141. package/tokens/breakpoints.d.ts +2 -2
  142. package/tokens/breakpoints.mjs +3 -3
  143. 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,7 +115,7 @@ is a no-op.
114
115
  # Shell & composition
115
116
 
116
117
  ## App shell — `.fdy-app`
117
- > **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).
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).
118
119
 
119
120
  The frame every application lives in. Do not hand-roll one from flexbox: the responsive sidebar,
120
121
  off-canvas drawer and backdrop are built in.
@@ -123,14 +124,14 @@ off-canvas drawer and backdrop are built in.
123
124
  `__content` `__topbar` `__navtoggle` `__title` `__main` `__backdrop`
124
125
  - Modifiers: `--nav-open` (mobile drawer open, ≤720px) · `--nav-collapsed` (collapse to zero width,
125
126
  ≥721px) · `--static` (embed the shell in a page instead of filling the viewport)
126
- - Also: `.fdy-skip` the skip-to-content link, first child of the shell.
127
- - **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
128
129
  is unchanged. It owns the toggle in both modes plus everything an overlay needs that hand-rolling
129
130
  reliably forgets: Escape, backdrop click, closing when a `.fdy-nav__item` is followed, focus moved
130
131
  into the panel on open and returned to `__navtoggle` on close, `inert` on `__content` while the
131
132
  overlay is up, and a Tab trap inside the panel. `aria-expanded` on `__navtoggle` answers "is the
132
133
  nav showing?" in both modes, so you never read the state classes yourself.
133
- - **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:
134
135
  collapsed at ≥721px (`width:0`) or off-canvas at ≤720px (`translateX(-100%)`). Both hide it from
135
136
  the eye; neither hides it from the keyboard, so without this a nav nobody can see still swallows
136
137
  Tab on the way to the page.
@@ -140,21 +141,21 @@ off-canvas drawer and backdrop are built in.
140
141
  - **`navOpen` is one idea, in both modes.** The typed wrappers expose "is the nav visible?" and the
141
142
  kit maps it: above the breakpoint a hidden nav is `--nav-collapsed`, below it a visible nav is
142
143
  `--nav-open`. Leave the prop unbound (`undefined` in Vue/React, `null` in Blazor) and the shell
143
- starts from the viewport a column on a wide screen, hidden on a narrow one. That default is why
144
+ starts from the viewport: a column on a wide screen, hidden on a narrow one. That default is why
144
145
  the prop is optional: a caller cannot express it as a single initial value before it knows the
145
146
  viewport.
146
147
  - **Binding your own state to the vanilla shell:** it emits a bubbling `fdy-app-nav` CustomEvent
147
- (`detail {visible}`) on every real change including the ones a viewport change causes, since a
148
+ (`detail {visible}`) on every real change, including the ones a viewport change causes, since a
148
149
  bound value that stayed `true` while the nav went off-canvas would be describing a panel nobody
149
150
  can see. Drive it with `FreedayAppShell.setVisible(root, visible)` rather than the class names;
150
151
  setting what is already set announces nothing, so a bound host cannot loop on its own echo.
151
152
  `FreedayAppShell.isVisible(root)` reads it back.
152
153
  - Skip the enhancer and nothing breaks: the classes still mean what they always meant and the
153
- behaviour is yours to write. `FreedayAppShell.init(root)` for markup mounted later hand it the
154
+ behaviour is yours to write. `FreedayAppShell.init(root)` for markup mounted later. Hand it the
154
155
  shell root itself, which is what the Blazor bridge does after its first render.
155
156
 
156
157
  **The nesting is fixed, not free-form:** `.fdy-app` is a flex **row** of `[__sidebar | __content]`,
157
- 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
158
159
  tall containing block. The brand goes in the **sidebar** (sized to match the topbar height), and
159
160
  `__main` already carries the page padding, so don't wrap your screen in another padded box.
160
161
 
@@ -187,15 +188,15 @@ tall containing block. The brand goes in the **sidebar** (sized to match the top
187
188
  Vertical section rhythm (`--space-8` between children). Assemble every screen from these; they
188
189
  carry the doctrine in markup.
189
190
 
190
- - `.fdy-page` the screen body
191
- - `.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
192
193
  left, `.fdy-page__actions` (**one** primary button) on the right
193
- - `.fdy-page-section` a region: `.fdy-page-section__head` + body, `--space-4` rhythm
194
- - `.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).
195
196
  It is `align-items:center`, so it is for **bare** controls. A `.fdy-field` with a *visible* label
196
- 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`
197
198
  (`align-items:flex-end`). In a toolbar, hide the label with `.fdy-visually-hidden`.
198
- - `.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**
199
200
  cards. `.fdy-stats--boxed` for one shared surface with dividers.
200
201
 
201
202
  ```html
@@ -230,7 +231,7 @@ carry the doctrine in markup.
230
231
  ```
231
232
 
232
233
  ## Type roles & text
233
- 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.
234
235
 
235
236
  | Class | Element | Use |
236
237
  |---|---|---|
@@ -246,11 +247,11 @@ One role per level of hierarchy — never re-use a card title for a page title.
246
247
 
247
248
  ## Utilities
248
249
  `.fdy-hidden` · `.fdy-visually-hidden` (screen-reader-only) · `.fdy-list-reset` (strip native
249
- 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) ·
250
251
  `.fdy-divider` (+`--vertical`) · `.fdy-kbd`.
251
252
 
252
253
  Responsive display: `.fdy-hide-below-sm|md|lg` · `.fdy-hide-above-sm|md|lg`.
253
- 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
254
255
  `import { breakpoints } from '@cahyo-dimas/freeday/breakpoints'`.
255
256
 
256
257
  **`breakpoints.nav` (721) is separate and is the one the shell uses.** `.fdy-app` switches the
@@ -258,7 +259,7 @@ sidebar from off-canvas drawer to static column at 721px, *not* at `md`. Any `ma
258
259
  utility variant that has to agree with the shell must use `nav`; using `md` leaves 721–959px broken
259
260
  (sidebar already static while your script still treats it as an overlay).
260
261
 
261
- 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
262
263
  `[data-density="compact"]` over inheriting custom properties, so one screen can be dense while the
263
264
  rest of the app is not. `data-density="comfortable"` is a real rule too, so the reverse also works:
264
265
  a compact root with one region opted back out (shared chrome that must match a sibling product).
@@ -268,13 +269,13 @@ a compact root with one region opted back out (shared chrome that must match a s
268
269
  # Actions
269
270
 
270
271
  ## Button — `.fdy-btn`
271
- **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.
272
273
 
273
274
  - Modifiers: `--ghost` `--danger` `--text` `--icon` (square, icon-only) `--sm` `--lg`
274
275
  - Parts: `.fdy-btn__icon` (wraps the `<svg>`) · `.fdy-btn-group` (segmented row, `role="group"` +
275
276
  `aria-label`)
276
277
  - **Toggle / segmented:** `aria-pressed="true"` gives a real pressed state (soft primary fill on
277
- `--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`
278
279
  is already the right attribute, and it is what turns `.fdy-btn-group` from a joined row into a
279
280
  complete segmented control.
280
281
  - A11y: `--icon` **requires** `aria-label`. Use `<button type="button">` unless it submits.
@@ -288,7 +289,7 @@ a compact root with one region opted back out (shared chrome that must match a s
288
289
  ```
289
290
 
290
291
  **Quiet destructive:** combine `--danger` with `--ghost` or `--text`. The ground stays quiet and only
291
- 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
292
293
  shares a screen with the one primary action; a solid Delete beside Save is a second primary in all
293
294
  but name.
294
295
 
@@ -330,7 +331,7 @@ native control, otherwise a `<div>` + explicitly associated label.
330
331
  - `.fdy-label` (+`--required`) · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`)
331
332
 
332
333
  **Grouped controls are a `<fieldset>`, not a new block.** Put `.fdy-field` on the fieldset and
333
- `.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:
334
335
 
335
336
  ```html
336
337
  <fieldset class="fdy-field">
@@ -342,7 +343,7 @@ native control, otherwise a `<div>` + explicitly associated label.
342
343
 
343
344
  **`.fdy-label--required` marks the label, not the accessibility tree.** The control already carries
344
345
  `required`; the asterisk is painted through `::after` with CSS alt text, so a screen reader never
345
- 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`
346
347
  cannot guarantee.
347
348
  - `[readonly]` is styled on input/textarea: full contrast, focusable, copyable.
348
349
 
@@ -360,8 +361,8 @@ cannot guarantee.
360
361
  </label>
361
362
  ```
362
363
 
363
- **Which `type` does `.fdy-input` cover?** Every text-like type `text` `email` `password` `tel`
364
- `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:
365
366
 
366
367
  | Type | What the UA still draws | What to do |
367
368
  |---|---|---|
@@ -386,7 +387,7 @@ Prefix/suffix addons around an input: text (`Rp`, `%`), a decorative icon
386
387
 
387
388
  ## Number field — `[data-fdy-number]`
388
389
  A number input with its increment/decrement affordance back, after `.fdy-input` removed the
389
- 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
390
391
  the shared border, focus ring and error promotion. Needs `freeday-number.js`.
391
392
 
392
393
  ```html
@@ -400,13 +401,13 @@ the shared border, focus ring and error promotion. Needs `freeday-number.js`.
400
401
  ```
401
402
 
402
403
  - **No custom event.** Stepping fires native bubbling `input` + `change` on the input, so `v-model`,
403
- `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.
404
405
  - `min` / `max` / `step` live on the **input**; the buttons never do the arithmetic themselves
405
406
  (`stepUp()`/`stepDown()` clamp for free) and go `disabled` at a bound, on a `disabled`/`readonly`
406
407
  field, and when `step="any"` (a stepper cannot express "no defined increment").
407
408
  - The buttons are **not tab stops** (`tabindex="-1"`): the input is already focusable and ↑/↓
408
409
  already step it, so extra stops would cost every keyboard user and buy nothing. Keep the
409
- `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.
410
411
  - `type="button"` is required. Inside a `<form>`, a bare `<button>` submits it.
411
412
 
412
413
  ## Checkbox · radio · switch
@@ -419,7 +420,7 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
419
420
  ```
420
421
 
421
422
  ## Select / combobox — `.fdy-combo`
422
- > **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).
423
424
 
424
425
  Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
425
426
 
@@ -444,12 +445,12 @@ Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
444
445
 
445
446
  **The selected tick is CSS.** `.fdy-combo__check` is an empty box in the markup; the glyph is painted
446
447
  by the stylesheet on `[aria-selected="true"]` with alt text, so it never enters the accessibility
447
- 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
448
449
  is carried by `aria-selected` alone, announced once, and `getByRole('option', { name: 'August' })`
449
450
  keeps matching after selection. Do not put a glyph in that span.
450
451
 
451
452
  ## Autocomplete — `.fdy-autocomplete`
452
- > **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).
453
454
 
454
455
  Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
455
456
 
@@ -458,7 +459,7 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
458
459
  `aria-controls` → the listbox `id`, `autocomplete="off"`.
459
460
 
460
461
  ## Cascade select — `.fdy-cascade`
461
- > **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).
462
463
 
463
464
  Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
464
465
  child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
@@ -479,7 +480,7 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
479
480
  ```
480
481
 
481
482
  ## Choose-from-list (CFL) — `data-fdy-cfl`
482
- > **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).
483
484
 
484
485
  A read-only field backed by master data: the button opens a searchable dialog, the picked row fills
485
486
  the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
@@ -490,7 +491,7 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
490
491
  - Inside: `.fdy-cfl__search` `__results` `__row` `__check` `__empty` `__count` `__actions`
491
492
  - Hooks: `data-fdy-cfl-search` (the search input), `data-fdy-cfl-empty`, `data-fdy-cfl-count`,
492
493
  `data-fdy-cfl-confirm` (multi only), `data-fdy-cfl-trigger`, `data-fdy-cfl-open`
493
- - 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.
494
495
  - **Multi-select in the typed wrappers** (Vue · React · Blazor): `multiple` ticks rows and commits
495
496
  them together on **Confirm** instead of committing the row that was clicked. Vue and React widen
496
497
  their model to an array (`Row[] | null`); Blazor takes a second pair, `Values` /
@@ -537,32 +538,32 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
537
538
  ```
538
539
 
539
540
  **`clearable` makes the value removable.** Without it a choose-from-list can be set but never unset,
540
- 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
541
542
  could not produce it. With it, a clear button appears beside the trigger whenever a row is picked
542
543
  and the field is editable; it emits `null` (Vue `update:modelValue`/`change`, React `onChange`,
543
544
  Blazor `ValueChanged`) and returns focus to the trigger. It is a second `.fdy-input-group__btn`, not
544
545
  a new class.
545
546
 
546
547
  ## Date picker — `data-fdy-datepicker`
547
- > **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).
548
549
 
549
550
  Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author an empty `<div>`**
550
- the enhancer builds everything.
551
+ The enhancer builds everything.
551
552
 
552
553
  - `.fdy-datepicker` (+`--error`) · `__trigger` `__value` (+`--placeholder`) `__icon` `__clear`
553
554
  `__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` (+`--months` `--years`)
554
555
  `__dow` `__day` `__month` `__year`
555
556
  - Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
556
557
  get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start.
557
- **Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor) use it in those stacks.
558
+ **Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor). Use it in those stacks.
558
559
  **Getting to a distant date.** The title is a **button**, and it drills up one level each press:
559
560
  day grid → 12-cell month grid (`.fdy-cal__grid--months` of `.fdy-cal__month`) → 12-cell year grid
560
- (`.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
561
562
  years, then pages of twelve years. Picking a year drops to that year's months, picking a month
562
563
  drops to its days: every level above the day grid is **navigation**, so nothing is committed until a
563
564
  day is selected. From the year grid the title steps back down to months.
564
565
 
565
- 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
566
567
  pages tile and a given year always sits in the same place. From August 2026 to March 2022 that is 7
567
568
  clicks; walking month by month is 53. To 1998 it is 8; walking is 336.
568
569
 
@@ -605,15 +606,15 @@ Click/Enter opens the file dialog; drop works too. Needs `freeday-upload.js`.
605
606
  - List: `.fdy-filelist` (+`--grid`) of `.fdy-file` (+`--success`, `--error`) · `__icon` `__meta`
606
607
  `__name` `__sub` `__progress` `__remove`
607
608
  - Attributes: `data-max-size` (bytes), `data-filelist="#id"`, `data-fdy-upload-simulate` (demo only
608
- 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)
609
610
  - A11y: the dropzone is `role="button" tabindex="0"` + `aria-label`.
610
611
 
611
- **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
612
613
  element. (The file list may be anywhere in the document; nothing is dispatched on the row, because a
613
614
  row in a sibling list would never bubble through the zone.)
614
615
 
615
616
  **The row is yours to drive.** `fdy-upload-add` carries `detail.row`, a small state machine over the
616
- 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
617
618
  transfer started; the kit never claims one it is not performing.
618
619
 
619
620
  | `detail.row` | State it renders |
@@ -627,15 +628,15 @@ transfer started; the kit never claims one it is not performing.
627
628
  | `.ready()` | back to **rest** (e.g. after a failed attempt the user will retry) |
628
629
  | `.el` | the row element |
629
630
 
630
- **If your request outlives the transfer, drive `waiting()`.** Server-side work after the last byte
631
- 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
632
633
  is read as a hang. `waiting()` is the state for it: the label says what the server is doing, and the
633
634
  bar stops claiming a percentage it no longer has.
634
635
 
635
636
  ```js
636
637
  zone.addEventListener('fdy-upload-add', (e) => {
637
638
  if (e.detail.rejected) return; // the kit already rendered the reason
638
- 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
639
640
  submitBtn.onclick = async () => {
640
641
  row.uploading();
641
642
  await send(file, (pct) => row.setProgress(pct));
@@ -648,7 +649,7 @@ zone.addEventListener('fdy-upload-remove', (e) => forget(e.detail.file)); // s
648
649
  ```
649
650
 
650
651
  **Bring your own row:** omit the file list entirely (no `data-filelist`, and no `.fdy-filelist`
651
- 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`
652
653
  still works, its element simply isn't attached. Note the fallback when `data-filelist` is absent is
653
654
  `parentNode.querySelector('.fdy-filelist')`, so a bare dropzone will adopt a list that happens to
654
655
  share its parent; give the dropzone its own container if you mean "no list".
@@ -675,7 +676,7 @@ A consistent filter row of `.fdy-field`s with a width rhythm (`--w-sm` · defaul
675
676
  `--w-xl` · `--w-2xl`), one `--w-grow` field (usually search) absorbing the slack, and
676
677
  `.fdy-filterbar__actions` pinned at the end. A control with **no label of its own** (a
677
678
  `.fdy-check`, `.fdy-switch`, `.fdy-radio`, or a lone `.fdy-btn`) is given `--control-h` so it
678
- 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
679
680
  labelled fields, which would otherwise drop a 20px checkbox below a 32px input's centre.
680
681
  `--actions-inline` keeps the actions on the control
681
682
  line. Zero JS. Composes with `.fdy-table-toolbar` on a shared baseline.
@@ -705,7 +706,7 @@ Linear multi-step flow: marker → check, one panel at a time, back/next. Needs
705
706
 
706
707
  **`__btn` is optional.** For a **read-only indicator** put `__marker` + `__label` straight in the
707
708
  `<li>`; for a **navigable** stepper wrap them in `<button class="fdy-step__btn">` (add `disabled`
708
- 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
709
710
  over the connector line, so omitting `__btn` cannot make the connector draw through the numbers.
710
711
 
711
712
  ```html
@@ -732,23 +733,23 @@ Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-tab
732
733
  </div>
733
734
  ```
734
735
 
735
- **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
736
737
  matrix, a timetable) or a wide one whose identity columns must stay visible. `.fdy-table--sticky`
737
738
  is the enabler: it separates the borders, because a collapsed border belongs to the table rather
738
739
  than the cell and scrolls out from under whatever you froze. Then ask for the axes you have:
739
740
 
740
- - **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
741
742
  wrapper (it scrolls both ways and is the scrollport the freeze resolves against; size it with
742
743
  `--fdy-table-frozen-h`, default `30rem`). It is a separate modifier because a table that only
743
744
  freezes columns scrolls with the **page**, and `top:0` against the page sticks the header under
744
- the viewport edge over the app's own top bar.
745
- - **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
746
747
  each frozen column its own `--fdy-freeze-left`: `0` for the first, then the summed widths of the
747
748
  frozen columns to its left. Mark the last one `.fdy-table__freeze--edge` so the reader can see
748
749
  where the frozen block ends. One frozen column needs no variable at all.
749
750
 
750
751
  The offsets are the caller's job because they depend on rendered column widths, which CSS cannot
751
- 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.
752
753
 
753
754
  ```html
754
755
  <div class="fdy-table-scroll fdy-table-scroll--frozen" style="--fdy-table-frozen-h:26rem">
@@ -769,7 +770,7 @@ sum — measure them once after render (and on resize) and set the variable.
769
770
  ```
770
771
 
771
772
  ## Data table — `.fdy-datatable`
772
- > **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.
773
774
 
774
775
  The interactive table: global search, sort, per-column filters, row selection + bulk bar,
775
776
  pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` + `data-fdy-table`
@@ -788,20 +789,24 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
788
789
  detail), `.fdy-table__state` (in-table empty/loading row)
789
790
  - Footer: `.fdy-table-footer` · `__info` (`data-fdy-table-info`) + optional `__size` (a label and a
790
791
  rows-per-page control) + `<nav class="fdy-pagination" data-fdy-table-pagination>`. The typed
791
- 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
792
793
  stylesheet reaches. In the raw path the control is app-authored markup (its options *are* the
793
794
  offer) carrying `data-fdy-table-page-size`; the enhancer only wires it, and listens for both
794
795
  `change` and the `fdy-change` a `.fdy-combo` emits, so either kind works.
795
796
  - Sort values: put the raw value in `data-sort-value` when the cell text is formatted.
796
- - **Language caveat:** every user-visible string the **vanilla enhancers** write is Indonesian by
797
- default the table's footer and bulk count (`Menampilkan 1–5 dari 7`, `N dipilih`), its pager and
798
- filter UI (`Sebelumnya`, `Berikutnya`, `Filter kolom`, `Berisi teks`, `Reset`, `Tutup`), and the
799
- same elsewhere (`Format tidak valid.`, `Tampilkan kata sandi`, `Menunggu server…`). The
800
- Vue/React/Blazor components are English throughout, so **an English app should use the typed
801
- wrapper**.
802
- - **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
803
808
  reads them through `textOf()`, so any of them can be replaced per element with
804
- **`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.
805
810
  (HTML lowercases attribute names, so a camelCase key like `filterText` is written
806
811
  `data-fdy-text-filter-text`; the run-together spelling still resolves.):
807
812
 
@@ -820,25 +825,25 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
820
825
  arrives overridable or not at all.
821
826
 
822
827
  **Testing note:** a column's filter button and the dialog it opens deliberately share one
823
- 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
824
829
  Playwright/Testing-Library suite that means `getByLabel('Filter Name')` resolves to two elements;
825
830
  reach for `getByRole('button', { name: 'Filter Name' })` instead.
826
831
 
827
832
  **Rows per page.** Pass `pageSizes` (Vue/React `:page-sizes="[10, 20, 50]"`, Blazor
828
833
  `PageSizes="…"`) and the footer grows a rows-per-page control between the range and the pager.
829
- 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
830
835
  at rather than dropping them on page 1.
831
836
 
832
- - **server mode** reported through `update:page` / `onPageChange` / `PageChanged`, the same event
837
+ - **server mode:** reported through `update:page` / `onPageChange` / `PageChanged`, the same event
833
838
  as a page click, carrying the new `size`. Read `size` to tell the two apart.
834
- - **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
835
840
  emits `update:pageSize` / `onPageSizeChange` / `PageSizeChanged` for a caller that wants to
836
841
  persist the choice. Changing the `pageSize` prop wins back.
837
842
 
838
- 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
839
844
  ninety-row list would remove the only way back to twenty.
840
845
 
841
- **Typed wrapper: `<FdyTableFooter>`** the footer alone (`page` in, `update:page` /
846
+ **Typed wrapper: `<FdyTableFooter>`.** The footer alone (`page` in, `update:page` /
842
847
  `onPageChange` / `PageChanged` out), for the one shape that cannot use the table's own: a
843
848
  **responsive** list, where a `.fdy-datatable` at `lg` and a `.fdy-list` below it are two renderings
844
849
  of one page of rows. A footer inside the table is inside the half a phone hides, so those screens
@@ -847,17 +852,17 @@ render it once, outside both, with `pager={false}` on the table.
847
852
  **Who draws the pager.** The table renders its own footer (range + pager) whenever there is more
848
853
  than one page. Two ways to take it over:
849
854
 
850
- - **client mode** pass `pageIndex` (with `pageSize`, without `page`) and drive it yourself;
851
- - **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
852
857
  your own control. In server mode this is the only way: the app already owns the page there, and
853
- was still being handed a second control. The typical shape is a responsive list a table at `lg`,
854
- `.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.
855
860
 
856
861
  Do not hide the footer with CSS. `display:none` works, but it reaches into a component's internals
857
862
  and breaks the moment the class changes.
858
863
 
859
864
  ## Pagination — `.fdy-pagination`
860
- 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
861
866
  `__list` / `__link` / `__ellipsis` elements do all the styling, and the data table targets
862
867
  `data-fdy-table-pagination`. Keep it on the wrapper anyway, for consistency with the rest of the kit.
863
868
 
@@ -872,7 +877,7 @@ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__ic
872
877
  replaces.
873
878
 
874
879
  ## Charts — `data-fdy-chart`
875
- > **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.
876
881
 
877
882
  Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`; call
878
883
  `FreedayChart.update(el)` after changing data (or use `FdyChart` in Vue/React/Blazor).
@@ -881,27 +886,27 @@ Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`
881
886
  - Roots: `.fdy-sparkline` · `.fdy-bars` · `.fdy-donut` · `.fdy-chart-xy` (built for line/area/bar)
882
887
  - Data: `data-values="1,2,3"` (single series) or `data-series='[{"label":"A","values":[…]}]'`
883
888
  (multi), `data-labels="Jan,Feb"`, `data-fdy-stacked`, `data-fdy-center` (donut)
884
- - 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
885
890
  names **or** `chart-1`…`chart-8` slots to pin a category's colour. Multi-series defaults to the
886
891
  validated categorical palette `--chart-1`…`--chart-8`.
887
892
  - Format: `data-fdy-format="number|percent|currency"`; legend `data-fdy-legend` (`none` to drop);
888
893
  axes `data-fdy-axes`
889
- - 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
890
895
  root height fights `aspect-ratio` instead of setting it): `.fdy-chart-xy__plot` is `width:100%`
891
896
  + `aspect-ratio:16/9` + `min-height:8rem`; `.fdy-bars` is a fixed `height:9rem`; `.fdy-sparkline`
892
897
  is an inline-block `8rem × 2.25rem`.
893
898
  - Axis type: `--fdy-chart-tick-size` (default `var(--text-xs)`) sets the y-tick and x-label size on
894
- `.fdy-chart-xy`. It is a real CSS size a cartesian chart's `viewBox` is measured to its plot, so
895
- 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
896
901
  many x-labels fit. Set the token rather than `font-size` on `__tick`/`__xlabel`, or the gutter and
897
902
  the autoskip will still be computed from the old size.
898
903
  - A11y: every chart is `role="img"` + an **author-supplied** `aria-label`, and that label is the
899
- *entire* text alternative write one that carries the numbers, not just the shape. The renderer
900
- marks everything it draws `aria-hidden="true"` legend, bar values and labels, donut centre, the
901
- 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
902
907
  **not** have done that: it is Children Presentational per ARIA, but browsers keep the subtree in
903
908
  the accessibility tree anyway, so the kit hides it rather than relying on the spec. That hiding
904
- 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
905
910
  exposed, because an image with neither a name nor any text is worse than one that leaks its
906
911
  legend. Any `<table>`/text placed inside the element is **pre-render only** and is replaced on
907
912
  render. Good: `aria-label="Monthly posting trend: Posted 62% → 88%, Draft 24% → 16%"`.
@@ -911,7 +916,7 @@ Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`
911
916
  `__center` `__hit`; `.fdy-sparkline__line` `__area` `__dot`.
912
917
 
913
918
  ## Tree view — `.fdy-tree`
914
- 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>`,
915
920
  `.fdy-tree__leaf` on a leaf `<li>`, plus `__chevron` and `__icon` svgs inside `<summary>`.
916
921
  `.fdy-tree--checkbox` + `data-fdy-tree` (needs `freeday-tree.js`) adds cascading selection:
917
922
  `.fdy-tree__check` on each `<input class="fdy-checkbox">`; checking a branch checks its children,
@@ -919,19 +924,19 @@ a partial set makes the branch indeterminate. Checking never toggles expansion.
919
924
 
920
925
  ## List (flat rows) — `.fdy-list`
921
926
  The **flat** row container: one bordered surface, hairline dividers, **no shadow**. This is what a
922
- 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
923
928
  `--shadow-lift` and turns ten rows into ten floating objects.
924
929
 
925
930
  - `.fdy-list` on a `<ul>`/`<ol>` (list-style is reset for you) or a `<div>`
926
- - `.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
927
932
  control (render it as a real `<button>`/`<a>`; the UA box is reset without losing the list surface)
928
933
  - Row internals: `.fdy-list__main` (truncating stack) → `.fdy-list__title` + `.fdy-list__meta`, and
929
934
  `.fdy-list__aside` pinned right
930
- - **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>`/
931
936
  `<div>`) dims the row and withdraws the hover tint and the pointer cursor, same as every other
932
937
  control in the kit. Do not hand-roll it: an undimmed row that still lights up under the pointer
933
938
  reads as clickable while it is refusing input.
934
- - 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.
935
940
 
936
941
  ```html
937
942
  <ul class="fdy-list">
@@ -982,11 +987,11 @@ Every field is optional; it returns the toast element. Classes (rendered for you
982
987
  `__body` `__title` `__text` `__close`.
983
988
 
984
989
  **Above modals, and not by z-index.** The region is a `popover` and re-enters the **top layer** as
985
- 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
986
991
  a toast of yours is hidden behind something, a bigger `z-index` is not the fix; check whether that
987
992
  something is in the top layer too. Where the Popover API is unavailable (before Chrome 114 /
988
993
  Safari 17 / Firefox 125) the region falls back to a fixed block at `z-index: 200`, which cannot
989
- 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.
990
995
 
991
996
  ## Tooltip — `.fdy-tooltip`
992
997
  Hover/focus only, never the sole carrier of information. Wrap in `.fdy-tooltip-wrap`; the trigger
@@ -1006,7 +1011,7 @@ Size-matched placeholders so nothing shifts when data lands: `--title` `--text`
1006
1011
  # Navigation
1007
1012
 
1008
1013
  ## Nav (menu) — `.fdy-nav`
1009
- 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
1010
1015
  `__icon` / `__label` / `__badge`; the current one gets `aria-current="page"`.
1011
1016
  `--flat` drops the surface. Nested groups are native `<details>`:
1012
1017
 
@@ -1030,17 +1035,17 @@ Navigation links — **vertical by default** (the app shell sidebar), horizontal
1030
1035
  </header>
1031
1036
  ```
1032
1037
 
1033
- > **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
1034
1039
  > `aria-selected` (invalid ARIA on an anchor) and never `role="tab"`. Use `.fdy-nav--horizontal`
1035
1040
  > for an application's **primary** navigation. For **routed sub-navigation** that should look like
1036
1041
  > tabs (`/settings/profile` · `/settings/billing`), put `.fdy-tabs__list` / `.fdy-tabs__tab` on
1037
- > 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
1038
1043
  > or `freeday-tabs.js`, which promise a keyboard contract routes do not have.
1039
1044
 
1040
1045
  ## App bar — `.fdy-appbar`
1041
1046
  Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--sticky`
1042
1047
  `--elevated` `--dense` `--primary` (on-colour controls). Parts `__brand` `__spacer` `__actions`.
1043
- 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
1044
1049
  for primary navigation (see Nav above).
1045
1050
 
1046
1051
  ## Breadcrumb — `.fdy-breadcrumb`
@@ -1051,7 +1056,7 @@ for primary navigation (see Nav above).
1051
1056
  ## Tabs — `.fdy-tabs`
1052
1057
  APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
1053
1058
  `.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
1054
- 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.
1055
1060
 
1056
1061
  ```html
1057
1062
  <div class="fdy-tabs" data-fdy-tabs>
@@ -1069,13 +1074,13 @@ so the same look serves routed sub-navigation built from plain links — see the
1069
1074
  # Overlays
1070
1075
 
1071
1076
  ## Modal — `.fdy-modal`
1072
- > **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).
1073
1078
 
1074
1079
  Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
1075
1080
  `--wide`; `--cfl` for the choose-from-list dialog.
1076
1081
 
1077
1082
  **It is in the TOP LAYER.** `showModal()` paints the dialog above every stacking context on the
1078
- 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
1079
1084
  `popover`, or another modal) can. It also makes the rest of the document inert, which means
1080
1085
  `elementFromPoint` outside the dialog returns the dialog: do not use a hit test to check what is
1081
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`
@@ -1083,34 +1088,34 @@ painted on top of a modal, because it will tell you the dialog is, even when it
1083
1088
  `data-close` on any button that should close it.
1084
1089
 
1085
1090
  **Stacking two overlays is supported.** Modal and drawer are both `<dialog>` opened with
1086
- `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
1087
1092
  modal opened from inside an open drawer paints **above** it, and neither needs a `z-index` for
1088
1093
  that. Escape closes the topmost first, so cancelling returns to the still-open drawer; a second
1089
1094
  Escape then closes the drawer. Closing them out of order is fine too: closing the drawer while the
1090
1095
  modal is up leaves the modal open, painted and dismissible.
1091
1096
 
1092
1097
  One caveat, and it is the browser's rather than the kit's: **the second overlay's Escape can only
1093
- be refused if both were opened by a real gesture.** A `showModal()` that runs from script a timer,
1094
- 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
1095
1100
  arrives **non-cancelable**, so `preventDefault()` on it is ignored and an "unsaved changes, stay
1096
1101
  open" guard silently stops working. Routing Escape through app state instead, which is what the
1097
1102
  typed wrappers do, is unaffected: the dialog closes and your state follows it.
1098
1103
 
1099
1104
  *Correction to 1.51.1, which said such a dialog has its close watcher grouped with the one below so
1100
1105
  that a single Escape closes both.* That happens only on a page that has received **no user input at
1101
- 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
1102
1107
  independent. Open overlays with real clicks in a test, or a stacking assertion fails for a reason
1103
1108
  that is not the kit. Both rules measured on Chromium 133 and 151, and pinned as a matrix in
1104
1109
  `browser/overlay-stack.mjs`.
1105
1110
 
1106
1111
  ## Drawer — `.fdy-drawer`
1107
- > **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).
1108
1113
 
1109
- 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`
1110
1115
  `__title` `__body` `__footer` `__close`. Open it from any
1111
1116
  `<button data-fdy-drawer="<dialog id>">`. Needs `freeday-drawer.js`.
1112
1117
 
1113
- 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
1114
1119
  opening a modal over an open drawer, which is supported and described under
1115
1120
  [Modal → *Stacking two overlays*](#modal--fdy-modal). It slides in on `transform` and never animates
1116
1121
  opacity: wait on its position, not on `opacity`, if you need to know when it has arrived.
@@ -1132,25 +1137,25 @@ is `role="group" aria-roledescription="slide"`.
1132
1137
  ## Card — `.fdy-card`
1133
1138
  Parts `__body` `__title` `__desc` `__footer`. Modifiers:
1134
1139
 
1135
- - `--elevated` raise it (use sparingly; most surfaces are flat)
1136
- - `--interactive` cursor + hover-lift for a card that *has* a click handler. **Presentational
1137
- 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
1138
1143
  know whether a handler exists, so the modifier without a control (a card that looks clickable and
1139
1144
  swallows every click) and a control without the modifier both fail silently. Pair it with a real
1140
- control normally the stretched target below.
1141
- - `--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
1142
1147
  the UA button box **without** losing the card surface/border
1143
- - `.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
1144
1149
  `<button>` or `<a>`.
1145
- - **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
1146
1151
  cursor and the `--interactive` hover-lift.
1147
1152
  - The card is `position:relative`, so a badge or ribbon you absolutely position inside it anchors
1148
- 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*.
1149
1154
 
1150
1155
  ### One card, one primary action, one escape hatch
1151
1156
 
1152
1157
  A card with **two** actions cannot be `--button`: interactive content nested in a `<button>` is
1153
- 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,
1154
1159
  while it stays a real `<button>`/`<a>` with real keyboard semantics.
1155
1160
 
1156
1161
  ```html
@@ -1167,34 +1172,32 @@ while it stays a real `<button>`/`<a>` with real keyboard semantics.
1167
1172
  ```
1168
1173
 
1169
1174
  - **The escape hatch needs nothing.** Every focusable element in a card that holds a stretched
1170
- 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
1171
1176
  a silent failure is what this pattern exists to remove. The `position` that raising needs is only a
1172
- **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)
1173
1178
  keeps its own `position:absolute` and is raised anyway.
1174
1179
  - **Name the stretched button for what the whole card does** ("Open workspace Alpha", not "Open").
1175
1180
  Its label is the accessible name of the entire hit area.
1176
- - **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
1177
1182
  overlap, and which one that is depends on source order.
1178
1183
  - Text under the overlay is no longer selectable; that is the cost of the pattern, not a bug.
1179
1184
  - **Cards and list rows.** The overlay anchors to the nearest *positioned* ancestor, and both hosts
1180
1185
  supply one: `.fdy-card` always, `.fdy-list__row` when it contains a stretched target. Everything
1181
- 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,
1182
1187
  make the element `position:relative` yourself, or the overlay resolves against whatever is
1183
1188
  positioned further up (in a list that used to be `.fdy-list` itself, so every row's target covered
1184
- 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).
1185
1190
  A row whose *whole* surface is one control is still better as `.fdy-list__row--button`.
1186
1191
  - `.fdy-btn` nudges itself on `:hover`/`:active`, and a transformed element becomes the containing
1187
- 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
1188
1193
  **re-anchors to the button mid-gesture** and the click never lands. `--stretch` neutralises those
1189
1194
  transforms and moves the press feedback to the card. This is why the pattern is shipped rather
1190
1195
  than documented.
1191
1196
 
1192
1197
  ## Badge — `.fdy-badge`
1193
- Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only
1194
- the text carries the meaning.
1198
+ Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only: the text carries the meaning.
1195
1199
 
1196
- `--tone-1`…`--tone-8` for a status vocabulary larger than the semantic palette. Semantics first
1197
- 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
1198
1201
  the rest, so two states a workflow distinguishes never share a look. Map each status to a FIXED
1199
1202
  tone, never to a hash of the string: a status is a closed set, and a hash reassigns colours the
1200
1203
  moment the set grows.
@@ -1207,19 +1210,19 @@ Put the count in the wrapper's `aria-label` and mark the overlay `aria-hidden="t
1207
1210
  ## Avatar — `.fdy-avatar`
1208
1211
  Initials or image. Sizes `--sm` / default / `--lg`; `.fdy-avatar-group` stacks them (label the
1209
1212
  group). `--tone-1`…`--tone-8` are decorative tints (WCAG AA in both themes) to distinguish
1210
- 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.
1211
1214
 
1212
1215
  Sizes: `--xs` 1.5rem · *(base)* 2.5rem · `--sm` 2rem · `--lg` 3.5rem. Use **`--xs` inside a control**
1213
- `.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
1214
1217
  ghost border cuts across the circle.
1215
1218
 
1216
1219
  ## Chip — `.fdy-chip`
1217
1220
  Three roles inside a `.fdy-chips` row:
1218
1221
 
1219
- - **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
1220
1223
  `.fdy-chip__remove` (`aria-label` says what it removes)
1221
- - **choice** `<button class="fdy-chip fdy-chip--choice" aria-pressed>` toggle
1222
- - **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
1223
1226
 
1224
1227
  Interactive chips are managed by `freeday-chip.js`: wrap the row in `data-fdy-chips` (+
1225
1228
  `data-single` for pick-one, `data-label` for the group name), give each `data-value`.
@@ -1233,5 +1236,5 @@ by the accompanying text, not the marker colour alone.
1233
1236
  ---
1234
1237
 
1235
1238
  *Anything not listed here is not part of the public surface. If a screen needs a primitive that
1236
- 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
1237
1240
  locally.*