@cahyo-dimas/freeday 2.0.0 → 2.2.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 (41) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/COMPONENTS.md +298 -2
  3. package/README.id.md +1 -1
  4. package/README.md +1 -1
  5. package/adapters/blazor/FdyAutocomplete.razor +4 -1
  6. package/adapters/blazor/FdyAutocomplete.razor.cs +26 -0
  7. package/adapters/blazor/FdyCascade.razor +6 -1
  8. package/adapters/blazor/FdyCascade.razor.cs +24 -0
  9. package/adapters/blazor/FdyCombo.razor +1 -0
  10. package/adapters/blazor/FdyCombo.razor.cs +13 -0
  11. package/adapters/blazor/FdyDatepicker.razor +15 -1
  12. package/adapters/blazor/FdyDatepicker.razor.cs +46 -0
  13. package/adapters/core/cfl-value.d.ts +11 -0
  14. package/adapters/core/cfl-value.js +31 -0
  15. package/adapters/react/components/FdyDrawer.tsx +3 -1
  16. package/adapters/react/components/FdyModal.tsx +3 -1
  17. package/adapters/react/index.d.ts +3 -0
  18. package/adapters/react/index.js +1 -0
  19. package/adapters/vue/components/FdyDrawer.vue +4 -1
  20. package/adapters/vue/components/FdyModal.vue +4 -1
  21. package/adapters/vue/index.d.ts +3 -0
  22. package/adapters/vue/index.js +1 -0
  23. package/dist/freeday-autocomplete.js +17 -1
  24. package/dist/freeday-carousel.js +5 -4
  25. package/dist/freeday-cascade.js +58 -7
  26. package/dist/freeday-cfl.js +5 -4
  27. package/dist/freeday-chart.js +33 -3
  28. package/dist/freeday-datepicker.js +109 -17
  29. package/dist/freeday-form.js +5 -4
  30. package/dist/freeday-mask.js +5 -4
  31. package/dist/freeday-select.js +18 -1
  32. package/dist/freeday-stepper.js +5 -4
  33. package/dist/freeday-table.js +9 -6
  34. package/dist/freeday-timepicker.js +19 -1
  35. package/dist/freeday-toast.js +5 -4
  36. package/dist/freeday-upload.js +5 -4
  37. package/dist/freeday.js +298 -64
  38. package/docs/agent-onboarding.md +9 -6
  39. package/docs/getting-started.md +15 -6
  40. package/docs/integrations.md +2 -2
  41. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,161 @@
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
+ **Kebijakan tipe (sejak 2.1.0).** Tipe publik yang **melebar** dihitung breaking meski runtime tak
7
+ berubah, karena yang gagal adalah build konsumen: `vue-tsc`/`tsc` menolak handler yang kemarin
8
+ benar. Perubahan seperti itu ditulis di bawah `### Changed: BREAKING (types)` — nama prop, tipe
9
+ lama → tipe baru, dan cara menyempitkannya — bukan di bawah `### Added`, betapapun aditifnya dari
10
+ sisi kit.
11
+
12
+ ## [2.2.0] - 2026-08-26
13
+ ### Added
14
+ - **The raw path can disable, lock and invalidate a picker** (`NEXT-UP.md` #12). `datepicker.css`
15
+ and `cascade.css` have styled `:disabled`, `[aria-readonly="true"]` and `[aria-invalid="true"]`
16
+ since they were written, and the enhancers never set any of them — so the states existed only in
17
+ the stacks that re-implement the control natively (Vue, React), and a **Blazor** app, whose
18
+ picker *is* the enhancer, could not disable a field at all. Now `data-disabled` /
19
+ `data-readonly` / `data-invalid` on the seed, plus `data-id` and `data-describedby` so the
20
+ trigger the enhancer builds can be labelled and described by a form's own markup.
21
+ - **`setState` on four enhancers** — `FreedayDatepicker`, `FreedayCascade`, `FreedayCombo`,
22
+ `FreedayAutocomplete`: `setState(root, { disabled, readonly, invalid })`. Not decoration: every
23
+ Blazor wrapper stops rendering once hydrated (`ShouldRender => !Hydrated`), so a parameter that
24
+ is only *rendered* goes quiet the moment a real form toggles it. The wrappers push through this
25
+ instead.
26
+ - **The Blazor pickers reach parity** with their Vue and React twins. `FdyDatepicker` goes from 6
27
+ parameters to 20 (the three states, `Id`, `Describedby`, and the nine calendar navigation
28
+ labels), `FdyAutocomplete` from 6 to 12, `FdyCascade` from 8 to 13, and `FdyCombo` finally has
29
+ `Describedby`. Four exemptions remain, each about the platform rather than effort, and each
30
+ named in `COMPONENTS.md`: `locale` (the enhancer formats through `Intl` from `<html lang>`),
31
+ `clearable`/`clearLabel` (this path builds no clear button), `ariaLabelledby` (the built trigger
32
+ is named by `Label`).
33
+ - **`closeLabel` on `FdyModal` and `FdyDrawer`** in Vue and React. Blazor has had `CloseLabel`
34
+ since it shipped; the other two hard-coded `aria-label="Close"`, so the gap ran in both
35
+ directions and this is the half nobody had reported yet.
36
+ - **The datepicker's ten labels are overridable**, as `data-fdy-text-prev-month`,
37
+ `-next-month`, `-prev-year`, `-next-year`, `-prev-years`, `-next-years`, `-choose-month`,
38
+ `-choose-year`, `-back-to-months`. Month and weekday NAMES still come from `Intl` via the page's
39
+ `lang`, which is the better hatch; these are the buttons around them.
40
+ ### Fixed
41
+ - **A chart series with no label read `Seri 1`.** Indonesian, in a release train whose 2.0.0
42
+ exists precisely to stop the enhancers mixing languages. It is `Series {n}` now, and
43
+ overridable. The donut's centre caption (`Total`) was hard-coded in the same file and is
44
+ overridable too; it is also built as an element rather than assigned through `innerHTML`, since
45
+ an author-supplied string is not markup.
46
+ - **The table's number-range filter offered `Min` and `Maks`.** Same leftover, same fix:
47
+ `filterMin` / `filterMax` in the strings table, English by default.
48
+ - **The cascade's own `Select` / `Select…` defaults and the timepicker's `Choose a time`** were
49
+ literals no host could reach. Both tables now carry them.
50
+ - **A `readonly` autocomplete opened its suggestion list.** The input carries `readonly` natively
51
+ and the enhancer never read it, so the list dropped over a field nobody could edit.
52
+ ### Changed
53
+ - **The hard-coded-string guard (#016) is inverted.** It used to find the line that writes to the
54
+ DOM and read the literals on it, which cannot see a string passed to a helper —
55
+ `navButton('‹', 'Previous month', fn)` writes its label inside `navButton`, where the value is a
56
+ variable. Ten datepicker labels, the timepicker's, the cascade's two, the table's two and the
57
+ chart's `Seri 1` all hid there, some of them for the whole life of the file. It now reads every
58
+ literal in every enhancer and asks the opposite question — is this prose, and is it in the
59
+ `TEXT` table — dropping non-prose by shape (selectors, CSS custom properties, kebab keys,
60
+ camelCase identifiers, URLs, locale tags) and one short vocabulary of DOM tokens. Verified by
61
+ reintroducing both blind spots and watching it fail.
62
+ ### Added: guards
63
+ - `test/blazor/FdyPickerStateTests.cs`: 7 bUnit tests covering what `dotnet build` cannot see —
64
+ each parameter reaching the markup the enhancer reads, an absent one leaving no empty attribute
65
+ behind, and a state change **after** the first render arriving through interop.
66
+ - `browser/picker-states.mjs`: 7 tests in real Chrome with real mouse gestures — a disabled picker
67
+ that will not open, a readonly one that keeps focus and still refuses, both `aria-invalid` and
68
+ the `--error` class, `setState` unlocking a picker, and a nav label overridden per element.
69
+ - `test/docs.test.mjs`: the Blazor picker parity guard, Vue's prop surface against the four
70
+ wrappers, with the four exemptions written out with their reasons rather than a bare list.
71
+ ### Notes on the shape of the fix
72
+ - The regression this work caused, and what caught it: naming the new flag `isDisabled` shadowed
73
+ an **existing** `isDisabled(date)` in the datepicker that decides whether a day falls outside
74
+ `min`/`max`, so the day grid threw on every render — while the panel still opened and only its
75
+ cells went missing. Nothing in the node suite noticed; `browser/vanilla.mjs`, which clicks
76
+ through the calendar in a real browser, failed immediately. The flags are named `state*` now,
77
+ with the collision written above them.
78
+
79
+ ## [2.1.0] - 2026-08-25
80
+ ### Added
81
+ - **`singleRow()`, exported from `@cahyo-dimas/freeday/vue` and `/react`** (#045). `FdyCfl` emits
82
+ `Row | Row[] | null` whatever its props are: the array is reachable only under `multiple`, the
83
+ null only under `clearable`, and the type cannot say which. So every single-select screen wrote
84
+ its own narrowing, and each one differently — a cast (which hides the day somebody adds
85
+ `clearable`), a silent ignore, or a throw. One implementation now lives in
86
+ `adapters/core/cfl-value.js` and both adapters re-export it: it returns `Row | null`, and
87
+ **throws** a `TypeError` on an array instead of keeping its first row, because quietly dropping
88
+ the rest is how a `multiple` added later goes unnoticed. Blazor never needed it — multi-select
89
+ there is a separate `Values` / `ValuesChanged` pair.
90
+ ```ts
91
+ import { singleRow } from '@cahyo-dimas/freeday/vue'; // or '@cahyo-dimas/freeday/react'
92
+ // (value: CflRow | CflRow[] | null) => CflRow | null
93
+ const onUpdate = (value: CflRow | CflRow[] | null): void => { picked.value = singleRow(value); };
94
+ ```
95
+ ### Changed: BREAKING (types)
96
+ - **A retroactive entry for 1.29.0 and 1.42.0** (#045) — nothing changed in this release; this is
97
+ the record those two are missing. Both widened `FdyCfl`'s emitted type — `Row` → `Row | null`
98
+ (1.29.0, with `clearable`) → `Row | Row[] | null` (1.42.0, with `multiple`) — and both shipped
99
+ under **Added**, which is honest from the kit's side and useless from the consumer's: the
100
+ combined effect is `vue-tsc` failing in a handler nobody touched. An app upgrading from before
101
+ 1.29.0 meets both at once, which is exactly how it was reported. Migration is `singleRow()`
102
+ above. The policy that should stop a third instance is at the top of this file.
103
+ ### Added: guards
104
+ - **The typed column contract is now pinned in both directions** (#040): `test/docs.test.mjs`
105
+ asserts the field list documented in `COMPONENTS.md` matches `FdyTableColumn` in
106
+ `adapters/core/table-model.d.ts` — a field in the type but not the docs fails, and so does a
107
+ field in the docs that the type does not have. Verified by mutation (drop `labelHidden` from the
108
+ table → red; invent a `sticky` row → red), since a guard nobody has watched fail is only a
109
+ green tick.
110
+ - `test/cfl-value.test.mjs`: three tests for `singleRow` (row passes through, `null`/`undefined`
111
+ become null, an array throws).
112
+ - **Every typed wrapper's props are pinned to their documentation** (`NEXT-UP.md` #11). One guard
113
+ reads all twelve `### Props — <FdyX>` tables and compares them with the Vue and React
114
+ declarations: a prop missing from a table fails, a documented prop neither adapter declares
115
+ fails, and a wrapper exported with no table at all fails. Mutation-verified in all three
116
+ directions, including the case that found a bug in the guard itself — a hyphenated prop name
117
+ (`aria-label`) was invisible to the first version of its parser.
118
+ ### Docs
119
+ - **`COMPONENTS.md` finally states the typed column contract** (#040). §Data table gained
120
+ "Columns — the typed `FdyTableColumn<T>`": all ten fields with their types, the Blazor spelling
121
+ (`PascalCase`, `required` Key/Label, the extra `Cell` slot), and the row-controls example.
122
+ `agent-onboarding.md` tells every consuming agent this file is closed — "if a class is not in
123
+ that file, it does not exist" — while three of the four stacks build a table only through
124
+ `columns`, whose shape appeared nowhere in it. One reporting app shipped `label: ''` on its
125
+ actions column as a result: a `<th>` announced as nothing.
126
+ - **Nine enhancers still claimed to be Indonesian.** The comment above every `TEXT` table read
127
+ "User-facing strings. Indonesian by default" — written before 2.0.0 flipped them all to English,
128
+ and shipped in `dist/`, where it is the first thing a consumer reading the source sees. It now
129
+ says English, names `data-fdy-text-<key>` as the override, and gives an Indonesian app on the
130
+ raw path as the example of who overrides.
131
+ - **The two docs a consumer actually reads on upgrade now mention the language.** 2.0.0's breaking
132
+ change was documented in `COMPONENTS.md` and this file, but neither `docs/getting-started.md` nor
133
+ the block projects paste into their agent instructions
134
+ (`docs/agent-onboarding.md`) said a word about it. Both now do, with `data-fdy-text-<key>` as the
135
+ whole migration and `<html lang>` for date names.
136
+ - **`HANDOFF.md` described a release that had already happened.** It stated npm was on 1.53.0 with
137
+ 1.54.0 and 2.0.0 sitting unpushed; npm had been on 2.0.0 since the morning, with the tag, the
138
+ branch and `origin/main` all on the same commit. The file that warns about stale snapshots was
139
+ the stale snapshot. Corrected, with the three commands that check it.
140
+ - **`CLAUDE.md` claimed 44 components**, a number matching nothing countable. Now 48 stylesheets in
141
+ `src/components/` and 26 enhancers in `src/freeday-*.js`, both of which anyone can re-count.
142
+ - **The typed props of all twelve wrappers are now in `COMPONENTS.md`** (`NEXT-UP.md` #11, the
143
+ general form of #040). A `### Props` table per wrapper: Vue and React names together, what each
144
+ does, and the Blazor differences stated rather than implied. Writing them measured something
145
+ nobody had: **four Blazor pickers are far thinner than their JS twins** — `FdyDatepicker` takes 6
146
+ parameters against 21 Vue props, `FdyAutocomplete` 6 against 11, `FdyCascade` 8 against 12, and
147
+ `FdyCombo` has no `Describedby`. Those wrappers render a seed element and do not splat unmatched
148
+ attributes, so `disabled`, `readonly`, `invalid`, the a11y ids, `clearable` and the datepicker's
149
+ ten navigation labels cannot be reached from a Blazor page at all. It runs the other way too:
150
+ `CloseLabel` exists only in Blazor, so the modal and drawer × is fixed at `Close` in Vue and
151
+ React. Filed as `NEXT-UP.md` #12 with its trigger; documented here as fact in the meantime,
152
+ because a consumer meeting it should find it written down rather than discover it.
153
+ - **`FdyAppShell` was missing from every list of the typed wrappers but the two READMEs.** It
154
+ shipped as the eleventh in 1.54.0 and `COMPONENTS.md`, `getting-started.md`, `agent-onboarding.md`
155
+ and `integrations.md` went on naming ten for a whole major version — `agent-onboarding.md`
156
+ saying both "ten" and "eleven" about the same set, in the same file, and `getting-started.md`
157
+ carrying a section heading that still listed the seven of some earlier release. All corrected,
158
+ and a guard now reads any run of six or more wrapper names as an enumeration and fails if it is
159
+ incomplete. It found two of these itself, one of them a list this release had already touched.
160
+
6
161
  ## [2.0.0] - 2026-08-25
7
162
  ### Changed: BREAKING
8
163
  - **The vanilla enhancers now write English by default** (`NEXT-UP.md` #6, owner's decision). Every
package/COMPONENTS.md CHANGED
@@ -36,7 +36,7 @@ live docs.
36
36
  alert, breadcrumb, timeline, accordion, tree-without-cascade…) are CSS-only.
37
37
  7. **On Vue, React or Blazor, eleven components have a typed wrapper. Use it.** `FdyCombo` ·
38
38
  `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` ·
39
- `FdyTable` · `FdyModal` · `FdyDrawer`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
39
+ `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
40
40
  `Freeday.Blazor` RCL. Each is flagged at its own section below. Hand-writing their raw markup in
41
41
  those stacks *looks* right at first, because the enhancer initialises once and the first render is
42
42
  correct. Then
@@ -117,6 +117,25 @@ is a no-op.
117
117
  ## App shell — `.fdy-app`
118
118
  > **Typed wrapper: `<FdyAppShell>`**. Vue (`v-model:navOpen`) · React (`navOpen`/`onNavOpenChange`) · Blazor (`@bind-NavOpen`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
119
119
 
120
+ ### Props — `<FdyAppShell>`
121
+
122
+ Vue and React names, `?` marking optional. Vue takes the regions as **slots** of the same names,
123
+ which is why React's list is longer while offering the same thing.
124
+
125
+ | Prop | Type | What it does |
126
+ |---|---|---|
127
+ | `navOpen?` | `boolean` | Whether the nav is visible. Optional because the default is the **viewport's**, and a caller cannot state that in one initial value. Vue binds `v-model:navOpen`. |
128
+ | `onNavOpenChange?` | `(open: boolean) => void` | React. Visibility changed: the toggle, Escape, the backdrop, or a followed nav item. Vue emits `update:navOpen`. |
129
+ | `title?` | `string` (Vue) · `ReactNode` | Topbar title. |
130
+ | `toggleLabel?` | `string` | Accessible name for the nav toggle button. |
131
+ | `toggleIcon?` | `ReactNode` | React. Replaces the default hamburger; Vue uses the slot of that name. |
132
+ | `skip?` | `ReactNode` | React. Skip link, rendered first in the tab order. |
133
+ | `brand?` · `nav?` · `topbar?` | `ReactNode` | React. Sidebar brand, sidebar nav, topbar contents. |
134
+ | `children?` | `ReactNode` | React. The page itself; Vue's default slot. |
135
+
136
+ Blazor takes the regions as `RenderFragment`s — `BrandContent`, `NavContent`, `TopbarContent`,
137
+ `SkipContent`, `TitleContent`, `ChildContent` — and binds with `@bind-NavOpen`.
138
+
120
139
  The frame every application lives in. Do not hand-roll one from flexbox: the responsive sidebar,
121
140
  off-canvas drawer and backdrop are built in.
122
141
 
@@ -422,6 +441,25 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
422
441
  ## Select / combobox — `.fdy-combo`
423
442
  > **Typed wrapper: `<FdyCombo>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
424
443
 
444
+ ### Props — `<FdyCombo>`
445
+
446
+ | Prop | Type | What it does |
447
+ |---|---|---|
448
+ | `modelValue` · `value` | `T extends string` | The selected value: `v-model` in Vue, `value` in React. |
449
+ | `onChange` | `(value: T) => void` | React. A selection was committed. Vue emits `update:modelValue` and `change`. |
450
+ | `options` | `ReadonlyArray<{ value: T; label: string }>` | The list. React exports the row type as `FdyComboOption<T>`. |
451
+ | `id?` | `string` | Explicit id for the combobox button; otherwise one is generated. |
452
+ | `ariaLabelledby?` | `string` | Id of the element that labels the combobox. |
453
+ | `placeholder?` | `string` | Shown when no option matches the current value. |
454
+ | `disabled?` | `boolean` | Greyed and out of the tab order, the native `disabled` semantics. |
455
+ | `readonly?` | `boolean` | Locked/view mode: keeps focus and tab order and shows its value, but cannot be opened or changed. Unlike `disabled` it is not greyed. |
456
+ | `invalid?` | `boolean` | Sets `aria-invalid`; pair it with `describedby` pointing at the error text. |
457
+ | `describedby?` | `string` | Id of the help or error text (`aria-describedby`). |
458
+
459
+ Blazor: the same names in `PascalCase`, bound with `@bind-Value` (`Value` / `ValueChanged`).
460
+ `FreedayCombo.setState(root, { disabled, readonly, invalid })` pushes a state change onto a combo
461
+ whose host has stopped re-rendering.
462
+
425
463
  Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
426
464
 
427
465
  - `.fdy-combo` (+`--error`, `--no-icon`) · `__button` `__value` (+`--placeholder`) `__listbox`
@@ -452,6 +490,23 @@ keeps matching after selection. Do not put a glyph in that span.
452
490
  ## Autocomplete — `.fdy-autocomplete`
453
491
  > **Typed wrapper: `<FdyAutocomplete>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
454
492
 
493
+ ### Props — `<FdyAutocomplete>`
494
+
495
+ | Prop | Type | What it does |
496
+ |---|---|---|
497
+ | `modelValue` · `value` | `string` | The typed text. Free text: the list suggests, it does not constrain. |
498
+ | `onChange` · `onSelect` | `(value: string) => void` | React. `onChange` on every keystroke, `onSelect` only when a suggestion is taken. Vue emits `update:modelValue` and `select`. |
499
+ | `options` | `ReadonlyArray<string>` | The suggestions to filter. |
500
+ | `emptyText?` | `string` | Shown when nothing matches. |
501
+ | `placeholder?` | `string` | |
502
+ | `id?` · `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Input id; its accessible name as text or as a reference; the help/error text it points at. |
503
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps focus and tab order and shows its value, but the input is not editable and the list will not open. Unlike `disabled` it is not greyed. |
504
+
505
+ Blazor carries all of these. The states live on the input natively, and
506
+ `FreedayAutocomplete.setState(root, { disabled, readonly, invalid })` changes them after the first
507
+ render. A `readonly` input no longer opens its suggestion list, which it used to do over a field
508
+ nobody could edit.
509
+
455
510
  Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
456
511
 
457
512
  - `.fdy-autocomplete` · `__listbox` `__option` `__empty`
@@ -461,6 +516,25 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
461
516
  ## Cascade select — `.fdy-cascade`
462
517
  > **Typed wrapper: `<FdyCascade>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
463
518
 
519
+ ### Props — `<FdyCascade>`
520
+
521
+ | Prop | Type | What it does |
522
+ |---|---|---|
523
+ | `modelValue` · `value` | `string` | The selected **leaf** value; `''` means nothing selected. |
524
+ | `onChange` | `(value: string, labels: string[]) => void` | React. Vue emits `update:modelValue` and `change`. `labels` is the whole path, root → leaf. |
525
+ | `options` | `ReadonlyArray<CascadeNode>` | The tree. `CascadeNode` = `{ label, value, children? }`; a node **with** children is a branch, one without is selectable. |
526
+ | `separator?` | `string` | What joins the path in the display. Default `" / "`, matching the enhancer. |
527
+ | `backLabel?` | `string` | Accessible name for the up-one-level button. Default `Back one level`. |
528
+ | `label?` | `string` | Accessible name for the trigger and its listbox. |
529
+ | `placeholder?` | `string` | |
530
+ | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
531
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | As on `<FdyCombo>`. |
532
+
533
+ Raw path: `data-disabled` / `data-readonly` / `data-invalid`, `data-id`, `data-describedby`, and
534
+ `FreedayCascade.setState(root, { disabled, readonly, invalid })` for a host that rendered once.
535
+ Blazor calls the tree `Nodes`, adds `SubmenuLabel`, and carries everything else except
536
+ `ariaLabelledby` — the trigger it builds is named by `Label`.
537
+
464
538
  Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
465
539
  child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
466
540
 
@@ -482,6 +556,37 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
482
556
  ## Choose-from-list (CFL) — `data-fdy-cfl`
483
557
  > **Typed wrapper: `<FdyCfl>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
484
558
 
559
+ ### Props — `<FdyCfl>`
560
+
561
+ Four of these are required and carry the whole component: `fetchPage`, `columns`, `display`,
562
+ `rowKey`. The rest are copy and state.
563
+
564
+ | Prop | Type | What it does |
565
+ |---|---|---|
566
+ | `modelValue` · `value` | `Row \| Row[] \| null` | The picked row. An array is reachable only under `multiple`, `null` only under `clearable`; narrow it with `singleRow()` (see the bullet below the raw-path hooks). |
567
+ | `onChange` | `(value: Row \| Row[] \| null) => void` | React. Vue emits `update:modelValue` and `change`. |
568
+ | `fetchPage` | `(query: string, page: number) => Promise<CflPage<Row>>` | The only data source — `{ rows, hasMore }`. Called on open, on search, and on Load more. Server state, never a global store. |
569
+ | `columns` | `ReadonlyArray<CflColumn<Row>>` | `{ key, label }` per dialog column. |
570
+ | `display` | `(row: Row) => string` | What the closed field shows once a row is picked. |
571
+ | `rowKey` | `(row: Row) => string` | Row identity: ticks in multi mode, and seeding them when the dialog re-opens. |
572
+ | `pageSize?` | `number` | Advisory only. Your `fetchPage` owns paging; this documents the intent. |
573
+ | `title?` | `string` | The dialog's heading. Default `Choose data`. |
574
+ | `searchPlaceholder?` | `string` | Placeholder **and** accessible name of the search box. Default `Search…`. |
575
+ | `loadingText?` · `emptyText?` · `retryText?` · `moreText?` | `string` | Dialog states. Defaults `Loading…`, `No results.`, `Try again`, `Load more`. |
576
+ | `closeLabel?` · `openLabel?` | `string` | Accessible names for the dialog's × and the field's open button. Defaults `Close`, `Open search`. |
577
+ | `multiple?` | `boolean` | Tick rows and commit them together on Confirm instead of committing the clicked row, and widen the value to an array. |
578
+ | `selectedText?` · `confirmText?` · `hintText?` | `string` | Footer copy. Defaults `{n} selected` (with `{n}` substituted), `Confirm`, `Click a row to choose it`. |
579
+ | `placeholder?` | `string` | Field placeholder while nothing is picked. |
580
+ | `clearable?` | `boolean` | A clear button, so an **optional** foreign key can be unset. Without it the value type admits a `null` the component can never produce. |
581
+ | `clearLabel?` | `string` | Accessible name for that button. Default `Clear selection`. |
582
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps the picked value visible, focusable and copyable, but the dialog cannot be opened. |
583
+ | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Field id; the element that labels it; the help/error text it points at. |
584
+
585
+ Blazor carries the same surface in `PascalCase` with three differences: multi-select is a second
586
+ pair (`Values` / `ValuesChanged`) rather than a widened single binding, the fetch delegate is
587
+ called `LoadPage`, and it has two parameters the JS adapters do not — `ErrorText` and
588
+ `SearchDebounceMs`.
589
+
485
590
  A read-only field backed by master data: the button opens a searchable dialog, the picked row fills
486
591
  the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
487
592
 
@@ -498,6 +603,13 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
498
603
  `ValuesChanged`, because a nullable union is not a C# shape. Closing without Confirm leaves the
499
604
  bound value untouched, and re-opening seeds the ticks from it. The field states `{n} selected`
500
605
  rather than one row's `display()`. Strings: `selectedText`, `confirmText`, `hintText`.
606
+ - **A single-select in Vue or React needs one narrowing, and the kit ships it.** The emitted value
607
+ is typed `Row | Row[] | null` whatever the props are, so a single-select handler cannot take it
608
+ as `Row`: the array is reachable only under `multiple`, the null only under `clearable`, and the
609
+ type cannot say which. Import the guard instead of writing one per app:
610
+ `import { singleRow } from '@cahyo-dimas/freeday/vue'` (or `/react`). It returns `Row | null`,
611
+ and throws if a `multiple` field's array ever reaches it, rather than silently keeping the first
612
+ row. Blazor is unaffected: multi-select there is a separate `Values` / `ValuesChanged` pair.
501
613
 
502
614
  ```html
503
615
  <div class="fdy-field">
@@ -547,6 +659,52 @@ a new class.
547
659
  ## Date picker — `data-fdy-datepicker`
548
660
  > **Typed wrapper: `<FdyDatepicker>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
549
661
 
662
+ ### Props — `<FdyDatepicker>`
663
+
664
+ | Prop | Type | What it does |
665
+ |---|---|---|
666
+ | `modelValue` · `value` | `string \| null` | ISO `YYYY-MM-DD`, or `''`/`null` for empty. |
667
+ | `onChange` | `(value: string) => void` | React. Vue emits `update:modelValue` and `change`. Clearing sends `''`. |
668
+ | `min?` · `max?` | `string` | ISO bounds; days outside them cannot be picked. |
669
+ | `locale?` | `string` | BCP-47 tag deciding month and weekday names. Defaults to the page's `<html lang>`, then `en`. |
670
+ | `placeholder?` | `string` | |
671
+ | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
672
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps focus and tab order and shows its date, but it cannot be opened, cleared or changed. |
673
+ | `clearable?` | `boolean` | Show a × in the trigger once a date is set, so an optional date can be unset. Sends `''`. Off by default. |
674
+ | `clearLabel?` | `string` | Accessible name for that ×. Default `Clear date`. |
675
+ | `prevMonthLabel?` · `nextMonthLabel?` | `string` | The month arrows. Defaults `Previous month` / `Next month`. |
676
+ | `chooseMonthLabel?` · `chooseYearLabel?` | `string` | The title buttons that drill into the month and year grids. Defaults `Choose month` / `Choose year`. |
677
+ | `prevYearLabel?` · `nextYearLabel?` | `string` | Year arrows, shown in the month grid. Defaults `Previous year` / `Next year`. |
678
+ | `prevYearsLabel?` · `nextYearsLabel?` | `string` | Page arrows, shown in the year grid. Defaults `Previous years` / `Next years`. |
679
+
680
+ The ten label props exist because month and weekday names follow `locale` while the buttons around
681
+ them do not: without these, a Spanish calendar would be navigated by English arrows.
682
+
683
+ On the **raw path** these all have hooks now: `data-disabled` / `data-readonly` / `data-invalid`,
684
+ `data-id`, `data-describedby`, and one `data-fdy-text-<key>` per label above
685
+ (`data-fdy-text-prev-month`, …). A host that renders its markup once and cannot re-render it can
686
+ change the three states afterwards with `FreedayDatepicker.setState(root, { disabled, readonly,
687
+ invalid })`.
688
+
689
+ Blazor carries the same surface bar four: `locale` (the enhancer formats through `Intl` from the
690
+ page's `<html lang>`), `clearable` and `clearLabel` (this path builds no clear button), and
691
+ `ariaLabelledby` (the trigger it builds is named by `Label`).
692
+
693
+ ### Props — `<FdyDateRange>`
694
+
695
+ | Prop | Type | What it does |
696
+ |---|---|---|
697
+ | `modelValue` · `value` | `DateRangeValue` = `{ start, end }`, each `string \| null` | Both ends at once, so the pair can never be half-applied. |
698
+ | `onChange` | `(value: DateRangeValue) => void` | React. Vue emits `update:modelValue` and `change`. |
699
+ | `min?` · `max?` | `string` | ISO bounds for both ends. The end can never precede the start. |
700
+ | `locale?` | `string` | As on the single picker. |
701
+ | `startPlaceholder?` · `endPlaceholder?` | `string` | One per field. |
702
+ | `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Names the group (`role="group"`), or points at the text that does. |
703
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | Applied to both pickers together. |
704
+
705
+ Blazor splits the value into two scalars, `From` / `FromChanged` and `To` / `ToChanged`, because a
706
+ C# binding cannot express one object bound in two places, and it adds `Separator`.
707
+
550
708
  Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author an empty `<div>`**
551
709
  The enhancer builds everything.
552
710
 
@@ -772,6 +930,75 @@ sum, so measure them once after render (and on resize) and set the variable.
772
930
  ## Data table — `.fdy-datatable`
773
931
  > **Typed wrapper: `<FdyTable>`**. controlled: `columns` + `rows`, with sort/filter/page events (`update:pageIndex` · `onPageIndexChange` · `PageIndexChanged`) and `process` for driving a card list off the same processed set. The markup below is the raw enhancer path.
774
932
 
933
+ ### Columns — the typed `FdyTableColumn<T>`
934
+
935
+ On Vue, React and Blazor, `columns` **is** the table: it is what those stacks write instead of the
936
+ markup hooks further down. Ten fields, `key` and `label` required. The authored doc comments live on
937
+ the type itself (`adapters/core/table-model.d.ts`, re-declared for Blazor in
938
+ `adapters/blazor/TableTypes.cs`); the list below is the discoverable copy, and `npm test` fails if
939
+ the two ever disagree.
940
+
941
+ | Field | Type | What it does |
942
+ |---|---|---|
943
+ | `key` | `string` | Row property key; the sort/filter identity and, unless `value` is set, the cell accessor. |
944
+ | `label` | `string` | Header label. |
945
+ | `labelHidden` | `boolean` | Render the label for assistive tech only; the header cell looks empty. |
946
+ | `sortable` | `boolean` | Show a sort toggle in the header. |
947
+ | `filter` | `'text' \| 'enum' \| 'number' \| 'date'` | Offer a column filter of this type (the funnel popover). |
948
+ | `align` | `'left' \| 'right' \| 'center'` | Cell alignment. Default `left`. |
949
+ | `mono` | `boolean` | Render cells in the monospace data font (`.fdy-mono`). |
950
+ | `sortType` | `'text' \| 'number' \| 'date'` | Override the comparator; defaults to one derived from `filter`, else `text`. |
951
+ | `value` | `(row: T) => unknown` | Custom accessor; defaults to `row[key]`. Feeds sort, filter and the default cell text. |
952
+ | `options` | `readonly string[]` | Explicit enum-filter options; they default to the distinct values in the rows on screen, so pass them in server-paged mode, where those are one page. |
953
+
954
+ Blazor spells the same fields `PascalCase` (`LabelHidden`, `SortType`), makes `Key`/`Label`
955
+ `required`, and adds one field with no TS twin: `Cell`, a `RenderFragment<TRow>` where Vue and React
956
+ use a slot.
957
+
958
+ A column of row controls is worth stating outright, because the obvious move is the wrong one:
959
+
960
+ ```ts
961
+ const columns: FdyTableColumn<User>[] = [
962
+ { key: 'email', label: 'Email', sortable: true, filter: 'text' },
963
+ { key: 'total', label: 'Total', align: 'right', mono: true, sortType: 'number' },
964
+ // Not `label: ''`. A <th> with no text announces as nothing, and the label also names this
965
+ // column's sort button and filter popover, so it has to stay meaningful.
966
+ { key: 'actions', label: 'Actions', labelHidden: true },
967
+ ];
968
+ ```
969
+
970
+ ### Props — `<FdyTable>`
971
+
972
+ Controlled by default: hand it `columns` + `rows` and it filters, sorts and paginates internally;
973
+ provide the state prop for a concern and you own that concern instead. React names the callbacks
974
+ `onXChange`, Vue emits `update:x`, Blazor exposes `XChanged`.
975
+
976
+ | Prop | Type | What it does |
977
+ |---|---|---|
978
+ | `columns` | `ReadonlyArray<FdyTableColumn<Row>>` | The column contract documented above. |
979
+ | `rows` | `ReadonlyArray<Row>` | The current page's rows in server mode, the whole set in client mode. |
980
+ | `rowKey` | `(row: Row) => string \| number` | Row identity: keys, expansion, activation. |
981
+ | `sort?` · `onSortChange?` | `FdySortState \| null` · `(sort) => void` | Pass `sort`, **even as `null`**, to own sorting; omit it for the internal sort. |
982
+ | `filters?` · `onFiltersChange?` | `FdyFilterMap` · `(filters) => void` | The same, for filtering. |
983
+ | `page?` · `onPageChange?` | `FdyPageState` · `(page) => void` | Its **presence switches the table into server mode**: you own paging and get `{ index, size, total }` back. |
984
+ | `pageSize?` | `number` | Client mode: rows per page. Absent or 0 renders every row with no pager. |
985
+ | `pageIndex?` · `onPageIndexChange?` | `number` · `(index) => void` | Client mode: own the 0-based index while the table keeps filtering, sorting and paginating — this is what lets one external pager drive both a table and a card list. |
986
+ | `pageSizes?` · `onPageSizeChange?` | `readonly number[]` · `(size) => void` | Offer a rows-per-page control. Server mode reports the pick through the page event (read its `size`); client mode applies it itself and reports it too, so it works with nothing wired. |
987
+ | `pager?` | `boolean` | Default true. Turn it off to withhold the table's own footer when the screen renders one. |
988
+ | `loading?` | `boolean` | Show the in-table loading state. |
989
+ | `emptyText?` · `empty?` | `string` · `ReactNode` | The empty state as text, or as markup (React `empty`, Vue's `empty` slot). |
990
+ | `ariaLabel?` | `string` | Names the table for assistive tech. |
991
+ | `rowActivatable?` · `onRowActivate?` | `boolean` · `(row) => void` | Rows become focusable and activate on click, Enter or Space. |
992
+ | `rowClass?` | `(row: Row) => string \| undefined` | Per-row class hook, e.g. marking the selected row. |
993
+ | `expandedKeys?` · `renderRowDetail?` | `ReadonlyArray<string \| number>` · `(row) => ReactNode` | Controlled expansion: these keys get a full-width detail row. Vue uses the `row-detail` slot. |
994
+ | `renderCell?` | `(column, row, value) => ReactNode` | React. Custom cell rendering; Vue uses the `cell` slot. |
995
+ | `toolbar?` | `ReactNode` | React. Content above the table; Vue uses the `toolbar` slot. |
996
+ | `onProcess?` | `(result: { rows, total }) => void` | React. The processed page after filter/sort/paginate, in **both** modes. Vue emits `process`, Blazor calls it `Process`. Render the same set elsewhere — a card list, a summary, a CSV export — without re-deriving the pipeline. |
997
+
998
+ Blazor matches this surface (`LoadingText` and `EmptyContent` in place of `emptyText`/`empty`,
999
+ `Toolbar` and `RowDetail` as `RenderFragment`s) and is the one adapter that also exposes
1000
+ `FiltersChanged`.
1001
+
775
1002
  The interactive table: global search, sort, per-column filters, row selection + bulk bar,
776
1003
  pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` + `data-fdy-table`
777
1004
  (`data-page-size="N"`).
@@ -822,7 +1049,11 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
822
1049
  (`data-fdy-text-required`, `data-fdy-text-type`, …), narrower than the per-field
823
1050
  `data-fdy-msg-<alias>` that still wins. `Freeday.toast()` takes `closeLabel` in its options
824
1051
  object. `npm test` asserts no enhancer string is written outside its `TEXT` table, so a new one
825
- arrives overridable or not at all.
1052
+ arrives overridable or not at all — a claim that was only half true until **2.2.0**, when the
1053
+ guard stopped looking for the line that writes to the DOM and started reading every literal in
1054
+ the file. The old shape could not see a string handed to a helper, which is how ten datepicker
1055
+ labels, the timepicker's, the cascade's two defaults, the table's `Min`/`Maks` filter
1056
+ placeholders and the chart's `Seri 1` all sat outside it.
826
1057
 
827
1058
  **Testing note:** a column's filter button and the dialog it opens deliberately share one
828
1059
  accessible name (`Filter <column>`), since a dialog named after its trigger is the normal pattern. In a
@@ -849,6 +1080,14 @@ ninety-row list would remove the only way back to twenty.
849
1080
  of one page of rows. A footer inside the table is inside the half a phone hides, so those screens
850
1081
  render it once, outside both, with `pager={false}` on the table.
851
1082
 
1083
+ ### Props — `<FdyTableFooter>`
1084
+
1085
+ | Prop | Type | What it does |
1086
+ |---|---|---|
1087
+ | `page` | `FdyPageState` = `{ index, size, total }` | The page being shown. `size` drives both the range text and the rows-per-page control's value. |
1088
+ | `onPageChange?` | `(page: FdyPageState) => void` | React. A page click or a size change. Vue emits `update:page`; Blazor `PageChanged`. |
1089
+ | `pageSizes?` | `readonly number[]` | Offer a rows-per-page control. Omit it and the footer is range + pager only. Picking a size reports the new `size` together with the index that still holds the row the reader was looking at. |
1090
+
852
1091
  **Who draws the pager.** The table renders its own footer (range + pager) whenever there is more
853
1092
  than one page. Two ways to take it over:
854
1093
 
@@ -879,6 +1118,25 @@ replaces.
879
1118
  ## Charts — `data-fdy-chart`
880
1119
  > **Typed wrapper: `<FdyChart>`**. data props in all three (`type` + `values` / `series`); it repaints on data change, so `FreedayChart.update(el)` is only for the raw path below.
881
1120
 
1121
+ ### Props — `<FdyChart>`
1122
+
1123
+ | Prop | Type | What it does |
1124
+ |---|---|---|
1125
+ | `type` | `'line' \| 'area' \| 'bar' \| 'sparkline' \| 'donut'` | Which chart to draw. |
1126
+ | `values?` | `ReadonlyArray<number>` | One series. |
1127
+ | `series?` | `ReadonlyArray<FdyChartSeries>` | Several: each `{ label, values }`. Use one or the other, not both. |
1128
+ | `labels?` | `ReadonlyArray<string>` | Category labels: the x axis, or the donut's slices. |
1129
+ | `format?` | `'number' \| 'percent' \| 'currency'` | How values are written in labels and the donut centre. |
1130
+ | `stacked?` | `boolean` | Bar charts: stack the series instead of grouping them side by side. |
1131
+ | `legend?` | `'auto' \| 'always' \| 'none'` | `auto` shows one when there is more than one series. |
1132
+ | `colors?` | `ReadonlyArray<string>` | Per-series override. Each entry is a semantic token name (`primary`, `accent`, `success`, `warning`, `danger`, `info`) or a categorical slot `chart-1`…`chart-8`. Omit for the default fixed-order palette. |
1133
+ | `color?` | `string` | The same, for a single-series chart (sparkline, simple bar or line). |
1134
+ | `center?` | `string \| number` | Donut only: what to print in the middle. |
1135
+ | `aria-label?` | `string` | React. The chart's accessible name, and in practice the **whole** text alternative: a named chart's painted subtree is `aria-hidden`, see the a11y note in this section. Vue and Blazor set it as a plain attribute (Blazor's parameter is `AriaLabel`). |
1136
+ | `children?` | `ReactNode` | React. Content inside the element before the chart paints. The renderer replaces it, so it is a pre-paint placeholder, not a text alternative. |
1137
+
1138
+ Blazor adds `ChildContent` for the same placeholder role.
1139
+
882
1140
  Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`; call
883
1141
  `FreedayChart.update(el)` after changing data (or use `FdyChart` in Vue/React/Blazor).
884
1142
 
@@ -889,6 +1147,10 @@ Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`
889
1147
  - Colour: `data-fdy-color="primary"` or `data-fdy-colors="success,warning,danger"` for semantic token
890
1148
  names **or** `chart-1`…`chart-8` slots to pin a category's colour. Multi-series defaults to the
891
1149
  validated categorical palette `--chart-1`…`--chart-8`.
1150
+ - **Its two strings are overridable**, and until 2.2.0 neither was: a series with no `label` fell
1151
+ back to `Seri 1` — Indonesian, three months after 2.0.0 turned the enhancers English — and the
1152
+ donut's centre caption was hard-coded. Both take the usual hook now:
1153
+ `data-fdy-text-series="Series {n}"` and `data-fdy-text-total="Total"`.
892
1154
  - Format: `data-fdy-format="number|percent|currency"`; legend `data-fdy-legend` (`none` to drop);
893
1155
  axes `data-fdy-axes`
894
1156
  - Sizing is already set by the kit. Override **these**, never a `height` on the chart root (a
@@ -1076,6 +1338,23 @@ so the same look serves routed sub-navigation built from plain links. See the no
1076
1338
  ## Modal — `.fdy-modal`
1077
1339
  > **Typed wrapper: `<FdyModal>`**. Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
1078
1340
 
1341
+ ### Props — `<FdyModal>`
1342
+
1343
+ | Prop | Type | What it does |
1344
+ |---|---|---|
1345
+ | `open` | `boolean` | Controlled: your state decides. The wrapper cancels the native `cancel` event so the DOM can never disagree with it. |
1346
+ | `title` | `string` (Vue) · `ReactNode` | Names the dialog through `aria-labelledby`. Vue also offers a `title` slot when the heading needs markup. |
1347
+ | `onClose` | `() => void` | React. Vue emits `close`. Fires from Escape, a backdrop click and the ×, all three of which exist only under `dismissible`. |
1348
+ | `size?` | `'sm' \| 'md' \| 'lg' \| 'wide'` | Dialog width. |
1349
+ | `dismissible?` | `boolean` | Whether Escape, the backdrop and the × can close it. Off means the reader has to take a footer action, which is the point of a blocking confirm. |
1350
+ | `closeLabel?` | `string` | Accessible name for the × button. Default `Close`. |
1351
+ | `footer?` | `ReactNode` | React. Vue: the `footer` slot. |
1352
+ | `children?` | `ReactNode` | React. The body. Vue: the default slot. |
1353
+
1354
+ Blazor binds `@bind-Open` and takes `TitleContent` / `ChildContent` / `FooterContent`, plus
1355
+ `OnClose`. `CloseLabel` used to be Blazor-only, which left the × in Vue and React fixed at `Close`;
1356
+ all three stacks carry it now.
1357
+
1079
1358
  Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
1080
1359
  `--wide`; `--cfl` for the choose-from-list dialog.
1081
1360
 
@@ -1111,6 +1390,23 @@ that is not the kit. Both rules measured on Chromium 133 and 151, and pinned as
1111
1390
  ## Drawer — `.fdy-drawer`
1112
1391
  > **Typed wrapper: `<FdyDrawer>`**. Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
1113
1392
 
1393
+ ### Props — `<FdyDrawer>`
1394
+
1395
+ The modal's props with `side` in place of `size`; the behaviour notes there apply here too.
1396
+
1397
+ | Prop | Type | What it does |
1398
+ |---|---|---|
1399
+ | `open` | `boolean` | Controlled, as with the modal. |
1400
+ | `title` | `string` (Vue) · `ReactNode` | Names the drawer (`aria-labelledby`). |
1401
+ | `onClose` | `() => void` | React. Vue emits `close`. |
1402
+ | `side?` | `'left' \| 'right'` | Which edge it slides from. |
1403
+ | `dismissible?` | `boolean` | Escape, backdrop and × on or off. |
1404
+ | `closeLabel?` | `string` | Accessible name for the × button. Default `Close`. |
1405
+ | `footer?` | `ReactNode` | React. Vue: the `footer` slot. |
1406
+ | `children?` | `ReactNode` | React. The body. Vue: the default slot. |
1407
+
1408
+ Blazor: `@bind-Open`, `TitleContent` / `ChildContent` / `FooterContent`, `OnClose`, `CloseLabel`.
1409
+
1114
1410
  Temporary side panel on native `<dialog>`, left by default, `--right` to flip. Parts `__header`
1115
1411
  `__title` `__body` `__footer` `__close`. Open it from any
1116
1412
  `<button data-fdy-drawer="<dialog id>">`. Needs `freeday-drawer.js`.
package/README.id.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **Lebih banyak _free day_ buat dev. UI kit-nya sudah siap pakai.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v2.0.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v2.0.0)
8
+ [![Release](https://img.shields.io/badge/release-v2.2.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v2.2.0)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic. Satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs. The UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v2.0.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v2.0.0)
8
+ [![Release](https://img.shields.io/badge/release-v2.2.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v2.2.0)
9
9
 
10
10
  A token-driven, framework-agnostic UI kit. One source of truth for color, typography,
11
11
  spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -8,7 +8,10 @@
8
8
  <div @ref="Root" data-fdy-autocomplete class="fdy-autocomplete">
9
9
  <input class="fdy-input" role="combobox" aria-expanded="false" aria-autocomplete="list"
10
10
  aria-controls="@_listId" autocomplete="off" value="@Value"
11
- placeholder="@Placeholder" aria-label="@AriaLabel" />
11
+ placeholder="@Placeholder" aria-label="@AriaLabel"
12
+ id="@Id" aria-labelledby="@AriaLabelledby" aria-describedby="@Describedby"
13
+ disabled="@Disabled" readonly="@Readonly"
14
+ aria-invalid="@(Invalid ? "true" : null)" />
12
15
  <ul class="fdy-autocomplete__listbox" id="@_listId" role="listbox" hidden>
13
16
  @foreach (string item in Options)
14
17
  {
@@ -25,8 +25,34 @@ public partial class FdyAutocomplete
25
25
  protected override async ValueTask OnHydratedAsync()
26
26
  => await SubscribeAsync("fdy-autocomplete-select", nameof(OnSelect));
27
27
 
28
+ /// <summary>Explicit id for the input.</summary>
29
+ [Parameter] public string? Id { get; set; }
30
+
31
+ /// <summary>Id of the element that labels the input, when a visible label does the naming.</summary>
32
+ [Parameter] public string? AriaLabelledby { get; set; }
33
+
34
+ /// <summary>Id of the help or error text the input describes itself with.</summary>
35
+ [Parameter] public string? Describedby { get; set; }
36
+
37
+ /// <summary>Greyed and out of the tab order.</summary>
38
+ [Parameter] public bool Disabled { get; set; }
39
+
40
+ /// <summary>Locked/view mode: focusable and showing its value, not editable, and the
41
+ /// suggestion list will not open.</summary>
42
+ [Parameter] public bool Readonly { get; set; }
43
+
44
+ /// <summary>Marks the field invalid (<c>aria-invalid</c>).</summary>
45
+ [Parameter] public bool Invalid { get; set; }
46
+
28
47
  protected override bool ShouldRender() => !Hydrated;
29
48
 
49
+ protected override async Task OnParametersSetAsync()
50
+ {
51
+ if (!Hydrated) return;
52
+ await JS.InvokeVoidAsync("FreedayAutocomplete.setState", Root,
53
+ new { disabled = Disabled, @readonly = Readonly, invalid = Invalid });
54
+ }
55
+
30
56
  /// <summary>Invoked by the bridge when the user picks a suggestion.</summary>
31
57
  [JSInvokable]
32
58
  public async Task OnSelect(SelectDetail detail)