@cahyo-dimas/freeday 1.54.0 → 2.1.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 (147) hide show
  1. package/CHANGELOG.md +713 -545
  2. package/COMPONENTS.md +417 -139
  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/cfl-value.d.ts +11 -0
  26. package/adapters/core/cfl-value.js +31 -0
  27. package/adapters/core/table-model.d.ts +2 -2
  28. package/adapters/core/table-model.js +8 -8
  29. package/adapters/react/components/FdyAppShell.tsx +5 -5
  30. package/adapters/react/components/FdyAutocomplete.tsx +1 -1
  31. package/adapters/react/components/FdyCascade.tsx +1 -1
  32. package/adapters/react/components/FdyCfl.tsx +11 -11
  33. package/adapters/react/components/FdyDateRange.tsx +2 -2
  34. package/adapters/react/components/FdyDatepicker.tsx +5 -5
  35. package/adapters/react/components/FdyDrawer.tsx +1 -1
  36. package/adapters/react/components/FdyModal.tsx +2 -2
  37. package/adapters/react/components/FdyTable.tsx +7 -7
  38. package/adapters/react/components/FdyTableFilter.tsx +1 -1
  39. package/adapters/react/components/FdyTableFooter.tsx +5 -5
  40. package/adapters/react/index.d.ts +4 -1
  41. package/adapters/react/index.js +1 -0
  42. package/adapters/react/useFreeday.js +3 -3
  43. package/adapters/vue/components/FdyAppShell.vue +5 -5
  44. package/adapters/vue/components/FdyAutocomplete.vue +1 -1
  45. package/adapters/vue/components/FdyCascade.vue +1 -1
  46. package/adapters/vue/components/FdyCfl.vue +8 -8
  47. package/adapters/vue/components/FdyChart.vue +2 -2
  48. package/adapters/vue/components/FdyCombo.vue +1 -1
  49. package/adapters/vue/components/FdyDateRange.vue +2 -2
  50. package/adapters/vue/components/FdyDatepicker.vue +5 -5
  51. package/adapters/vue/components/FdyDrawer.vue +3 -3
  52. package/adapters/vue/components/FdyModal.vue +3 -3
  53. package/adapters/vue/components/FdyTable.vue +7 -7
  54. package/adapters/vue/components/FdyTableFilter.vue +1 -1
  55. package/adapters/vue/components/FdyTableFooter.vue +5 -5
  56. package/adapters/vue/index.d.ts +5 -2
  57. package/adapters/vue/index.js +1 -0
  58. package/adapters/vue/useFreeday.js +3 -3
  59. package/dist/asset.d.ts +3 -3
  60. package/dist/freeday-app-shell.js +9 -9
  61. package/dist/freeday-autocomplete.js +2 -2
  62. package/dist/freeday-breakpoint.js +1 -1
  63. package/dist/freeday-carousel.js +8 -7
  64. package/dist/freeday-cascade.js +10 -9
  65. package/dist/freeday-cfl.js +12 -11
  66. package/dist/freeday-chart.js +11 -11
  67. package/dist/freeday-chip.js +3 -3
  68. package/dist/freeday-datepicker.js +11 -7
  69. package/dist/freeday-datetime.js +5 -5
  70. package/dist/freeday-drawer.js +1 -1
  71. package/dist/freeday-form.js +17 -16
  72. package/dist/freeday-mask.js +9 -8
  73. package/dist/freeday-menu.js +1 -1
  74. package/dist/freeday-number.js +3 -3
  75. package/dist/freeday-popover.js +3 -3
  76. package/dist/freeday-rating.js +2 -2
  77. package/dist/freeday-select.js +5 -5
  78. package/dist/freeday-slider.js +2 -2
  79. package/dist/freeday-stepper.js +10 -9
  80. package/dist/freeday-table.js +18 -17
  81. package/dist/freeday-tabs.js +1 -1
  82. package/dist/freeday-timepicker.js +2 -2
  83. package/dist/freeday-toast.js +11 -10
  84. package/dist/freeday-tree.js +3 -3
  85. package/dist/freeday-upload.js +28 -27
  86. package/dist/freeday.bundle.css +176 -176
  87. package/dist/freeday.css +170 -170
  88. package/dist/freeday.d.ts +2 -2
  89. package/dist/freeday.js +189 -176
  90. package/dist/freeday.tokens.css +5 -5
  91. package/docs/agent-onboarding.md +49 -46
  92. package/docs/getting-started.md +49 -40
  93. package/docs/integrations.md +18 -18
  94. package/docs/reference-screen.html +14 -14
  95. package/package.json +4 -3
  96. package/src/base.css +6 -6
  97. package/src/components/accordion.css +4 -4
  98. package/src/components/alert.css +1 -1
  99. package/src/components/app-shell.css +10 -10
  100. package/src/components/appbar.css +3 -3
  101. package/src/components/autocomplete.css +1 -1
  102. package/src/components/avatar.css +3 -3
  103. package/src/components/badge.css +5 -5
  104. package/src/components/breadcrumb.css +1 -1
  105. package/src/components/breakpoints.css +4 -4
  106. package/src/components/button.css +13 -13
  107. package/src/components/card.css +8 -8
  108. package/src/components/carousel.css +2 -2
  109. package/src/components/cascade.css +2 -2
  110. package/src/components/cfl.css +4 -4
  111. package/src/components/chart.css +3 -3
  112. package/src/components/chip.css +2 -2
  113. package/src/components/combo.css +3 -3
  114. package/src/components/composition.css +7 -7
  115. package/src/components/datepicker.css +6 -6
  116. package/src/components/datetimepicker.css +1 -1
  117. package/src/components/description-list.css +1 -1
  118. package/src/components/divider.css +1 -1
  119. package/src/components/drawer.css +1 -1
  120. package/src/components/file-upload.css +5 -5
  121. package/src/components/filterbar.css +7 -7
  122. package/src/components/form-grid.css +2 -2
  123. package/src/components/input-group.css +6 -6
  124. package/src/components/input.css +6 -6
  125. package/src/components/kbd.css +1 -1
  126. package/src/components/list.css +6 -6
  127. package/src/components/menu.css +3 -3
  128. package/src/components/modal.css +1 -1
  129. package/src/components/pagination.css +1 -1
  130. package/src/components/progress.css +1 -1
  131. package/src/components/rating.css +2 -2
  132. package/src/components/selection.css +4 -4
  133. package/src/components/skeleton.css +1 -1
  134. package/src/components/slider.css +1 -1
  135. package/src/components/spinner.css +1 -1
  136. package/src/components/states.css +1 -1
  137. package/src/components/stepper.css +2 -2
  138. package/src/components/table.css +15 -15
  139. package/src/components/tabs.css +3 -3
  140. package/src/components/timeline.css +1 -1
  141. package/src/components/timepicker.css +1 -1
  142. package/src/components/toast.css +3 -3
  143. package/src/components/tooltip.css +1 -1
  144. package/src/components/tree.css +2 -2
  145. package/tokens/breakpoints.d.ts +2 -2
  146. package/tokens/breakpoints.mjs +3 -3
  147. package/tokens/tokens.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,27 +3,195 @@
3
3
  Semua perubahan penting dicatat di sini. Format longgar mengikuti
