@cahyo-dimas/freeday 1.53.0 → 2.0.0

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