4
4
  [Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
5
5
 
6
- ## [1.54.0] 2026-08-25
6
+ **Kebijakan tipe (sejak 2.1.0).** Tipe publik yang **melebar** dihitung breaking meski runtime tak
7
+ berubah, karena yang gagal adalah build konsumen: `vue-tsc`/`tsc` menolak handler yang kemarin
8
+ benar. Perubahan seperti itu ditulis di bawah `### Changed: BREAKING (types)` — nama prop, tipe
9
+ lama → tipe baru, dan cara menyempitkannya — bukan di bawah `### Added`, betapapun aditifnya dari
10
+ sisi kit.
11
+
12
+ ## [2.1.0] - 2026-08-25
13
+ ### Added
14
+ - **`singleRow()`, exported from `@cahyo-dimas/freeday/vue` and `/react`** (#045). `FdyCfl` emits
15
+ `Row | Row[] | null` whatever its props are: the array is reachable only under `multiple`, the
16
+ null only under `clearable`, and the type cannot say which. So every single-select screen wrote
17
+ its own narrowing, and each one differently — a cast (which hides the day somebody adds
18
+ `clearable`), a silent ignore, or a throw. One implementation now lives in
19
+ `adapters/core/cfl-value.js` and both adapters re-export it: it returns `Row | null`, and
20
+ **throws** a `TypeError` on an array instead of keeping its first row, because quietly dropping
21
+ the rest is how a `multiple` added later goes unnoticed. Blazor never needed it — multi-select
22
+ there is a separate `Values` / `ValuesChanged` pair.
23
+ ```ts
24
+ import { singleRow } from '@cahyo-dimas/freeday/vue'; // or '@cahyo-dimas/freeday/react'
25
+ // (value: CflRow | CflRow[] | null) => CflRow | null
26
+ const onUpdate = (value: CflRow | CflRow[] | null): void => { picked.value = singleRow(value); };
27
+ ```
28
+ ### Changed: BREAKING (types)
29
+ - **A retroactive entry for 1.29.0 and 1.42.0** (#045) — nothing changed in this release; this is
30
+ the record those two are missing. Both widened `FdyCfl`'s emitted type — `Row` → `Row | null`
31
+ (1.29.0, with `clearable`) → `Row | Row[] | null` (1.42.0, with `multiple`) — and both shipped
32
+ under **Added**, which is honest from the kit's side and useless from the consumer's: the
33
+ combined effect is `vue-tsc` failing in a handler nobody touched. An app upgrading from before
34
+ 1.29.0 meets both at once, which is exactly how it was reported. Migration is `singleRow()`
35
+ above. The policy that should stop a third instance is at the top of this file.
36
+ ### Added: guards
37
+ - **The typed column contract is now pinned in both directions** (#040): `test/docs.test.mjs`
38
+ asserts the field list documented in `COMPONENTS.md` matches `FdyTableColumn` in
39
+ `adapters/core/table-model.d.ts` — a field in the type but not the docs fails, and so does a
40
+ field in the docs that the type does not have. Verified by mutation (drop `labelHidden` from the
41
+ table → red; invent a `sticky` row → red), since a guard nobody has watched fail is only a
42
+ green tick.
43
+ - `test/cfl-value.test.mjs`: three tests for `singleRow` (row passes through, `null`/`undefined`
44
+ become null, an array throws).
45
+ - **Every typed wrapper's props are pinned to their documentation** (`NEXT-UP.md` #11). One guard
46
+ reads all twelve `### Props — <FdyX>` tables and compares them with the Vue and React
47
+ declarations: a prop missing from a table fails, a documented prop neither adapter declares
48
+ fails, and a wrapper exported with no table at all fails. Mutation-verified in all three
49
+ directions, including the case that found a bug in the guard itself — a hyphenated prop name
50
+ (`aria-label`) was invisible to the first version of its parser.
51
+ ### Docs
52
+ - **`COMPONENTS.md` finally states the typed column contract** (#040). §Data table gained
53
+ "Columns — the typed `FdyTableColumn<T>`": all ten fields with their types, the Blazor spelling
54
+ (`PascalCase`, `required` Key/Label, the extra `Cell` slot), and the row-controls example.
55
+ `agent-onboarding.md` tells every consuming agent this file is closed — "if a class is not in
56
+ that file, it does not exist" — while three of the four stacks build a table only through
57
+ `columns`, whose shape appeared nowhere in it. One reporting app shipped `label: ''` on its
58
+ actions column as a result: a `<th>` announced as nothing.
59
+ - **Nine enhancers still claimed to be Indonesian.** The comment above every `TEXT` table read
60
+ "User-facing strings. Indonesian by default" — written before 2.0.0 flipped them all to English,
61
+ and shipped in `dist/`, where it is the first thing a consumer reading the source sees. It now
62
+ says English, names `data-fdy-text-<key>` as the override, and gives an Indonesian app on the
63
+ raw path as the example of who overrides.
64
+ - **The two docs a consumer actually reads on upgrade now mention the language.** 2.0.0's breaking
65
+ change was documented in `COMPONENTS.md` and this file, but neither `docs/getting-started.md` nor
66
+ the block projects paste into their agent instructions
67
+ (`docs/agent-onboarding.md`) said a word about it. Both now do, with `data-fdy-text-<key>` as the
68
+ whole migration and `<html lang>` for date names.
69
+ - **`HANDOFF.md` described a release that had already happened.** It stated npm was on 1.53.0 with
70
+ 1.54.0 and 2.0.0 sitting unpushed; npm had been on 2.0.0 since the morning, with the tag, the
71
+ branch and `origin/main` all on the same commit. The file that warns about stale snapshots was
72
+ the stale snapshot. Corrected, with the three commands that check it.
73
+ - **`CLAUDE.md` claimed 44 components**, a number matching nothing countable. Now 48 stylesheets in
74
+ `src/components/` and 26 enhancers in `src/freeday-*.js`, both of which anyone can re-count.
75
+ - **The typed props of all twelve wrappers are now in `COMPONENTS.md`** (`NEXT-UP.md` #11, the
76
+ general form of #040). A `### Props` table per wrapper: Vue and React names together, what each
77
+ does, and the Blazor differences stated rather than implied. Writing them measured something
78
+ nobody had: **four Blazor pickers are far thinner than their JS twins** — `FdyDatepicker` takes 6
79
+ parameters against 21 Vue props, `FdyAutocomplete` 6 against 11, `FdyCascade` 8 against 12, and
80
+ `FdyCombo` has no `Describedby`. Those wrappers render a seed element and do not splat unmatched
81
+ attributes, so `disabled`, `readonly`, `invalid`, the a11y ids, `clearable` and the datepicker's
82
+ ten navigation labels cannot be reached from a Blazor page at all. It runs the other way too:
83
+ `CloseLabel` exists only in Blazor, so the modal and drawer × is fixed at `Close` in Vue and
84
+ React. Filed as `NEXT-UP.md` #12 with its trigger; documented here as fact in the meantime,
85
+ because a consumer meeting it should find it written down rather than discover it.
86
+ - **`FdyAppShell` was missing from every list of the typed wrappers but the two READMEs.** It
87
+ shipped as the eleventh in 1.54.0 and `COMPONENTS.md`, `getting-started.md`, `agent-onboarding.md`
88
+ and `integrations.md` went on naming ten for a whole major version — `agent-onboarding.md`
89
+ saying both "ten" and "eleven" about the same set, in the same file, and `getting-started.md`
90
+ carrying a section heading that still listed the seven of some earlier release. All corrected,
91
+ and a guard now reads any run of six or more wrapper names as an enumeration and fails if it is
92
+ incomplete. It found two of these itself, one of them a list this release had already touched.
93
+
94
+ ## [2.0.0] - 2026-08-25
95
+ ### Changed: BREAKING
96
+ - **The vanilla enhancers now write English by default** (`NEXT-UP.md` #6, owner's decision). Every
97
+ user-facing string they render, 39 of them across 9 enhancers, was Indonesian while the typed
98
+ wrappers were English, so an app that touched both paths read as two products. A Blazor app
99
+ touches both **by construction**: `FreedayBlazor.initAll` runs every enhancer. The report that
100
+ opened this was an English back-office finding `aria-label="Filter kolom"` on its own table.
101
+ Examples: `Menampilkan 1–5 dari 7` → `Showing 1–5 of 7`, `{n} dipilih` → `{n} selected`,
102
+ `Sebelumnya`/`Berikutnya` → `Previous`/`Next`, `Wajib diisi.` → `Required.`,
103
+ `Tampilkan kata sandi` → `Show password`, `Menunggu server…` → `Waiting for the server…`.
104
+ - **The datepicker's locale fallback follows.** `document.documentElement.lang || 'id'` is now
105
+ `|| 'en'`. This is the string nobody would have found by grepping for prose: it decides month and
106
+ weekday names through `Intl`, so leaving it would have produced English labels around Indonesian
107
+ months, which is the mixed interface this release exists to remove. The page's own `lang` still
108
+ wins, which makes `<html lang="id">` the whole migration for dates.
109
+ - **Migrating an Indonesian app** costs no fork: every string was already overridable per element
110
+ with `data-fdy-text-<key>` (kebab-cased), and `data-fdy-msg-*` for validation messages. That hatch
111
+ shipped in 1.39.0, so the half of `NEXT-UP.md` #6 written as "build an override hook" was already
112
+ done, and only the default was ever in question. `COMPONENTS.md` §Language documents it.
113
+ - **Called 2.0.0 rather than 1.55.0 on purpose.** No API changed, but every raw-path screen renders
114
+ different words, and a major version is the only signal npm gives a consumer that an upgrade needs
115
+ looking at. Hiding that behind a minor is the same class of quiet mismatch the last four releases
116
+ have been about.
117
+ ### Changed
118
+ - **Seven messages were translated correctly and still read like a machine.** The flip to English
119
+ produced complete, formal sentences where a product writes short ones: `Value is too small.` /
120
+ `Value is too large.` are now `Too small.` / `Too large.` (matching `Too short.` / `Too long.`
121
+ beside them), `Values do not match.` is `Values don’t match.`, `Does not match the expected
122
+ format.` is `Doesn’t match the required format.`, `Not a valid increment.` drops the jargon for
123
+ `Not a valid step.`, and upload's `File type not supported.` / `Larger than the {max} limit.` are
124
+ `Unsupported file type.` / `Over the {max} limit.` Contractions carry most of it, written with a
125
+ typographic apostrophe so the string needs no escaping.
126
+ - **The donut legend separator is `·`, not an em dash.** It rendered `posted — 76%`; the kit uses a
127
+ middot everywhere else it separates two short things, and it is the one place the dash was in
128
+ front of a reader rather than in a comment. The example app's choose-from-list display line
129
+ (`code — name`) follows.
130
+
131
+ ### Added: guards
132
+ - The English guard now covers `src/freeday-*.js`, not just the typed adapters, so the promise is
133
+ enforced on every path instead of stated on one. Verified by reverting a single string: it fails.
134
+ - **And a word list is not enough, which this release proved twice.** The list missed `Lanjut` and
135
+ `Selesai` in the stepper and `Mengunggah…` in upload, and its own comment already warned that
136
+ `pencarian` hides behind `\bcari\b`. So the complete vocabulary is now pinned: all 39 strings,
137
+ asserted whole, so changing one has to be deliberate and visible in a diff.
138
+ - `browser/text-override.mjs` and `browser/upload-states.mjs` assert the new defaults. The override
139
+ half is untouched and still passes, because it is the migration path and had to keep working in the
140
+ same breath as the change that makes it necessary.
141
+ ### Added: guards
142
+ - **The Blazor stack had no behavioural gate at all, and now has one.** Its only check was
143
+ `dotnet build`, which is compilation, across twelve typed components, and `NEXT-UP.md` #5 described a
144
+ manual runtime verification via `drive-*.mjs` + CDP that does not exist in the repository. That
145
+ gap mattered most for the component added the same night: `FdyAppShell` reconciles a nullable
146
+ two-way binding against a JS enhancer, which is precisely the kind of logic a compiler cannot see.
147
+ `test/blazor/` (bUnit + xunit, 7 tests) renders the components for real: who wins on hydrate when
148
+ the caller binds a value versus leaves it null, what the enhancer's `fdy-app-nav` event does to
149
+ the binding, and that the two do not chase each other. Verified by sabotage: removing the echo
150
+ guard fails one test, breaking the hydrate push fails another, one each.
151
+ - It is wired into `npm run test:blazor` and into `ci.yml`, so it runs where releases are made
152
+ rather than where someone remembers. Deliberately placed OUTSIDE `adapters/`, which `package.json`
153
+ ships as a whole directory, and a test project has no business in a consumer's `node_modules`.
154
+
155
+ ### Fixed: the kit's own docs
156
+ - **The docs site shipped the wrong version number for three releases.** `docs/index.html` said
157
+ `v1.51.0` in its eyebrow and footer through 1.52.0, 1.52.1 and 1.53.0, and
158
+ `docs/getting-started.md` told readers to install `^1.34.0`, eighteen releases stale. `NEXT-UP.md`
159
+ already carried the right instruction (don't work from a memorised list, `git grep` the old
160
+ version) and it was still missed every time, because a runbook step is something a person has to
161
+ remember. The stamps are correct now and a test asserts them against `package.json`, so the next
162
+ miss fails the suite instead of reaching the live site.
163
+ - **`HANDOFF.md` was 419 lines of per-version history under a header saying "this is not a
164
+ changelog"**, a duplicate of `CHANGELOG.md` that stopped being updated twenty releases before
165
+ 2.0.0. It is a snapshot again: what the kit is now, what the guards cover, how a release goes out,
166
+ and what is known-unfinished. The same cleanup was done once before, in v1.20.0, for the same
167
+ reason: a stale snapshot is worse than none.
168
+ ### Known
169
+ - `docs/index.html` is Indonesian prose with its own ID→EN toggle for authored demo chrome, and that
170
+ toggle does not reach enhancer-rendered strings. In Indonesian mode the demos now show English
171
+ widget text. Left as-is deliberately: the page's job is to show what the kit actually does, and
172
+ putting `data-fdy-text-*` on all 42 enhancer instances would hide the default it documents.
173
+
174
+ ## [1.54.0] - 2026-08-25
7
175
  ### Added
8
- - **`<FdyAppShell>` the shell becomes the eleventh typed component**, in Vue, React and Blazor
176
+ - **`<FdyAppShell>` makes the shell the eleventh typed component**, in Vue, React and Blazor
9
177
  (`NEXT-UP.md` #8, stage 2). One model: `navOpen` means *the nav is visible to the reader*, and the
10
- kit owns the mapping above the nav breakpoint a hidden nav is `--nav-collapsed`, below it a
178
+ kit owns the mapping: above the nav breakpoint a hidden nav is `--nav-collapsed`, below it a
11
179
  visible nav is `--nav-open`. An app never reasons about the viewport to answer a question about
12
180
  its own UI. Vue `v-model:navOpen` · React `navOpen`/`onNavOpenChange` · Blazor `@bind-NavOpen`.
13
181
  - **The prop is optional in all three, and that is the design.** Unbound, the shell starts from the
14
182
  viewport: a column on a wide screen, hidden on a narrow one. A caller cannot express that as a
15
183
  single initial value before it knows the viewport, so `undefined` (Vue/React) and `null` (Blazor)
16
184
  mean "you decide". Bound, the caller wins and is pushed down on mount.
17
- - **`adapters/core/app-shell.js`** (+ `.d.ts`) the Tab trap, `inert` bookkeeping and focus restore
185
+ - **`adapters/core/app-shell.js`** (+ `.d.ts`) holds the Tab trap, `inert` bookkeeping and focus restore
18
186
  shared by the Vue and React wrappers, following `adapters/core/table-model.js`. `NAV_QUERY` is
19
187
  built from `tokens/breakpoints.mjs` rather than repeating `721`, so the JS and `app-shell.css`
20
188
  cannot drift. Blazor does not use it: `freeday-blazor.js` says the enhancers stay the source of
21
189
  truth, so the Blazor shell renders `data-fdy-app` and BINDS to `freeday-app-shell.js`. Two
22
190
  implementations, not four, and a guard holds them to one contract.
23
- - **`fdy-app-nav`** the enhancer now emits a bubbling CustomEvent (`detail {visible}`) on every
191
+ - **`fdy-app-nav`:** the enhancer now emits a bubbling CustomEvent (`detail {visible}`) on every
24
192
  real change, and takes `FreedayAppShell.setVisible(root, visible)` / `isVisible(root)` from
25
193
  outside. That is what the Blazor binding rides on, and what any vanilla app persisting a collapsed
26
- preference needed. The event fires for viewport-driven changes too narrowing hides a nav that
194
+ preference needed. The event fires for viewport-driven changes too, since narrowing hides a nav that
27
195
  was a visible column, and a bound value that stayed `true` would describe a panel nobody can see.
28
196
  Setting what is already set announces nothing, so a bound host cannot loop on its own echo.
29
197
  ### Docs
@@ -33,21 +201,21 @@ Semua perubahan penting dicatat di sini. Format longgar mengikuti
33
201
  - `NEXT-UP.md` #8 is struck through rather than deleted, and its number kept: the CHANGELOG, the
34
202
  design spec and two guard headers already say "NEXT-UP #8", so renumbering the rows under it would
35
203
  quietly break references that are already written down.
36
- ### Added guards
37
- - `browser/adapter.mjs` runs one description of the shell contract against BOTH typed stacks
204
+ ### Added: guards
205
+ - `browser/adapter.mjs` runs one description of the shell contract against BOTH typed stacks:
38
206
  viewport default, focus into the panel, the Tab cycle, Escape returning focus, `inert`, and a nav
39
207
  link closing the overlay. Verified by sabotage: with `applyShellState` and the focus calls stubbed
40
208
  out, React fails three of three and Vue two of three, the third being the sidebar click path the
41
209
  sabotage did not touch.
42
210
  - `browser/app-shell.mjs` gains the host-binding pair (`fdy-app-nav` ordering, `setVisible` doing the
43
211
  whole job and staying silent on a no-op) and the path only Blazor takes: a shell that does not
44
- exist at `DOMContentLoaded`, hydrated afterwards by handing `initAll` the component's own root
45
- which is the element itself, not a descendant of it.
212
+ exist at `DOMContentLoaded`, hydrated afterwards by handing `initAll` the component's own root,
213
+ which is the element itself and not a descendant of it.
46
214
  - `dotnet build` on the Blazor RCL is clean (0 warnings, 0 errors).
47
215
 
48
- ## [1.53.0] 2026-08-24
216
+ ## [1.53.0] - 2026-08-24
49
217
  ### Added
50
- - **`freeday-app-shell.js` the shell finally ships its own behaviour** (`NEXT-UP.md` #8, reported
218
+ - **`freeday-app-shell.js`: the shell finally ships its own behaviour** (`NEXT-UP.md` #8, reported
51
219
  twice). `.fdy-app` shipped `__navtoggle`, `__backdrop` and the `--nav-open` / `--nav-collapsed`
52
220
  classes with **zero JS**, and `COMPONENTS.md` told every consumer to wire it in two sentences that
53
221
  never mentioned Escape, focus, `inert` or focus restore. Follow that exactly and you get an
@@ -62,10 +230,10 @@ Semua perubahan penting dicatat di sini. Format longgar mengikuti
62
230
  ≥721px is `width:0;overflow:hidden`; off-canvas at ≤720px is `translateX(-100%)`. Both hide the
63
231
  panel from the eye and neither hides it from the keyboard, so a nav nobody could see still
64
232
  swallowed every Tab on the way into the page. `__sidebar` is now `inert` whenever the nav is not
65
- visible one rule for both modes rather than two patches.
233
+ visible, one rule for both modes rather than two patches.
66
234
  - **Crossing the breakpoint with the overlay open stranded the page.** Widening the window left
67
235
  `--nav-open` set and `__content` `inert`, so the page underneath could never be clicked or read
68
- again the same failure that produced `breakpoints.nav` in v1.20.0. The enhancer clears the
236
+ again, the same failure that produced `breakpoints.nav` in v1.20.0. The enhancer clears the
69
237
  overlay state on the media-query change, and deliberately does *not* move focus while doing it: a
70
238
  resize is not a user asking to go somewhere.
71
239
  ### Changed
@@ -74,26 +242,26 @@ Semua perubahan penting dicatat di sini. Format longgar mengikuti
74
242
  close-on-nav-click, `reference-screen.html` handled neither, and *neither* trapped focus, marked
75
243
  the content `inert`, or restored focus. Two versions in one repository disagreeing is the whole
76
244
  argument for the kit owning this.
77
- ### Added guards
245
+ ### Added: guards
78
246
  - `browser/app-shell.mjs` (5 tests), each verified to fail with the enhancer switched off: collapse
79
247
  and its `inert` at ≥721px, the overlay and its `inert` at ≤720px, focus entering the panel and
80
248
  cycling inside it under **trusted** Tab presses, Escape returning focus to the toggle, backdrop
81
249
  and nav-item dismissal, and the breakpoint crossing. One of the five originally passed against a
82
- shell with no behaviour at all the nav never opened, so "it is closed" was true and meaningless
83
- and now asserts it opened first.
250
+ shell with no behaviour at all, because the nav never opened, so "it is closed" was true and
251
+ meaningless. It now asserts it opened first.
84
252
  - `setViewport()` in the browser harness (`Emulation.setDeviceMetricsOverride`). A responsive
85
253
  contract cannot be tested at a fixed window size, and the moment a layout *crosses* a breakpoint
86
254
  is exactly where its state gets stranded.
87
255
 
88
- ## [1.52.1] 2026-08-24
256
+ ## [1.52.1] - 2026-08-24
89
257
  ### Docs
90
258
  - **1.51.1's close-watcher caveat named the wrong condition** (#048). It said a `showModal()` with
91
- no transient activation "from a timer, or after an `await` that outlived the click" has its
259
+ no transient activation ("from a timer, or after an `await` that outlived the click") has its
92
260
  close watcher grouped with the dialog below, so one Escape closes both. A consumer audited three
93
261
  call sites of exactly that shape and could not reproduce it. They were right, and the reason is
94
262
  that the line is **sticky** activation, not transient: grouping needs a page that has received no
95
- user input *at all*. Six seconds after a single click transient activation long expired,
96
- `navigator.userActivation.isActive === false` two stacked overlays are still independent. So the
263
+ user input *at all*. Six seconds after a single click, with transient activation long expired and
264
+ `navigator.userActivation.isActive === false`, two stacked overlays are still independent. So the
97
265
  grouping is reachable in a test harness and nowhere else, which is exactly where the kit met it.
98
266
  - **What the advice was protecting was real, but it is a different failure.** Open either overlay
99
267
  from script and the second one's `cancel` event arrives **non-cancelable**: Escape still closes
@@ -101,55 +269,55 @@ Semua perubahan penting dicatat di sini. Format longgar mengikuti
101
269
  on that dialog silently stops working. Only when both overlays are opened by a real gesture is that
102
270
  veto available. The Modal entry now says this instead.
103
271
  - The note also proposed documenting that the typed wrappers' `@cancel.prevent` makes them immune to
104
- the grouping. **Measured, they are not** in a page with no activation the `cancel` event is not
272
+ the grouping. **Measured, they are not.** In a page with no activation the `cancel` event is not
105
273
  cancelable at all, so preventing it does nothing and both dialogs close regardless. That claim is
106
274
  not in the docs, and this is why.
107
- ### Added guards
108
- - `browser/overlay-stack.mjs` gains the whole matrix as one assertion four ways to open two
109
- overlays × what one Escape leaves open × whether it could be refused so a row changing is
275
+ ### Added: guards
276
+ - `browser/overlay-stack.mjs` gains the whole matrix as one assertion: four ways to open two
277
+ overlays × what one Escape leaves open × whether it could be refused, so a row changing is
110
278
  reported as news about the browser rather than a regression in the kit. Verified identical on
111
279
  Chromium 133 (the suite's engine) and Chrome 151.
112
280
  - The fixture records each `cancel` event's `cancelable` flag; nothing prevents it, so the older
113
281
  tests measure what they always did.
114
282
 
115
- ### Fixed the kit's own suite
283
+ ### Fixed: the kit's own suite
116
284
  - **The browser guards did not run where the releases are made.** CI ran the 59 unit tests and
117
- published; the 58 browser tests across 18 specs pixel paint order for stacked overlays, chart
118
- accessible subtrees, the user-activation matrix above ran only when someone remembered to run
285
+ published; the 58 browser tests across 18 specs (pixel paint order for stacked overlays, chart
286
+ accessible subtrees, the user-activation matrix above) ran only when someone remembered to run
119
287
  them locally. Automating the release through OIDC in 1.52.0 removed the someone. `ci.yml` now runs
120
288
  both suites on every push and is *called* by `publish.yml` (`needs: test`), so the step list that
121
289
  guards a release cannot drift from the one that runs during development, and a tag with red guards
122
290
  never reaches npm.
123
291
  - **A job that merely ran the command would have been green while running nothing.** Every browser
124
292
  spec skips itself when no Chrome is found, and a skipped suite still exits 0. So the job checks
125
- the binary before it starts and then parses its own summary, failing on any skip "the job was
293
+ the binary before it starts and then parses its own summary, failing on any skip. "The job was
126
294
  green" and "the guards ran" were separate facts, and only one of them was enforceable. Both
127
295
  demonstrated on real runs: the assertion green at 58/0, and a deliberately broken `CHROME_BIN`
128
296
  red.
129
297
  - **The harness can now drive any installed Chrome.** `chrome-headless-shell` is headless by
130
298
  construction; an ordinary Chrome binary is not and, on a machine with no display, looks for one
131
299
  and dies. It now gets `--headless=new` (and `--no-sandbox` only under `CI`), which is what lets
132
- `CHROME_BIN=<some other Chrome>` compare engines the manoeuvre that settled #048. CI runs the
300
+ `CHROME_BIN=<some other Chrome>` compare engines, the manoeuvre that settled #048. CI runs the
133
301
  runner's Chrome stable, local runs Chromium 133, so both engines `COMPONENTS.md` names are
134
302
  genuinely exercised.
135
- - Spec: `docs/superpowers/specs/2026-08-24-browser-guards-in-ci-design.md`. No shipped file changed
303
+ - Spec: `docs/superpowers/specs/2026-08-24-browser-guards-in-ci-design.md`. No shipped file changed:
136
304
  `browser/` and `.github/` are not in the published tarball, so there is no release for this.
137
305
 
138
- ## [1.52.0] 2026-08-24
306
+ ## [1.52.0] - 2026-08-24
139
307
  ### Fixed
140
308
  - **A chart's legend, bar values and donut centre were exposed to assistive tech after all** (#047).
141
- `COMPONENTS.md` explained the a11y contract with the ARIA spec `role="img"` is *Children
309
+ `COMPONENTS.md` explained the a11y contract with the ARIA spec, where `role="img"` is *Children
142
310
  Presentational*, therefore "rendered bar values, axis ticks and legends are not exposed". Measured
143
311
  against Chrome's accessibility tree, that is not what happens: a donut kept `list` → three
144
312
  `listitem` → `StaticText "posted — 76%"` and its centre total `1687`, the legacy `.fdy-bars` kept
145
- every value and label (`12`, `Jan`, `30`, `Feb`…), and a two-series cartesian kept its legend all
146
- live, none ignored. What actually protected the advice was only that AT treats a *named*
313
+ every value and label (`12`, `Jan`, `30`, `Feb`…), and a two-series cartesian kept its legend, all
314
+ live and none ignored. What actually protected the advice was only that AT treats a *named*
147
315
  `role="img"` as a leaf and does not descend; the docs presented a habit as a guarantee. Axis ticks
148
316
  were the one true claim, and for an unstated reason: the renderer already sets `aria-hidden` on the
149
317
  `<svg>` itself.
150
318
  `ensureImg()` now marks the rendered children `aria-hidden="true"`, so the author's `aria-label`
151
319
  really is the entire text alternative and cannot be double-announced by an AT that does descend.
152
- Nothing is removed and nothing moves the legend and the centre are still in the DOM and still
320
+ Nothing is removed and nothing moves: the legend and the centre are still in the DOM and still
153
321
  painted.
154
322
  - **The hiding requires a name.** A chart with no `aria-label`/`aria-labelledby` keeps its contents
155
323
  exposed: pruning the subtree of an unlabelled chart would leave an image with no name *and* no
@@ -158,63 +326,63 @@ Semua perubahan penting dicatat di sini. Format longgar mengikuti
158
326
  ### Docs
159
327
  - The Charts a11y bullet now describes the kit instead of the spec: what is hidden, that `role="img"`
160
328
  alone would not have hidden it, and what happens when the label is missing.
161
- ### Added guards
329
+ ### Added: guards
162
330
  - `browser/chart-a11y.mjs` (5 tests) over donut, legacy bars and cartesian: the accessible subtree
163
331
  exposes no text of its own, the author label survives as the name, the legend is still rendered and
164
332
  visible (hidden, not deleted), and an unlabelled chart still reads. The first three were verified to
165
333
  fail against 1.51.1.
166
- - `axSubtree()` in the browser harness every AX node under an element, ignored ones included. The
334
+ - `axSubtree()` in the browser harness reports every AX node under an element, ignored ones included. The
167
335
  DOM cannot answer this class of question: `aria-hidden` changes no markup, so a `querySelector`
168
336
  assert passes identically before and after the fix. Same reason the 1.51.0 scaling bug needed a
169
337
  real measurement rather than a source read.
170
338
 
171
- ## [1.51.1] 2026-08-24
339
+ ## [1.51.1] - 2026-08-24
172
340
  ### Docs
173
341
  - **`COMPONENTS.md` never said whether two overlays may be open at once** (#046). Modal and Drawer
174
342
  were documented in isolation, so "can a confirm modal open over an open drawer?" had no answer in
175
- the file and the built CSS does not answer it either: the only overlay `z-index` in the bundle
343
+ the file, and the built CSS does not answer it either: the only overlay `z-index` in the bundle
176
344
  belongs to the toast region. Both entries now state the rule, because both components are
177
345
  `<dialog>` opened with `showModal()` and therefore enter the **top layer**, where paint order is
178
346
  the order they were opened: the later modal is above the drawer with no `z-index` from either,
179
347
  Escape closes the topmost first (so cancelling returns to the still-open drawer), and closing them
180
348
  out of order leaves the survivor painted and dismissible.
181
349
  - **The one caveat is the browser's, and it is now written down**: open each overlay from the
182
- gesture that asked for it. A `showModal()` with no user activation from a timer, or after an
183
- `await` that outlived the click has its close watcher GROUPED with the dialog below it, and a
350
+ gesture that asked for it. A `showModal()` with no user activation, from a timer or after an
351
+ `await` that outlived the click, has its close watcher GROUPED with the dialog below it, and a
184
352
  single Escape then closes both. Measured, not assumed: two untrusted `.click()` opens, one Escape,
185
353
  both dialogs shut.
186
- ### Added guards
187
- - `browser/overlay-stack.mjs` (3 tests) proves that paragraph instead of restating it the #041
354
+ ### Added: guards
355
+ - `browser/overlay-stack.mjs` (3 tests) proves that paragraph instead of restating it, the #041
188
356
  lesson applied to a documentation change. Paint order is asserted in **pixels** at the overlap of
189
357
  the two, because a modal dialog makes the rest of the document inert and `elementFromPoint` will
190
358
  name the dialog whatever is really on top; the overlays are opened with **trusted** clicks, or the
191
359
  close-watcher grouping above would fail the Escape assertion for a reason that is not the kit's.
192
- - **`pressKey` in the browser harness sent every key but Tab with virtual-key code 0** delivered
193
- to the page, but not acted on by the browser: Escape reached a `keydown` listener yet never closed
360
+ - **`pressKey` in the browser harness sent every key but Tab with virtual-key code 0**, delivered
361
+ to the page but not acted on by the browser: Escape reached a `keydown` listener yet never closed
194
362
  a native `<dialog>`. A dismissal guard written with it would have passed for the wrong reason and
195
363
  reported a broken kit as fine. It now carries the real `windowsVirtualKeyCode` /
196
364
  `nativeVirtualKeyCode` for the keys the suite presses.
197
365
 
198
- ## [1.51.0] 2026-08-21
366
+ ## [1.51.0] - 2026-08-21
199
367
  ### Fixed
200
368
  - **A cartesian chart's axis text and dots grew with the container** (#042). `renderCartesian` built
201
369
  a fixed `viewBox="0 0 320 180"` and let `.fdy-chart-xy__plot` stretch it to `width:100%`, so every
202
370
  size the renderer expressed in user units was multiplied by `plotWidth / 320`. `font-size:9px` on
203
371
  a tick is not nine screen pixels at any width but 320px: measured on a real console it rendered
204
372
  13px at a 350px plot and **81px at a 2232px** one, making the chart's smallest, most secondary
205
- text the largest type on the page. `r="2.2"` did the same to the dots 4.8px across at 350px,
373
+ text the largest type on the page. `r="2.2"` did the same to the dots: 4.8px across at 350px,
206
374
  30.7px at 2232px. The chart now measures `__plot` and sizes the `viewBox` to it in CSS pixels, so
207
375
  one user unit is one pixel and every declared size means what it says at every width; a
208
376
  `ResizeObserver` repaints it when the box changes (a collapsing sidebar moves a plot ~300px at a
209
- fixed viewport, so this is the common case, not an edge case). A chart that measures 0 detached,
210
- or inside `display:none` falls back to the old box and repaints when it is laid out.
377
+ fixed viewport, so this is the common case, not an edge case). A chart that measures 0, whether detached
378
+ or inside `display:none`, falls back to the old box and repaints when it is laid out.
211
379
  - **A currency y-axis was clipped by a gutter sized for a percent one** (#042). `PL` was a constant
212
380
  38 user units, which was ~4 characters at one scale and something else at every other. It is now
213
381
  derived from the widest formatted tick the axis will actually draw, capped at 35% of the plot.
214
382
  ### Changed
215
383
  - **Axis type is a real CSS size**, set by `--fdy-chart-tick-size` on `.fdy-chart-xy` (default
216
384
  `var(--text-xs)`). `font-size` moved off `.fdy-chart-xy__tick` / `__xlabel` onto
217
- `.fdy-chart-xy__plot`, which the SVG text inherits one computed read gives the renderer the real
385
+ `.fdy-chart-xy__plot`, which the SVG text inherits, so one computed read gives the renderer the real
218
386
  pixel size it needs for the y-gutter and for the x-label autoskip, so an override flows to the
219
387
  text *and* to the geometry around it. Before this, chart text could not be restyled at all: any
220
388
  value set was multiplied by a scale the consumer could not see or compute.
@@ -224,80 +392,80 @@ Semua perubahan penting dicatat di sini. Format longgar mengikuti
224
392
  chart is `role="img"` + `aria-label` "with a `<table>` fallback inside the element"; the renderer
225
393
  builds no table and wipes the element on every render path. Worse, `role="img"` makes descendants
226
394
  presentational, so the bar values, ticks and legend the renderer *does* draw are not exposed
227
- either the author's `aria-label` is the entire text alternative, and the docs now say so and ask
395
+ either, so the author's `aria-label` is the entire text alternative, and the docs now say so and ask
228
396
  for one that carries the numbers. The four dead fallback tables in `docs/index.html` and
229
397
  `docs/reference-screen.html`, which no reader has ever reached, are gone with it.
230
398
  - **The kit's own chart sizing is written down** (#041): `.fdy-chart-xy__plot` is
231
399
  `aspect-ratio:16/9` + `min-height:8rem`, `.fdy-bars` a fixed `9rem`, `.fdy-sparkline` an
232
- inline-block `8rem × 2.25rem` with the note to override those rather than the chart root, since
400
+ inline-block `8rem × 2.25rem`, with the note to override those rather than the chart root, since
233
401
  a root `height` fights `aspect-ratio` instead of setting it.
234
- ### Added guards
235
- - **`browser/chart-scale.mjs`** the axis label box and dot diameter are measured at plot widths of
402
+ ### Added: guards
403
+ - **`browser/chart-scale.mjs`** measures the axis label box and dot diameter at plot widths of
236
404
  350 / 696 / 1400 / 2232px and must not vary by more than a pixel, must stay smaller than body
237
405
  text, and the `viewBox` must equal the measured plot. A second spec widens a container and asserts
238
406
  the repaint. This class of bug is invisible to a source read: `font-size:9px` and `r="2.2"` look
239
- like correct sizes, and the one thing in the plot that *was* protected
240
- `vector-effect:non-scaling-stroke` on the line is what made the other three look protected too.
407
+ like correct sizes, and the one thing in the plot that *was* protected,
408
+ `vector-effect:non-scaling-stroke` on the line, is what made the other three look protected too.
241
409
  Only a real layout tells them apart.
242
410
 
243
- ## [1.50.0] 2026-08-20
411
+ ## [1.50.0] - 2026-08-20
244
412
  ### Fixed
245
413
  - **A toast raised from inside a modal was painted behind it** (#027). `.fdy-toast-region` carried
246
414
  `z-index: 200`, which can never win: `.fdy-modal` is a native `<dialog>` opened with `showModal()`
247
415
  and therefore lives in the **top layer**, above the whole z-index universe. An error raised by an
248
416
  action taken inside a dialog was dimmed by that dialog's backdrop and mostly hidden under the
249
- dialog so the reader saw a confirmation still asking a question they had already answered, and
417
+ dialog, so the reader saw a confirmation still asking a question they had already answered, and
250
418
  no reason why. The region is now `popover="manual"` and `freeday-toast.js` calls `showPopover()`
251
- as each toast lands (per toast, not once the top layer is ordered by when you joined, so a
419
+ as each toast lands (per toast, not once, because the top layer is ordered by when you joined, so a
252
420
  dialog opened after the last toast would otherwise sit above it). Guarded: where `showPopover` is
253
421
  unavailable or throws, the region stays exactly what it was.
254
422
  - **A checkbox shrank when its label was long** (#027). `.fdy-check` is an inline-flex row and its
255
- box declared a width but a flex item with a width is still shrinkable, so a label that wrapped
423
+ box declared a width, but a flex item with a width is still shrinkable, so a label that wrapped
256
424
  to three lines squeezed the box from 18px to 13px while leaving it 18px tall. A group of five
257
425
  rendered three different sizes, none of them square. `flex: none` on `.fdy-check input`,
258
- `.fdy-radio input`, the `.fdy-switch` track and the standalone `.fdy-checkbox` all four are
426
+ `.fdy-radio input`, the `.fdy-switch` track and the standalone `.fdy-checkbox`. All four are
259
427
  flex children somewhere in the kit.
260
- ### Added guards
261
- - **`pixelAt(x, y)` in `browser/harness.mjs`** a 1×1 `Page.captureScreenshot`, decoded in-process.
428
+ ### Added: guards
429
+ - **`pixelAt(x, y)` in `browser/harness.mjs`** takes a 1×1 `Page.captureScreenshot`, decoded in-process.
262
430
  Stacking above a modal cannot be asserted with `elementFromPoint`: a modal dialog makes the rest
263
431
  of the document inert, so a hit test outside it returns the dialog whatever the paint order is,
264
432
  reporting a working fix as broken. The new guard measures the colour actually composited at the
265
- toast's centre. It waits for both fade-ins to finish first a screenshot taken mid-animation
433
+ toast's centre. It waits for both fade-ins to finish first, since a screenshot taken mid-animation
266
434
  returns a blend of the two elements and flakes in both directions.
267
435
  - **Selection controls are asserted square and equal** across a group whose labels run one to three
268
436
  lines, so a box that shrank in both axes cannot pass a width-only comparison. All four controls
269
437
  the fix touched are measured, each in the container it actually ships in: the standalone
270
- `.fdy-checkbox` inside `.fdy-filter__check` the flex row all three typed adapters render in a
271
- filter popover and a CFL option list and the switch track against a switch of its own, since it
438
+ `.fdy-checkbox` inside `.fdy-filter__check`, the flex row all three typed adapters render in a
439
+ filter popover and a CFL option list, and the switch track against a switch of its own, since it
272
440
  is the one control that is not square. The first version of this guard held the standalone
273
441
  checkbox in a plain block, where it cannot shrink at all, so removing `flex: none` from the one
274
442
  box every adapter puts on screen left it passing.
275
443
 
276
- ## [1.49.0] 2026-08-20
444
+ ## [1.49.0] - 2026-08-20
277
445
  ### Added
278
- - **`FdyTableColumn.labelHidden`** (#026). A column of row CONTROLS an edit button, a row menu
446
+ - **`FdyTableColumn.labelHidden`** (#026). A column of row CONTROLS, an edit button or a row menu,
279
447
  could be named or quiet, not both: `label` renders as bare text in the `<th>`, so a designer who
280
448
  does not want a word above a column of icons was left with an empty header, which assistive tech
281
449
  announces as nothing. With `labelHidden: true` the label renders inside `.fdy-visually-hidden`:
282
450
  the cell looks empty and the column is still named. The label keeps naming the column's filter
283
451
  popover and sort button, so it stays meaningful either way.
284
- Contract and ALL THREE typed adapters Vue, React and Blazor. Each renders its own header, so
452
+ Contract and ALL THREE typed adapters: Vue, React and Blazor. Each renders its own header, so
285
453
  each spends the flag itself; Blazor takes `LabelHidden` on `FdyTableColumn<TRow>`.
286
- ### Added guards
454
+ ### Added: guards
287
455
  - **The column contract now has to reach all three adapters** (#026). Vue and React consume
288
- `table-model.d.ts` and TypeScript keeps them honest, but Blazor RE-DECLARES the column in C# a
289
- hand copy, and the surface that can fall behind silently. A test asserts every contract property
456
+ `table-model.d.ts` and TypeScript keeps them honest, but Blazor RE-DECLARES the column in C#, a
457
+ hand copy and the surface that can fall behind silently. A test asserts every contract property
290
458
  exists on Blazor's `FdyTableColumn<TRow>`; coverage, not equality, so it needs no exemption list.
291
- - **A hidden label is clipped, not dropped** asserted in a real browser for both JS adapters
459
+ - **A hidden label is clipped, not dropped**, asserted in a real browser for both JS adapters
292
460
  (`browser/adapter.mjs`), because the claim is about computed geometry: a span carrying the class
293
461
  with no CSS behind it passes any string match and still prints the word.
294
462
 
295
- ## [1.48.1] 2026-08-20
463
+ ## [1.48.1] - 2026-08-20
296
464
  ### Fixed
297
465
  - **A date column sorted correctly and filtered nothing** (#025). `FdyTable`'s client-mode date
298
466
  filter read a cell with `dateOnly`, which **sliced** the string, while its date SORT reads the same
299
- cell with `toTime`, which **parses**. A date column normally renders a formatted date that is
300
- what `value` is for so `"18 Mar 2024"` sliced to `"18 Mar 202"` and compared as text against an
467
+ cell with `toTime`, which **parses**. A date column normally renders a formatted date, which is
468
+ what `value` is for, so `"18 Mar 2024"` sliced to `"18 Mar 202"` and compared as text against an
301
469
  ISO bound: every row failed, silently. The working sort is what made the broken filter look
302
470
  trustworthy.
303
471
  - **A `Date` cell was read as its UTC day, not the reader's** (#025). `toISOString()` east of
@@ -305,49 +473,49 @@ Semua perubahan penting dicatat di sini. Format longgar mengikuti
305
473
  a row dated the 18th.
306
474
  - Text that is not a date now yields `''` and is EXCLUDED by an active date filter, rather than
307
475
  slicing to something that compares as less than every bound and matching everything.
308
- ### Added guards
476
+ ### Added: guards
309
477
  - Three tests in `test/table-model.test.mjs`, each verified to fail against the old body: a
310
478
  formatted `value`, a `Date` cell at a positive UTC offset, and unparseable text. The existing date
311
479
  test only ever filtered a column with no accessor holding an already-ISO string, which is the one
312
480
  shape neither bug can affect.
313
481
 
314
- ## [1.48.0] 2026-08-20
482
+ ## [1.48.0] - 2026-08-20
315
483
  ### Reverted
316
484
  - **1.47.0's readonly focus ring (#024) is withdrawn.** The report was measured wrong. The control in
317
- question a `CflField`'s readonly display input sits inside `.fdy-input-group`, and
485
+ question, a `CflField`'s readonly display input, sits inside `.fdy-input-group`, and
318
486
  `.fdy-input-group:focus-within` already carries the border and the 3px ring for the WHOLE control;
319
487
  `.fdy-input-group .fdy-input:focus` clears the inner input deliberately so the two do not nest. The
320
488
  audit measured the INPUT, found no shadow on it, and concluded there was no focus indicator.
321
- Measured on the group, focus shows `--color-primary` at full strength 6.56:1. 1.47.0 therefore
489
+ Measured on the group, focus shows `--color-primary` at full strength: 6.56:1. 1.47.0 therefore
322
490
  painted a muted ring INSIDE the blue one, which is a regression rather than a fix.
323
- ### Added guards
324
- - `test/css.test.mjs` pins the arrangement the group rings, the inner input does not so the next
491
+ ### Added: guards
492
+ - `test/css.test.mjs` pins the arrangement, the group rings and the inner input does not, so the next
325
493
  reader finds the answer instead of repeating the conclusion.
326
494
 
327
- ## [1.46.0] 2026-08-19
495
+ ## [1.46.0] - 2026-08-19
328
496
  The docs site catches up with the components, and writing it out as a consumer found a defect in
329
497
  #016's own contract.
330
498
  ### Fixed
331
499
  - **`data-fdy-text-*` could not be written the way HTML allows** (#023). The keys in each enhancer's
332
- `TEXT` table are camelCase, and `textOf` looked up `'data-fdy-text-' + key` but **HTML lowercases
500
+ `TEXT` table are camelCase, and `textOf` looked up `'data-fdy-text-' + key`, but **HTML lowercases
333
501
  attribute names**, so `data-fdy-text-filterText` becomes `...filtertext` while
334
502
  `data-fdy-text-filter-text`, the spelling anybody would actually reach for, is a DIFFERENT
335
503
  attribute the enhancer never read. It failed silently, which is the only way an override can fail.
336
504
  The key is kebab-cased for the lookup now; the run-together spelling still resolves, so markup
337
505
  written against 1.39.0 keeps working. Nine enhancers.
338
506
  It survived #016's own guard because all seven overrides that guard asserts use **single-word**
339
- keys, which have no case to lose the guard was not wrong, it was unrepresentative. The spec now
507
+ keys, which have no case to lose, so the guard was not wrong but unrepresentative. The spec now
340
508
  overrides `filterText`, reached by clicking a column filter open. Mutation-tested.
341
509
  ### Docs
342
- - **`docs/index.html` was nine releases behind** it stated v1.34.0 and demoed none of what landed
510
+ - **`docs/index.html` was nine releases behind.** It stated v1.34.0 and demoed none of what landed
343
511
  after it. Brought level with the components, as live demos rather than a feature list, because
344
512
  that is what the rest of the page is:
345
- - **New section Tabel beku.** `.fdy-table--sticky` / `--sticky-head`, `.fdy-table__freeze` +
513
+ - **New section, Tabel beku.** `.fdy-table--sticky` / `--sticky-head`, `.fdy-table__freeze` +
346
514
  `--fdy-freeze-left`, `.fdy-table-scroll--frozen` + `--fdy-table-frozen-h` (1.35.0, 1.37.0), with
347
515
  a rate matrix you can actually scroll on both axes, and the reason the two axes are separate
348
516
  modifiers.
349
- - **New section Teks enhancer (i18n).** The `data-fdy-text-*` contract (1.39.0), demoed as the
350
- same data table rendered entirely in English the demo that found #023.
517
+ - **New section, Teks enhancer (i18n).** The `data-fdy-text-*` contract (1.39.0), demoed as the
518
+ same data table rendered entirely in English, the demo that found #023.
351
519
  - **Badge tone scale** `--tone-1`…`--tone-8` (1.44.0), with the distinction the note drew: semantic
352
520
  variants state a judgement, tones distinguish without implying one.
353
521
  - **Date picker** now states the days → months → years drill (1.36.0); **Choose-from-list** states
@@ -356,158 +524,158 @@ The docs site catches up with the components, and writing it out as a consumer f
356
524
  and a `Previous` pager, and the frozen table computes `border-collapse:separate` with
357
525
  `position:sticky` cells.
358
526
 
359
- ## [1.45.0] 2026-08-19
527
+ ## [1.45.0] - 2026-08-19
360
528
  Two defects in 1.42.0's multi-select CFL, found by the app that asked for it (IDU_EMATE_APPL_WEB, #022).
361
529
  ### Fixed
362
530
  - **The row tick rendered as the browser's checkbox, not the kit's** (#022), in all three adapters.
363
531
  They shipped `class="fdy-check"` on a bare `<input>`; that class styles a wrapping `<label>` and
364
532
  its `input` DESCENDANT, so on the input itself it applied `inline-flex` and a cursor and nothing
365
- else a raw UA control in the middle of a styled dialog. `COMPONENTS.md` already said which class
533
+ else, leaving a raw UA control in the middle of a styled dialog. `COMPONENTS.md` already said which class
366
534
  a bare input in a table takes: `.fdy-checkbox`. Every behavioural assertion in `browser/cfl-multi`
367
535
  passed while it was wrong, because none of them looked at the rendering.
368
536
  - **`aria-selected` broke `vue-tsc` in consuming apps** (#022). `String(isPicked(row))` widens to
369
- `string`, and Vue types the attribute as `Booleanish` so a strict app type-checking the kit's own
537
+ `string`, and Vue types the attribute as `Booleanish`, so a strict app type-checking the kit's own
370
538
  `.vue` source failed on a file it does not own. Now `'true' : 'false'`.
371
- ### Added guards
539
+ ### Added: guards
372
540
  - `browser/cfl-multi.mjs` asserts the tick's computed `appearance` is `none`, which is the tell that
373
541
  the kit painted the box rather than the UA. Mutation-tested.
374
542
 
375
- ## [1.44.0] 2026-08-19
543
+ ## [1.44.0] - 2026-08-19
376
544
  One note from the back-office app (IDU_EMATE_APPL_WEB, #021).
377
545
  ### Added
378
- - **`--tone-1`…`--tone-8` on `.fdy-badge`** a status vocabulary larger than the semantic palette.
546
+ - **`--tone-1`…`--tone-8` on `.fdy-badge`** give a status vocabulary larger than the semantic palette.
379
547
  `.fdy-avatar--tone-*` and `.fdy-chip--tone-*` already carried the categorical scale; the badge,
380
548
  which is the component that actually renders status, did not. The reporting app's document lists
381
- carry **ten distinct statuses in one column** Draft, Submitted, Approved, Completed, Settled,
382
- Closed, Transferred, InDeclaration, Open, Rejected against five modifiers, so Approved, Closed
549
+ carry **ten distinct statuses in one column**: Draft, Submitted, Approved, Completed, Settled,
550
+ Closed, Transferred, InDeclaration, Open, Rejected, against five modifiers, so Approved, Closed
383
551
  and Completed came out the same green and Draft, InDeclaration and Transferred the same grey.
384
552
  Semantics still come first: a state that IS good, bad or waiting takes `--success` / `--danger` /
385
553
  `--warning`, and these are for the rest. Same `--tone-*` tokens and the same 18%/50% mix as the
386
554
  other two, so the existing contrast test already measures them.
387
- ### Added guards
555
+ ### Added: guards
388
556
  - `test/css.test.mjs` asserts all eight bind their own token (a gap silently reuses a colour) and
389
- that the mix stays identical to the avatar's and the chip's one contrast test covers the three,
557
+ that the mix stays identical to the avatar's and the chip's, so one contrast test covers the three,
390
558
  so a badge that drifted to its own ratio would be gated by a test measuring something else.
391
559
  `test/contrast.test.mjs` renamed from "avatar tones" to "categorical tones" to say so.
392
560
 
393
- ## [1.43.0] 2026-08-19
561
+ ## [1.43.0] - 2026-08-19
394
562
  Three crowding reports from one settlement screen (IDU_EMATE_APPL_WEB, #020). All three measured in
395
- Chromium rather than read off the CSS each is a layout outcome, invisible in the stylesheet.
563
+ Chromium rather than read off the CSS, since each is a layout outcome invisible in the stylesheet.
396
564
  ### Fixed
397
565
  - **A control in a table column collapsed** (#020). `.fdy-input` is `width:100%`, which in an
398
566
  auto-layout table contributes **no intrinsic width**: the column shrank to its header text and took
399
- the control with it. A NOTE box measured **36px** too narrow to read what you typed into it.
567
+ the control with it. A NOTE box measured **36px**, too narrow to read what you typed into it.
400
568
  Controls that are direct children of a `td` now keep a `7rem` floor (112px measured). Per-control
401
569
  rather than per-column, because the table cannot know which columns hold controls. `width:100%` was
402
570
  never wrong; it is only in a table that 100% of nothing is nothing.
403
571
  - **A stat value wrapped between the currency and the number** (#020). `.fdy-stat__value` is
404
572
  `--text-3xl`; `IDR 300,000.00` needs **224px** at that size and `.fdy-stats` tracks are
405
573
  `minmax(11rem,1fr)`, so a three-across row at 660px gave it ~205px and it broke onto two lines.
406
- It now shrinks **only when its own column cannot hold it** a 205px track renders 22.7px on one
574
+ It now shrinks **only when its own column cannot hold it**: a 205px track renders 22.7px on one
407
575
  line, a 420px track still renders exactly 31px. Shrinking the token instead would have taken 31px
408
576
  from every consumer to fix a column that was 19px short. Scoped to a stat inside `.fdy-stats`,
409
577
  where the width comes from the grid track: `container-type:inline-size` on a standalone
410
578
  `.fdy-stat` would stop it sizing to its own content, and the unconditional rule stays as the
411
579
  fallback where `@container` is missing.
412
580
  - **`.fdy-filelist` claimed space above itself and none below** (#020), so the "Add files" button
413
- every upload UI puts after it sat flush against the last row a measured **0px**. Symmetric now,
581
+ every upload UI puts after it sat flush against the last row, a measured **0px**. Symmetric now,
414
582
  with `:last-child` dropping the bottom margin so nothing pays for room it does not need.
415
- ### Added guards
416
- - `browser/crowding.mjs` measures all three. The stat case asserts **both** directions one line
417
- when narrow, still exactly `31px` when wide because a fix that quietly shrank every dashboard
583
+ ### Added: guards
584
+ - `browser/crowding.mjs` measures all three. The stat case asserts **both** directions: one line
585
+ when narrow, still exactly `31px` when wide, because a fix that quietly shrank every dashboard
418
586
  would pass a "does it wrap?" test.
419
587
 
420
- ## [1.42.0] 2026-08-19
588
+ ## [1.42.0] - 2026-08-19
421
589
  One note from the back-office app (IDU_EMATE_APPL_WEB, #019), and a string the #009 guard could not see.
422
590
  ### Added
423
591
  - **`multiple` on the typed `FdyCfl`** (Vue · React · Blazor). The enhancer has offered
424
592
  `data-fdy-cfl-multiple` since it shipped and `COMPONENTS.md` documented it; all three wrappers were
425
593
  single-valued **at the type level**, so there was nothing to widen from the outside. An app that
426
- started on the enhancer and later adopted the wrapper silently lost a capability which is the
594
+ started on the enhancer and later adopted the wrapper silently lost a capability, which is the
427
595
  actual defect, more than the missing feature. The settlement screen that reported it gathers six or
428
596
  eight approved claims onto one document and was doing six open→search→click cycles.
429
597
  Rows tick instead of committing, the footer counts them, and **Confirm** commits the set. Vue and
430
598
  React widen their model to `Row[] | null`; Blazor takes `Values` / `ValuesChanged` instead, because
431
599
  a nullable union is not a C# shape. New strings: `selectedText` (`{n} selected`), `confirmText`,
432
- `hintText` the last one making the single-mode footer overridable too, which it never was.
600
+ `hintText`, the last one making the single-mode footer overridable too, which it never was.
433
601
  ### Notes on the shape of the fix
434
602
  - **The ticks are component state, not the model.** Closing without Confirm has to leave the caller's
435
603
  value exactly as it was, so the dialog seeds its ticks from the bound value on every open. Binding
436
604
  them straight to the model would make Cancel a lie.
437
605
  - **The field states `{n} selected`.** `display()` takes ONE row: naming one of six would be wrong and
438
606
  naming all six does not fit a 22rem control.
439
- - **Blazor gained a modal footer**, which it never had Vue and React both commit from one.
607
+ - **Blazor gained a modal footer**, which it never had, while Vue and React both commit from one.
440
608
  - The tick checkbox is `aria-hidden` and inert. `.fdy-cfl__check input` is already
441
609
  `pointer-events:none` in the kit's CSS, so the row owns the click and a handler on the box would
442
610
  have been unreachable code; `aria-selected` on the row is what carries the state.
443
611
  ### Fixed
444
612
  - **`FdyCfl`'s second retry button said `Coba lagi`** (Vue · React). The error state inside the
445
613
  results branch had a hard-coded Indonesian literal while the button two states above it correctly
446
- read `retryText ?? 'Try again'` so the component both had the prop and ignored it. #009's guard
614
+ read `retryText ?? 'Try again'`, so the component both had the prop and ignored it. #009's guard
447
615
  missed it because **neither `coba` nor `lagi` is in its word list**, which is #015's lesson landing
448
616
  a third time: a word list is a floor, not a ceiling. Both now read `retryText`.
449
- ### Added guards
617
+ ### Added: guards
450
618
  - **The multi flow is driven by real clicks, in both adapters** (#019). `browser/cfl-multi.mjs`
451
619
  asserts that a click ticks rather than commits, that the dialog stays open, that `aria-selected`
452
620
  and the footer count follow, that a second click unticks, that Confirm hands back an **array** in
453
621
  order, and that closing without Confirm discards the ticks *and* does not remember them on re-open.
454
622
  Vue and React run the same script, so a divergence between the two fails here instead of in an app.
455
- A typecheck cannot tell you whether a mode ever renders. Mutation-tested committing
623
+ A typecheck cannot tell you whether a mode ever renders. Mutation-tested: committing
456
624
  `picked[0]` instead of the array fails it by name.
457
625
 
458
- ## [1.41.0] 2026-08-19
459
- One note from the account app (IDU_EMATE_ACCT_WEB, #018) the first raised from that repo.
626
+ ## [1.41.0] - 2026-08-19
627
+ One note from the account app (IDU_EMATE_ACCT_WEB, #018), the first raised from that repo.
460
628
  ### Fixed
461
629
  - **`.fdy-input-group` rendered 1px shorter than every control beside it** (#018). The group
462
630
  declares a `1.5px` border and its inner input subtracted `3px` to compensate, so the border box
463
631
  should land on `--control-h`. **At `devicePixelRatio: 1` the engine resolves each 1.5px border to
464
632
  1px**, the compensation over-subtracts, and a search field beside a button measured 39px against
465
- 40px. At dpr 2 the halves survive and the arithmetic is right which is why it shipped: it is
633
+ 40px. At dpr 2 the halves survive and the arithmetic is right, which is why it shipped: it is
466
634
  invisible on a retina display and visible on every 1× monitor.
467
635
  The group now states `height: var(--control-h)` (it is already `border-box`) and the input
468
636
  `height: auto; align-self: stretch`. The group is `align-items: stretch` already, so the input and
469
- the addons fill whatever the border leaves at any border width, any rounding, and under
637
+ the addons fill whatever the border leaves, at any border width, any rounding, and under
470
638
  `data-density="compact"` without a second `calc` to keep in step. The defect was not the number;
471
639
  it was compensating in one rule for a value declared in another.
472
- ### Added guards
640
+ ### Added: guards
473
641
  - **Control heights are measured in a real browser** (#018). No stylesheet assertion could have
474
642
  caught this: the CSS reads as correct arithmetic, and the value it is correct about is one the
475
643
  engine rounds. `browser/control-heights.mjs` measures `.fdy-input-group` against `.fdy-btn`,
476
644
  `.fdy-input` and `.fdy-combo` with `getBoundingClientRect`, at both densities. Written **failing
477
- first** against 1.40.0 it reproduced 39 vs 40 before the fix landed.
645
+ first** against 1.40.0, it reproduced 39 vs 40 before the fix landed.
478
646
  - The note's own sweep is recorded: `calc(var(--control-h)` has five uses in `src/`, and the four in
479
647
  `button.css` step a control deliberately by a spacing token. This was the last compensation of its
480
648
  kind.
481
649
 
482
- ## [1.40.0] 2026-08-19
650
+ ## [1.40.0] - 2026-08-19
483
651
  One note from the back-office app (IDU_EMATE_APPL_WEB, #017).
484
652
  ### Fixed
485
653
  - **A picker inside a `.fdy-field` now fills it.** `.fdy-field`, `.fdy-combo` and
486
654
  `.fdy-input-group` all cap at 22rem; `.fdy-datepicker` capped at 14rem and `.fdy-timepicker`
487
- at 11rem. In a two-column form grid that left a **224px** date box beside a **319px** combo
488
- measured, not eyeballed and the row read as ragged with no fix available to the consuming
655
+ at 11rem. In a two-column form grid that left a **224px** date box beside a **319px** combo,
656
+ measured rather than eyeballed, and the row read as ragged with no fix available to the consuming
489
657
  app, because the cap sits on the component. `.fdy-field > .fdy-datepicker` / `> .fdy-timepicker`
490
658
  / `> .fdy-daterange` drop the cap; a picker standing on its own keeps its narrower width, which
491
659
  is right for a short value.
492
660
 
493
- ### Added guards
661
+ ### Added: guards
494
662
  - **The enhancer string contract is asserted in a real browser** (#016). 1.39.0 moved every
495
663
  user-facing string in **nine** enhancers into a `TEXT` table read through `textOf()`, and only
496
- **one of the nine** upload had any browser coverage at all. The node guard proves the
664
+ **one of the nine**, upload, had any browser coverage at all. The node guard proves the
497
665
  literals *sit* in the table; it reads the source as text and cannot prove the wiring *runs*, so a
498
666
  `zone` out of scope or a `getAttribute` on a non-element would have thrown at init with all 51
499
667
  node tests still green. `browser/text-override.mjs` initialises table, stepper, mask, carousel,
500
668
  cascade, form and toast on real markup and asserts **both halves**: the documented Indonesian
501
- default still renders, and `data-fdy-text-*` wins. The default is a contract too an enhancer
669
+ default still renders, and `data-fdy-text-*` wins. The default is a contract too: an enhancer
502
670
  that quietly turned English would be a breaking change wearing a bugfix's clothes.
503
671
  Mutation-tested: making `textOf()` ignore the attribute fails it by name and value.
504
672
 
505
- ## [1.39.0] 2026-08-19
673
+ ## [1.39.0] - 2026-08-19
506
674
  The sweep #013 §2 filed and #015 restated: the vanilla enhancers' strings are reachable now.
507
675
  ### Added
508
676
  - **Every enhancer string is overridable with `data-fdy-text-<key>`** (#016). The defaults stay
509
- Indonesian that is documented and deliberate for the raw path, and changing it would break
510
- every app that adopted it but they were also *unreachable*, and that is the half that was
677
+ Indonesian, which is documented and deliberate for the raw path, and changing it would break
678
+ every app that adopted it, but they were also *unreachable*, and that is the half that was
511
679
  wrong. A Blazor app meets these enhancers through `FreedayBlazor.initAll`, which runs every one
512
680
  of them, while COMPONENTS.md promises Blazor is English throughout; both promises can only hold
513
681
  if the default stays and the string can be changed. Each enhancer now keeps its strings in one
@@ -515,60 +683,60 @@ The sweep #013 §2 filed and #015 restated: the vanilla enhancers' strings are r
515
683
  `{label}` are substituted where the string supports them. Validation messages take the same shape
516
684
  on the `<form>`, narrower than the per-field `data-fdy-msg-<alias>` that still wins;
517
685
  `Freeday.toast()` takes `closeLabel`.
518
- - **34 strings across 9 files**, not the 19 across 7 a word-list scan had counted `carousel`
686
+ - **34 strings across 9 files**, not the 19 across 7 a word-list scan had counted: `carousel`
519
687
  (`Slide {n}`), `cascade` (`Kembali satu tingkat`), `mask` (`Tampilkan kata sandi`), `stepper`
520
688
  (`Selesai` / `Lanjut`) and four upload status strings were all invisible to it. #015 had just
521
689
  finished making this point about `\bcari\b` and `pencarian`; the recount was found by scanning
522
- by SINK what lands in a `textContent`, an `aria-label`, a `title` or a `placeholder` which
690
+ by SINK: what lands in a `textContent`, an `aria-label`, a `title` or a `placeholder`, which
523
691
  cannot be fooled by an affix.
524
692
  ### Fixed
525
693
  - **Blazor's `FdyCascade` rendered Indonesian** (#016). `Label` and `Placeholder` were `string?`
526
694
  with no default, so an app that did not set them emitted `data-label=""` and the enhancer fell
527
- back to `Pilih` / `Pilih…`. Vue and React have defaulted to `Select` / `Select…` since #015
695
+ back to `Pilih` / `Pilih…`. Vue and React have defaulted to `Select` / `Select…` since #015:
528
696
  same drift as #009 and #015, third time, and the parameter existing is what made it invisible.
529
697
  They are non-nullable with English defaults now, and `BackLabel` / `SubmenuLabel` reach the two
530
698
  strings the enhancer builds itself, which had no route from .NET at all.
531
699
  - **`Freeday.toast()` from Blazor labelled its close button `Tutup`.** `FreedayBlazor.toast` now
532
- defaults `closeLabel` to `Close` in the bridge, not the enhancer, so a hand-written page still
700
+ defaults `closeLabel` to `Close`, in the bridge rather than the enhancer, so a hand-written page still
533
701
  gets the documented Indonesian.
534
- ### Added guards
702
+ ### Added: guards
535
703
  - **An enhancer string is overridable, not hard-coded** (#016). `npm test` asserts that a literal
536
704
  reaching a `textContent`, `aria-label`, `title` or `placeholder` lives in that file's `TEXT`
537
705
  table. Scoped by SINK rather than by a word list, deliberately: a second word list would have the
538
- same hole one affix over, and this one is also blind to no language. Mutation-tested putting
706
+ same hole one affix over, and this one is also blind to no language. Mutation-tested: putting
539
707
  `'Tutup'` back into the toast fails it by file and line.
540
708
 
541
- ## [1.38.0] 2026-08-19
709
+ ## [1.38.0] - 2026-08-19
542
710
  One note from the back-office app (IDU_EMATE_APPL_WEB, #015).
543
711
  ### Fixed
544
- - **`FdyCfl`'s trigger said `"Buka pencarian"`** (Vue · React) the accessible name of the button
712
+ - **`FdyCfl`'s trigger said `"Buka pencarian"`** (Vue · React), the accessible name of the button
545
713
  that opens the picker, and so the first thing a screen-reader user meets on every
546
714
  choose-from-list. #009 replaced twelve strings around it and missed this one. It takes an
547
715
  `openLabel` prop defaulting to `'Open search'` now. Blazor was not Indonesian but was using
548
- `SearchPlaceholder` as that button's `aria-label` a placeholder doing a label's job and now
716
+ `SearchPlaceholder` as that button's `aria-label`, a placeholder doing a label's job, and now
549
717
  takes the same `OpenLabel`, so all four stacks name the control identically.
550
- - **`FdyCascade`'s up-one-level button said `"Kembali satu tingkat"`** (Vue · React) new
718
+ - **`FdyCascade`'s up-one-level button said `"Kembali satu tingkat"`** (Vue · React), new
551
719
  `backLabel`, default `'Back one level'`.
552
720
  ### Changed
553
721
  - **The language guard now lists derived forms, not just roots.** `\bcari\b` cannot match
554
- *pen·cari·an*, and `kembali` was never listed Indonesian derives by affix, so a whole-word list
722
+ *pen·cari·an*, and `kembali` was never listed. Indonesian derives by affix, so a whole-word list
555
723
  misses most of the UI vocabulary. Widening it is what surfaced the `FdyCascade` string nobody had
556
724
  reported.
557
725
 
558
- ### Fixed the test suite
726
+ ### Fixed: the test suite
559
727
  - **`rules()` read only the last line of a selector**, so a rule written across several lines was
560
728
  reported under a fragment of its own name and every guard in `test/css.test.mjs` was blind to it
561
- (#014). A guard that matches nothing PASSES, which is the worst way for a test to fail it
729
+ (#014). A guard that matches nothing PASSES, which is the worst way for a test to fail: it
562
730
  surfaced only because the first draft of the filter-bar rule happened to be wrapped. The helper
563
731
  now strips comments from the whole source first (a comment quoting CSS carries braces, and those
564
732
  would split a rule in half) and reads the selector whole. Asserted directly rather than left to
565
733
  the CSS staying one-rule-per-line: that is a convention, and this is the thing that would quietly
566
- stop enforcing it. Mutation-tested restoring the last-line read fails the new test by name.
734
+ stop enforcing it. Mutation-tested: restoring the last-line read fails the new test by name.
567
735
 
568
- ## [1.37.0] 2026-08-19
736
+ ## [1.37.0] - 2026-08-19
569
737
  One note from the back-office app (IDU_EMATE_APPL_WEB, #014).
570
738
  ### Added
571
- - **`.fdy-table__freeze` + `--fdy-freeze-left`** freeze **any number** of columns, not just the
739
+ - **`.fdy-table__freeze` + `--fdy-freeze-left`** freeze **any number** of columns, not just the
572
740
  first. 1.35.0 froze `tbody th` at a hard-coded `left:0`, which suits a rate matrix's single row
573
741
  header and cannot express a wide table with two identity columns: they all landed on 0 and
574
742
  stacked. Mark every cell of a frozen column and give that column its offset; the offsets belong
@@ -581,17 +749,17 @@ One note from the back-office app (IDU_EMATE_APPL_WEB, #014).
581
749
  ### Changed
582
750
  - **BREAKING (from 1.35.0): the header freeze moved to `.fdy-table--sticky-head`.** Freezing a row
583
751
  needs a vertical scrollport; a table that only freezes columns scrolls with the **page**, where
584
- `top:0` sticks the header under the viewport edge over the consuming app's own top bar.
752
+ `top:0` sticks the header under the viewport edge, over the consuming app's own top bar.
585
753
  `.fdy-table--sticky` is now the enabler (separate borders) and each axis is asked for
586
754
  explicitly. `tbody th` no longer freezes implicitly; use `.fdy-table__freeze`. 1.35.0 and 1.36.0
587
755
  are unpublished, so no released version carried the old contract.
588
756
 
589
- ## [1.36.0] 2026-08-19
757
+ ## [1.36.0] - 2026-08-19
590
758
  One note from the back-office app (IDU_EMATE_APPL_WEB, #013).
591
759
  ### Added
592
760
  - **A year grid in the calendar** (vanilla · Vue · React · Blazor). 1.32.0's month grid killed
593
761
  one-click-per-month and left one-click-per-year behind it: the month grid's arrows step a year
594
- each, so 2026 → 1998 was **28 clicks**. The title now drills twice days → months → years with
762
+ each, so 2026 → 1998 was **28 clicks**. The title now drills twice, days → months → years, with
595
763
  the arrows following the level shown, and picking a year drops to that year's months. Every level
596
764
  above the day grid stays **navigation**: nothing is committed until a day is picked. Year pages
597
765
  are aligned (2016–2027, 2028–2039) rather than centred on the year in view, so the pages tile
@@ -604,18 +772,18 @@ One note from the back-office app (IDU_EMATE_APPL_WEB, #013).
604
772
  ### Fixed
605
773
  - **`freeday-datepicker.js` spoke Indonesian to the user.** Its placeholder and every nav
606
774
  `aria-label` were Indonesian, and because `FdyDatepicker.razor` delegates to the vanilla
607
- enhancer, **every Blazor app inherited them** while #009's language guard, which scans
775
+ enhancer, **every Blazor app inherited them**, while #009's language guard, which scans
608
776
  `adapters/` only, passed. The datepicker is English now. The rest of `src/` is NOT audited: see
609
777
  improvement note #013 §2, which lists the enhancers still affected.
610
778
 
611
- ## [1.35.0] 2026-08-19
779
+ ## [1.35.0] - 2026-08-19
612
780
  One note from the back-office app (IDU_EMATE_APPL_WEB, #012).
613
781
  ### Added
614
- - **`.fdy-table--sticky` + `.fdy-table-scroll--frozen`** a table frozen on one axis or both, for
782
+ - **`.fdy-table--sticky` + `.fdy-table-scroll--frozen`** freeze a table on one axis or both, for
615
783
  a grid read against two axes at once (a rate matrix, a timetable). This is a kit class rather
616
784
  than four lines in an app because `position:sticky` on its own produces a **visibly broken**
617
785
  table: `.fdy-table` collapses its borders, a collapsed border is painted by the table rather than
618
- by either cell, and so a frozen cell scrolls out from under its own rule the frozen column
786
+ by either cell, and so a frozen cell scrolls out from under its own rule, and the frozen column
619
787
  arrives with nothing separating it from the data. The modifier separates the borders and puts
620
788
  each one back on the cell that sticks. `<thead>` freezes at the top, `<th scope="row">` at the
621
789
  left, the first header cell is the corner, and the row header paints the **body** surface so a
@@ -623,21 +791,21 @@ One note from the back-office app (IDU_EMATE_APPL_WEB, #012).
623
791
  the freeze resolves against (the base `.fdy-table-scroll` scrolls one axis, so `top:0` never
624
792
  engages); size it with `--fdy-table-frozen-h`, default `30rem`.
625
793
 
626
- ## [1.34.0] 2026-08-19
794
+ ## [1.34.0] - 2026-08-19
627
795
  One note from the back-office app (IDU_EMATE_APPL_WEB, #008), and the second half of #005 it
628
796
  finally makes answerable.
629
797
  ### Added
630
798
  - **`pageSizes` on `FdyTable`** (Vue · React · Blazor) and `data-fdy-table-page-size` for the
631
- enhancer. The footer stated the range and moved the page and stopped there no control for **how
799
+ enhancer. The footer stated the range and moved the page and stopped there, with no control for **how
632
800
  many rows a page holds**, which is the first thing anybody changes on a ninety-row list. The three
633
801
  numbers are one object: the component already receives `index`, `size` and `total`, rendered two
634
- of them, and left the third to the caller so the caller withheld the whole footer with
802
+ of them, and left the third to the caller, so the caller withheld the whole footer with
635
803
  `pager={false}` and rebuilt all three to add the one. Give it `pageSizes` and it grows the control
636
804
  between the range and the pager. Server mode reports the pick through the existing
637
805
  `update:page` / `onPageChange` / `PageChanged`, carrying a new `size`; client mode applies it
638
806
  itself (so it works with nothing wired) and also emits `update:pageSize` / `onPageSizeChange` /
639
807
  `PageSizeChanged` for a caller that wants to persist it.
640
- - **`FdyTableFooter`** (Vue · React · Blazor) the footer as a component of its own. This is the
808
+ - **`FdyTableFooter`** (Vue · React · Blazor) is the footer as a component of its own. This is the
641
809
  half of #005 that `pager={false}` could only get out of the way: a **responsive** list renders one
642
810
  page of rows twice, a `.fdy-datatable` at `lg` and a `.fdy-list` below it, and a footer that lives
643
811
  inside the table lives inside the half a phone hides. Those screens took the footer over and
@@ -646,56 +814,56 @@ finally makes answerable.
646
814
  - **`pageIndexForSize(pageIndex, oldSize, newSize)`** in the shared table model. Resizing the page
647
815
  has two obvious answers and both are wrong: back to page 1 throws away the reader's place, and
648
816
  keeping the same index can land past the end (page 5 of 5 at twenty rows is page 2 of 2 at fifty).
649
- Anchoring on the first visible row always resolves, and it is what the reader expects the row
817
+ Anchoring on the first visible row always resolves, and it is what the reader expects: the row
650
818
  they were looking at is still on screen.
651
819
  ### Notes on the shape of the fix
652
820
  - **A footer with a size control stays visible on a single page.** The old rule withheld the whole
653
821
  band whenever `totalPages === 1`, which would have made "100 rows" on a ninety-row list a one-way
654
822
  door: the control that got you there disappears with the pager. Visibility is now "there is a
655
- pager **or** there is a size control", and it lives in `FdyTableFooter` one decision, four
823
+ pager **or** there is a size control", and it lives in `FdyTableFooter`: one decision, four
656
824
  stacks.
657
825
  - **No wrapper element.** `.fdy-table-footer__size` takes `margin-left:auto`, so the size control and
658
826
  the pager travel together on the right while the range stays left. The footer's DOM is byte-
659
827
  identical for every table that does not offer one.
660
828
  - **`.fdy-combo`, not a native `<select>`.** The first cut of this shipped a native one, reasoning
661
829
  that three one-word options do not need a custom listbox. They do: an OS menu is unthemeable, and
662
- on macOS it drops a dark grey panel into a light page which is the reason `FdyCombo` exists, and
830
+ on macOS it drops a dark grey panel into a light page, which is the reason `FdyCombo` exists, and
663
831
  its own source header says so. Caught by a consuming app in a screenshot within the hour, before
664
832
  the release reached npm. The raw path still accepts either: the enhancer listens for `change` *and*
665
833
  for the `fdy-change` a `.fdy-combo` emits.
666
- ### Added guards
834
+ ### Added: guards
667
835
  - `pageIndexForSize` is unit-tested at the boundaries (first page, both directions, same-size no-op,
668
836
  a negative index, a zero size).
669
837
  - The control is measured in a real browser, on two tables sharing one page state, **driven by real
670
838
  clicks on the popup** rather than a synthetic `change`: the server-mode pick must reach the caller
671
- **with the right index** (page 3 of five-row pages → page 2 of ten), and the client-mode table
672
- deliberately wired to nothing must still grow to 25 rows, because a control that only reports is
839
+ **with the right index** (page 3 of five-row pages → page 2 of ten), and the client-mode table,
840
+ deliberately wired to nothing, must still grow to 25 rows, because a control that only reports is
673
841
  a control that lies. Opening the popup is the half that would have caught the native `<select>`.
674
842
  ### Fixed
675
843
  - **The typed `FdyCfl` dialog was Indonesian end to end, in every stack but Blazor** (#009).
676
- COMPONENTS.md promises the Vue/React/Blazor components are English throughout that is the line an
677
- English app adopts the wrappers on and the CFL broke it in **twelve strings** (`Pilih data`,
844
+ COMPONENTS.md promises the Vue/React/Blazor components are English throughout, which is the line an
845
+ English app adopts the wrappers on, and the CFL broke it in **twelve strings** (`Pilih data`,
678
846
  `Tutup`, `Cari…`, `Memuat…`, `Coba lagi`, `Tidak ada hasil.`, `Hasil pencarian`, `Muat lebih
679
847
  banyak`, `Klik baris untuk memilih`) with **not one prop reaching any of them**. Blazor already had
680
848
  the whole set as English-defaulted parameters, so Vue and React now take the same props with the
681
849
  same defaults: a parity gap closing, not a translation being chosen. `FdyCascade` (`Pilih` /
682
850
  `Pilih…`) and `FdyAutocomplete` (`Tak ada hasil.`) get English defaults too; both were already
683
- overridable. The enhancers keep their Indonesian documented, deliberate, a separate decision
851
+ overridable. The enhancers keep their Indonesian: documented, deliberate, a separate decision
684
852
  (NEXT-UP #6).
685
853
 
686
854
  - **A guard for each.** The paragraph margins are checked against the classes the DOCS show on a
687
- `<p>`, so a new one is covered by documenting it the step nobody skips. The grid ordering is
855
+ `<p>`, so a new one is covered by documenting it, the step nobody skips. The grid ordering is
688
856
  asserted as an ORDER, because a specificity bug is invisible in either rule and only their sequence
689
857
  shows it. Both mutation-tested.
690
858
  - **A guard for it.** `npm test` scans every string literal in `adapters/{vue,react,blazor}` for
691
859
  twenty unambiguous Indonesian words. One of the twelve was found by looking at a screen; the other
692
- eleven by the sweep that one prompted reading finds the instance, only a mechanical check finds
860
+ eleven by the sweep that one prompted; reading finds the instance, only a mechanical check finds
693
861
  the class. Matched by word rather than by a list of components, so a new adapter is covered the day
694
862
  it lands.
695
863
 
696
864
  - **`.fdy-eyebrow` and `.fdy-cfl__empty` never cleared the UA's `<p>` margin** (#010). Every other
697
865
  class the kit documents on a paragraph sets `margin:0`; these two did not, so a browser's 1em
698
- landed as spacing nobody wrote 12px above the eyebrow and 12px below it. A consuming app reported
866
+ landed as spacing nobody wrote: 12px above the eyebrow and 12px below it. A consuming app reported
699
867
  it as two complaints ("the gap from the category to the top bar is too big" and "the gap between
700
868
  the title and the record value is too loose"); they were one missing declaration, and it made the
701
869
  top of every page 44px against 32px at the sides.
@@ -705,7 +873,7 @@ finally makes answerable.
705
873
  combobox has to agree with is the form controls, so the height goes back to `--control-h`.
706
874
  - **`.fdy-list__title` did not set its own type** (#010). The class carries weight, colour and
707
875
  truncation but inherited font-size and family, so on the `<span>` the docs show it is 16px body
708
- text and on an `<h3>` a legitimate element for a row that names a record the UA's 1.17em and
876
+ text and on an `<h3>`, a legitimate element for a row that names a record, where the UA's 1.17em and
709
877
  base.css's display-font rule made it 18.7px Sora. A phone row's name then shouted over its meta,
710
878
  and a long one wrapped to two lines instead of ellipsising. A class that names a role owns the type
711
879
  for that role.
@@ -713,73 +881,73 @@ finally makes answerable.
713
881
  for one class; asserting it as an invariant found the rest. `.fdy-alert__title`,
714
882
  `.fdy-dropzone__title` and `.fdy-toast__title` stated no `font-size` and no `margin`;
715
883
  `.fdy-state__title`, `.fdy-cal__title`, `.fdy-app__brand-title` and `.fdy-app__brand-subtitle`
716
- stated no `margin`. On the elements the docs show, all eight rendered correctly and always had
884
+ stated no `margin`. On the elements the docs show, all eight rendered correctly and always had,
717
885
  a `<span>` brings no margin and no size of its own. On a heading, which is the honest markup for
718
886
  an empty state's title or a row that names a record, the UA supplied `1.17em`, `bold` and `1em 0`
719
887
  and the kit's type scale simply did not apply. `font-size:inherit` is the fix where the class had
720
888
  no size of its own: it is what the documented span already did, and on a heading it declines the
721
889
  UA's `em` instead of inheriting it. **No rendering changes for markup that follows the docs.**
722
- ### Added guards
890
+ ### Added: guards
723
891
  - **A title class must render the same whatever element carries it** (#011). `npm test` asserts
724
892
  `margin`, `font-size` and `font-weight` on every `.fdy-*title` rule that sets type and is not a
725
893
  flex/grid box. This is #010's guard with the right invariant: that one checks the classes the docs
726
894
  show on a `<p>`, so it could only ever see the documented element, and the kit does not own that
727
- choice heading level is the consuming app's semantics. Scoped to title roles deliberately:
895
+ choice, because heading level is the consuming app's semantics. Scoped to title roles deliberately:
728
896
  asserted over every class that sets type it fails 53 times on `.fdy-btn--sm`, `.fdy-avatar--xs`
729
897
  and friends, and a guard that loud is one somebody silences. It found the eighth class after the
730
898
  fix list had been written by eye. Mutation-tested.
731
899
  - **The month grid rendered seven months across** (#010). `.fdy-cal__grid--months` sets three
732
900
  columns and `.fdy-cal__grid` sets seven; both sit on the same element and weigh the same, so
733
- source order decides and the modifier was declared *before* the base it modifies, with a comment
901
+ source order decides, and the modifier was declared *before* the base it modifies, with a comment
734
902
  directly above it describing the 3×4 layout it was failing to produce. Moved after. Neither rule is
735
903
  wrong on its own, which is why reading them found nothing.
736
904
 
737
- ### Fixed the kit's own suite
905
+ ### Fixed: the kit's own suite
738
906
  - `browser/fixtures/theme-subtree.html` had no explicit theme on `<html>`, so its "light app" probe
739
907
  fell to `prefers-color-scheme`. On a machine in dark mode both probes read the dark ink and the
740
908
  test failed claiming `data-theme` had gone back to being root-scoped. Pinned; the subtree
741
909
  behaviour under test is unchanged.
742
910
 
743
- ## [1.33.0] 2026-08-18
911
+ ## [1.33.0] - 2026-08-18
744
912
  Three notes from the back-office app, four findings, all four executed.
745
913
  ### Added
746
914
  - **`pager={false}` on `FdyTable`** (Vue · React · Blazor). Server mode rendered its own footer
747
915
  unconditionally: `hasPager` was `pageSize > 0 && totalPages > 1` with no way in, so a responsive
748
- list a table at `lg`, `.fdy-list` below it, one pager serving both so the views cannot disagree
916
+ list: a table at `lg`, `.fdy-list` below it, one pager serving both so the views cannot disagree,
749
917
  showed the kit's footer *and* the app's, stacked, on every list with more than one page. The only
750
918
  workaround was `display:none` on `.fdy-table-footer`, an app reaching into a component's internals.
751
919
  Client mode already had its hatch (`pageIndex`); this gives server mode one, and in server mode it
752
- is the honest shape the app owns the page there anyway, it was simply being handed a second
920
+ is the honest shape, since the app owns the page there anyway, it was simply being handed a second
753
921
  control.
754
922
  - **`usePopover` is exported from the Vue adapter.** Every kit dropdown uses it to escape an
755
923
  ancestor's overflow clip (`.fdy-card` is `overflow:hidden`, so a panel inside one is cut at the
756
924
  card's edge), and a consuming app that had to build a control the kit does not ship had to
757
- re-implement it ~70 lines, from the kit's own description, that will not improve when the kit's
925
+ re-implement it: ~70 lines, from the kit's own description, that will not improve when the kit's
758
926
  positioning does. *Correction to the report: React has exported it all along; only Vue was missing
759
- it. Blazor has no composable layer its panels go through the JS bridge.*
927
+ it. Blazor has no composable layer; its panels go through the JS bridge.*
760
928
  - **Quiet destructive buttons.** `--danger` was the solid treatment and nothing else, so
761
- `fdy-btn--ghost fdy-btn--danger` which every reader parses as "quiet destructive" rendered a
929
+ `fdy-btn--ghost fdy-btn--danger`, which every reader parses as "quiet destructive", rendered a
762
930
  second SOLID button. That is a hierarchy defect: the base class is already the one primary per
763
931
  screen, and a solid Delete beside Save is a second primary in all but name.
764
932
  `.fdy-menu__item--danger` has always modelled the quiet form; it now reaches `.fdy-btn`.
765
933
  ### Fixed
766
934
  - **`.fdy-eyebrow` and `.fdy-nav__grouplabel` spend a text ink.** Both set type in
767
935
  `--color-text-subtle`, which is gated at 3.0 *by design* (placeholders, separators, decorative
768
- glyphs) and measures 4.41:1 axe reports it as a serious `color-contrast` failure. Identical to
936
+ glyphs) and measures 4.41:1, so axe reports it as a serious `color-contrast` failure. Identical to
769
937
  `.fdy-stat__label` in 1.30.0, one release earlier: same ink, same 4.41, same argument.
770
- The nav group label is the one the report did not know about the sweep it asked for found it,
938
+ The nav group label is the one the report did not know about; the sweep it asked for found it,
771
939
  and that rule was *already* brightening to `--color-text-muted` on hover, which is the CSS
772
940
  admitting its resting state was too faint.
773
941
  ### Notes on the shape of the fix
774
942
  - The report proposed scoping the solid danger with `:not(.fdy-btn--ghost):not(.fdy-btn--text)`.
775
- Shipped without it: the pairing rules are more specific and come later, so they already win
943
+ Shipped without it: the pairing rules are more specific and come later, so they already win,
776
944
  the `:not()` would be a second mechanism expressing the same intent, and the mutation run proved
777
945
  it changed nothing.
778
946
  - **A regression written and caught inside this change:** Vue casts an omitted Boolean prop to
779
947
  `false`, not `undefined`, so `props.pager !== false` withheld the footer from every table that
780
948
  never mentions `pager`. The existing controlled-`pageIndex` guard failed within seconds. `pager`
781
949
  now goes through `withDefaults`, the same trap `FdyModal`'s `dismissible` hit in v1.18.0.
782
- ### Added guards
950
+ ### Added: guards
783
951
  - **The prose-ink invariant** note 007 asked for: any rule that sets a `font-size` *and* spends
784
952
  `--color-text-subtle` is typesetting prose in an ink gated for decoration, and fails. Matched by
785
953
  shape, not by a list of names, so the third instance cannot arrive the way the second did. Two
@@ -791,7 +959,7 @@ Three notes from the back-office app, four findings, all four executed.
791
959
  fills are: a gradient is a background-*image*, so the CSS reads fine either way and only the engine
792
960
  shows which treatment won.
793
961
 
794
- ## [1.32.1] 2026-08-18
962
+ ## [1.32.1] - 2026-08-18
795
963
  Docs only. Asked by a consumer: *"if I upgrade, will the agent in my project know what it gained?"*
796
964
  The answer was no, and two reasons why.
797
965
  ### Fixed
@@ -799,25 +967,25 @@ The answer was no, and two reasons why.
799
967
  reads first, and it listed everything in the package except the one file that says what changed
800
968
  between the version the project had and the one it now has. It now points at
801
969
  `node_modules/@cahyo-dimas/freeday/CHANGELOG.md` explicitly, plus a short *reach for this instead
802
- of hand-rolling that* table covering the last nine releases the additions most likely to replace
970
+ of hand-rolling that* table covering the last nine releases, the additions most likely to replace
803
971
  something an app already worked around.
804
- - **Three classes existed only in shorthand** in `COMPONENTS.md` `.fdy-label--required`,
972
+ - **Three classes existed only in shorthand** in `COMPONENTS.md`: `.fdy-label--required`,
805
973
  `.fdy-cal__month` and `.fdy-cal__grid--months` were written as `(+--required)` and `__month`, so
806
974
  the full name appeared **zero** times and a grep for the class found nothing. The drift guard
807
975
  cannot see this either: it verifies fully-written names and skips shorthand by design, so a class
808
976
  introduced only in shorthand is invisible to both the reader and the test.
809
977
 
810
- ## [1.32.0] 2026-08-18
978
+ ## [1.32.0] - 2026-08-18
811
979
  Improvement note 004, both halves: a calendar you could not steer, and a check mark that talked.
812
980
  ### Added
813
- - **The calendar title is a control.** It was a `<div>` the only thing naming the month, and not
814
- clickable so the only pointer route to another month was one click per month: August 2026 to
981
+ - **The calendar title is a control.** It was a `<div>`, the only thing naming the month, and not
982
+ clickable, so the only pointer route to another month was one click per month: August 2026 to
815
983
  March 2022 is **53 clicks**. `Shift`+`PageUp` already jumped a year (APG, correct) but had no
816
984
  affordance whatsoever; the reporting repo's own Playwright helper clicked "previous month"
817
985
  **twenty-four times** and shipped, which is the strongest possible evidence that nobody finds a
818
986
  shortcut nothing mentions.
819
987
  Pressing the title now drills to a 12-cell month grid where the arrows step **years**. Same
820
- journey: **7 clicks**. Picking a month is navigation, not selection the day grid comes back with
988
+ journey: **7 clicks**. Picking a month is navigation, not selection, so the day grid comes back with
821
989
  the roving cell clamped into the new month and nothing is committed until a day is chosen.
822
990
  Shipped in the vanilla enhancer, Vue and React; `FdyDateRange` composes the picker in both
823
991
  adapters and the Blazor `FdyDatepicker` wraps the enhancer, so all of them inherit it.
@@ -827,7 +995,7 @@ Improvement note 004, both halves: a calendar you could not steer, and a check m
827
995
  ### Fixed
828
996
  - **A combo option's accessible name no longer changes when it is selected.** The tick lived inside
829
997
  `role="option"` as text, so the selected option announced `✓August` while every other announced
830
- `August`: the state was read twice once as `aria-selected`, once as decoration and
998
+ `August`: the state was read twice, once as `aria-selected` and once as decoration, and
831
999
  `getByRole('option', { name: 'August' })` stopped matching the one option that was selected. The
832
1000
  glyph is now painted by CSS with alt text (`content:"✓" / ""`), the same technique
833
1001
  `.fdy-label--required` uses. The conditional disappears from all four stacks, and the vanilla
@@ -836,13 +1004,13 @@ Improvement note 004, both halves: a calendar you could not steer, and a check m
836
1004
  - **A bug written and caught inside this change:** swapping the calendar head with `v-if`/`v-else`
837
1005
  destroys the very button the user just pressed, focus falls to `<body>`, and the panel's own
838
1006
  `focusout` handler closes it mid-navigation. The head is now one element set whose labels and
839
- handlers switch, and `focusout` ignores a **null** `relatedTarget` focus lost because an element
1007
+ handlers switch, and `focusout` ignores a **null** `relatedTarget`, so focus lost because an element
840
1008
  was removed is not focus leaving the control, and a pointer that really lands outside is handled
841
1009
  by the mousedown path.
842
1010
  - The report's `aria-hidden` suggestion would also have worked; CSS alt text was chosen because it
843
1011
  removes the conditional from four implementations instead of adding an attribute to each.
844
- ### Added guards
845
- - `browser/harness.mjs` gains **`axName(selector)`** the accessible name as the *engine* computes
1012
+ ### Added: guards
1013
+ - `browser/harness.mjs` gains **`axName(selector)`**, the accessible name as the *engine* computes
846
1014
  it (CDP `Accessibility.getPartialAXTree`). This mattered immediately: the first version of the
847
1015
  name invariant read `textContent`, which never contains CSS generated content, so it passed with
848
1016
  the tick back in the accessibility tree. With `axName`, that mutation reports `"✓ Badge"`.
@@ -850,64 +1018,64 @@ Improvement note 004, both halves: a calendar you could not steer, and a check m
850
1018
  shows 12 months and moves focus into the grid, the arrows step years, and picking a month commits
851
1019
  nothing.
852
1020
 
853
- ## [1.31.0] 2026-08-18
1021
+ ## [1.31.0] - 2026-08-18
854
1022
  Improvement note 002: density was a one-way door.
855
1023
  ### Added
856
1024
  - **`[data-density="comfortable"]` is now a real rule.** The kit shipped only the `compact` block,
857
- and its own comment promised per-subtree density true in one direction only. These are
1025
+ and its own comment promised per-subtree density, true in one direction only. These are
858
1026
  inheriting custom properties, so once `<html>` is compact *every* subtree is compact and a
859
1027
  `comfortable` wrapper matched nothing at all. An app that is dense overall (a back office where
860
1028
  most screens are data grids) could not opt its shared chrome back out: measured against a sibling
861
- product, the logo sat 12px from the edge instead of 16 and the avatar 12 instead of 20 four
1029
+ product, the logo sat 12px from the edge instead of 16 and the avatar 12 instead of 20: four
862
1030
  pixels in two places, from `--space-4` and `--control-h`, which reads as sloppiness rather than as
863
1031
  a token. The workaround, restating the five defaults on a local class, is exactly the copy that
864
1032
  goes stale when the kit retunes a step.
865
1033
  Note the kit's own README has been telling people to write `data-density="comfortable"` on `<html>`
866
- since long before a rule existed for it harmless there, because it matched the defaults by
1034
+ since long before a rule existed for it, harmless there because it matched the defaults by
867
1035
  accident, and broken the moment anyone needed it on a wrapper.
868
1036
  ### Notes on the shape of the fix
869
1037
  - The five values are **derived from the tokens' own defaults** at build time, not written out a
870
1038
  second time. Same key set by construction, so a token that gains a `$compact` value automatically
871
1039
  gains its way back out, and a retuned default can never leave the two blocks disagreeing. The
872
- report's hand-written block was correct in all five values this only removes the chance for it
1040
+ report's hand-written block was correct in all five values; this only removes the chance for it
873
1041
  to stop being correct.
874
- ### Added guards
1042
+ ### Added: guards
875
1043
  - `test/build.test.mjs` asserts the two density blocks cover the *same* tokens, that every
876
1044
  comfortable value equals the `:root` default, and that none of them equals its compact counterpart
877
1045
  (a block that resets nothing is as green as one that works).
878
1046
  - `browser/theme.mjs` measures it in a real engine: a compact root, a `comfortable` island that
879
- returns to a 40px control and `--space-4: 1rem`, and a `compact` island nested back inside that
1047
+ returns to a 40px control and `--space-4: 1rem`, and a `compact` island nested back inside that,
880
1048
  because the point of the rule is the box that comes out, not the declaration going in.
881
1049
 
882
- ## [1.30.0] 2026-08-18
1050
+ ## [1.30.0] - 2026-08-18
883
1051
  Note 008 (adopting 1.29.0) plus a direct design report: *"the input border looks too dark next to the
884
1052
  card and button-group borders."*
885
1053
  ### Fixed
886
1054
  - **The dark theme's contrast has not actually been tested since v1.21.0.** `contrast.test.mjs` looked
887
1055
  for `:root[data-theme="dark"]`; un-rooting the theme selectors in 1.21.0 changed the emitted
888
1056
  selector to `[data-theme="dark"]`, so the regex matched nothing, `dark` fell back to the **light**
889
- values, and every `WCAG contrast DARK` assertion re-tested the light theme under a dark label
1057
+ values, and every `WCAG contrast, DARK` assertion re-tested the light theme under a dark label,
890
1058
  54 declarations ignored, all green. Fixed, and the scope now has a guard of its own: a dark scope
891
1059
  that parses to nothing, or that resolves to the light surface, fails loudly. *No latent dark-theme
892
- failures were hiding behind it the theme passes on its own merits.*
1060
+ failures were hiding behind it; the theme passes on its own merits.*
893
1061
  - **`--color-control-border` retuned** to a new ramp step `--slate-450` (`#798295`). It was
894
1062
  `--slate-500`, the same ink as `--color-text-subtle`: a control **boundary** carrying text-level
895
1063
  contrast (4.69:1 on surface where WCAG 1.4.11 asks 3:1), which is what makes a form look heavier
896
1064
  than the cards around it. Now ≈3.9/3.6/3.4 across the three light surfaces. The same step
897
- *strengthens* the dark theme, which was sitting at **3.02:1** on `surface-3` 0.02 above the
1065
+ *strengthens* the dark theme, which was sitting at **3.02:1** on `surface-3`, 0.02 above the
898
1066
  floor, and unguarded because of the bug above; it is now 3.66:1.
899
1067
  - **`.fdy-stat__label` uses `--color-text-muted`.** It spent `--color-text-subtle`, which is gated at
900
- 3.0 *on purpose* (placeholders, dividers, decorative glyphs) and measures 4.41:1 on `surface-2` —
1068
+ 3.0 *on purpose* (placeholders, dividers, decorative glyphs) and measures 4.41:1 on `surface-2`,
901
1069
  under AA for text, on the only thing naming the number above it. Reported in 006 §6 and still
902
1070
  carried by the app at 1.29.0.
903
1071
  ### Added
904
1072
  - **`.fdy-btn--stretch` now works in a `.fdy-list__row`** (note 008 §1). The pattern existed for
905
1073
  "one card, one primary action, one escape hatch" and had no equivalent for the same sentence with
906
- *row* in it an app rendering both grid and list from one component had to hand-roll the row half.
1074
+ *row* in it, so an app rendering both grid and list from one component had to hand-roll the row half.
907
1075
  The row anchors the overlay itself, opt-in through `:has(.fdy-btn--stretch)` so a plain row is
908
1076
  untouched and nothing an app pinned inside one starts resolving against a new containing block.
909
1077
  Measured before the fix: **every** row's overlay covered the whole list and the last one in the DOM
910
- won, so a click on row one opened row two under row one's name.
1078
+ won, so a click on row one opened row two, under row one's name.
911
1079
  ### Notes on the shape of the fix
912
1080
  - **Note 008 §2 is not reproducible** and no code shipped for it. With a positive control to prove
913
1081
  the measurement works (pointing at the card body *does* put the stretched target in `:hover`),
@@ -915,78 +1083,78 @@ card and button-group borders."*
915
1083
  hatch`, and the stretched button is a sibling, not an ancestor. The `pointer-events:none` rule the
916
1084
  report carries fixes something the kit's own composition does not do.
917
1085
  - The report's contrast figures (4.41 / 4.69) were re-derived from the 1.29 ramp and are exact.
918
- ### Added guards
1086
+ ### Added: guards
919
1087
  - `contrast.test.mjs`: the control border is now asserted on **all three** surfaces (`surface-3` was
920
1088
  missing, and it is the worst case that pinned the dark theme to the floor) and at **3.25**, not
921
- 3.0 a boundary that clears the floor by 0.02 has no headroom, and the margin is what catches a
1089
+ 3.0, and a boundary that clears the floor by 0.02 has no headroom, and the margin is what catches a
922
1090
  revert to the old ink.
923
1091
  - `css.test.mjs`: a stat label must spend a *text* ink, and a list row hosting a stretched target
924
- must anchor it both are class↔token pairings that every token-level assertion passes straight
1092
+ must anchor it; both are class↔token pairings that every token-level assertion passes straight
925
1093
  through.
926
1094
  - `browser/card-stretch.mjs`: the list case, asserting **row one** (the point that hides the bug is
927
1095
  row two, which passes even when the overlay belongs to another row).
928
1096
 
929
- ## [1.29.0] 2026-08-18
1097
+ ## [1.29.0] - 2026-08-18
930
1098
  Improvement note 001 from a second consuming app (a 40-screen back office). Eight findings; six
931
1099
  executed, one already documented, one left as an owner decision.
932
1100
  ### Added
933
1101
  - **`clearable` on `FdyCfl`** (Vue · React · Blazor). The value type already said `Row | null`, but
934
- the emit was `Row` only a choose-from-list could be **set and never unset**, which breaks every
1102
+ the emit was `Row` only, so a choose-from-list could be **set and never unset**, which breaks every
935
1103
  *optional* foreign key (a device with no project, an expense with no workflow, a top-level record
936
1104
  with no parent). A user who picked the wrong row had to reload the form. The clear control is a
937
1105
  second `.fdy-input-group__btn`, emits `null`, returns focus to the trigger, and never touches the
938
1106
  dialog.
939
- - **`.fdy-label--required`** the marker is painted through `::after` with **CSS alt text**
1107
+ - **`.fdy-label--required`** paints the marker through `::after` with **CSS alt text**
940
1108
  (`content:"*" / ""`), so the glyph appears and the accessibility tree gets nothing. The control
941
1109
  already carries `required`; a `<span>` an app has to remember to mark `aria-hidden` can be
942
1110
  forgotten, this cannot.
943
- - **`.fdy-icon`** a `1em` square, `flex:none` box for the icons the kit deliberately does not
1111
+ - **`.fdy-icon`** is a `1em` square, `flex:none` box for the icons the kit deliberately does not
944
1112
  ship. Every icon slot it *does* ship (`.fdy-btn__icon`, `.fdy-nav__icon`, `.fdy-state__icon`)
945
1113
  already assumed something sensible inside; a standalone icon had no contract.
946
- - **`.fdy-text-success` · `.fdy-text-warning` · `.fdy-text-danger`** the kit had three
1114
+ - **`.fdy-text-success` · `.fdy-text-warning` · `.fdy-text-danger`**: the kit had three
947
1115
  *de-emphasis* text roles and no *state* role, so a consequential sentence ("Amount changed from X
948
1116
  to Y", "No approver assigned") had to fall back to `.fdy-text-caption`, which de-emphasises
949
1117
  exactly the line that should stand out. A badge is wrong (prose, not a status) and an alert is
950
1118
  wrong (a block, not one line in a row).
951
1119
  ### Fixed
952
1120
  - **`.fdy-menu__item:focus-visible` gets a real ring.** It marked focus with the same fill as
953
- `:hover`, and `freeday-menu.js` moves real DOM focus so arrowing through a menu was invisible,
1121
+ `:hover`, and `freeday-menu.js` moves real DOM focus, so arrowing through a menu was invisible,
954
1122
  and hover and focus were identical to everyone else. `.fdy-nav__item`, in the same kit, has always
955
1123
  done it correctly. Reported by two different apps before it was fixed.
956
1124
  - **A grouped `<fieldset>` now has its spacing.** `fieldset.fdy-field` already reset the UA border,
957
- but the **rendered legend is laid out outside the flex flow**, so `gap` never reached it
1125
+ but the **rendered legend is laid out outside the flex flow**, so `gap` never reached it,
958
1126
  measured 0px between legend and first control. One declaration on the reset the kit already
959
1127
  ships, not a new block; `COMPONENTS.md` documents the compose (`.fdy-field` on the fieldset,
960
1128
  `.fdy-label` on the legend), which is what was actually missing.
961
1129
  ### Notes on the shape of the fix
962
1130
  - **§7 (an Indonesian `aria-label`) is not what it looked like.** The report assumed the rest of the
963
- kit's output is English; it is not *every* user-visible string the vanilla enhancers write is
1131
+ kit's output is English; it is not. *Every* user-visible string the vanilla enhancers write is
964
1132
  Indonesian (`Sebelumnya`, `Berikutnya`, `Berisi teks`, `Reset`, `Tutup`, `Menampilkan …`,
965
1133
  `Bulan berikutnya`, `Format tidak valid.`). Translating one would **create** the mixed interface
966
- the note objects to. The real choice an override hook, or switching the defaults and breaking
967
- every Indonesian consumer is recorded in `NEXT-UP.md` #6 with its trigger; `COMPONENTS.md` now
1134
+ the note objects to. The real choice, an override hook or switching the defaults and breaking
1135
+ every Indonesian consumer, is recorded in `NEXT-UP.md` #6 with its trigger; `COMPONENTS.md` now
968
1136
  states the caveat in full and points English apps at the typed wrappers.
969
1137
  - **§8 needed no code**, only the line it asked for: the filter button and its dialog share an
970
1138
  accessible name on purpose, so a test suite wants `getByRole`, not `getByLabel`.
971
1139
  - Two traps found by measuring rather than reading: `.fdy-icon` as a bare inline element **ignored
972
1140
  `width` entirely** (it does not apply to non-replaced inline boxes) and rendered 936px wide until
973
1141
  it got a `display`; and stacking two colour roles (`.fdy-help.fdy-text-warning`) silently loses to
974
- whichever rule the bundle happens to emit later documented, and the docs page no longer does it.
975
- ### Added guards
1142
+ whichever rule the bundle happens to emit later. That is documented, and the docs page no longer does it.
1143
+ ### Added: guards
976
1144
  - Adapter specs for `clearable` in both Vue and React (real clicks: emits `null`, empties the field,
977
1145
  the control disappears with the value it cleared, focus lands on the trigger, dialog stays shut).
978
1146
  - CSS gate: menu focus must render as more than the hover fill; the required marker must keep its
979
1147
  alt text; `.fdy-icon` must keep a display that accepts a width; the state roles must spend the
980
- exact inks the contrast gate proves readable that last one closes the seam between the two test
1148
+ exact inks the contrast gate proves readable, and that last one closes the seam between the two test
981
1149
  files, where a class could drift to a weaker token while every token assertion stayed green.
982
1150
  - `contrast.test.mjs` now also asserts the state inks on **plain** surfaces, not just over their own
983
1151
  `-soft` fills.
984
1152
 
985
- ## [1.28.0] 2026-08-18
1153
+ ## [1.28.0] - 2026-08-18
986
1154
  Improvement note 007, written while fixing a workspace picker whose cards showed a pointer cursor
987
1155
  and swallowed every click.
988
1156
  ### Added
989
- - **`.fdy-btn--stretch`** the pattern for **one card, one primary action, one escape hatch**, which
1157
+ - **`.fdy-btn--stretch`** is the pattern for **one card, one primary action, one escape hatch**, which
990
1158
  the kit had no shape for: `--button` is a card that *is* one control, `--interactive` a card that
991
1159
  merely *has* one, and a card with two actions can be neither (interactive content nested in a
992
1160
  `<button>` is invalid HTML). The primary control keeps its real `<button>`/`<a>` semantics and
@@ -995,14 +1163,14 @@ and swallowed every click.
995
1163
  button nudges itself with a transform on `:hover`/`:active`, a transformed element becomes the
996
1164
  containing block for its own absolutely positioned descendants, and the overlay therefore
997
1165
  re-anchors from the card to the button's own box mid-gesture. `mousedown` lands on the button,
998
- `mouseup` somewhere else, and the browser fires `click` on their common ancestor the button never
1166
+ `mouseup` somewhere else, and the browser fires `click` on their common ancestor, so the button never
999
1167
  gets one. The symptom is identical to the bug being fixed.
1000
1168
  The report found this on `--text`/`--ghost`, which break on press. The kit's **base** rule is
1001
1169
  `.fdy-btn:hover{transform:translateY(-1px)}`, so the default and `--danger` fills break one step
1002
- earlier, on hover: neutralising only `:active` the obvious half-fix still leaks.
1170
+ earlier, on hover: neutralising only `:active`, the obvious half-fix, still leaks.
1003
1171
  - **Escape hatches are raised automatically.** Every focusable element in a card holding a stretched
1004
1172
  target sits above the overlay without markup or CSS from the consumer, because forgetting a
1005
- `z-index` here fails silently the control looks and hovers exactly as before and simply never
1173
+ `z-index` here fails silently: the control looks and hovers exactly as before and simply never
1006
1174
  receives the click.
1007
1175
  ### Fixed
1008
1176
  - `--interactive` is now documented as **presentational**: the only affordance in the kit whose
@@ -1012,13 +1180,13 @@ and swallowed every click.
1012
1180
  ### Notes on the shape of the fix
1013
1181
  - **Raising the escape hatch must not outweigh the app.** The first version declared
1014
1182
  `position:relative` at normal specificity and *measurably* dragged an absolutely positioned corner
1015
- dismiss button back into the flow a control the app had pinned itself, moved by the kit, with no
1183
+ dismiss button back into the flow, a control the app had pinned itself, moved by the kit, with no
1016
1184
  error. `z-index` still wins, but the `position` it needs is now a zero-specificity `:where()`
1017
1185
  default that any app rule beats. The two rules must stay split; the gate asserts it.
1018
1186
  - **Cards only.** The overlay anchors to the nearest *positioned* ancestor. `.fdy-list__row` is not
1019
- positioned (`.fdy-list` is), so a stretched target in a list row would cover the whole list
1187
+ positioned (`.fdy-list` is), so a stretched target in a list row would cover the whole list,
1020
1188
  documented, with `.fdy-list__row--button` as the answer for clickable rows.
1021
- ### Added guards
1189
+ ### Added: guards
1022
1190
  - `browser/card-stretch.mjs` drives real presses: the target receives clicks from anywhere on the
1023
1191
  card, the click is dispatched **on the button** rather than on a common ancestor, the secondary
1024
1192
  action and an inline link keep their own clicks, and an app-pinned control keeps its own
@@ -1027,43 +1195,43 @@ and swallowed every click.
1027
1195
  - `test/css.test.mjs` guards all of it at the CI gate. Mutation-checked against six defects,
1028
1196
  including both of the ones written during this change.
1029
1197
 
1030
- ## [1.27.0] 2026-08-14
1198
+ ## [1.27.0] - 2026-08-14
1031
1199
  Improvement note #44, found while building a settings screen whose only numeric field looked like it
1032
1200
  belonged to a different application.
1033
1201
  ### Fixed
1034
1202
  - **`.fdy-input[type="number"]` no longer shows the user agent's spin buttons.** They are OS widgets
1035
1203
  in OS colours that no theme reaches, so on a dark surface they read as a light-grey artefact glued
1036
- to an otherwise themed field the one unthemed control on a page where everything else is themed
1204
+ to an otherwise themed field, the one unthemed control on a page where everything else is themed
1037
1205
  to the last pixel. Both halves ship, because they cover different engines: `appearance: textfield`
1038
1206
  (Firefox) and `::-webkit-outer/inner-spin-button { appearance: none }` (Blink/WebKit).
1039
1207
  ### Added
1040
- - **`[data-fdy-number]` + `freeday-number.js`** the increment affordance back on the kit's terms,
1208
+ - **`[data-fdy-number]` + `freeday-number.js`** put the increment affordance back on the kit's terms,
1041
1209
  since hiding the native buttons removes it. **Not a new block:** it is an `.fdy-input-group` with
1042
1210
  two `__btn`s, so it inherits the shared border, `:focus-within` ring and `:has()` error promotion
1043
1211
  that already existed. The reported `.fdy-number` block would have duplicated all of it.
1044
1212
  - **No custom event.** Stepping dispatches native bubbling `input` + `change` on the input, so
1045
- `v-model` / `onChange` / `@bind` work with no adapter and no new API the input stays the source
1213
+ `v-model` / `onChange` / `@bind` work with no adapter and no new API, because the input stays the source
1046
1214
  of truth. That is also why this needs no typed wrapper in any of the four stacks.
1047
1215
  - `min`/`max`/`step` live on the input and the buttons never redo the arithmetic (`stepUp()`/
1048
1216
  `stepDown()` clamp for free). They go `disabled` at a bound, on a `disabled`/`readonly` field,
1049
- and when `step="any"` which has no defined increment and makes `stepUp()` throw, so a stepper
1217
+ and when `step="any"`, which has no defined increment and makes `stepUp()` throw, so a stepper
1050
1218
  cannot honestly express it.
1051
1219
  - A `MutationObserver` watches `disabled`/`readonly`/`min`/`max`/`step`: a framework changes those
1052
1220
  without firing an event, and a button that still looks enabled while doing nothing is the exact
1053
- lie this state machine exists to prevent. (First observer in the kit the alternative was a
1221
+ lie this state machine exists to prevent. (First observer in the kit; the alternative was a
1054
1222
  button whose appearance silently drifts from its behaviour.)
1055
1223
  - The buttons are **not tab stops** (`tabindex="-1"`): the input is already focusable and ↑/↓
1056
1224
  already step it, so two extra stops per field cost every keyboard user and buy nothing. They keep
1057
1225
  an `aria-label`, and `type="button"` so they cannot submit their form.
1058
1226
  - `.fdy-input-group__btn` gains a **leading-position** rule (divider on the correct side when the
1059
- button comes first) and a **`:disabled`** state dim + `not-allowed`, with `:hover` withdrawn,
1227
+ button comes first) and a **`:disabled`** state: dim + `not-allowed`, with `:hover` withdrawn,
1060
1228
  because a disabled button still matches `:hover`.
1061
1229
  - `COMPONENTS.md` now answers **which `type` `.fdy-input` covers**, which is the question that would
1062
1230
  have prevented this note: every text-like type themes identically, and the two that keep a native
1063
1231
  widget are named with what to do about each.
1064
1232
  ### Notes on the shape of the fix
1065
- - **`type="search"` deliberately left alone.** Its WebKit clear (×) button is unthemed too and it
1066
- is on the kit's own docs pages, 10 of them but it is the only way to empty the field. Stripping
1233
+ - **`type="search"` deliberately left alone.** Its WebKit clear (×) button is unthemed too, and it
1234
+ is on the kit's own docs pages, 10 of them, but it is the only way to empty the field. Stripping
1067
1235
  it removes function, not chrome; the number arrows only removed a mouse-only increment that ↑/↓
1068
1236
  still provides. `type="date"`/`time` likewise keep their picker indicator; the kit ships its own
1069
1237
  datepicker/timepicker and the docs now say to use those instead.
@@ -1071,27 +1239,27 @@ belonged to a different application.
1071
1239
  `docs/reference-screen.html` and `examples/` (against `search` ×10, `text` ×12, `date` ×4). The kit
1072
1240
  never used the input it shipped. The docs page now carries a real number field, in the input-group
1073
1241
  section where it belongs.
1074
- ### Added guards
1242
+ ### Added: guards
1075
1243
  - `test/css.test.mjs` asserts both engine halves survive (each is invisible in review and each brings
1076
1244
  the artefact back on one engine only), and states in-file why `search`/`date` are excluded.
1077
1245
  - `browser/number.mjs` drives real clicks and a real Tab: stepping fires native `input` + `change`,
1078
1246
  bounds disable the right button, a disabled button changes nothing, `readonly` and `step="any"`
1079
1247
  are inert without throwing, and Tab skips the buttons. Mutation-checked against five defects.
1080
- - `browser/harness.mjs` gains `pressKey` tab order only moves for a trusted key, so the claim is
1248
+ - `browser/harness.mjs` gains `pressKey`, since tab order only moves for a trusted key, so the claim is
1081
1249
  measured rather than read off an attribute.
1082
1250
 
1083
- ## [1.26.0] 2026-08-14
1251
+ ## [1.26.0] - 2026-08-14
1084
1252
  Improvement note #43, found while chasing a "the upload is stuck" report on a 626 KB PDF: the
1085
1253
  transfer took about a second, the server then spent nearly a minute reading the document.
1086
1254
  ### Added
1087
- - **`row.waiting(label)`** the state between `setProgress` and `done`: the bytes are gone, the
1255
+ - **`row.waiting(label)`** is the state between `setProgress` and `done`: the bytes are gone, the
1088
1256
  server has not answered. The row's only long-running state was named after the *transfer*, so it
1089
- kept saying "Mengunggah…" for the whole minute of server-side work and `setProgress(100)` made it
1257
+ kept saying "Mengunggah…" for the whole minute of server-side work, and `setProgress(100)` made it
1090
1258
  worse, because a full bar that then sits still is the most convincing "hung" signal a UI can
1091
1259
  produce. There was no way out within the row's API: `done()` claims success, `fail()` claims an
1092
1260
  error, `ready()` walks backwards. Consumers were rendering a second status line outside the row and
1093
1261
  leaving the row to contradict it.
1094
- The bar goes **indeterminate** and drops `aria-valuenow` a progressbar with no value is exactly
1262
+ The bar goes **indeterminate** and drops `aria-valuenow`, and a progressbar with no value is exactly
1095
1263
  what ARIA calls indeterminate, which is the contract `COMPONENTS.md` already stated for
1096
1264
  `.fdy-progress`. The label is the consumer's, because only they know what the server is doing
1097
1265
  (`Membaca PDF…`, `Memindai…`); it falls back to `Menunggu server…`.
@@ -1100,107 +1268,107 @@ transfer took about a second, the server then spent nearly a minute reading the
1100
1268
  ### Notes on the shape of the fix
1101
1269
  - The report's patch would have shipped the symptom it set out to remove. It put the modifier on the
1102
1270
  **bar** (`.fdy-progress--indeterminate` styles `.fdy-progress__bar`, so it belongs on the
1103
- container on the bar it matches nothing) and then set an inline `width:100%`, which beats the
1271
+ container, so on the bar it matches nothing) and then set an inline `width:100%`, which beats the
1104
1272
  modifier's own width anyway. Both mistakes render a full, frozen bar. The note also hedged that
1105
1273
  `.fdy-progress--indeterminate` might not exist; it has all along.
1106
1274
  - **Leaving the state needs more care than entering it.** `.fdy-progress__bar` is a plain block div:
1107
1275
  with no width it fills its track. So `uploading()`/`setProgress()` restore an explicit width when
1108
1276
  they clear the modifier, or a retried row paints a *full* bar while meaning 0%. `done()`, `fail()`
1109
- and `ready()` need no counterpart they drop the progress element outright, modifier and all
1277
+ and `ready()` need no counterpart, since they drop the progress element outright, modifier and all
1110
1278
  (contrary to the note, which expected a line in each).
1111
1279
  - **No `.fdy-file--waiting` class.** `uploading` has none either; only `--success`/`--error` do,
1112
1280
  because they carry colour. A documented class with no rule is markup that looks like it does
1113
1281
  something.
1114
1282
  - Under `prefers-reduced-motion: reduce` the kit's indeterminate treatment is a dimmed **full** bar
1115
- (no animation left to carry the meaning) pre-existing behaviour for every indeterminate progress,
1283
+ (no animation left to carry the meaning), pre-existing behaviour for every indeterminate progress,
1116
1284
  not introduced here. For those users the honest signal is the label, not the bar.
1117
- ### Added guards
1285
+ ### Added: guards
1118
1286
  - `browser/upload-states.mjs` gains a third spec, measuring what the **engine renders** rather than
1119
1287
  what the source declares: both ways to get this wrong are invisible in a code read. Mutation-checked
1120
- against five defects, including the report's own two modifier-on-bar, inline `width:100%`, no
1288
+ against five defects, including the report's own two: modifier-on-bar, inline `width:100%`, no
1121
1289
  width restored on return, `aria-valuenow` kept, and label ignored.
1122
1290
 
1123
- ## [1.25.0] 2026-08-13
1124
- Improvement note #42, found while adopting 1.24.0 the other half of the same integration.
1291
+ ## [1.25.0] - 2026-08-13
1292
+ Improvement note #42, found while adopting 1.24.0, the other half of the same integration.
1125
1293
  ### Fixed
1126
1294
  - **`fdy-upload-remove` now fires on the dropzone**, the same element as `fdy-upload-add`. It was
1127
- dispatched on the *row*, which lives in the file list and the kit's own markup contract puts that
1295
+ dispatched on the *row*, which lives in the file list, and the kit's own markup contract puts that
1128
1296
  list as a **sibling** of the dropzone, so the event never bubbled through the zone. A consumer
1129
1297
  following the documentation got `add` and never got `remove`: no error, right event name, right
1130
1298
  element, and the other event on that element working. Their state kept a file the user had already
1131
1299
  taken away.
1132
- The kit's own header comment was the source of the mistake it said both events are emitted "on the
1300
+ The kit's own header comment was the source of the mistake: it said both events are emitted "on the
1133
1301
  dropzone" while the code dispatched one of them somewhere else. It now states the target for each,
1134
1302
  and why (a row in a sibling list can never reach the zone). `COMPONENTS.md` says it once beside the
1135
1303
  row state table, with the removal listener in the worked example.
1136
1304
  ### Notes on the shape of the fix
1137
1305
  - The report proposed dispatching on **both** the row and the zone, for backward compatibility. Not
1138
- taken, and the guard proves why: when the file list is **nested inside** the dropzone which
1139
- `data-filelist` permits the row already bubbles through the zone, so a second dispatch makes a
1306
+ taken, and the guard proves why: when the file list is **nested inside** the dropzone, which
1307
+ `data-filelist` permits, the row already bubbles through the zone, so a second dispatch makes a
1140
1308
  plain zone listener fire **twice** per removal. Firing on both also leaves the pair asymmetric (one
1141
1309
  `add`, two `remove`s for anyone delegating on a common ancestor), which is the same class of silent
1142
1310
  bug this note is about. One canonical target is the honest fix.
1143
1311
  - Dispatching on the zone also fixes the **listless** case from 1.24.0: a row that was never attached
1144
1312
  to the document bubbles to nothing at all, so its × was previously unobservable.
1145
1313
  - *Migration:* a consumer that worked around the old behaviour by listening on the file list must
1146
- move that listener to the dropzone. That position was never documented it was the bug.
1147
- ### Added guards
1314
+ move that listener to the dropzone. That position was never documented; it was the bug.
1315
+ ### Added: guards
1148
1316
  - `browser/upload-states.mjs` gains a second spec: removal fires on the dropzone **exactly once**, in
1149
1317
  both layouts (list as sibling, list nested inside the zone), and delegation on a shared ancestor
1150
- sees no duplicates. Mutation-checked against *both* rejected designs reverting to the row target
1318
+ sees no duplicates. Mutation-checked against *both* rejected designs: reverting to the row target
1151
1319
  fails it, and so does the report's dispatch-on-both.
1152
1320
 
1153
- ## [1.24.0] 2026-08-13
1321
+ ## [1.24.0] - 2026-08-13
1154
1322
  Improvement note #41: the upload row had no "chosen, not yet sent" state, so every consumer-driven
1155
1323
  integration showed a transfer that had not started.
1156
1324
  ### Fixed
1157
1325
  - **A dropped file now rests until the consumer starts the transfer.** `handleFiles` called
1158
- `row.uploading()` unconditionally and *before* dispatching `fdy-upload-add`, so a consumer could
1326
+ `row.uploading()` unconditionally, and *before* dispatching `fdy-upload-add`, so a consumer could
1159
1327
  not pre-empt it. Between the drop and the app's own submit button (which may be a minute, while the
1160
1328
  user fills in the rest of the form) the row claimed to be uploading, with a progress bar that never
1161
1329
  moved. A user watching that reasonably concludes the upload has hung and reports a bug against a
1162
1330
  transfer that was never started. The state machine was missing its start state: `done()` claims
1163
1331
  success, `fail()` claims an error, and `uploading()` was where it already was.
1164
- The demo path is unchanged with `data-fdy-upload-simulate` the kit *is* performing a transfer, so
1332
+ The demo path is unchanged: with `data-fdy-upload-simulate` the kit *is* performing a transfer, so
1165
1333
  showing one stays correct. That attribute already marks the only place the old default was right,
1166
1334
  which is why this changes the default rather than adding an opt-in: making correct integrations opt
1167
1335
  in to correctness is backwards.
1168
1336
  - **`fdy-upload-add` no longer depends on rendering a row.** The guard was `if (!list || !fileList)
1169
- return;`, so a dropzone with no file list lost the event that tells the app a file arrived
1337
+ return;`, so a dropzone with no file list lost the event that tells the app a file arrived,
1170
1338
  "bring your own row" silently cost you the notification. Rendering and announcing are now separate:
1171
1339
  no list means no row is attached, and the event still fires with a working `detail.row`.
1172
1340
  Deliberately **no new attribute** for this. The reporter proposed `data-rows="off"`; `list` is used
1173
1341
  in exactly two places, so decoupling covers the same case without growing the API surface, and no
1174
1342
  page can be relying on "no event".
1175
1343
  ### Added
1176
- - **`row.ready()`** the rest state, and the way back to it (a failed attempt the user will retry).
1344
+ - **`row.ready()`** is the rest state, and the way back to it (a failed attempt the user will retry).
1177
1345
  It reuses the existing `dropProgress()`, so the bar is removed exactly as `done()`/`fail()` do it.
1178
1346
  ### Docs
1179
1347
  - **`COMPONENTS.md` documents the row state machine at all.** `uploading()` / `setProgress()` /
1180
- `done()` / `fail()` had **zero** mentions anywhere in the shipped docs a consumer holding
1348
+ `done()` / `fail()` had **zero** mentions anywhere in the shipped docs, so a consumer holding
1181
1349
  `detail.row` had no supported way to know they existed, which is a fair part of why the old default
1182
1350
  went unquestioned. The section now carries the state table, a worked example, the
1183
1351
  bring-your-own-row position, and `data-fdy-upload-simulate` marked demo-only.
1184
1352
  Two corrections to the report while transcribing it: the function is `handleFiles` (not `addFiles`)
1185
- and the attribute is `data-fdy-upload-simulate` (not `data-simulate`) the latter matters, since
1353
+ and the attribute is `data-fdy-upload-simulate` (not `data-simulate`), and the latter matters, since
1186
1354
  documenting the wrong name would have consumers set an attribute that does nothing.
1187
- ### Added guards
1188
- - **`browser/upload-states.mjs`** drops a real `File` and asserts the rest state shows no progress
1355
+ ### Added: guards
1356
+ - **`browser/upload-states.mjs`** drops a real `File` and asserts the rest state shows no progress
1189
1357
  bar, that `uploading()` → `setProgress()` → `done()` still chains, that the simulate path is
1190
1358
  untouched, and that a listless dropzone still dispatches. Mutation-checked on both halves.
1191
1359
  The fixture wraps the listless dropzone in its own container **on purpose**: with no
1192
1360
  `data-filelist` the enhancer falls back to `parentNode.querySelector('.fdy-filelist')`, so a bare
1193
- dropzone sharing a parent with another list adopts it the first version of this guard was
1361
+ dropzone sharing a parent with another list adopts it; the first version of this guard was
1194
1362
  testing nothing, and the mutation run is what exposed that.
1195
1363
 
1196
- ## [1.23.0] 2026-08-13
1364
+ ## [1.23.0] - 2026-08-13
1197
1365
  Consumption round 6 (`improvement-notes/006`). The reporter filed two of the three as **their own**
1198
- bugs rather than the kit's and they were right about the code, but in both cases the kit had a way
1366
+ bugs rather than the kit's, and they were right about the code, but in both cases the kit had a way
1199
1367
  to make the mistake impossible and had not taken it. Those are the two most valuable findings here.
1200
1368
  ### Fixed
1201
1369
  - **`initAll(ctx)` now enhances `ctx` itself, not just its descendants.** `querySelectorAll` never
1202
- matches its own root, so a framework ref placed **on** the widget `<div ref="menu" data-fdy-menu>`,
1203
- which is the ordinary shape when a component's root element *is* the widget meant the one element
1370
+ matches its own root, so a framework ref placed **on** the widget, `<div ref="menu" data-fdy-menu>`,
1371
+ which is the ordinary shape when a component's root element *is* the widget, meant the one element
1204
1372
  that needed enhancing was the only one that could not be found. It failed with **no error, no
1205
1373
  warning, and a UI that looked finished**: the markup rendered and simply never opened. Measured
1206
1374
  before the fix: `data-fdy-menu-ready` stayed `null` and `aria-expanded` stayed `"false"` while
@@ -1210,7 +1378,7 @@ to make the mistake impossible and had not taken it. Those are the two most valu
1210
1378
  matched by the descendant query costs nothing. `useFreeday` (Vue/React) and `FreedayBlazor.initAll`
1211
1379
  delegate to these, so they inherit the fix with no change of their own.
1212
1380
  - **The stepper connector no longer draws through the markers when `__btn` is omitted.** The lift now
1213
- lives on `.fdy-step__marker` the part that must always exist instead of only on
1381
+ lives on `.fdy-step__marker`, the part that must always exist, instead of only on
1214
1382
  `.fdy-step__btn`. The connector is `position:absolute; z-index:0`, and a positioned box paints
1215
1383
  *after* in-flow inline content in the same stacking context, so a marker that was merely inline got
1216
1384
  painted over: not for lack of a z-index, but for lack of being positioned at all. An
@@ -1218,14 +1386,14 @@ to make the mistake impossible and had not taken it. Those are the two most valu
1218
1386
  a visible defect in shipped UI. Measured: hit-testing the marker's centre returned the connector's
1219
1387
  own `.fdy-step` before, the marker after. `__btn` keeps its z-index for the navigable case.
1220
1388
  ### Added
1221
- - **`.fdy-avatar--xs`** (1.5rem, `--text-xs`) an avatar inside a control had nothing to reach for:
1389
+ - **`.fdy-avatar--xs`** (1.5rem, `--text-xs`): an avatar inside a control had nothing to reach for.
1222
1390
  `.fdy-btn--sm` is `calc(var(--control-h) - var(--space-2))` = **2rem**, exactly `--avatar--sm`'s
1223
1391
  size, so the monogram filled a small button edge to edge and the ghost border crossed the circle
1224
1392
  (measured: trigger 32px tall, avatar 32×32). 1.5rem inside 2rem leaves the ~4px the kit's other
1225
- controls give their icons. The report suggested `--text-2xs`; that token does not exist `--text-xs`
1393
+ controls give their icons. The report suggested `--text-2xs`; that token does not exist, so `--text-xs`
1226
1394
  is the smallest the kit has, and inventing a type step for one avatar size is not worth it.
1227
- ### Added guards
1228
- - **`browser/root-init.mjs`** mounts a widget *after* load, initialises it through its own root, and
1395
+ ### Added: guards
1396
+ - **`browser/root-init.mjs`** mounts a widget *after* load, initialises it through its own root, and
1229
1397
  drives a **real click** to prove it opens; repeated for a second enhancer so the fix reads as the
1230
1398
  shared pattern rather than a one-off. Mutation-checked on both. Being marked `-ready` is explicitly
1231
1399
  not accepted as passing: an early version of this check reported success against markup the enhancer
@@ -1236,54 +1404,54 @@ to make the mistake impossible and had not taken it. Those are the two most valu
1236
1404
  never said which parts were optional); avatar gains its size table with the in-control guidance.
1237
1405
  - Both `useFreeday` docstrings now show the ref-on-the-widget shape alongside the wrapping one.
1238
1406
 
1239
- ## [1.22.0] 2026-08-12
1240
- Two bodies of work. **(a)** three more findings from consumption round 5 the report grew §5-§7
1407
+ ## [1.22.0] - 2026-08-12
1408
+ Two bodies of work. **(a)** three more findings from consumption round 5, where the report grew §5-§7
1241
1409
  after 1.21.0 was cut; **(b)** a **routing** failure found in the same adopted project, which had been
1242
1410
  built on raw markup + enhancers inside a framework that has typed wrappers because nothing in the
1243
1411
  package ever told it otherwise. (b) was prepared as 1.21.1 and folded in here rather than shipped as
1244
1412
  a separate patch minutes earlier.
1245
- ### Added from consumption round 5 (§5, §6)
1246
- - **`.fdy-nav--horizontal`** the same navigation links laid out as a **row**, for a top-nav
1413
+ ### Added: from consumption round 5 (§5, §6)
1414
+ - **`.fdy-nav--horizontal`** lays the same navigation links out as a **row**, for a top-nav
1247
1415
  application that puts its primary nav in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar.
1248
1416
  Deliberately a modifier, not a new block: the item, its states and `aria-current="page"` are
1249
1417
  unchanged. On `.fdy-appbar--primary` the links go on-colour automatically (full on-colour ink in
1250
1418
  every state; the current page is carried by the background wash plus the semibold weight the base
1251
1419
  rule already applies, so no dimmed variant trades away the bar's guaranteed contrast).
1252
1420
  Reported as "there is no horizontal navigation component": `.fdy-nav` was the sidebar's vertical
1253
- menu and `.fdy-appbar` had `__brand`/`__spacer`/`__actions` but nothing to put *links* in so a
1421
+ menu and `.fdy-appbar` had `__brand`/`__spacer`/`__actions` but nothing to put *links* in, so a
1254
1422
  top-nav app had a component for the bar and none for what the bar is mostly made of.
1255
1423
  - **`.fdy-tabs__tab` also honours `aria-current="page"`**, so the tab *look* legitimately serves
1256
1424
  **routed** sub-navigation (`/settings/profile` · `/settings/billing`) built from real links. A link
1257
- cannot be `aria-selected` that is invalid ARIA on an anchor so the app that borrowed the class
1425
+ cannot be `aria-selected`, which is invalid ARIA on an anchor, so the app that borrowed the class
1258
1426
  had to restate the active style for a second attribute. Keep `role="tab"` + `freeday-tabs.js` for
1259
1427
  **in-page** tabs, where the roving-tabindex / one-panel-per-tab contract is real.
1260
- - **Pressed state for toggle buttons** `aria-pressed="true"` now styles: soft primary fill + strong
1428
+ - **Pressed state for toggle buttons:** `aria-pressed="true"` now styles a soft primary fill + strong
1261
1429
  ink on `--ghost`/`--text`, an inverted-gradient sunken look on the solid button, and the same
1262
1430
  treatment in the danger hue. No new class or markup contract, since `aria-pressed` is already the
1263
- correct attribute and it is what turns `.fdy-btn-group` from a joined row into a **complete**
1431
+ correct attribute, and it is what turns `.fdy-btn-group` from a joined row into a **complete**
1264
1432
  segmented control. Previously "which one am I on" was left to each app to hand-tint, which is a
1265
1433
  colour decision the kit should own.
1266
1434
  *Found while building it:* one shared `.fdy-btn[aria-pressed="true"]` rule **cannot** work. A
1267
1435
  gradient is a background-**image**, so the `background` shorthand resets `background-color` to
1268
- transparent at equal specificity the later rule won and the ghost segment lost its fill entirely.
1436
+ transparent, so at equal specificity the later rule won and the ghost segment lost its fill entirely.
1269
1437
  Each fill variant now defines its own pressed look. The CSS read correctly in the file; only a real
1270
1438
  engine showed it, which is why the new guard lives in `browser/`.
1271
- ### Fixed from consumption round 5 (§7)
1439
+ ### Fixed: from consumption round 5 (§7)
1272
1440
  - **Hard rule 1 in `COMPONENTS.md` now records its one exception.** The rule says a modifier is
1273
1441
  always written beside its block class, but `.fdy-input-group__addon--icon` is standalone by design
1274
- adding the base `__addon` gives a search glyph the grey fill and divider of a `Rp` / `%` prefix.
1442
+ Adding the base `__addon` gives a search glyph the grey fill and divider of a `Rp` / `%` prefix.
1275
1443
  An agent following the rule literally produced the wrong control; one following the CSS comment
1276
1444
  broke the stated rule. The exception is now written down, and it is the only one.
1277
- ### Fixed the package now routes by stack
1445
+ ### Fixed: the package now routes by stack
1278
1446
  - **The paste-block in `docs/agent-onboarding.md` never mentioned the adapters.** That block is the
1279
1447
  only text that lands in a consuming project's agent instructions, so it is the only text an agent
1280
- re-reads on every task and it opened with "a CSS kit + enhancers, **not a component framework**",
1448
+ re-reads on every task, and it opened with "a CSS kit + enhancers, **not a component framework**",
1281
1449
  which actively steers a Vue/React project away from the typed components. It now opens with a
1282
1450
  **step 0 routing table** (Vue → `/vue` · React → `/react` · Blazor → RCL · everything else → raw
1283
1451
  markup), names the ten wrapped components, and states the failure mode: the raw path renders
1284
1452
  correctly the first time and then silently stops hydrating.
1285
1453
  - **`COMPONENTS.md` flags the wrapper at each of the ten sections**, so the choice is visible at the
1286
- point of use rather than only on an onboarding page read once with each component's *real*
1454
+ point of use rather than only on an onboarding page read once, with each component's *real*
1287
1455
  binding (`v-model` / `value`+`onChange` / `@bind-Value` for the six value-bound ones; `open`+`close`
1288
1456
  for modal and drawer; data props for chart; `columns`+`rows`+events for table). Hard rule 7 states
1289
1457
  it up front.
@@ -1293,126 +1461,126 @@ a separate patch minutes earlier.
1293
1461
  to the raw path, and notes that Vue/React re-implement the ten natively while Blazor wraps the
1294
1462
  enhancer over interop.
1295
1463
  ### Notes
1296
- - Root cause was structural, not editorial: at **v1.18.0** `docs/getting-started.md` the only
1297
- per-stack router the kit had was **not in `files`**, so `npm i` delivered a README whose
1464
+ - Root cause was structural, not editorial: at **v1.18.0** `docs/getting-started.md`, the only
1465
+ per-stack router the kit had, was **not in `files`**, so `npm i` delivered a README whose
1298
1466
  "Starting a project? per stack" link pointed at a path that does not exist inside `node_modules`.
1299
1467
  `adapters/` *was* shipped, so the wrappers sat in the install, unused, with nothing pointing at
1300
1468
  them. v1.20.0 started shipping the docs; this release makes them route.
1301
1469
 
1302
- ## [1.21.0] 2026-08-12
1303
- Fifth round of real-app consumption feedback, written while that app adopted 1.20.0. Two findings
1470
+ ## [1.21.0] - 2026-08-12
1471
+ Fifth round of real-app consumption feedback, written while that app adopted 1.20.0. Two findings,
1304
1472
  and a **withdrawal**: the reporter retracted 1.20's rejected §A themselves after isolating the real
1305
1473
  mechanism, which turned out to be a genuine kit bug hiding behind it. Both halves of that rejection
1306
1474
  reproduce here, so the rejection stands and the withdrawal is recorded rather than re-litigated.
1307
1475
  ### Fixed
1308
1476
  - **Hidden labels could scroll the whole page.** `.fdy-visually-hidden` is `position:absolute`, and
1309
1477
  `clip` hides *painting*, not *layout*. With no positioned ancestor its containing block is the
1310
- document, and `overflow` clips only what is contained inside the overflow box so a hidden label in
1478
+ document, and `overflow` clips only what is contained inside the overflow box, so a hidden label in
1311
1479
  a horizontally scrolling table (the kit's own recommended way to name an icon button) parks at its
1312
1480
  static position and drags the document sideways. Measured in Chrome: **1351px** of phantom page
1313
1481
  scroll from 11 spans in one table; `overflow-x:hidden` on the scroller, the shell, `body` and `html`
1314
1482
  each changed it by **0px**; removing the spans took it to 0. Every clipping/scrolling container that
1315
- holds consumer markup now declares `position:relative` `.fdy-table-scroll`, `.fdy-table-wrap`,
1316
- `.fdy-list`, `.fdy-card`, `.fdy-tabs__list`, `.fdy-carousel__viewport`, `.fdy-accordion` each
1483
+ holds consumer markup now declares `position:relative`: `.fdy-table-scroll`, `.fdy-table-wrap`,
1484
+ `.fdy-list`, `.fdy-card`, `.fdy-tabs__list`, `.fdy-carousel__viewport`, `.fdy-accordion`, each
1317
1485
  measured to take its own case to 0 with the scroller still scrolling internally. The rest are
1318
1486
  already inside something the kit positions (`<dialog>`, the sticky sidebar, a fixed popover) and are
1319
1487
  listed with that reason in the new test.
1320
1488
  **`.fdy-accordion` is the one to note:** it was contained only by its panel's reveal *animation*
1321
1489
  (a transform makes a containing block), and that animation sits behind
1322
- `prefers-reduced-motion: no-preference` so the bug was reachable **only** by readers who asked for
1490
+ `prefers-reduced-motion: no-preference`, so the bug was reachable **only** by readers who asked for
1323
1491
  reduced motion. Measured with the animation off: 2906px → 0.
1324
1492
  *Possible migration:* if you absolutely positioned something inside a card, list or table scroller
1325
1493
  and relied on it escaping, it now anchors to that container. Anchoring is the reason for the change.
1326
1494
  - **`--button` rows and cards ignored `:disabled`.** `.fdy-list__row--button` adopts the UA button box
1327
- but not its disabled state, so a row disabled mid-flight kept its hover tint and pointer cursor a
1495
+ but not its disabled state, so a row disabled mid-flight kept its hover tint and pointer cursor, a
1328
1496
  control answering the pointer while refusing input. `:disabled` and `[aria-disabled="true"]` now dim
1329
1497
  it and withdraw hover, for `.fdy-list__row--button`, `.fdy-list__row--interactive`,
1330
1498
  `.fdy-card--button` and `.fdy-card--interactive`. The report suggested `cursor:default`; the kit uses
1331
1499
  `opacity:.5` + `cursor:not-allowed` on every other disabled control, and consistency wins.
1332
1500
  ### Changed
1333
- - **`data-theme` is no longer root-scoped** the same move density made in 1.20.0, and for the same
1501
+ - **`data-theme` is no longer root-scoped**, the same move density made in 1.20.0, and for the same
1334
1502
  reason. The two *explicit* selectors are now bare `[data-theme="dark"]` / `[data-theme="light"]`, so
1335
- a `<section data-theme="dark">` inverts that region and **every component inside it follows** —
1503
+ a `<section data-theme="dark">` inverts that region and **every component inside it follows**,
1336
1504
  card surfaces, inputs, and text roles like `.fdy-title-page` that set `color: var(--color-text)`
1337
1505
  explicitly and therefore never saw a consumer's hand-rolled override. A dark brand panel beside a
1338
1506
  light sign-in form is an ordinary layout; it should not cost a re-colouring pass. Setting the
1339
1507
  attribute on `<html>` is unchanged, and a `[data-theme="light"]` island nested inside a dark region
1340
1508
  wins in turn.
1341
- **The system default stays root-scoped, deliberately** the report asked for "the two generated
1509
+ **The system default stays root-scoped, deliberately.** The report asked for "the two generated
1342
1510
  selectors", but there are three. Un-rooting `@media (prefers-color-scheme: dark) { :root:not(...) }`
1343
1511
  would make it match every element that does not itself carry `data-theme="light"`, including the
1344
1512
  *children* of a light island, dragging them back to dark. Measured: with the un-rooted variant that
1345
1513
  island renders light ink on a light surface; root-scoped, it stays correct.
1346
- ### Added guards
1347
- - **`test/css.test.mjs`** the containment invariant, CI-gated: a rule that declares `overflow` must
1514
+ ### Added: guards
1515
+ - **`test/css.test.mjs`** holds the containment invariant, CI-gated: a rule that declares `overflow` must
1348
1516
  also be positioned, or be listed with the ancestor that already contains it. Single-line truncation
1349
1517
  (`text-overflow:ellipsis` on a label) is excluded by shape, not by name, so new truncating labels
1350
1518
  don't accumulate in an allowlist. A new clipping container fails the test until someone decides
1351
1519
  which case it is.
1352
- - **`browser/layout.mjs`** the same bug end-to-end in real Chrome (`npm run test:browser`): ten
1520
+ - **`browser/layout.mjs`** reproduces the same bug end-to-end in real Chrome (`npm run test:browser`): ten
1353
1521
  hidden labels in a wide table, asserting the page cannot scroll horizontally *and* that the scroller
1354
1522
  still scrolls. A static test cannot see this failure; only a layout engine can. Both guards
1355
- mutation-checked reverting any single `position:relative` fails them.
1356
- - **`browser/state.mjs`** pressed toggles and nav orientation in a real engine: the pressed ghost
1523
+ mutation-checked: reverting any single `position:relative` fails them.
1524
+ - **`browser/state.mjs`** covers pressed toggles and nav orientation in a real engine: the pressed ghost
1357
1525
  segment must keep a flat background-colour (the mutation that reproduces the shorthand bug fails
1358
1526
  it), the solid toggle must read as inset, `--horizontal` must actually be a row with the current
1359
1527
  link marked, on-colour nav ink must stay on-colour in both states, and a routed tab must take its
1360
1528
  underline from `aria-current`. Mutation-checked against all three fixes.
1361
- - **`browser/theme.mjs`** subtree theming end-to-end: a `.fdy-title-page` and a `.fdy-card` inside
1529
+ - **`browser/theme.mjs`** covers subtree theming end-to-end: a `.fdy-title-page` and a `.fdy-card` inside
1362
1530
  `<section data-theme="dark">` take the dark tokens, a nested light island goes back, and
1363
1531
  `data-theme` on `<html>` still themes everything. `test/build.test.mjs` guards the selector shape
1364
1532
  (not root-scoped, media block still is, and the block order the cascade depends on); this guards the
1365
1533
  behaviour that shape exists for. Mutation-checked: re-rooting the selectors fails it.
1366
1534
  ### Notes
1367
1535
  - Not adopted from the report, deliberately: `.fdy-datatable`, `.fdy-modal__body`, `.fdy-drawer__body`
1368
- were listed as needing the same fix. Measured: they do not escape `<dialog>` is `position:fixed`
1536
+ were listed as needing the same fix. Measured: they do not escape, because `<dialog>` is `position:fixed`
1369
1537
  and is already their containing block, and the datatable's own scrolling child now carries it. They
1370
1538
  are in the test's allowlist with that reason instead of carrying a declaration that does nothing.
1371
1539
 
1372
- ## [1.20.0] 2026-08-12
1540
+ ## [1.20.0] - 2026-08-12
1373
1541
  Two bodies of work in one release (1.19.0 was prepared but never committed, tagged or published, so
1374
1542
  it is folded in here rather than left as a phantom version):
1375
1543
  **(a)** make the kit consumable by an **AI coding agent** in a new or migrating project, and clean the
1376
- repo to production level; **(b)** act on the fourth round of real-app consumption feedback five
1544
+ repo to production level; **(b)** act on the fourth round of real-app consumption feedback: five
1377
1545
  confirmed gaps, one rejected premise, and one documentation bug of our own that chasing it exposed.
1378
- ### Added from consumption round 4
1379
- - **`.fdy-list` / `.fdy-list__row` the flat row container** (`src/components/list.css`). One bordered
1546
+ ### Added: from consumption round 4
1547
+ - **`.fdy-list` / `.fdy-list__row`, the flat row container** (`src/components/list.css`). One bordered
1380
1548
  surface, `--color-border-muted` hairline dividers, **no shadow**; `--interactive` for hover, and
1381
1549
  `--button` for when the row *is* the control (UA box reset without losing the list surface), plus
1382
1550
  `__main` / `__title` / `__meta` / `__aside` internals. `USAGE.md` §3 has always said list rows should
1383
- be flat, but the only container the kit shipped was `.fdy-card` which carries `--shadow-lift`, a
1551
+ be flat, but the only container the kit shipped was `.fdy-card`, which carries `--shadow-lift`, a
1384
1552
  real 34px lift, so a responsive table that becomes a list below `md` had to choose between ten
1385
1553
  floating cards or a hand-built box that needs a colour and escapes the token system.
1386
1554
  - **`FdyTable`: controlled client-side page index.** `pageIndex` + `update:pageIndex` (Vue) /
1387
1555
  `pageIndex` + `onPageIndexChange` (React) / `PageIndex` + `PageIndexChanged` (Blazor). The table
1388
1556
  still filters/sorts/paginates; only *which page* moves to the parent. This is what makes the
1389
1557
  `process` event from 1.18 pay off on a **responsive** screen: the pager renders inside
1390
- `.fdy-datatable`, so a screen that hides the table below `md` loses it and in client mode the index
1558
+ `.fdy-datatable`, so a screen that hides the table below `md` loses it, and in client mode the index
1391
1559
  was a private ref with no prop, no event and no `goTo`. The two available options were "lose
1392
1560
  pagination on mobile" or "own the page index in server mode", where `process` hands back exactly the
1393
1561
  array you passed in. One index can now also span several tables, which is what a grouped list needs.
1394
1562
  Reported after a real rebuild where `process` deleted, by measurement, zero lines.
1395
1563
  - **`breakpoints.nav` (721)** in `tokens/breakpoints.mjs` + its type. `.fdy-app` switches the sidebar
1396
- between off-canvas drawer and static column at 721px a number that was in `app-shell.css` and
1564
+ between off-canvas drawer and static column at 721px, a number that was in `app-shell.css` and
1397
1565
  nowhere else, so every consumer of the shell hand-mirrored it. Getting it wrong is not cosmetic:
1398
1566
  aligning to `md` (960) leaves 721–959px with a static sidebar that the script still treats as an
1399
1567
  overlay, so opening the nav makes the page `inert` around a user with no way back out. A new test
1400
1568
  asserts `nav` equals the CSS's `min-width` and that the `max-width` query is `nav - 1`, so the two
1401
1569
  cannot drift.
1402
- - **Blazor `FdyTable` gained `Process`** (+ the `FdyTableProcess<TRow>` record) 1.18 added the
1570
+ - **Blazor `FdyTable` gained `Process`** (+ the `FdyTableProcess<TRow>` record); 1.18 added the
1403
1571
  `process` event to Vue and React only, which left Blazor unable to drive a card list from the
1404
1572
  processed set at all.
1405
1573
  - **Browser guards for the controlled page index**, Vue and React, in `browser/adapter.mjs`: a table
1406
1574
  whose pager lives *outside* the component, asserting both directions (parent → rows, and internal
1407
1575
  pager → parent event). Mutation-checked: reverting the component to its private index fails them.
1408
- ### Changed from consumption round 4
1576
+ ### Changed: from consumption round 4
1409
1577
  - **`data-density="compact"` is no longer root-scoped.** The generated selector was
1410
1578
  `:root[data-density="compact"]`; it is now a bare `[data-density="compact"]`. These are inheriting
1411
- custom properties, so density can be set on a route wrapper or a single section which is how it is
1579
+ custom properties, so density can be set on a route wrapper or a single section, which is how it is
1412
1580
  actually decided (per screen), not on `<html>` for the whole app. Setting it on the root still works
1413
1581
  identically. A build test now asserts the selector is not root-scoped.
1414
- ### Fixed from consumption round 4
1415
- - **`USAGE.md` §3 misdescribed the kit's own elevation scale** our bug, found while checking the
1582
+ ### Fixed: from consumption round 4
1583
+ - **`USAGE.md` §3 misdescribed the kit's own elevation scale.** Our bug, found while checking the
1416
1584
  report. It prescribed `--shadow-1` for "a card" and `--shadow-4` for "modal / drawer", but `.fdy-card`
1417
1585
  uses `--shadow-lift` (≈6× heavier than `--shadow-1`) and `.fdy-modal` uses `--shadow-lift-hover`;
1418
1586
  `--shadow-1` is what the *data containers* use. §3 now carries the real component→token map, names
@@ -1420,15 +1588,15 @@ confirmed gaps, one rejected premise, and one documentation bug of our own that
1420
1588
  warns about: docs restating a scale the components don't follow.
1421
1589
  - **`.fdy-toolbar` vs `.fdy-filterbar` is now documented.** `.fdy-toolbar` is `align-items:center`
1422
1590
  (right for bare controls); a `.fdy-field` with a *visible* label sits half a label-height low in it.
1423
- Labelled fields belong in `.fdy-filterbar` (`align-items:flex-end` which is why 1.11.2 had to
1591
+ Labelled fields belong in `.fdy-filterbar` (`align-items:flex-end`, which is why 1.11.2 had to
1424
1592
  arbitrate the composed case). The difference existed but was written down nowhere, so the wrong
1425
1593
  choice was only visible once rendered. Documented in `USAGE.md` §7 and `COMPONENTS.md`; no CSS
1426
1594
  changed, so no existing screen shifts.
1427
- ### Notes one report item rejected
1595
+ ### Notes: one report item rejected
1428
1596
  - **"`.fdy-page-section` and `.fdy-table-scroll` do not compose" does not reproduce, and the proposed
1429
1597
  `min-width:0` fix is a no-op.** Measured on `docs/reference-screen.html` (the same
1430
1598
  section → datatable → scroller chain) with the table forced to 1400px in a 688px column: page
1431
- overflow **0px**, the datatable held its column, the scroller scrolled internally and adding
1599
+ overflow **0px**, the datatable held its column, the scroller scrolled internally, and adding
1432
1600
  `min-width:0` changed nothing. Two mechanisms explain it: `.fdy-page-section` is flex **column**, so
1433
1601
  `min-width:auto` (a main-axis rule) cannot cause horizontal growth; and `.fdy-table-scroll` has
1434
1602
  `overflow-x:auto`, which gives it an automatic minimum size of **0**, so no ancestor flex can stretch
@@ -1436,48 +1604,48 @@ confirmed gaps, one rejected premise, and one documentation bug of our own that
1436
1604
  (`.fdy-app__content`). The 129px overflow the reporter measured is real but originates in their own
1437
1605
  wrapper chain; to locate it, walk the datatable's ancestors at the failing width and find the first
1438
1606
  with `scrollWidth > clientWidth`. Third round in a row where a proposed fix would have changed
1439
- nothing the premise gets verified before the patch, every time.
1607
+ nothing: the premise gets verified before the patch, every time.
1440
1608
  ### Added
1441
- - **`COMPONENTS.md` the complete public class surface in one flat file.** Every component with its
1609
+ - **`COMPONENTS.md`, the complete public class surface in one flat file.** Every component with its
1442
1610
  block/element/modifier classes, a minimal markup skeleton, its enhancer hook and its a11y contract,
1443
1611
  plus the full enhancer table (markup hook → script → global → events). The gap it closes: 425
1444
1612
  `.fdy-*` classes exist in `src/components/`, but only ~33 appeared as literal strings anywhere in
1445
- the shipped docs the rest lived in a 2,552-line HTML page and in the CSS source, neither of which
1613
+ the shipped docs; the rest lived in a 2,552-line HTML page and in the CSS source, neither of which
1446
1614
  an agent (or a hurried human) reads.
1447
- - **`docs/agent-onboarding.md`** the entry point for an agent working in a *consuming* project: a
1615
+ - **`docs/agent-onboarding.md`** is the entry point for an agent working in a *consuming* project: a
1448
1616
  paste-ready block for that project's `CLAUDE.md`/`AGENTS.md`, what ships in the package and which
1449
1617
  question each file answers, the order to build a new screen in, a **migration mapping table**
1450
1618
  (Bootstrap / MudBlazor / utility-class conventions → Freeday) with a safe conversion order, and a
1451
1619
  pre-completion verification checklist.
1452
- - **`docs/reference-screen.html`** one complete business screen assembled the intended way:
1620
+ - **`docs/reference-screen.html`** is one complete business screen assembled the intended way:
1453
1621
  `.fdy-app` → `.fdy-page` → `.fdy-page__header` → `.fdy-stats` → sections holding a chart and a
1454
1622
  full data table, plus a native-`<dialog>` modal, theme/density toggles and the shell's nav-toggle
1455
1623
  wiring. v1.18.0 shipped the composition primitives but nothing demonstrated them end-to-end.
1456
- - **`test/docs.test.mjs` drift guard for the agent-facing docs.** Every fully-written `.fdy-*`
1624
+ - **`test/docs.test.mjs`, the drift guard for the agent-facing docs.** Every fully-written `.fdy-*`
1457
1625
  class named in `COMPONENTS.md`, `USAGE.md`, `docs/agent-onboarding.md` and
1458
1626
  `docs/reference-screen.html` must exist in the kit (CSS selectors ∪ classes the enhancers
1459
1627
  query/set), and every stylesheet in `src/components/` must be represented in `COMPONENTS.md`. An
1460
1628
  API doc that names a class the CSS never defines is worse than an omission: it invites markup that
1461
1629
  silently does nothing.
1462
- - **`reference/` the two source assets moved out of the repo root** (`git mv`, so history follows):
1630
+ - **`reference/`, the two source assets moved out of the repo root** (`git mv`, so history follows):
1463
1631
  `Foundation Design System.html` → `reference/foundation-design-system.html`, and
1464
1632
  `auth_web_ui_layout_patterns.png` → `reference/layout-patterns.png` (the `auth_` prefix was
1465
- misleading the sheet is about app layout archetypes, not authentication). Neither was ever in the
1633
+ misleading, since the sheet is about app layout archetypes rather than authentication). Neither was ever in the
1466
1634
  tarball and neither is now; the folder name states that. `reference/README.md` carries their
1467
1635
  provenance plus a **15 archetypes → Freeday primitives** table that names, per archetype, what the
1468
1636
  kit covers and which shapes it has **no** component for (kanban columns, calendar month grid, chat
1469
- bubbles, canvas) so those get built as layout instead of as invented `fdy-` classes. The drift
1637
+ bubbles, canvas), so those get built as layout instead of as invented `fdy-` classes. The drift
1470
1638
  guard checks this file too.
1471
1639
  ### Removed
1472
1640
  - **`src/*.js` no longer ships.** All 24 enhancers in `src/` were **byte-identical** to their `dist/`
1473
- copies, and no `exports` path points into `src/` so a bundler-based consumer could never import
1641
+ copies, and no `exports` path points into `src/`, so a bundler-based consumer could never import
1474
1642
  them. `files` now lists `src/base.css` + `src/components` (kept: per-component CSS is genuinely
1475
1643
  easier to read than the 117 kB bundle, and `docs/agent-onboarding.md` points agents at it).
1476
- **−25 files, −196 kB unpacked** per install (the compressed tarball barely moves identical files
1477
- deflate to almost nothing but the install footprint and "what am I even looking at" do).
1644
+ **−25 files, −196 kB unpacked** per install (the compressed tarball barely moves, since identical files
1645
+ deflate to almost nothing, but the install footprint and "what am I even looking at" do).
1478
1646
  - **4 completed SDD implementation plans + 1 superseded spec deleted** (`docs/superpowers/plans/*`,
1479
1647
  `specs/2026-07-22-…-polish-design.md`, **−1452 lines**). Cited by nobody, served publicly by Pages,
1480
- and describing work that shipped months of releases ago `CHANGELOG.md` plus git history already
1648
+ and describing work that shipped months of releases ago, when `CHANGELOG.md` plus git history already
1481
1649
  record it. Only the canonical design spec remains.
1482
1650
  - **`NEXT-UP.md`: 421 → ~125 lines.** Held 15 `Update …` release-log blocks (v1.8.0–v1.18.0), three
1483
1651
  2026-07-27 post-mortems of resolved items, and a frozen "current condition" snapshot still claiming
@@ -1485,15 +1653,15 @@ confirmed gaps, one rejected premise, and one documentation bug of our own that
1485
1653
  purely forward-looking: the demand-driven backlog, the deliberate YAGNI list, two durable invariants,
1486
1654
  and the release runbook.
1487
1655
  - **`HANDOFF.md`: 134 → 85 lines.** Its "where we are" section was 90 lines re-narrating every release
1488
- back to v1.3.1 a third copy of the changelog. Now: current state, then a pointer.
1656
+ back to v1.3.1, a third copy of the changelog. Now: current state, then a pointer.
1489
1657
  - `src/components/.gitkeep`, in a directory holding 47 stylesheets.
1490
1658
  ### Fixed
1491
1659
  - **`CLAUDE.md`'s roadmap said "v0.1 (sekarang, token-first)"** at v1.19.0, and listed datepicker,
1492
- data grid, filter bar, pagination, states and wizard as *future* work all shipped long ago. It is
1660
+ data grid, filter bar, pagination, states and wizard as *future* work, all shipped long ago. It is
1493
1661
  the first file every agent session reads, so it was the most expensive stale text in the repo.
1494
1662
  Replaced with the actual status (feature-complete, demand-driven) plus a real structure map that
1495
1663
  distinguishes authored `src/` from generated `dist/`, and names `freeday.bundle.css` as the file to link.
1496
- - **The release runbook listed version-reference locations from memory, and was wrong** it named
1664
+ - **The release runbook listed version-reference locations from memory, and was wrong.** It named
1497
1665
  `examples/*/README.md` (no version in them) and claimed `docs/getting-started.md` has 4 (it has 1).
1498
1666
  Replaced the hand-maintained list with the command that derives it:
1499
1667
  `git grep -n '<old-version>' -- . | grep -v CHANGELOG`.
@@ -1505,9 +1673,9 @@ confirmed gaps, one rejected premise, and one documentation bug of our own that
1505
1673
  exported it, there is no `data:…;base64` font anywhere in the file, and it renders via the Google
1506
1674
  Fonts CDN it preconnects to. Corrected, so no future session goes looking for fonts that aren't there.
1507
1675
  - **`docs/getting-started.md` §5 pointed consumers at `Foundation Design System.html` for component
1508
- markup** a file that is not in the npm package, which became a dangling reference the moment this
1676
+ markup**, a file that is not in the npm package, which became a dangling reference the moment this
1509
1677
  release started shipping that doc. Now points at `COMPONENTS.md` and `reference-screen.html`.
1510
- - **`docs/getting-started.md`'s app-shell snippet was structurally wrong** and it is the first thing
1678
+ - **`docs/getting-started.md`'s app-shell snippet was structurally wrong**, and it is the first thing
1511
1679
  a consumer copies. It showed `__topbar` as a direct child *before* `__sidebar`, put `__brand` inside
1512
1680
  the topbar, and nested `__content` *inside* `__main`. `.fdy-app` is a flex **row** of
1513
1681
  `[__sidebar | __content]`, `__content` is the column holding `__topbar` + `__main` (it gives the
@@ -1518,29 +1686,29 @@ confirmed gaps, one rejected premise, and one documentation bug of our own that
1518
1686
  - Same file: "compose the screen inside `__content`" → **`__main`** (which is the padded area).
1519
1687
  ### Changed
1520
1688
  - **`files` now ships the docs an agent needs**: `COMPONENTS.md`, `docs/getting-started.md`,
1521
- `docs/integrations.md`, `docs/agent-onboarding.md`, `docs/reference-screen.html` listed
1689
+ `docs/integrations.md`, `docs/agent-onboarding.md`, `docs/reference-screen.html`, listed
1522
1690
  **file-by-file, not as `"docs"`**, so the internal planning docs under `docs/superpowers/` stay out
1523
1691
  of the tarball. (`files` does not honour `.gitignore`; whitelisting a directory ships whatever sits
1524
- inside it the v1.16.0 `bin/obj` lesson.) Tarball 263.6 kB → 297.9 kB, 177 → 182 files.
1692
+ inside it, the v1.16.0 `bin/obj` lesson.) Tarball 263.6 kB → 297.9 kB, 177 → 182 files.
1525
1693
  The `examples/` apps are deliberately **not** shipped (127 MB of `node_modules` + 208 MB of .NET
1526
1694
  `bin/obj` live under them); the repo link in `docs/agent-onboarding.md` covers that need.
1527
1695
  ### Notes
1528
1696
  - Documenting the surface surfaced one inconsistency, recorded rather than patched (this release
1529
- touches no CSS): **`.fdy-pagination` has no CSS rule** the block class on the `<nav>` is a naming
1697
+ touches no CSS): **`.fdy-pagination` has no CSS rule.** The block class on the `<nav>` is a naming
1530
1698
  hook only, its `__list`/`__link`/`__ellipsis` elements carry all the styling, and the data table
1531
1699
  targets `data-fdy-table-pagination`. `COMPONENTS.md` says so, and the drift guard lists it as a
1532
1700
  known structural hook.
1533
1701
 
1534
- ## [1.18.0] 2026-08-11
1702
+ ## [1.18.0] - 2026-08-11
1535
1703
  ### Fixed
1536
- - **`FdyModal` / `FdyDrawer` (Vue) were non-dismissible when `dismissible` was omitted an
1704
+ - **`FdyModal` / `FdyDrawer` (Vue) were non-dismissible when `dismissible` was omitted, an
1537
1705
  accessibility defect.** Vue's boolean-cast delivers an omitted Boolean prop as `false`, not
1538
1706
  `undefined`, so the defensive `props.dismissible !== false` evaluated to `false`: no Escape, no
1539
- backdrop close, and (modal) no close button rendered the opposite of the documented
1707
+ backdrop close, and (modal) no close button rendered, the opposite of the documented
1540
1708
  `dismissible: true` default. Both now use `withDefaults(…, { dismissible: true })`. React was
1541
1709
  unaffected (it leaves omitted props `undefined`); `FdyCombo`/`FdyDatepicker` use `=== true`, which
1542
1710
  is correct for an intended-`false` default.
1543
- - **`.fdy-card--interactive` hover transform now respects reduced motion** the `-3px` lift is dropped
1711
+ - **`.fdy-card--interactive` hover transform now respects reduced motion.** The `-3px` lift is dropped
1544
1712
  under `prefers-reduced-motion` (the shadow still signals the affordance).
1545
1713
  ### Added
1546
1714
  - **Page-composition primitives + type roles (`src/components/composition.css`).** `.fdy-page`,
@@ -1548,21 +1716,21 @@ confirmed gaps, one rejected premise, and one documentation bug of our own that
1548
1716
  deliberately *not* a card, so a metric strip doesn't become an identical-card grid), and three title
1549
1717
  roles `.fdy-title-page` / `-section` / `-card` (+ `.fdy-eyebrow`, `.fdy-text-muted`/`-caption`). They
1550
1718
  encode *how a page is assembled* so independently-built screens cohere.
1551
- - **`USAGE.md` the usage doctrine.** Which token/role/shadow to use when: type roles, spacing rhythm,
1719
+ - **`USAGE.md`, the usage doctrine.** Which token/role/shadow to use when: type roles, spacing rhythm,
1552
1720
  elevation, one-primary-per-screen, semantic-vs-categorical colour, density, and the shell-down
1553
1721
  composition order. The line where a component library becomes a design system.
1554
1722
  - **General categorical palette `--tone-1`…`--tone-8`** (a chart-neutral alias of the validated,
1555
- theme-aware `--chart-1`…`8`) + **`.fdy-chip--tone-N`** to match `.fdy-avatar--tone-N` non-semantic
1723
+ theme-aware `--chart-1`…`8`) + **`.fdy-chip--tone-N`** to match `.fdy-avatar--tone-N`, for non-semantic
1556
1724
  category colours (chips, tags, legends), WCAG AA in light & dark (gated).
1557
- - **`FdyTable` surfaces its processed rows** a `process` event (Vue) / `onProcess` callback (React),
1558
- `{ rows, total }`, in **both** client and server modes so the same filtered/sorted/paged set can
1725
+ - **`FdyTable` surfaces its processed rows** through a `process` event (Vue) / `onProcess` callback (React),
1726
+ `{ rows, total }`, in **both** client and server modes, so the same filtered/sorted/paged set can
1559
1727
  drive a responsive card list, a selection summary, or CSV export without re-deriving the pipeline.
1560
1728
  - **`./table-model` export.** The pure `filterRows`/`sortRows`/`paginate`/`cellValue`/… functions
1561
1729
  (`adapters/core/table-model.js`) are now reachable as `@cahyo-dimas/freeday/table-model`, so a consumer
1562
1730
  can pre-compute exactly what `FdyTable` does instead of re-deriving it and risking drift.
1563
1731
  ### Changed
1564
1732
  - **`data-density="compact"` now steps the mid-range spacing scale** (`--space-3`…`--space-6`) as well as
1565
- `--control-h`, so Freeday components and a utility theme built on `var(--space-N)` actually densify;
1733
+ `--control-h`, so Freeday components, and a utility theme built on `var(--space-N)`, actually densify;
1566
1734
  previously it changed only control height.
1567
1735
  - **Docs:** getting-started leads with the `.fdy-app` shell + a loud "load the fonts yourself" step (the
1568
1736
  package names Sora / IBM Plex Sans / JetBrains Mono but bundles none); README links `USAGE.md`.
@@ -1571,24 +1739,24 @@ Additive except the two bug fixes (which restore documented behaviour). Gate: `n
1571
1739
  `typecheck:react` 0 · `test:browser` 6/6 · new CSS browser-verified in real Chrome. From real-app
1572
1740
  consumption feedback (three instalments).
1573
1741
 
1574
- ## [1.17.0] 2026-08-11
1742
+ ## [1.17.0] - 2026-08-11
1575
1743
  ### Added
1576
1744
  - **Avatar identity tones (`--tone-1`…`--tone-8`).** Decorative tints (from the categorical chart
1577
- palette) with a theme-aware, text-leaning foreground, so same-initial avatars common where many
1578
- names share a prefix stay distinguishable. Verified WCAG **AA** (≥4.5:1) in light **and** dark,
1745
+ palette) with a theme-aware, text-leaning foreground, so same-initial avatars, common where many
1746
+ names share a prefix, stay distinguishable. Verified WCAG **AA** (≥4.5:1) in light **and** dark,
1579
1747
  guarded by `test/contrast.test.mjs` (which gained a `color-mix()` evaluator). Hash a stable index
1580
1748
  off the full name, not the initials.
1581
1749
  - **Size-matched skeletons (`.fdy-skeleton--avatar` + `--avatar-sm`/`--avatar-lg`).** Reserve the exact
1582
1750
  `.fdy-avatar` box (2 / 2.5 / 3.5rem) so data landing causes no layout shift (a bare `--circle` has no
1583
1751
  dimensions of its own).
1584
- - **`.fdy-card--button`.** When the whole card *is* the control a real `<button>` picker row, so it is
1585
- keyboard-reachable and announced this resets the UA button box **without** stripping the card's own
1752
+ - **`.fdy-card--button`.** When the whole card *is* the control, a real `<button>` picker row that is
1753
+ keyboard-reachable and announced, this resets the UA button box **without** stripping the card's own
1586
1754
  surface/border.
1587
1755
  - **`.fdy-list-reset`.** Opt-in list reset for consumers who pair Freeday with a utility framework run
1588
- preflight-off (`base.css` is intentionally a *light* reset it does not strip `ul`/`ol`/`p` margins).
1756
+ preflight-off (`base.css` is intentionally a *light* reset that does not strip `ul`/`ol`/`p` margins).
1589
1757
  - **Toast `key` + `Freeday.dismiss()`.** `toast({ …, key })` replaces an existing same-key toast in
1590
1758
  place instead of stacking a duplicate (a burst of identical failures shows one, refreshed);
1591
- `Freeday.dismiss(node | key)` closes one early. `toast()` already returned the element both are now
1759
+ `Freeday.dismiss(node | key)` closes one early. `toast()` already returned the element, and both are now
1592
1760
  in the `window.Freeday` type declarations.
1593
1761
  ### Docs
1594
1762
  - State the scope boundary (components + tokens, **not** layout; pair with a utility framework run
@@ -1600,9 +1768,9 @@ consumption feedback (three instalments).
1600
1768
  Additive; no breaking changes. Gate: `node --test` 22/22 (incl. avatar-tone contrast, both themes) ·
1601
1769
  `typecheck:react` 0 · new CSS/JS browser-verified in real Chrome. From real-app consumption feedback.
1602
1770
 
1603
- ## [1.16.0] 2026-08-11
1771
+ ## [1.16.0] - 2026-08-11
1604
1772
  ### Added
1605
- - **Native Blazor component library (`adapters/blazor/`) first release, 10/10 parity.** A Razor
1773
+ - **Native Blazor component library (`adapters/blazor/`), first release, 10/10 parity.** A Razor
1606
1774
  Class Library (`Freeday.Blazor`, net8.0) so Blazor consumers get typed `<FdyX>` components with
1607
1775
  `@bind`, instead of hand-writing `fdy-*` markup + JS interop. Ten components at parity with the
1608
1776
  Vue/React adapters: `FdyModal`, `FdyDrawer`, `FdyCombo<TValue>`, `FdyDatepicker`, `FdyAutocomplete`,
@@ -1613,39 +1781,39 @@ Additive; no breaking changes. Gate: `node --test` 22/22 (incl. avatar-tone cont
1613
1781
  go through interop). Shared CRTP base `FreedayComponentBase<TSelf>` types the `DotNetObjectReference`
1614
1782
  so `[JSInvokable]` dispatch resolves. Consumed via `<ProjectReference>`; the host loads
1615
1783
  `dist/freeday.js` + `adapters/blazor/freeday-blazor.js` as `<script>`s.
1616
- - **`FdyTable<TRow>`** is a full controlled data table client mode (sort + four column-filter types
1784
+ - **`FdyTable<TRow>`** is a full controlled data table: client mode (sort + four column-filter types
1617
1785
  + pagination over `Rows`) and server mode (set `Page` → the headers/filters/pager only raise
1618
1786
  `SortChanged`/`FiltersChanged`/`PageChanged` intent), plus row activation, expandable detail rows,
1619
- cell templates, and loading/empty states. Its sort/filter/paginate logic is `TableModel` a pure
1787
+ cell templates, and loading/empty states. Its sort/filter/paginate logic is `TableModel`, a pure
1620
1788
  C# port of `adapters/core/table-model.js`.
1621
1789
  - **`freeday-blazor.js` bridge**: `chartUpdate` (repaint a chart from its data-* attributes) and a
1622
1790
  generic `onOutside` light-dismiss primitive (outside-pointer / Esc).
1623
1791
  - **`src/freeday-select.js`** gained an additive `setValue` (silent programmatic select) so a Blazor
1624
1792
  `@bind-Value` can push an external combo value without the enhancer echoing an `fdy-change` back.
1625
1793
 
1626
- ## [1.15.0] 2026-08-10
1794
+ ## [1.15.0] - 2026-08-10
1627
1795
  ### Added
1628
1796
  - **Type declarations for every export (#40).** Only `./vue` and `./react` shipped a `types`
1629
- condition; the other eight exports had none. A TypeScript consumer's two side-effect imports
1630
- `import '@cahyo-dimas/freeday/css'` and `import '@cahyo-dimas/freeday'` plus `./enhancers/*` and
1797
+ condition; the other eight exports had none. A TypeScript consumer's two side-effect imports,
1798
+ `import '@cahyo-dimas/freeday/css'` and `import '@cahyo-dimas/freeday'`, plus `./enhancers/*` and
1631
1799
  `./breakpoints` therefore failed to resolve (TS 5 leaves them untyped / `implicitly any`; TS 6 makes
1632
1800
  it a hard `noUncheckedSideEffectImports` error), and `window.Freeday` was reconstructed by hand in
1633
1801
  every consumer. Now every export names a `types` condition: `dist/asset.d.ts` (a one-line stub for the
1634
1802
  side-effect-only CSS/enhancer imports), `tokens/breakpoints.d.ts` (the breakpoint scale), and
1635
- `dist/freeday.d.ts` which declares the `window.Freeday` global from the source: `toast(opts?)` with
1803
+ `dist/freeday.d.ts`, which declares the `window.Freeday` global from the source: `toast(opts?)` with
1636
1804
  all-optional `variant` / `title` / `message` / `timeout`, returning the toast `HTMLElement` (looser and
1637
1805
  more correct than the guesses consumers had been copying). `files` now also ships
1638
1806
  `tokens/breakpoints.d.ts`. Runtime unchanged; purely additive.
1639
1807
 
1640
- ## [1.14.0] 2026-08-01
1808
+ ## [1.14.0] - 2026-08-01
1641
1809
  ### Added
1642
- - **Read-only state across the form controls (#39).** `input` / `textarea` gain a `[readonly]` rule
1810
+ - **Read-only state across the form controls (#39).** `input` / `textarea` gain a `[readonly]` rule,
1643
1811
  full-contrast text on a muted surface with a softened border and no focus-ring escalation, distinct
1644
1812
  from `:disabled` (which dims the value to placeholder-grey and drops it from tab order). A new
1645
- `readonly` prop lands on all six controlled select-type adapters `FdyCombo`, `FdyCascade`,
1813
+ `readonly` prop lands on all six controlled select-type adapters: `FdyCombo`, `FdyCascade`,
1646
1814
  `FdyDatepicker`, `FdyDateRange`, `FdyAutocomplete`, `FdyCfl` (Vue + React): the control stays focusable
1647
1815
  and shows its value with `aria-readonly="true"` but can't be opened or changed (`FdyDatepicker`'s clear
1648
- button is suppressed too). Read-only ≠ disabled it keeps tab order, full contrast, and stays
1816
+ button is suppressed too). Read-only ≠ disabled: it keeps tab order, full contrast, and stays
1649
1817
  selectable / copyable / submittable, so a real value the user may not edit reads as data, not an empty
1650
1818
  placeholder. Verified in-browser (readonly `opacity:1` vs disabled `0.5`; text at full `--color-text`).
1651
1819
  - **Interaction-state coverage completed.** Added the missing `disabled` state to `timepicker`, `rating`
@@ -1653,28 +1821,28 @@ Additive; no breaking changes. Gate: `node --test` 22/22 (incl. avatar-tone cont
1653
1821
  (`[aria-disabled]` / `.is-disabled`), and `datepicker`; and the missing `invalid` state to `rating`
1654
1822
  (`.fdy-rating--error`). The vanilla datetime composer now reflects `data-fdy-disabled` /
1655
1823
  `data-fdy-invalid` from its wrapper onto **both** child pickers so a datetime reads as one control
1656
- (applied on a macrotask, so it lands after both child triggers are built a microtask would run
1824
+ (applied on a macrotask, so it lands after both child triggers are built; a microtask would run
1657
1825
  before the later-registered timepicker enhancer). All additive; no breaking changes.
1658
1826
 
1659
- ## [1.13.1] 2026-08-01
1827
+ ## [1.13.1] - 2026-08-01
1660
1828
  ### Fixed
1661
- - **`FdyCombo` can be selected with the mouse again (#38).** Clicking an option did nothing — the
1829
+ - **`FdyCombo` can be selected with the mouse again (#38).** Clicking an option did nothing. The
1662
1830
  listbox closed and no `update:modelValue` fired (keyboard select still worked, which is why a
1663
1831
  keyboard-only smoke test missed it). The option `<li>` isn't focusable, so a `mousedown` moved focus
1664
- off the combobox button; the root's `focusout` handler then closed the list removing the `<li>` —
1832
+ off the combobox button; the root's `focusout` handler then closed the list, removing the `<li>`,
1665
1833
  *before* `mouseup`, so the browser never generated a `click` and `choose()` was never reached. Added
1666
1834
  `@mousedown.prevent` (Vue) / `onMouseDown` `preventDefault` (React) on the option so focus stays on the
1667
- button and the click lands the same pattern the sibling `FdyDatepicker` / `FdyAutocomplete` (and the
1835
+ button and the click lands, the same pattern the sibling `FdyDatepicker` / `FdyAutocomplete` (and the
1668
1836
  vanilla `freeday-select.js`, fixed in v1.6.1) already use. Both adapters; no API change; keyboard and
1669
1837
  hover paths untouched. Verified with trusted CDP mouse events (a synthetic `dispatchEvent` won't
1670
- reproduce it untrusted events run no default action, so focus never moves).
1838
+ reproduce it, since untrusted events run no default action and focus never moves).
1671
1839
 
1672
- ## [1.13.0] 2026-07-30
1840
+ ## [1.13.0] - 2026-07-30
1673
1841
  ### Added
1674
- - **`FdyDatepicker` clear affordance an optional date can now be unset (#37A).** A new `clearable`
1842
+ - **`FdyDatepicker` clear affordance: an optional date can now be unset (#37A).** A new `clearable`
1675
1843
  prop renders a small × button in the trigger (overlaid in the calendar-icon slot) whenever a date is
1676
1844
  set, emitting `''` (`update:modelValue` + `change` in Vue, `onChange('')` in React) to return to
1677
- empty closing a functional regression vs the native `<input type="date">`, which offered a clear
1845
+ empty, closing a functional regression against the native `<input type="date">`, which offered a clear
1678
1846
  control. Off by default; both adapters. Verified in-browser: the × is right-aligned in the icon slot,
1679
1847
  vertically centred, hides the calendar icon while shown, and never overlaps the value text (even a
1680
1848
  long, ellipsised one).
@@ -1686,81 +1854,81 @@ Additive; no breaking changes. Gate: `node --test` 22/22 (incl. avatar-tone cont
1686
1854
  - **`FdyDatepicker` default UI strings are now English** (`Select date` placeholder, `Previous month` /
1687
1855
  `Next month` nav labels) instead of Indonesian, matching the kit's English-first public docs. Pass
1688
1856
  `placeholder` / `prevMonthLabel` / `nextMonthLabel` for other languages. Scope is the Vue + React
1689
- `FdyDatepicker` adapters only the vanilla `freeday-datepicker.js` enhancer (used by the Indonesian
1857
+ `FdyDatepicker` adapters only; the vanilla `freeday-datepicker.js` enhancer (used by the Indonesian
1690
1858
  docs demos) keeps its Indonesian defaults.
1691
1859
 
1692
- ## [1.12.0] 2026-07-29
1860
+ ## [1.12.0] - 2026-07-29
1693
1861
  ### Added
1694
1862
  - **Chart colour overrides can now name a categorical palette slot (#36).** `data-fdy-colors` (and the
1695
1863
  `<FdyChart colors>` prop) previously resolved every name through the semantic tier
1696
- (`var(--color-<name>)`), so an override could only reach `primary`/`info`/`accent`/… never the
1864
+ (`var(--color-<name>)`), so an override could only reach `primary`/`info`/`accent`/…, never the
1697
1865
  validated categorical palette `--chart-1..8`. An app that needs stable *per-category* colours (rather
1698
1866
  than per-array-index) had to override, and overriding then dropped each series onto the nearest
1699
- semantic hue — e.g. a three-way split landing on three neighbouring blues: contrast-passing but
1867
+ semantic hue: a three-way split landing on three neighbouring blues, say, contrast-passing but
1700
1868
  unreadable as a comparison. `colorVar` now maps a name of the form `chart-1`..`chart-8` to
1701
1869
  `var(--chart-N)`, so an override can pin a series to the validated palette
1702
- (`colors={['chart-1','chart-2','chart-3']}`). Backward compatible no semantic token name starts
1870
+ (`colors={['chart-1','chart-2','chart-3']}`). Backward compatible, since no semantic token name starts
1703
1871
  with `chart-`, so semantic overrides are unchanged; the single-series `data-fdy-color` / `color`
1704
1872
  accepts the `chart-N` form too. Verified in-browser: explicit `chart-1..3` now render the exact
1705
1873
  fixed-order palette (blue/orange/green) and equal the default, while `success,warning,danger` still
1706
1874
  resolve to their semantic tokens. (`--chart-1..8` are public tokens in `dist/freeday.tokens.css`, so
1707
- matching swatches/bars elsewhere can reference `var(--chart-N)` directly no adapter export needed.)
1875
+ matching swatches/bars elsewhere can reference `var(--chart-N)` directly, with no adapter export needed.)
1708
1876
 
1709
- ## [1.11.2] 2026-07-28
1877
+ ## [1.11.2] - 2026-07-28
1710
1878
  ### Fixed
1711
1879
  - **A filter bar docked in a table toolbar no longer loses its alignment (#35).** `.fdy-filterbar`
1712
1880
  (`align-items:flex-end`, for labelled fields) and `.fdy-table-toolbar` (`align-items:center`, for
1713
1881
  bare buttons) are both single-class rules of equal specificity, so composing them on one element
1714
- (`<div class="fdy-table-toolbar fdy-filterbar">`) let whichever came later in the bundle win the
1715
- toolbar centring every control and floating the trailing actions ~13px above the inputs beside
1882
+ (`<div class="fdy-table-toolbar fdy-filterbar">`) let whichever came later in the bundle win, and the
1883
+ toolbar won, centring every control and floating the trailing actions ~13px above the inputs beside
1716
1884
  them. Added a `.fdy-table-toolbar.fdy-filterbar` rule (specificity 0,2,0) that settles it toward the
1717
- filterbar's `flex-end`. Additive only affects elements carrying both classes; standalone toolbars
1885
+ filterbar's `flex-end`. Additive, so it only affects elements carrying both classes; standalone toolbars
1718
1886
  and filterbars are untouched. Verified in-browser: the composed bar's controls now share one
1719
1887
  baseline (computed `align-items: flex-end`).
1720
1888
 
1721
- ## [1.11.1] 2026-07-28
1889
+ ## [1.11.1] - 2026-07-28
1722
1890
  ### Fixed
1723
1891
  - **Tall modals now scroll their body instead of clipping the footer (#34).** `.fdy-modal` had a
1724
1892
  `max-height` + `overflow:hidden` but never established a flex context, so its three children laid out
1725
1893
  as blocks: the body's height was its own content height (nothing for `overflow:auto` to scroll), and
1726
- the dialog's cap clipped whatever stuck out including `.fdy-modal__footer` and its submit button, on
1894
+ the dialog's cap clipped whatever stuck out, including `.fdy-modal__footer` and its submit button, on
1727
1895
  a short viewport (a form you could fill but not save). Gave the modal the column-flex treatment the
1728
1896
  sibling `.fdy-drawer` already uses: `display:flex` on `.fdy-modal[open]`, `flex-direction:column` on
1729
1897
  the base, `flex:none` on header/footer, and `flex:1;min-height:0` on the body. Modals that already fit
1730
1898
  are unchanged. Verified in-browser (body scrolls, footer stays inside) including the `.fdy-modal--cfl`
1731
- picker no regression.
1899
+ picker, so no regression.
1732
1900
 
1733
- ## [1.11.0] 2026-07-28
1901
+ ## [1.11.0] - 2026-07-28
1734
1902
  Follow-ups from the doc-ai-automation migration (improvement notes #31–#33). Additive / corrective.
1735
1903
 
1736
1904
  ### Fixed
1737
1905
  - **Badges no longer wrap and break the pill (#31).** `.fdy-badge` had `line-height:1` but no
1738
1906
  `white-space`, so a two-word label (e.g. "Needs review") wrapped and collided with the pill padding,
1739
- turning the capsule into a cramped lozenge. Added `white-space:nowrap` matching the sibling
1907
+ turning the capsule into a cramped lozenge. Added `white-space:nowrap`, matching the sibling
1740
1908
  `.fdy-badge-ov`, which already had it. A too-narrow badge now overflows its column (which
1741
1909
  `.fdy-table-scroll` already handles) instead of wrapping.
1742
1910
  - **Donut chart honours `legend="none"` (#33).** `freeday-chart.js` read `data-fdy-legend` only in the
1743
1911
  cartesian renderer; the donut appended its legend unconditionally, so `<FdyChart type="donut"
1744
1912
  legend="none">` still printed the built-in legend even when the caller supplied its own. Gated the
1745
- donut legend on the same attribute (`auto` still shows it a donut is unreadable without labels).
1913
+ donut legend on the same attribute (`auto` still shows it, since a donut is unreadable without labels).
1746
1914
 
1747
1915
  ### Added
1748
1916
  - **`.fdy-filterbar--actions-inline` modifier (#32).** Opt-in: sits the trailing actions inline after
1749
1917
  the last field instead of at the far edge, so a wrapped bar reads as attached to its filters. Also
1750
1918
  **documented** the filterbar's wrap behaviour: a wrapped bar breaks flex lines on each item's
1751
- *flex-basis* (the grow field's `flex:1 1 16rem` reserves 16rem), not its rendered width so the
1919
+ *flex-basis* (the grow field's `flex:1 1 16rem` reserves 16rem), not its rendered width, so the
1752
1920
  grow basis decides the wrap point; to avoid stranded actions, tune the `--w-grow` basis or drop a
1753
1921
  field. (The note's margin-swap proposal to un-strand the actions was **tested in-browser and does not
1754
- work** margins don't change flex line-breaking so only the modifier + docs were taken.)
1922
+ work**, because margins don't change flex line-breaking, so only the modifier + docs were taken.)
1755
1923
 
1756
- ## [1.10.0] 2026-07-28
1924
+ ## [1.10.0] - 2026-07-28
1757
1925
  Follow-ups from the doc-ai-automation migration (improvement notes #27–#30). Additive / corrective;
1758
1926
  no breaking changes.
1759
1927
 
1760
1928
  ### Fixed
1761
1929
  - **`<fieldset class="fdy-field">` no longer leaks native browser chrome (#27).** The kit shipped no
1762
1930
  `fieldset` reset, so a grouped field (a radio/checkbox group with a `<legend>`) showed the UA groove
1763
- border, extra padding, and `min-inline-size:min-content` the last of which also blocked the
1931
+ border, extra padding, and `min-inline-size:min-content`, the last of which also blocked the
1764
1932
  filterbar column widths from shrinking it. Added a scoped `fieldset.fdy-field` reset so it lays out
1765
1933
  identically to the `<div>` form.
1766
1934
  - **Chart x-axis: the last label no longer collides with its neighbour (#29).** `freeday-chart.js`
@@ -1779,167 +1947,167 @@ no breaking changes.
1779
1947
  it need not burn the single `--w-grow` slot. The date-range pickers also gained a `min-width` (7rem)
1780
1948
  so a squeezed range compresses and wraps instead of clipping the date (was `min-width:0`).
1781
1949
 
1782
- ## [1.9.0] 2026-07-28
1783
- Follow-ups to 1.8.0's `FdyTable` (improvement notes #25/#26). Additive the #25 change **widens** a
1950
+ ## [1.9.0] - 2026-07-28
1951
+ Follow-ups to 1.8.0's `FdyTable` (improvement notes #25/#26). Additive, since the #25 change **widens** a
1784
1952
  generic bound, so every call site that compiled before still compiles.
1785
1953
 
1786
1954
  ### Fixed
1787
1955
  - **`FdyTable` now accepts `interface`-typed rows.** The row generic was constrained to
1788
1956
  `Record<string, unknown>`, to which a TypeScript `interface` is never assignable (interfaces get no
1789
- implicit index signature) so a normally-typed DTO array failed to compile and `FdyTable` was
1957
+ implicit index signature), so a normally-typed DTO array failed to compile and `FdyTable` was
1790
1958
  effectively unusable in a strict-TS app. Widened the bound to `object` in both adapters; the row type
1791
1959
  now **infers** correctly (`cell-*` slots / `renderCell` are properly typed) and no cast is pushed
1792
- onto consumers. The component never indexes rows directly all access goes through the
1960
+ onto consumers. The component never indexes rows directly; all access goes through the
1793
1961
  already-unconstrained core, which is unchanged.
1794
1962
 
1795
1963
  ### Added
1796
1964
  - **`FdyTable` row activation (opt-in).** `rowActivatable` makes rows focusable and emits
1797
1965
  `row-activate` (Vue) / calls `onRowActivate` (React) on click, or Enter/Space while the row itself is
1798
- focused a control inside a cell keeps its own event (the `target !== currentTarget` guard). Adds a
1966
+ focused, and a control inside a cell keeps its own event (the `target !== currentTarget` guard). Adds a
1799
1967
  `rowClass(row)` hook for per-row state (e.g. a selected row) and a `.fdy-table__row--activatable`
1800
1968
  class (pointer + `:focus-visible` ring; hover tint is already inherited from the base row rule).
1801
1969
  Replaces the hand-rolled `<tr tabindex="0" @click @keydown>` + duplicated row CSS consumers were
1802
1970
  each re-writing.
1803
1971
 
1804
- ## [1.8.0] 2026-07-28
1805
- Release **1.8 framework-safe data table, modal/drawer wrappers, and a monospace utility**.
1972
+ ## [1.8.0] - 2026-07-28
1973
+ Release **1.8: framework-safe data table, modal/drawer wrappers, and a monospace utility**.
1806
1974
  Non-breaking, purely additive. No existing selector, markup, or enhancer changed.
1807
1975
 
1808
1976
  ### Added
1809
- - **`FdyTable` (Vue + React) a controlled data table.** Reads `rows` as the source of truth on
1810
- every render, so it is safe over a `v-for` / `.map()` bound to reactive data unlike the
1977
+ - **`FdyTable` (Vue + React), a controlled data table.** Reads `rows` as the source of truth on
1978
+ every render, so it is safe over a `v-for` / `.map()` bound to reactive data, unlike the
1811
1979
  `freeday-table.js` enhancer, which snapshots the DOM and corrupts a framework list on the first
1812
1980
  refetch. Two modes: **client** (component sorts/filters, and paginates when `pageSize` is set,
1813
1981
  over the full `rows`) and **server** (`page` prop present → `rows` render as given and the
1814
1982
  headers/filters/pager only emit `update:sort` / `update:filters` / `update:page`). Type-aware
1815
1983
  column filters (**text / enum / number / date**) in a top-layer popover, sort toggles, and a
1816
- page-window pager all over the existing `.fdy-table*` / `.fdy-filter*` / `.fdy-pagination__*`
1984
+ page-window pager, all over the existing `.fdy-table*` / `.fdy-filter*` / `.fdy-pagination__*`
1817
1985
  classes. Column options: `sortable`, `filter`, `align`, `mono`, `sortType`, `value`, `options`.
1818
1986
  Sort/filter/paginate logic lives in a shared, framework-agnostic core (`adapters/core/table-model.js`)
1819
1987
  covered by `node --test` (`test/table-model.test.mjs`).
1820
- - **`FdyModal` + `FdyDrawer` (Vue + React) controlled `<dialog>` wrappers.** Reconcile a reactive
1988
+ - **`FdyModal` + `FdyDrawer` (Vue + React), controlled `<dialog>` wrappers.** Reconcile a reactive
1821
1989
  `open` boolean with the native `<dialog>`'s method-driven open/close: guarded
1822
1990
  `showModal()`/`close()`, `@cancel`/`onCancel` + `preventDefault` so Esc routes through app state,
1823
1991
  and backdrop-click via `event.target === dialogEl`. `open` + `close` event/`onClose`, `title`,
1824
1992
  `size`/`side`, `dismissible` (default true), and `footer`. No new modal CSS.
1825
1993
  - **`.fdy-mono` utility.** Alignment-neutral monospace (data font + tabular figures) for identifier /
1826
- code / IP / timestamp cells or inline spans the piece that previously only existed welded into
1994
+ code / IP / timestamp cells or inline spans, the piece that previously only existed welded into
1827
1995
  the right-aligned `.fdy-table__num`. `FdyTable` applies it for any `mono: true` column.
1828
- - **`.fdy-drawer__footer`** a bottom action bar for drawers (mirrors `.fdy-modal__footer`).
1996
+ - **`.fdy-drawer__footer`** is a bottom action bar for drawers (mirrors `.fdy-modal__footer`).
1829
1997
 
1830
- ## [1.7.1] 2026-07-27
1831
- Docs & distribution patch. **No component/code changes** `dist/`, `src/`, `adapters/`, and
1998
+ ## [1.7.1] - 2026-07-27
1999
+ Docs & distribution patch. **No component/code changes**: `dist/`, `src/`, `adapters/`, and
1832
2000
  `tokens/` are byte-identical to 1.7.0.
1833
2001
 
1834
2002
  ### Changed
1835
2003
  - **Distributed on public npm as `@cahyo-dimas/freeday`.** Install with `npm i @cahyo-dimas/freeday`
1836
- `npm ci` now works in CI without auth or an SSH key (the old `git+https` workaround is gone).
2004
+ `npm ci` now works in CI without auth or an SSH key (the old `git+https` workaround is gone).
1837
2005
  Releases publish via GitHub Actions **OIDC Trusted Publishing** (no tokens).
1838
- - **Docs are now English-first** `README.md`, `docs/getting-started.md`, `docs/integrations.md`,
2006
+ - **Docs are now English-first**: `README.md`, `docs/getting-started.md`, `docs/integrations.md`,
1839
2007
  and the example READMEs. `docs/index.html` stays Indonesian for now.
1840
2008
 
1841
2009
  ### Added
1842
2010
  - **`README.id.md`** (Bahasa Indonesia) with a language-toggle row on both READMEs; now shipped in
1843
2011
  the published package.
1844
2012
 
1845
- ## [1.7.0] 2026-07-24
1846
- Rilis **1.7 tree checkbox, form-grid, & tiga section docs full-width**. Non-breaking, aditif.
2013
+ ## [1.7.0] - 2026-07-24
2014
+ Rilis **1.7: tree checkbox, form-grid, & tiga section docs full-width**. Non-breaking, aditif.
1847
2015
 
1848
2016
  ### Added
1849
- - **Tree checkbox (cascading) `freeday-tree.js` + `.fdy-tree--checkbox`.** Varian tree yang
2017
+ - **Tree checkbox (cascading): `freeday-tree.js` + `.fdy-tree--checkbox`.** Varian tree yang
1850
2018
  bisa dipilih: centang cabang → semua anak ikut; sebagian anak → cabang jadi native
1851
2019
  `:indeterminate`. Checkbox = `<input class="fdy-checkbox">` native (Space toggle, nama lewat
1852
2020
  `aria-label`); klik checkbox `stopPropagation` sehingga memilih **tak pernah** membuka/menutup
1853
2021
  cabang, sedangkan teks summary tetap toggle `<details>`. Opt-in via `data-fdy-tree`; init
1854
2022
  post-order menghitung state cabang dari leaf yang sudah tercentang saat load. Zero-dependency,
1855
2023
  `window.FreedayTree`. Tree dasar tetap tanpa JS.
1856
- - **`.fdy-form-grid` layout header/dokumen.** Pelengkap dua-dimensi dari stack `.fdy-field`
2024
+ - **`.fdy-form-grid`, layout header/dokumen.** Pelengkap dua-dimensi dari stack `.fdy-field`
1857
2025
  satu kolom dan baris `.fdy-filterbar`: field tersusun di kolom yang rapi (`auto-fit`, reflow ke
1858
- satu kolom di layar sempit); `.fdy-field--full` membentang penuh (mis. alamat). Zero JS murni
2026
+ satu kolom di layar sempit); `.fdy-field--full` membentang penuh (mis. alamat). Zero JS, murni
1859
2027
  layout di atas `.fdy-field`.
1860
- - **File upload layout melebar.** Modifier `.fdy-dropzone--row` (ikon di samping teks, bukan
2028
+ - **File upload, layout melebar.** Modifier `.fdy-dropzone--row` (ikon di samping teks, bukan
1861
2029
  bertumpuk) dan `.fdy-filelist--grid` (baris file berdampingan di layar lebar).
1862
2030
 
1863
2031
  ### Changed
1864
- - **Docs tiga section jadi full-width & selaras.** Tree (kini menampilkan varian dasar +
2032
+ - **Docs: tiga section jadi full-width & selaras.** Tree (kini menampilkan varian dasar +
1865
2033
  checkbox berdampingan), Form (validasi inline + contoh **header + detail**: `.fdy-form-grid` +
1866
2034
  tabel item), dan File upload semuanya melebar mengisi kolom konten agar lebar semua section rata.
1867
2035
  Terverifikasi lewat gestur mouse asli (headless CDP `Input.dispatchMouseEvent`): cascade
1868
2036
  centang/indeterminate, checkbox tak melipat cabang, teks summary tetap toggle.
1869
2037
 
1870
- ## [1.6.2] 2026-07-24
1871
- Rilis **patch lisensi**. Tak ada perubahan kode.
2038
+ ## [1.6.2] - 2026-07-24
2039
+ Rilis **patch untuk lisensi**. Tak ada perubahan kode.
1872
2040
 
1873
2041
  ### Changed
1874
2042
  - **Lisensi jadi [MIT](LICENSE)** (`Copyright (c) 2026 Cahyo D. Kurnianto`). Sebelumnya
1875
- `"license": "UNLICENSED"` tanpa file `LICENSE` di repo publik itu berarti *all rights
2043
+ `"license": "UNLICENSED"` tanpa file `LICENSE`, dan di repo publik itu berarti *all rights
1876
2044
  reserved*, jadi tak ada yang boleh memakainya secara legal. Sekarang bebas dipakai/ubah/
1877
2045
  distribusi asal menyertakan copyright. `package.json`/`package-lock.json` → `"MIT"`, tambah
1878
2046
  file `LICENSE`, catat di README + footer docs. `"private": true` dipertahankan (pengaman
1879
2047
  anti-`npm publish`, tak terkait lisensi).
1880
2048
 
1881
- ## [1.6.1] 2026-07-24
1882
- Rilis **patch perbaikan bug pemilihan combo dengan mouse**. Non-breaking.
2049
+ ## [1.6.1] - 2026-07-24
2050
+ Rilis **patch: perbaikan bug pemilihan combo dengan mouse**. Non-breaking.
1883
2051
 
1884
2052
  ### Fixed
1885
- - **`freeday-select.js` (combo/select vanilla) memilih opsi dengan mouse tidak berfungsi.**
2053
+ - **`freeday-select.js` (combo/select vanilla): memilih opsi dengan mouse tidak berfungsi.**
1886
2054
  Menekan (mousedown) sebuah opsi mem-blur tombol combo; handler `focusout` lalu memanggil
1887
2055
  `close()` yang menyembunyikan listbox **sebelum** event `click` opsi sempat menjalankan
1888
- `choose()` sehingga pilihan hilang dan nilai tak berubah (hanya terlihat dengan mouse asli;
2056
+ `choose()`, sehingga pilihan hilang dan nilai tak berubah (hanya terlihat dengan mouse asli;
1889
2057
  navigasi keyboard tak terpengaruh). Perbaikan: `preventDefault` pada `mousedown` listbox agar
1890
2058
  tombol tetap fokus, jadi `click` mendarat normal. Terverifikasi lewat gestur mouse asli
1891
2059
  (headless CDP `Input.dispatchMouseEvent`) pada playground docs dan section Select. Memengaruhi
1892
2060
  konsumen vanilla `freeday/js`; adapter Vue/React punya implementasi sendiri dan tak terdampak.
1893
2061
 
1894
- ## [1.6.0] 2026-07-24
1895
- Rilis **1.6 wrapper input ekstra (Vue + React) + filter-bar**. Non-breaking, aditif.
2062
+ ## [1.6.0] - 2026-07-24
2063
+ Rilis **1.6: wrapper input ekstra (Vue + React) + filter-bar**. Non-breaking, aditif.
1896
2064
 
1897
2065
  ### Added
1898
- - **`FdyDateRange` (Vue + React)** rentang tanggal terkontrol `{start, end}` yang menyusun dua
2066
+ - **`FdyDateRange` (Vue + React)** adalah rentang tanggal terkontrol `{start, end}` yang menyusun dua
1899
2067
  `FdyDatepicker` di atas layout `.fdy-daterange` yang sudah ada (akhir tak bisa mendahului awal,
1900
2068
  lewat min/max saling terkait). `v-model` di Vue, `value`/`onChange` di React. Tanpa CSS baru.
1901
- - **`FdyAutocomplete` (Vue + React)** editable combobox WAI-ARIA APG terkontrol, port dari
2069
+ - **`FdyAutocomplete` (Vue + React)** adalah editable combobox WAI-ARIA APG terkontrol, port dari
1902
2070
  enhancer `freeday-autocomplete.js` di atas CSS `.fdy-autocomplete` yang sama: filter saat
1903
2071
  mengetik (substring case-insensitive pada query ter-trim), ↑/↓ dengan wrap, Enter commit,
1904
2072
  Esc/Tab/klik-luar menutup, `aria-activedescendant` + empty state, dropdown top-layer via
1905
- Popover API. `onSelect`/`select` hanya saat commit terpisah dari perubahan ketikan, jadi
2073
+ Popover API. `onSelect`/`select` hanya saat commit, terpisah dari perubahan ketikan, jadi
1906
2074
  options yang sudah difilter server bisa dilempar masuk apa adanya.
1907
- - **`FdyCascade` (Vue + React)** picker hierarkis drill-down terkontrol, port dari enhancer
2075
+ - **`FdyCascade` (Vue + React)** adalah picker hierarkis drill-down terkontrol, port dari enhancer
1908
2076
  `freeday-cascade.js` di atas CSS `.fdy-cascade` yang sama: satu level sekaligus, branch masuk,
1909
2077
  kontrol back naik, leaf memilih (value = value leaf, tampilan = path penuh), buka ulang di level
1910
2078
  terpilih. Keyboard: Up/Down/Home/End, ArrowRight/Enter/Space aktifkan, ArrowLeft/Backspace naik,
1911
2079
  Esc tutup; `aria-activedescendant` + crumb live. Model data = **pohon `CascadeNode` bertipe**,
1912
2080
  menggantikan `<ul>` bersarang tersembunyi milik enhancer.
1913
- - **`.fdy-filterbar`** layout primitive untuk baris filter yang konsisten, di atas `.fdy-field`:
2081
+ - **`.fdy-filterbar`** adalah layout primitive untuk baris filter yang konsisten, di atas `.fdy-field`:
1914
2082
  ritme lebar kolom (`--w-sm` / default / `--w-lg` / `--w-xl`), satu field `--w-grow` menyerap sisa
1915
2083
  ruang, `__actions` menempel di ujung sejajar kontrol, wrap rapi di layar sempit. Nol JS.
1916
2084
  Menutup gap "filter row ragged" dari adoption backlog #11.
1917
2085
 
1918
2086
  ### Changed
1919
- - Contoh `react-faktur` kini menggerakkan **Kategori** lewat `FdyCascade` menghapus jembatan
2087
+ - Contoh `react-faktur` kini menggerakkan **Kategori** lewat `FdyCascade`, menghapus jembatan
1920
2088
  event `fdy-cascade-change` manual; ditambah field Periode (`FdyDateRange`) & Kota (`FdyAutocomplete`).
1921
2089
  - Contoh `vue-faktur` mendapat demo Vue-native untuk ketiga komponen baru.
1922
2090
 
1923
- Dengan rilis ini adapter **Vue dan React simetris penuh** tujuh komponen controlled typed yang
2091
+ Dengan rilis ini adapter **Vue dan React simetris penuh**: tujuh komponen controlled typed yang
1924
2092
  sama di kedua sisi: `FdyCombo` · `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` ·
1925
2093
  `FdyCascade` · `FdyCfl` · `FdyChart`.
1926
2094
 
1927
- ## [1.5.0] 2026-07-24
1928
- Rilis **1.5 React adapter parity**. Non-breaking, aditif.
2095
+ ## [1.5.0] - 2026-07-24
2096
+ Rilis **1.5: React adapter parity**. Non-breaking, aditif.
1929
2097
  ### Added
1930
- - **React adapter parity** komponen controlled typed `FdyCombo` / `FdyDatepicker` / `FdyCfl` /
2098
+ - **React adapter parity**: komponen controlled typed `FdyCombo` / `FdyDatepicker` / `FdyCfl` /
1931
2099
  `FdyChart` + `usePopover`, di atas CSS kit yang sama (aksesibilitas WAI-ARIA APG, dropdown
1932
2100
  top-layer lewat Popover API). Aplikasi React tak lagi butuh fallback `<select>`/`<input
1933
2101
  type="date">` native. Dikonsumsi lewat `freeday/react`; Vite men-transpile source component
1934
2102
  langsung tanpa config tambahan, konsumen Next.js mungkin butuh
1935
2103
  `transpilePackages: ['freeday']`.
1936
2104
 
1937
- ## [1.4.1] 2026-07-23
1938
- Rilis **1.4.1 — patch**. Dropdown tak lagi ter-clip di dalam card / scroll container.
2105
+ ## [1.4.1] - 2026-07-23
2106
+ Rilis **patch 1.4.1**. Dropdown tak lagi ter-clip di dalam card / scroll container.
1939
2107
  ### Fixed
1940
2108
  - **Dropdown lepas dari clipping ancestor** (`.fdy-card{overflow:hidden}`, scroll container, atau
1941
- ancestor ber-`transform`). Semua dropdown `position:absolute` **combo/select, datepicker,
1942
- cascade, autocomplete, timepicker, menu** kini di-render di **top layer** lewat native
2109
+ ancestor ber-`transform`). Semua dropdown `position:absolute` (**combo/select, datepicker,
2110
+ cascade, autocomplete, timepicker, menu**) kini di-render di **top layer** lewat native
1943
2111
  **Popover API** (`popover="manual"`) dan diposisikan `fixed` ke trigger (flip ke atas bila
1944
2112
  sempit di bawah, lebar mengikuti trigger, reposisi saat scroll/resize). Panel tetap DOM child
1945
2113
  komponennya, jadi focus, outside-click, dan ARIA tak berubah. Berlaku untuk enhancer vanilla
@@ -1950,32 +2118,32 @@ Rilis **1.4.1 — patch**. Dropdown tak lagi ter-clip di dalam card / scroll con
1950
2118
  `freeday/vue` (internal, tak diekspor). Panel dropdown kini set `color:var(--color-text)` eksplisit
1951
2119
  (UA `[popover]` default-nya `CanvasText`).
1952
2120
 
1953
- ## [1.4.0] 2026-07-23
1954
- Rilis **1.4 motion & native charts**. Dua fitur besar (gerak enter/exit lintas komponen +
2121
+ ## [1.4.0] - 2026-07-23
2122
+ Rilis **1.4: motion & native charts**. Dua fitur besar (gerak enter/exit lintas komponen +
1955
2123
  chart native yang cukup untuk mem-pensiun-kan Chart.js) plus satu fix layout. Non-breaking.
1956
2124
 
1957
2125
  ### Added
1958
- - **Chart native parity untuk drop Chart.js.** Tipe baru **`line`** & **`area`** dan **`bar`
2126
+ - **Chart native, parity untuk drop Chart.js.** Tipe baru **`line`** & **`area`** dan **`bar`
1959
2127
  multi-seri + `data-fdy-stacked`**, semuanya dengan **sumbu ber-tema** (y-gridline + tick,
1960
2128
  x-label autoskip, baseline nol) dari `--chart-grid`/`--chart-tick`. Data multi-seri lewat
1961
2129
  **`data-series`** (JSON `[{label,values}]`); `data-values` tetap jadi shortcut seri-tunggal.
1962
2130
  **`data-fdy-format="number|percent|currency"`** memformat tick + tooltip; **legenda otomatis**
1963
2131
  untuk ≥2 seri (`data-fdy-legend="auto|always|none"`); hover band per-kategori. Warna dari token
1964
2132
  → **chart re-warna otomatis saat `data-theme` berubah** (tak perlu observer/re-paint kanvas).
1965
- - **`FreedayChart.update(el)`** render ulang idempoten saat data berubah (auto-init tetap sekali).
1966
- - **`freeday/vue` → `<FdyChart>`** wrapper reaktif & typed di atas renderer (re-render on prop
2133
+ - **`FreedayChart.update(el)`** me-render ulang secara idempoten saat data berubah (auto-init tetap sekali).
2134
+ - **`freeday/vue` → `<FdyChart>`** adalah wrapper reaktif & typed di atas renderer (re-render on prop
1967
2135
  change): `type` · `series`/`values` · `labels` · `format` · `stacked` · `legend` · `colors`/`color`
1968
2136
  · `center`; `aria-label` fallthrough. Tipe `FdyChartSeries` diekspor. Diverifikasi `vue-tsc` + `vite build`.
1969
2137
 
1970
2138
  ### Changed
1971
- - **Motion pass gerak yang bermakna, semua hormati `prefers-reduced-motion`.**
2139
+ - **Motion pass: gerak yang bermakna, semua hormati `prefers-reduced-motion`.**
1972
2140
  - **Drawer & Modal** kini beranimasi **masuk _dan_ keluar** (slide / fade-scale + backdrop blur)
1973
- via `@starting-style` + `allow-discrete` pada `<dialog>` native sebelumnya hanya masuk.
1974
- - **Sidebar app-shell** collapse/expand desktop kini **meluncur** (animasi `width`) sebelumnya
2141
+ via `@starting-style` + `allow-discrete` pada `<dialog>` native; sebelumnya hanya masuk.
2142
+ - **Sidebar app-shell** collapse/expand desktop kini **meluncur** (animasi `width`); sebelumnya
1975
2143
  `display:none` (snap). Mobile (off-canvas) tak berubah.
1976
2144
  - **Accordion** & **Tabs** me-reveal konten (fade + rise) saat dibuka/diganti.
1977
2145
  - **Baris tabel** transisi `background` halus saat hover.
1978
- - **`.fdy-daterange` responsif** dua picker kini boleh menyusut (`min-width:0`) & wrap, jadi tak
2146
+ - **`.fdy-daterange` responsif**: dua picker kini boleh menyusut (`min-width:0`) & wrap, jadi tak
1979
2147
  lagi overflow di row/HP < ~23rem; di lebar cukup tetap hug ~23rem seperti sebelumnya.
1980
2148
 
1981
2149
  ### Migrasi konsumen (opsional, aman)
@@ -1983,10 +2151,10 @@ chart native yang cukup untuk mem-pensiun-kan Chart.js) plus satu fix layout. No
1983
2151
  2. **Motion & daterange:** otomatis, tanpa perubahan kode.
1984
2152
  3. **Drop Chart.js (opsional):** ganti wrapper chart lokal dengan `<FdyChart type="line" :series>`
1985
2153
  (atau bentuk `data-fdy-chart`), hapus folder wrapper chart lokal, dan buang `chart.js` dari
1986
- `package.json`. Machinery tema chart (probe warna + observer) tak lagi perlu token yang urus.
2154
+ `package.json`. Machinery tema chart (probe warna + observer) tak lagi perlu, karena token yang urus.
1987
2155
 
1988
- ## [1.3.1] 2026-07-23
1989
- Rilis **1.3.1 — patch**. Buang rail aksen "eyebrow" di state aktif/terpilih.
2156
+ ## [1.3.1] - 2026-07-23
2157
+ Rilis **patch 1.3.1**. Buang rail aksen "eyebrow" di state aktif/terpilih.
1990
2158
  ### Removed
1991
2159
  - **Rail aksen 3px pada state aktif/terpilih** (`box-shadow:inset 3px 0 0 var(--color-primary)`)
1992
2160
  dihapus dari nav app-shell (`[aria-current="page"]`), opsi cascade & autocomplete
@@ -1995,27 +2163,27 @@ Rilis **1.3.1 — patch**. Buang rail aksen "eyebrow" di state aktif/terpilih.
1995
2163
  soft-bg satu baris penuh). Murni visual, non-breaking; kontras tetap **9/9**.
1996
2164
  ### Kept
1997
2165
  - Indikator fungsional yang **beda motif** sengaja dipertahankan: rail fokus keyboard CFL
1998
- (`:focus-visible`, wajib a11y pembeda fokus vs terpilih), underline tab aktif, checkmark
2166
+ (`:focus-visible`, wajib a11y sebagai pembeda fokus vs terpilih), underline tab aktif, checkmark
1999
2167
  combo, ring highlight combo.
2000
2168
 
2001
- ## [1.3.0] 2026-07-23
2002
- Rilis **1.3 accent Azure**. Warna sekunder/aksen berubah dari **teal → biru Microsoft Azure**.
2169
+ ## [1.3.0] - 2026-07-23
2170
+ Rilis **1.3: accent Azure**. Warna sekunder/aksen berubah dari **teal → biru Microsoft Azure**.
2003
2171
  ### Changed
2004
- - **Accent: teal → sky / Azure blue** `--color-accent` kini `#0078d4` (light) / `#47a1e6` (dark),
2172
+ - **Accent: teal → sky / Azure blue.** `--color-accent` kini `#0078d4` (light) / `#47a1e6` (dark),
2005
2173
  menggantikan teal `#0d9488`. Ramp primitif `teal` diganti `sky` (biru Azure muda);
2006
2174
  `--color-accent-hover` & `--color-on-accent` ikut disesuaikan (on-accent dark `#06233f` agar
2007
2175
  kontras di atas accent biru-muda dark). Semua konsumen `--color-accent` ikut otomatis: varian
2008
2176
  opt-in `.fdy-fab--accent`/`.fdy-slider--accent`/`.fdy-badge-ov--accent`, chrome docs, swatch
2009
- identitas. Identitas kit kini **Azure** (primary biru-deep + accent biru-muda) teal dibuang.
2177
+ identitas. Identitas kit kini **Azure** (primary biru-deep + accent biru-muda); teal dibuang.
2010
2178
  Kontras WCAG tetap **9/9** (accent/surface + on-accent/accent lolos light & dark). Non-breaking
2011
2179
  (nilai token berubah; nama token/kelas/API tetap).
2012
2180
 
2013
- ## [1.2.2] 2026-07-23
2014
- Rilis **1.2.2 — patch**. Polish app-shell + koreksi warna indikator aktif/terpilih.
2181
+ ## [1.2.2] - 2026-07-23
2182
+ Rilis **patch 1.2.2**. Polish app-shell + koreksi warna indikator aktif/terpilih.
2015
2183
  ### Fixed
2016
- - **Brand link-safe (#17)** `.fdy-app__brand` kini set `text-decoration:none`; saat brand
2184
+ - **Brand link-safe (#17):** `.fdy-app__brand` kini set `text-decoration:none`; saat brand
2017
2185
  dijadikan link "logo → home", judul/subjudul tak lagi ber-underline default browser.
2018
- - **Indikator aktif/terpilih: teal → primary (#18 + extend)** — bar/underline/check pada state
2186
+ - **Indikator aktif/terpilih: teal → primary (#18 + extend).** Bar/underline/check pada state
2019
2187
  aktif/terpilih sebelumnya pakai `--color-accent` (teal), yang **clash** dengan bg
2020
2188
  `--color-primary-soft` + teks `--color-primary-strong` pada palet re-theme (accent ≠ primary).
2021
2189
  Kini semua cue "you-are-here / terpilih" seragam di keluarga **primary**: item nav, underline
@@ -2023,33 +2191,33 @@ Rilis **1.2.2 — patch**. Polish app-shell + koreksi warna indikator aktif/terp
2023
2191
  (`.fdy-fab--accent`, `.fdy-badge-ov--accent`, `.fdy-slider--accent`) tak berubah. Membalik
2024
2192
  bagian "teal sebagai aksen fungsional" dari v1.1.0. Murni CSS, backward-compatible.
2025
2193
 
2026
- ## [1.2.1] 2026-07-23
2027
- Rilis **1.2.1 — patch**. Perbaikan CSS ikon depan `.fdy-input-group`.
2194
+ ## [1.2.1] - 2026-07-23
2195
+ Rilis **patch 1.2.1**. Perbaikan CSS ikon depan `.fdy-input-group`.
2028
2196
  ### Fixed
2029
- - **Ikon depan input-group tak ke-center + gap** `.fdy-input-group__addon--icon` dipakai
2197
+ - **Ikon depan input-group tak ke-center + gap.** `.fdy-input-group__addon--icon` dipakai
2030
2198
  standalone tapi tak set `display`/`align-items`, jadi `<svg>`-nya nempel ke atas kotak flex
2031
2199
  yang ter-stretch (ikon melayang tinggi); dan padding-kiri ikon menumpuk dengan padding-kiri
2032
2200
  `.fdy-input` → gap lebar. Kini `--icon` self-contained (`inline-flex` + center, meniru
2033
2201
  `.fdy-input-group__btn`) dan `.fdy-input` setelah ikon memangkas padding-kirinya. Murni CSS,
2034
2202
  backward-compatible. Kena semua field search/find (topbar search, filter tabel, dialog CFL, dst).
2035
2203
 
2036
- ## [1.2.0] 2026-07-23
2037
- Rilis **1.2 Vue input wrappers (native reimpl)**. Komponen Vue idiomatik dengan `v-model`
2204
+ ## [1.2.0] - 2026-07-23
2205
+ Rilis **1.2: Vue input wrappers (native reimpl)**. Komponen Vue idiomatik dengan `v-model`
2038
2206
  nyata di atas class CSS kit, supaya form Vue tak perlu fallback ke `<select>`/`<input type=date>`
2039
2207
  native. **Non-breaking**: murni tambahan (file `.vue` + export baru); enhancer, `useFreeday`,
2040
2208
  dan semua class/token lama tak berubah. `vue` tetap peerDependency opsional.
2041
2209
  ### Added
2042
- - **`FdyCombo`** (`import { FdyCombo } from 'freeday/vue'`) combobox select-only WAI-ARIA APG
2210
+ - **`FdyCombo`** (`import { FdyCombo } from 'freeday/vue'`) adalah combobox select-only WAI-ARIA APG
2043
2211
  dengan `v-model` (generic `T extends string`), keyboard penuh (↑↓/Home/End/Enter/Esc/typeahead)
2044
2212
  + outside-click; props `options`/`placeholder`/`disabled`/`invalid`/`describedby`/`ariaLabelledby`.
2045
- - **`FdyDatepicker`** kalender native di atas `datepicker.css`; `v-model` string ISO
2213
+ - **`FdyDatepicker`** adalah kalender native di atas `datepicker.css`; `v-model` string ISO
2046
2214
  `YYYY-MM-DD`, `min`/`max`, `locale` (Intl), keyboard grid penuh, `invalid`/`ariaLabelledby`.
2047
- - **`FdyCfl`** choose-from-list **controlled + async**: `v-model:Row|null` +
2215
+ - **`FdyCfl`** adalah choose-from-list **controlled + async**: `v-model:Row|null` +
2048
2216
  `fetchPage(query,page) => Promise<{rows,hasMore}>`, `columns`/`display`/`rowKey`. `<dialog>`
2049
2217
  native, search debounced, state loading/empty/error(+retry), pagination append, cache opsional,
2050
2218
  guard respons out-of-order. Ini kontrak integrasi field lookup (map nilai → master data server).
2051
- - **Error-state kit** varian `--error`/`[aria-invalid]` ditambah ke `combo`/`cascade`/
2052
- `datepicker`/`timepicker`, dan `input-group :has()` diperluas menangkap error kontrol jadi
2219
+ - **Error-state kit:** varian `--error`/`[aria-invalid]` ditambah ke `combo`/`cascade`/
2220
+ `datepicker`/`timepicker`, dan `input-group :has()` diperluas menangkap error kontrol, jadi
2053
2221
  validasi vanilla (`freeday-form`) **dan** prop `invalid` wrapper Vue sama-sama ber-styling.
2054
2222
  (Field CFL = `.fdy-input`+input-group, jadi error-nya via `aria-invalid` pada input, bukan
2055
2223
  kelas `.fdy-cfl--error`.)
@@ -2061,59 +2229,59 @@ dan semua class/token lama tak berubah. `vue` tetap peerDependency opsional.
2061
2229
  - Follow-up diketahui (warisan datepicker, non-blocking): grid `role="grid"` datar tanpa `row`;
2062
2230
  hari fokus-awal yang ter-`min`/`max`-disable bisa bikin grid tak ter-fokus keyboard di config tepi.
2063
2231
 
2064
- ## [1.1.0] 2026-07-23
2065
- Rilis **1.1 "Precision" visual polish + pengerasan adopsi**. Dua bagian: **(1) penyegaran
2066
- visual** token-driven radius lebih tajam, elevation overlay lebih tegas, hierarki tipografi
2232
+ ## [1.1.0] - 2026-07-23
2233
+ Rilis **1.1: "Precision" visual polish + pengerasan adopsi**. Dua bagian: **(1) penyegaran
2234
+ visual** token-driven: radius lebih tajam, elevation overlay lebih tegas, hierarki tipografi
2067
2235
  heading lebih jelas, teal (`--color-accent`) sebagai indikator fungsional non-teks untuk state
2068
- aktif/terpilih; **(2) pengerasan adopsi** dari pemakaian project nyata palet chart tervalidasi,
2236
+ aktif/terpilih; **(2) pengerasan adopsi** dari pemakaian project nyata: palet chart tervalidasi,
2069
2237
  ukuran modal, nav statis, brand shell, export breakpoint, drawer `initAll`, dan tokenisasi kontrol.
2070
2238
  **Non-breaking**: tak ada rename kelas/token/API (`fdy-` prefix, `--color-*`,
2071
2239
  `window.Freeday*` tetap stabil). **AA-gated**: `npm test` tetap hijau (9/9), termasuk 6
2072
2240
  assertion kontras baru yang menjaga teal-on-surface ≥3:1 di light & dark.
2073
2241
  ### Changed
2074
- - **Radius ramp lebih tajam** skala dirapatkan ke `3/4/6/10/14/999px` (dari nilai lama yang
2242
+ - **Radius ramp lebih tajam.** Skala dirapatkan ke `3/4/6/10/14/999px` (dari nilai lama yang
2075
2243
  lebih membulat), kesan lebih presisi di semua kontrol & container.
2076
- - **Motion lebih responsif** durasi dasar `--dur-base` 200ms→180ms, `--dur-slow` 320ms→280ms.
2077
- - **Elevation overlay dijatah ulang** bayangan `--shadow-3` (menu/dropdown/popover) dan
2244
+ - **Motion lebih responsif.** Durasi dasar `--dur-base` 200ms→180ms, `--dur-slow` 320ms→280ms.
2245
+ - **Elevation overlay dijatah ulang.** Bayangan `--shadow-3` (menu/dropdown/popover) dan
2078
2246
  `--shadow-4` (modal) kini lebih tajam & lebih hadir secara visual; bayangan resting
2079
2247
  (kartu, tombol) tidak berubah.
2080
- - **Hierarki heading "Precision"** `h1`–`h4` kini memakai `--font-display` (Sora) / 700 /
2248
+ - **Hierarki heading "Precision".** `h1`–`h4` kini memakai `--font-display` (Sora) / 700 /
2081
2249
  `--tracking-tighter` (token baru, -0.03em) / `--leading-snug`; ukuran font tidak berubah.
2082
- - **State interaksi disatukan** — hover/active/disabled/focus-visible dirapikan lintas
2250
+ - **State interaksi disatukan.** Hover/active/disabled/focus-visible dirapikan lintas
2083
2251
  input/combo/cascade/chip/pagination/selection; memperbaiki bug nyata di mana
2084
2252
  `.fdy-chip--filter` tidak punya focus ring sama sekali.
2085
- - **`.fdy-table-scroll` scroll tanpa syarat** tak lagi hanya berlaku di dalam `.fdy-datatable`
2253
+ - **`.fdy-table-scroll` scroll tanpa syarat**, tak lagi hanya berlaku di dalam `.fdy-datatable`
2086
2254
  (`.fdy-table-wrap` untuk shell berbingkai, `.fdy-table-scroll` untuk scroll polos standalone).
2087
- - **`selection.css` di-tokenkan** nilai mentah (`#fff` thumb switch, inset `rgba`, dimensi
2255
+ - **`selection.css` di-tokenkan.** Nilai mentah (`#fff` thumb switch, inset `rgba`, dimensi
2088
2256
  `1.15`/`2.4`/`1.35rem`) → token (`--color-switch-thumb`, `--color-shadow-inset`, `--control-box`,
2089
2257
  `--control-switch-w/-h`). Nol perubahan visual.
2090
- - **Default warna seri donut** saat `data-fdy-colors` lebih pendek dari jumlah seri, celah kini
2258
+ - **Default warna seri donut:** saat `data-fdy-colors` lebih pendek dari jumlah seri, celah kini
2091
2259
  diisi `--chart-N` fixed-order (bukan cycle palet semantik lama). Non-breaking.
2092
2260
  ### Added
2093
- - **Teal sebagai aksen fungsional** `--color-accent` dipakai sebagai indikator non-teks
2261
+ - **Teal sebagai aksen fungsional.** `--color-accent` dipakai sebagai indikator non-teks
2094
2262
  untuk state aktif/terpilih: item nav aktif, underline tab aktif, baris tabel terpilih,
2095
- opsi combo/cascade/autocomplete terpilih. Bukan dekorasi murni penanda status.
2263
+ opsi combo/cascade/autocomplete terpilih. Bukan dekorasi, murni penanda status.
2096
2264
  - **Token baru** `--tracking-tighter` (-0.03em) untuk heading Sora.
2097
2265
  - **6 assertion kontras WCAG (`AA_UI`, 3:1)** baru di `test/contrast.test.mjs` yang menjaga
2098
2266
  teal-on-surface di light & dark tetap lolos ambang kontrol non-teks.
2099
- - **Palet chart kategorikal** token `--chart-1..8` (+ `--chart-grid`/`--chart-tick`), 8 warna
2267
+ - **Palet chart kategorikal:** token `--chart-1..8` (+ `--chart-grid`/`--chart-tick`), 8 warna
2100
2268
  colorblind-safe **tervalidasi** (validator dataviz, light & dark di surface Freeday: dark
2101
2269
  all-pass, light lolos + relief rule). Donut menggambar seri **fixed-order** (tanpa cycle).
2102
2270
  Token chart sengaja di luar `contrast.test.mjs` (a11y via validator palet + legend/label).
2103
- - **Modal size** `.fdy-modal--sm/--md/--lg/--wide` (24/32/48/60rem, responsif, anti-overflow).
2104
- - **`.fdy-nav--flat`** varian nav grup statis (tanpa caret / garis antar-grup / affordance
2271
+ - **Modal size:** `.fdy-modal--sm/--md/--lg/--wide` (24/32/48/60rem, responsif, anti-overflow).
2272
+ - **`.fdy-nav--flat`** adalah varian nav grup statis (tanpa caret / garis antar-grup / affordance
2105
2273
  collapse) + dukung markup non-`<details>` yang benar-benar tak bisa dilipat.
2106
- - **Brand shell berstruktur** `.fdy-app__brand-mark`/`-title`/`-subtitle` (ikon + 2 baris di
2274
+ - **Brand shell berstruktur:** `.fdy-app__brand-mark`/`-title`/`-subtitle` (ikon + 2 baris di
2107
2275
  header shell); pemakaian teks satu-baris tetap jalan.
2108
- - **`FreedayDrawer.initAll`** drawer kini ter-hydrate `useFreeday` di subtree Vue/React.
2109
- - **Export skala breakpoint** `freeday/breakpoints` (`{ sm:600, md:960, lg:1280, xl:1920 }`)
2276
+ - **`FreedayDrawer.initAll`** membuat drawer ter-hydrate `useFreeday` di subtree Vue/React.
2277
+ - **Export skala breakpoint:** `freeday/breakpoints` (`{ sm:600, md:960, lg:1280, xl:1920 }`)
2110
2278
  untuk `matchMedia` / menyelaraskan `@media` app ke skala kit.
2111
- - **Docs** contoh **product-appbar** siap-copas (breadcrumb + input-group search + menu);
2279
+ - **Docs:** contoh **product-appbar** siap-copas (breadcrumb + input-group search + menu);
2112
2280
  install `git+https` untuk CI (repo privat tanpa SSH key); klarifikasi `.fdy-btn` = primary,
2113
2281
  `.fdy-table-wrap`, dan density `--control-h` untuk komponen custom.
2114
2282
 
2115
- ## [1.0.0] 2026-07-22
2116
- Rilis **1.0 project-ready**, sekaligus **rebrand ke Freeday**. Definisi v1.0 terpenuhi:
2283
+ ## [1.0.0] - 2026-07-22
2284
+ Rilis **1.0, project-ready**, sekaligus **rebrand ke Freeday**. Definisi v1.0 terpenuhi:
2117
2285
  kontras WCAG AA lolos audit otomatis · installable via git · integrasi Vue/React/Blazor
2118
2286
  terbukti di contoh faktur yang jalan · docs adopsi lengkap.
2119
2287
  ### Changed
@@ -2127,51 +2295,51 @@ terbukti di contoh faktur yang jalan · docs adopsi lengkap.
2127
2295
  ### Docs
2128
2296
  - Root redirect `index.html` → `docs/` untuk GitHub Pages (URL bersih `/freeday-ui-kit/`).
2129
2297
 
2130
- ## [0.9.6] 2026-07-22
2298
+ ## [0.9.6] - 2026-07-22
2131
2299
  ### Docs
2132
- - **Whole-app language toggle (ID ⇄ EN)** the topbar toggle now switches the entire docs,
2300
+ - **Whole-app language toggle (ID ⇄ EN):** the topbar toggle now switches the entire docs,
2133
2301
  not just the landing: topbar, sidebar labels + group headers, every section heading, and
2134
2302
  every description. Keyed by section id / nav href, round-trips cleanly. (In-demo sample
2135
2303
  labels stay as illustrative content.)
2136
- - **Proper state icons** the empty / error state cards use duotone SVG icons instead of
2304
+ - **Proper state icons:** the empty / error state cards use duotone SVG icons instead of
2137
2305
  emoji (🗂️/⚠️).
2138
2306
  - Hero CTA spacing loosened (removed a stale `.doc-hero p` rule that zeroed the gap).
2139
2307
  ### Changed
2140
- - **Icons stay single-tone** reverted the date/time picker glyphs to minimalist single-tone
2141
- (duotone is reserved for the larger illustrative icons states and the theme toggle).
2308
+ - **Icons stay single-tone.** Reverted the date/time picker glyphs to minimalist single-tone
2309
+ (duotone is reserved for the larger illustrative icons, meaning states, and the theme toggle).
2142
2310
 
2143
- ## [0.9.5] 2026-07-22
2311
+ ## [0.9.5] - 2026-07-22
2144
2312
  ### Added
2145
- - **Two-tone (duotone) control icons** the date/time picker triggers (and the datetime
2313
+ - **Two-tone (duotone) control icons:** the date/time picker triggers (and the datetime
2146
2314
  composer that reuses them) now render with a low-opacity fill behind the stroke, matching the
2147
2315
  sun/moon theme toggle. A coherent duotone treatment for the control-glyph family.
2148
2316
  ### Docs
2149
- - **Landing language toggle (ID ⇄ EN)** a topbar button swaps the landing copy (hero, stats,
2317
+ - **Landing language toggle (ID ⇄ EN):** a topbar button swaps the landing copy (hero, stats,
2150
2318
  framework-integration, footer) between Indonesian and English via `[data-i18n]`; inline
2151
2319
  markup (bold/code) is preserved. Version references synced to v0.9.5.
2152
2320
 
2153
- ## [0.9.4] 2026-07-22
2321
+ ## [0.9.4] - 2026-07-22
2154
2322
  ### Added
2155
- - **Sidebar-menu component** `.fdy-nav` (App shell) now supports per-node `.fdy-nav__icon`
2323
+ - **Sidebar-menu component:** `.fdy-nav` (App shell) now supports per-node `.fdy-nav__icon`
2156
2324
  and `.fdy-nav__badge` (count), collapsible sections `.fdy-nav__group`/`.fdy-nav__grouplabel`,
2157
2325
  and nested items `.fdy-nav__tree` + `.fdy-nav__sub` (native `<details>`, zero-JS). Any node
2158
2326
  can be icon+text, text-only, or badged. New "Sidebar menu" docs section.
2159
- - **Responsive App shell** on mobile (≤720px) the sidebar becomes an **off-canvas drawer**
2327
+ - **Responsive App shell:** on mobile (≤720px) the sidebar becomes an **off-canvas drawer**
2160
2328
  opened by the topbar toggle, with a `.fdy-app__backdrop`; on desktop the toggle collapses it.
2161
2329
  ### Changed
2162
- - **Brand ↔ topbar aligned** `.fdy-app__brand` is now the same height as the topbar
2330
+ - **Brand ↔ topbar aligned:** `.fdy-app__brand` is now the same height as the topbar
2163
2331
  (`--space-16`) so the logo and page title sit on the same line. `.fdy-app__title` truncates
2164
2332
  instead of forcing overflow.
2165
2333
  ### Fixed
2166
- - **Awkward Indonesian copy** dropzone “Jatuhkan …” → “Seret berkas ke sini …”.
2334
+ - **Awkward Indonesian copy:** dropzone “Jatuhkan …” → “Seret berkas ke sini …”.
2167
2335
  ### Docs
2168
2336
  - Theme toggle is now a two-tone **sun/moon icon** button. Clearer **notification badge** demo
2169
2337
  (bell + count) explaining the overlay badge works on any element. Responsive fixes so nothing
2170
2338
  overflows down to 320px.
2171
2339
 
2172
- ## [0.9.3] 2026-07-22
2340
+ ## [0.9.3] - 2026-07-22
2173
2341
  ### Added
2174
- - **Chart hover (Chart.js-style)** bar & donut charts now show an interactive tooltip that
2342
+ - **Chart hover (Chart.js-style):** bar & donut charts now show an interactive tooltip that
2175
2343
  follows the cursor (`label: value`, donut adds `(%)`). Bars dim while one is hovered (the
2176
2344
  hovered bar stays lit); donut gains a transparent SVG sector per slice that pops on hover.
2177
2345
  ### Changed
@@ -2179,29 +2347,29 @@ terbukti di contoh faktur yang jalan · docs adopsi lengkap.
2179
2347
  scrolls away). New `.fdy-app__navtoggle` (hamburger) + `.fdy-app--nav-collapsed` to
2180
2348
  collapse/hide the sidebar. Roomier `--space-8` main padding.
2181
2349
  ### Fixed
2182
- - **Anchor buttons underlined** `.fdy-btn` now sets `text-decoration:none`, so links styled
2350
+ - **Anchor buttons underlined:** `.fdy-btn` now sets `text-decoration:none`, so links styled
2183
2351
  as buttons (e.g. hero CTAs) no longer show an underline.
2184
2352
  ### Docs
2185
2353
  - Nav categories are collapsible `<details>` groups with chevrons (PrimeVue-style); more
2186
2354
  section whitespace; topbar hamburger to collapse the sidebar.
2187
2355
 
2188
- ## [0.9.2] 2026-07-22
2356
+ ## [0.9.2] - 2026-07-22
2189
2357
  ### Fixed
2190
- - **Hover kontrol form tak terlihat** sejak border resting jadi `--color-control-border`
2358
+ - **Hover kontrol form tak terlihat.** Sejak border resting jadi `--color-control-border`
2191
2359
  (slate-500) untuk a11y, hover yang menuju `--color-text-subtle` (juga slate-500) tak berubah.
2192
2360
  Semua kontrol (input, textarea, combo, cascade, time/date picker, input-group, dropzone) kini
2193
2361
  hover ke `--color-text-muted` (slate-600) → menggelap terlihat.
2194
- - **Split button rusak saat hover** `translateY(-1px)` per-tombol mengangkat satu paruh saja
2362
+ - **Split button rusak saat hover.** `translateY(-1px)` per-tombol mengangkat satu paruh saja
2195
2363
  sehingga jahitan tak sejajar. Kini `.fdy-btn-split` menjadi satu unit: tiap paruh tak
2196
2364
  mengangkat/berbayang sendiri; split terangkat & berbayang sebagai satu kesatuan, plus garis
2197
2365
  pemisah tipis antar paruh.
2198
2366
 
2199
- ## [0.9.1] 2026-07-22
2367
+ ## [0.9.1] - 2026-07-22
2200
2368
  ### Added
2201
- - **Adapter React** (`freeday/react`) hook `useFreeday(rootRef?)` yang meng-*hydrate* enhancer
2369
+ - **Adapter React** (`freeday/react`) memberi hook `useFreeday(rootRef?)` yang meng-*hydrate* enhancer
2202
2370
  di subtree pada mount + tiap commit (idempotent). Tipe `event.detail` di
2203
2371
  `adapters/react/index.d.ts`. Contoh jalan `examples/react-faktur/` (Vite + React 19 + TS).
2204
- - **Interop Blazor** (`freeday/blazor`, `adapters/blazor/freeday-blazor.js`) script klasik yang
2372
+ - **Interop Blazor** (`freeday/blazor`, `adapters/blazor/freeday-blazor.js`) adalah script klasik yang
2205
2373
  mendaftarkan `window.FreedayBlazor` dengan `initAll` / `on` / `off` / `toast` / `toggleTheme`.
2206
2374
  `on(el, event, dotNetRef, method)` meneruskan detail (JSON-safe) ke method `[JSInvokable]` C#.
2207
2375
  Contoh jalan `examples/blazor-faktur/` (Blazor WASM, .NET 10, code-behind `.razor` + `.razor.cs`).
@@ -2211,13 +2379,13 @@ terbukti di contoh faktur yang jalan · docs adopsi lengkap.
2211
2379
  - `package.json`: export `./react` + `./blazor`, `react` peerDependency opsional (`>=18`).
2212
2380
  `version` 0.9.0 → 0.9.1.
2213
2381
 
2214
- ## [0.9.0] 2026-07-22
2382
+ ## [0.9.0] - 2026-07-22
2215
2383
  ### Added
2216
- - **Adapter Vue 3** (`freeday/vue`) composable `useFreeday(rootRef?)` yang meng-*hydrate*
2384
+ - **Adapter Vue 3** (`freeday/vue`) memberi composable `useFreeday(rootRef?)` yang meng-*hydrate*
2217
2385
  semua enhancer di subtree komponen saat mount + tiap update (idempotent). Tipis: enhancer
2218
2386
  tetap sumber kebenaran, event `fdy-*` mengalir lewat `v-on` native. Tipe `event.detail`
2219
2387
  disertakan (`FdyCascadeChangeDetail`, dst) di `adapters/vue/index.d.ts`.
2220
- - **Contoh jalan** `examples/vue-faktur/` layar faktur nyata (Vite + Vue 3 + TS) yang memakai
2388
+ - **Contoh jalan** `examples/vue-faktur/` adalah layar faktur nyata (Vite + Vue 3 + TS) yang memakai
2221
2389
  form validation + mask + cascade + datepicker + combo + table via adapter. `npm install &&
2222
2390
  npm run dev`. Membuktikan kontrak integrasi end-to-end (diverifikasi headless: hydrate,
2223
2391
  validasi men-gate submit, event → state Vue, tema light/dark).
@@ -2225,22 +2393,22 @@ terbukti di contoh faktur yang jalan · docs adopsi lengkap.
2225
2393
  - `package.json`: export `./vue`, `adapters/` masuk `files`, `vue` sebagai peerDependency
2226
2394
  opsional (`^3.4.0`). `version` 0.8.1 → 0.9.0.
2227
2395
 
2228
- ## [0.8.1] 2026-07-22
2396
+ ## [0.8.1] - 2026-07-22
2229
2397
  ### Added
2230
- - **Form validation** (`freeday-form.js`, `[data-fdy-validate]`) Constraint Validation
2398
+ - **Form validation** (`freeday-form.js`, `[data-fdy-validate]`) memakai Constraint Validation
2231
2399
  API di-wire ke error inline aksesibel: `aria-invalid` + pesan ter-`aria-describedby`,
2232
2400
  fokus ke field invalid pertama saat submit, re-validasi live tiap blur/input. Pesan
2233
2401
  custom `data-fdy-msg-*`, cocok antar-field `data-fdy-match`. Event `fdy-form-invalid`/
2234
2402
  `fdy-form-valid`, `window.FreedayForm`.
2235
- - **Password reveal + input mask** (`freeday-mask.js`) `[data-fdy-password]` tambah
2403
+ - **Password reveal + input mask** (`freeday-mask.js`): `[data-fdy-password]` tambah
2236
2404
  toggle tampil/sembunyi (memakai ulang chrome tombol input-group); `[data-fdy-mask]`
2237
2405
  format saat mengetik (`#` digit, `A` huruf, `*` alnum, sisanya literal), raw value di
2238
2406
  `dataset.fdyRaw` + event `fdy-mask`. `window.FreedayMask`.
2239
- - **Cascade select** (`cascade.css` + `freeday-cascade.js`, `[data-fdy-cascade]`)
2407
+ - **Cascade select** (`cascade.css` + `freeday-cascade.js`, `[data-fdy-cascade]`):
2240
2408
  pemilih hierarki drill-down dari `<ul>` bersarang; cabang membuka level berikut, daun
2241
2409
  memilih (nilai = jalur lengkap), back/crumb + keyboard penuh. Event `fdy-cascade-change`,
2242
2410
  `window.FreedayCascade`.
2243
- - **Chip choice/filter** (`chip.css` + `freeday-chip.js`) chip interaktif
2411
+ - **Chip choice/filter** (`chip.css` + `freeday-chip.js`): chip interaktif
2244
2412
  (`fdy-chip--choice` / `fdy-chip--filter`, `<button aria-pressed>`) dalam grup
2245
2413
  `[data-fdy-chips]` (`data-single` = pilih-satu), plus wiring hapus untuk
2246
2414
  `.fdy-chip__remove`. Event `fdy-chip-change` / `fdy-chip-remove`, `window.FreedayChip`.
@@ -2249,27 +2417,27 @@ terbukti di contoh faktur yang jalan · docs adopsi lengkap.
2249
2417
  `aria-invalid="true"` (bukan cuma kelas `--error`), jadi enhancer cukup toggle satu
2250
2418
  atribut aksesibel.
2251
2419
 
2252
- ## [0.8.0] 2026-07-22
2420
+ ## [0.8.0] - 2026-07-22
2253
2421
  ### Added
2254
2422
  - **Installable sebagai paket GitHub.** `npm i github:cahyo-dimas/freeday-ui-kit#v0.8.0`.
2255
2423
  `package.json` kini punya `exports`, `files`, `sideEffects`. Jalur import:
2256
2424
  `freeday/css` (token+komponen), `freeday` (semua enhancer), `freeday/tokens`,
2257
2425
  `freeday/css/components`, `freeday/enhancers/<nama>`.
2258
- - `dist/freeday.bundle.css` token + komponen dalam satu file (satu import, anti-footgun
2426
+ - `dist/freeday.bundle.css` berisi token + komponen dalam satu file (satu import, anti-footgun
2259
2427
  "lupa token").
2260
2428
  - Skrip rilis: lifecycle `version` build-ulang + `git add dist`; `prepack` build-ulang.
2261
2429
  ### Changed
2262
2430
  - `version` 0.1.0 → 0.8.0 (mulai semver bersih; komponen baru berikutnya = rilis minor 0.8.x).
2263
2431
 
2264
- ## [0.7.0] 2026-07-22
2432
+ ## [0.7.0] - 2026-07-22
2265
2433
  ### Added
2266
- - **Time picker** (`freeday-timepicker.js`, `[data-fdy-timepicker]`) trigger + popup daftar
2434
+ - **Time picker** (`freeday-timepicker.js`, `[data-fdy-timepicker]`): trigger + popup daftar
2267
2435
  waktu (listbox WAI-ARIA), 24 jam, `data-step`/`data-min`/`data-max`, keyboard penuh.
2268
- - **Datetime picker** (`freeday-datetime.js`, `[data-fdy-datetimepicker]`) komposisi date +
2436
+ - **Datetime picker** (`freeday-datetime.js`, `[data-fdy-datetimepicker]`): komposisi date +
2269
2437
  time, satu event `fdy-datetime-change` (`YYYY-MM-DDTHH:MM`).
2270
2438
  - **Konvensi varian ikon**: `data-fdy-no-icon` / `<template data-fdy-icon>` di date/time/datetime
2271
2439
  picker; `fdy-combo--no-icon` + `.fdy-combo__icon` di select; contoh ikon depan di input-group.
2272
- - Test regresi kontras WCAG (`test/contrast.test.mjs`) audit graf token (light + dark),
2440
+ - Test regresi kontras WCAG (`test/contrast.test.mjs`): audit graf token (light + dark),
2273
2441
  composite fill `-soft`, tegakkan 4.5:1 teks / 3:1 batas kontrol.
2274
2442
  ### Fixed
2275
2443
  - **A11y (WCAG 1.4.11 & AA):** border kontrol form 1.55:1 → token `--color-control-border`