@cahyo-dimas/freeday 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,94 @@
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.1.0] - 2026-08-25
13
+ ### Added
14
+ - **`singleRow()`, exported from `@cahyo-dimas/freeday/vue` and `/react`** (#045). `FdyCfl` emits
15
+ `Row | Row[] | null` whatever its props are: the array is reachable only under `multiple`, the
16
+ null only under `clearable`, and the type cannot say which. So every single-select screen wrote
17
+ its own narrowing, and each one differently — a cast (which hides the day somebody adds
18
+ `clearable`), a silent ignore, or a throw. One implementation now lives in
19
+ `adapters/core/cfl-value.js` and both adapters re-export it: it returns `Row | null`, and
20
+ **throws** a `TypeError` on an array instead of keeping its first row, because quietly dropping
21
+ the rest is how a `multiple` added later goes unnoticed. Blazor never needed it — multi-select
22
+ there is a separate `Values` / `ValuesChanged` pair.
23
+ ```ts
24
+ import { singleRow } from '@cahyo-dimas/freeday/vue'; // or '@cahyo-dimas/freeday/react'
25
+ // (value: CflRow | CflRow[] | null) => CflRow | null
26
+ const onUpdate = (value: CflRow | CflRow[] | null): void => { picked.value = singleRow(value); };
27
+ ```
28
+ ### Changed: BREAKING (types)
29
+ - **A retroactive entry for 1.29.0 and 1.42.0** (#045) — nothing changed in this release; this is
30
+ the record those two are missing. Both widened `FdyCfl`'s emitted type — `Row` → `Row | null`
31
+ (1.29.0, with `clearable`) → `Row | Row[] | null` (1.42.0, with `multiple`) — and both shipped
32
+ under **Added**, which is honest from the kit's side and useless from the consumer's: the
33
+ combined effect is `vue-tsc` failing in a handler nobody touched. An app upgrading from before
34
+ 1.29.0 meets both at once, which is exactly how it was reported. Migration is `singleRow()`
35
+ above. The policy that should stop a third instance is at the top of this file.
36
+ ### Added: guards
37
+ - **The typed column contract is now pinned in both directions** (#040): `test/docs.test.mjs`
38
+ asserts the field list documented in `COMPONENTS.md` matches `FdyTableColumn` in
39
+ `adapters/core/table-model.d.ts` — a field in the type but not the docs fails, and so does a
40
+ field in the docs that the type does not have. Verified by mutation (drop `labelHidden` from the
41
+ table → red; invent a `sticky` row → red), since a guard nobody has watched fail is only a
42
+ green tick.
43
+ - `test/cfl-value.test.mjs`: three tests for `singleRow` (row passes through, `null`/`undefined`
44
+ become null, an array throws).
45
+ - **Every typed wrapper's props are pinned to their documentation** (`NEXT-UP.md` #11). One guard
46
+ reads all twelve `### Props — <FdyX>` tables and compares them with the Vue and React
47
+ declarations: a prop missing from a table fails, a documented prop neither adapter declares
48
+ fails, and a wrapper exported with no table at all fails. Mutation-verified in all three
49
+ directions, including the case that found a bug in the guard itself — a hyphenated prop name
50
+ (`aria-label`) was invisible to the first version of its parser.
51
+ ### Docs
52
+ - **`COMPONENTS.md` finally states the typed column contract** (#040). §Data table gained
53
+ "Columns — the typed `FdyTableColumn<T>`": all ten fields with their types, the Blazor spelling
54
+ (`PascalCase`, `required` Key/Label, the extra `Cell` slot), and the row-controls example.
55
+ `agent-onboarding.md` tells every consuming agent this file is closed — "if a class is not in
56
+ that file, it does not exist" — while three of the four stacks build a table only through
57
+ `columns`, whose shape appeared nowhere in it. One reporting app shipped `label: ''` on its
58
+ actions column as a result: a `<th>` announced as nothing.
59
+ - **Nine enhancers still claimed to be Indonesian.** The comment above every `TEXT` table read
60
+ "User-facing strings. Indonesian by default" — written before 2.0.0 flipped them all to English,
61
+ and shipped in `dist/`, where it is the first thing a consumer reading the source sees. It now
62
+ says English, names `data-fdy-text-<key>` as the override, and gives an Indonesian app on the
63
+ raw path as the example of who overrides.
64
+ - **The two docs a consumer actually reads on upgrade now mention the language.** 2.0.0's breaking
65
+ change was documented in `COMPONENTS.md` and this file, but neither `docs/getting-started.md` nor
66
+ the block projects paste into their agent instructions
67
+ (`docs/agent-onboarding.md`) said a word about it. Both now do, with `data-fdy-text-<key>` as the
68
+ whole migration and `<html lang>` for date names.
69
+ - **`HANDOFF.md` described a release that had already happened.** It stated npm was on 1.53.0 with
70
+ 1.54.0 and 2.0.0 sitting unpushed; npm had been on 2.0.0 since the morning, with the tag, the
71
+ branch and `origin/main` all on the same commit. The file that warns about stale snapshots was
72
+ the stale snapshot. Corrected, with the three commands that check it.
73
+ - **`CLAUDE.md` claimed 44 components**, a number matching nothing countable. Now 48 stylesheets in
74
+ `src/components/` and 26 enhancers in `src/freeday-*.js`, both of which anyone can re-count.
75
+ - **The typed props of all twelve wrappers are now in `COMPONENTS.md`** (`NEXT-UP.md` #11, the
76
+ general form of #040). A `### Props` table per wrapper: Vue and React names together, what each
77
+ does, and the Blazor differences stated rather than implied. Writing them measured something
78
+ nobody had: **four Blazor pickers are far thinner than their JS twins** — `FdyDatepicker` takes 6
79
+ parameters against 21 Vue props, `FdyAutocomplete` 6 against 11, `FdyCascade` 8 against 12, and
80
+ `FdyCombo` has no `Describedby`. Those wrappers render a seed element and do not splat unmatched
81
+ attributes, so `disabled`, `readonly`, `invalid`, the a11y ids, `clearable` and the datepicker's
82
+ ten navigation labels cannot be reached from a Blazor page at all. It runs the other way too:
83
+ `CloseLabel` exists only in Blazor, so the modal and drawer × is fixed at `Close` in Vue and
84
+ React. Filed as `NEXT-UP.md` #12 with its trigger; documented here as fact in the meantime,
85
+ because a consumer meeting it should find it written down rather than discover it.
86
+ - **`FdyAppShell` was missing from every list of the typed wrappers but the two READMEs.** It
87
+ shipped as the eleventh in 1.54.0 and `COMPONENTS.md`, `getting-started.md`, `agent-onboarding.md`
88
+ and `integrations.md` went on naming ten for a whole major version — `agent-onboarding.md`
89
+ saying both "ten" and "eleven" about the same set, in the same file, and `getting-started.md`
90
+ carrying a section heading that still listed the seven of some earlier release. All corrected,
91
+ and a guard now reads any run of six or more wrapper names as an enumeration and fails if it is
92
+ incomplete. It found two of these itself, one of them a list this release had already touched.
93
+
6
94
  ## [2.0.0] - 2026-08-25
7
95
  ### Changed: BREAKING
8
96
  - **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,24 @@ 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
+ with one absence — there is no `Describedby`, so a Blazor combo cannot point at its own error text.
461
+
425
462
  Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
426
463
 
427
464
  - `.fdy-combo` (+`--error`, `--no-icon`) · `__button` `__value` (+`--placeholder`) `__listbox`
@@ -452,6 +489,22 @@ keeps matching after selection. Do not put a glyph in that span.
452
489
  ## Autocomplete — `.fdy-autocomplete`
453
490
  > **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
491
 
492
+ ### Props — `<FdyAutocomplete>`
493
+
494
+ | Prop | Type | What it does |
495
+ |---|---|---|
496
+ | `modelValue` · `value` | `string` | The typed text. Free text: the list suggests, it does not constrain. |
497
+ | `onChange` · `onSelect` | `(value: string) => void` | React. `onChange` on every keystroke, `onSelect` only when a suggestion is taken. Vue emits `update:modelValue` and `select`. |
498
+ | `options` | `ReadonlyArray<string>` | The suggestions to filter. |
499
+ | `emptyText?` | `string` | Shown when nothing matches. |
500
+ | `placeholder?` | `string` | |
501
+ | `id?` · `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Input id; its accessible name as text or as a reference; the help/error text it points at. |
502
+ | `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. |
503
+
504
+ Blazor takes six of these — `Value` / `ValueChanged`, `Options`, `Placeholder`, `AriaLabel`,
505
+ `EmptyText` — and no state flags or ids. Its wrapper renders a fixed seed element and does not
506
+ splat unmatched attributes, so those are not reachable from a Blazor page at all.
507
+
455
508
  Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
456
509
 
457
510
  - `.fdy-autocomplete` · `__listbox` `__option` `__empty`
@@ -461,6 +514,22 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
461
514
  ## Cascade select — `.fdy-cascade`
462
515
  > **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
516
 
517
+ ### Props — `<FdyCascade>`
518
+
519
+ | Prop | Type | What it does |
520
+ |---|---|---|
521
+ | `modelValue` · `value` | `string` | The selected **leaf** value; `''` means nothing selected. |
522
+ | `onChange` | `(value: string, labels: string[]) => void` | React. Vue emits `update:modelValue` and `change`. `labels` is the whole path, root → leaf. |
523
+ | `options` | `ReadonlyArray<CascadeNode>` | The tree. `CascadeNode` = `{ label, value, children? }`; a node **with** children is a branch, one without is selectable. |
524
+ | `separator?` | `string` | What joins the path in the display. Default `" / "`, matching the enhancer. |
525
+ | `backLabel?` | `string` | Accessible name for the up-one-level button. Default `Back one level`. |
526
+ | `label?` | `string` | Accessible name for the trigger and its listbox. |
527
+ | `placeholder?` | `string` | |
528
+ | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
529
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | As on `<FdyCombo>`. |
530
+
531
+ Blazor calls the tree `Nodes`, adds `SubmenuLabel`, and takes neither the state flags nor the ids.
532
+
464
533
  Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
465
534
  child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
466
535
 
@@ -482,6 +551,37 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
482
551
  ## Choose-from-list (CFL) — `data-fdy-cfl`
483
552
  > **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
553
 
554
+ ### Props — `<FdyCfl>`
555
+
556
+ Four of these are required and carry the whole component: `fetchPage`, `columns`, `display`,
557
+ `rowKey`. The rest are copy and state.
558
+
559
+ | Prop | Type | What it does |
560
+ |---|---|---|
561
+ | `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). |
562
+ | `onChange` | `(value: Row \| Row[] \| null) => void` | React. Vue emits `update:modelValue` and `change`. |
563
+ | `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. |
564
+ | `columns` | `ReadonlyArray<CflColumn<Row>>` | `{ key, label }` per dialog column. |
565
+ | `display` | `(row: Row) => string` | What the closed field shows once a row is picked. |
566
+ | `rowKey` | `(row: Row) => string` | Row identity: ticks in multi mode, and seeding them when the dialog re-opens. |
567
+ | `pageSize?` | `number` | Advisory only. Your `fetchPage` owns paging; this documents the intent. |
568
+ | `title?` | `string` | The dialog's heading. Default `Choose data`. |
569
+ | `searchPlaceholder?` | `string` | Placeholder **and** accessible name of the search box. Default `Search…`. |
570
+ | `loadingText?` · `emptyText?` · `retryText?` · `moreText?` | `string` | Dialog states. Defaults `Loading…`, `No results.`, `Try again`, `Load more`. |
571
+ | `closeLabel?` · `openLabel?` | `string` | Accessible names for the dialog's × and the field's open button. Defaults `Close`, `Open search`. |
572
+ | `multiple?` | `boolean` | Tick rows and commit them together on Confirm instead of committing the clicked row, and widen the value to an array. |
573
+ | `selectedText?` · `confirmText?` · `hintText?` | `string` | Footer copy. Defaults `{n} selected` (with `{n}` substituted), `Confirm`, `Click a row to choose it`. |
574
+ | `placeholder?` | `string` | Field placeholder while nothing is picked. |
575
+ | `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. |
576
+ | `clearLabel?` | `string` | Accessible name for that button. Default `Clear selection`. |
577
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps the picked value visible, focusable and copyable, but the dialog cannot be opened. |
578
+ | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Field id; the element that labels it; the help/error text it points at. |
579
+
580
+ Blazor carries the same surface in `PascalCase` with three differences: multi-select is a second
581
+ pair (`Values` / `ValuesChanged`) rather than a widened single binding, the fetch delegate is
582
+ called `LoadPage`, and it has two parameters the JS adapters do not — `ErrorText` and
583
+ `SearchDebounceMs`.
584
+
485
585
  A read-only field backed by master data: the button opens a searchable dialog, the picked row fills
486
586
  the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
487
587
 
@@ -498,6 +598,13 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
498
598
  `ValuesChanged`, because a nullable union is not a C# shape. Closing without Confirm leaves the
499
599
  bound value untouched, and re-opening seeds the ticks from it. The field states `{n} selected`
500
600
  rather than one row's `display()`. Strings: `selectedText`, `confirmText`, `hintText`.
601
+ - **A single-select in Vue or React needs one narrowing, and the kit ships it.** The emitted value
602
+ is typed `Row | Row[] | null` whatever the props are, so a single-select handler cannot take it
603
+ as `Row`: the array is reachable only under `multiple`, the null only under `clearable`, and the
604
+ type cannot say which. Import the guard instead of writing one per app:
605
+ `import { singleRow } from '@cahyo-dimas/freeday/vue'` (or `/react`). It returns `Row | null`,
606
+ and throws if a `multiple` field's array ever reaches it, rather than silently keeping the first
607
+ row. Blazor is unaffected: multi-select there is a separate `Values` / `ValuesChanged` pair.
501
608
 
502
609
  ```html
503
610
  <div class="fdy-field">
@@ -547,6 +654,46 @@ a new class.
547
654
  ## Date picker — `data-fdy-datepicker`
548
655
  > **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
656
 
657
+ ### Props — `<FdyDatepicker>`
658
+
659
+ | Prop | Type | What it does |
660
+ |---|---|---|
661
+ | `modelValue` · `value` | `string \| null` | ISO `YYYY-MM-DD`, or `''`/`null` for empty. |
662
+ | `onChange` | `(value: string) => void` | React. Vue emits `update:modelValue` and `change`. Clearing sends `''`. |
663
+ | `min?` · `max?` | `string` | ISO bounds; days outside them cannot be picked. |
664
+ | `locale?` | `string` | BCP-47 tag deciding month and weekday names. Defaults to the page's `<html lang>`, then `en`. |
665
+ | `placeholder?` | `string` | |
666
+ | `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
667
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps focus and tab order and shows its date, but it cannot be opened, cleared or changed. |
668
+ | `clearable?` | `boolean` | Show a × in the trigger once a date is set, so an optional date can be unset. Sends `''`. Off by default. |
669
+ | `clearLabel?` | `string` | Accessible name for that ×. Default `Clear date`. |
670
+ | `prevMonthLabel?` · `nextMonthLabel?` | `string` | The month arrows. Defaults `Previous month` / `Next month`. |
671
+ | `chooseMonthLabel?` · `chooseYearLabel?` | `string` | The title buttons that drill into the month and year grids. Defaults `Choose month` / `Choose year`. |
672
+ | `prevYearLabel?` · `nextYearLabel?` | `string` | Year arrows, shown in the month grid. Defaults `Previous year` / `Next year`. |
673
+ | `prevYearsLabel?` · `nextYearsLabel?` | `string` | Page arrows, shown in the year grid. Defaults `Previous years` / `Next years`. |
674
+
675
+ The ten label props exist because month and weekday names follow `locale` while the buttons around
676
+ them do not: without these, a Spanish calendar would be navigated by English arrows.
677
+
678
+ Blazor's picker is much thinner — `Value` / `ValueChanged`, `Label`, `Placeholder`, `Min`, `Max`
679
+ — and its seed element takes no unmatched attributes, so state flags, ids, `clearable` and every
680
+ label above are unreachable there.
681
+
682
+ ### Props — `<FdyDateRange>`
683
+
684
+ | Prop | Type | What it does |
685
+ |---|---|---|
686
+ | `modelValue` · `value` | `DateRangeValue` = `{ start, end }`, each `string \| null` | Both ends at once, so the pair can never be half-applied. |
687
+ | `onChange` | `(value: DateRangeValue) => void` | React. Vue emits `update:modelValue` and `change`. |
688
+ | `min?` · `max?` | `string` | ISO bounds for both ends. The end can never precede the start. |
689
+ | `locale?` | `string` | As on the single picker. |
690
+ | `startPlaceholder?` · `endPlaceholder?` | `string` | One per field. |
691
+ | `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Names the group (`role="group"`), or points at the text that does. |
692
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | Applied to both pickers together. |
693
+
694
+ Blazor splits the value into two scalars, `From` / `FromChanged` and `To` / `ToChanged`, because a
695
+ C# binding cannot express one object bound in two places, and it adds `Separator`.
696
+
550
697
  Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author an empty `<div>`**
551
698
  The enhancer builds everything.
552
699
 
@@ -772,6 +919,75 @@ sum, so measure them once after render (and on resize) and set the variable.
772
919
  ## Data table — `.fdy-datatable`
773
920
  > **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
921
 
922
+ ### Columns — the typed `FdyTableColumn<T>`
923
+
924
+ On Vue, React and Blazor, `columns` **is** the table: it is what those stacks write instead of the
925
+ markup hooks further down. Ten fields, `key` and `label` required. The authored doc comments live on
926
+ the type itself (`adapters/core/table-model.d.ts`, re-declared for Blazor in
927
+ `adapters/blazor/TableTypes.cs`); the list below is the discoverable copy, and `npm test` fails if
928
+ the two ever disagree.
929
+
930
+ | Field | Type | What it does |
931
+ |---|---|---|
932
+ | `key` | `string` | Row property key; the sort/filter identity and, unless `value` is set, the cell accessor. |
933
+ | `label` | `string` | Header label. |
934
+ | `labelHidden` | `boolean` | Render the label for assistive tech only; the header cell looks empty. |
935
+ | `sortable` | `boolean` | Show a sort toggle in the header. |
936
+ | `filter` | `'text' \| 'enum' \| 'number' \| 'date'` | Offer a column filter of this type (the funnel popover). |
937
+ | `align` | `'left' \| 'right' \| 'center'` | Cell alignment. Default `left`. |
938
+ | `mono` | `boolean` | Render cells in the monospace data font (`.fdy-mono`). |
939
+ | `sortType` | `'text' \| 'number' \| 'date'` | Override the comparator; defaults to one derived from `filter`, else `text`. |
940
+ | `value` | `(row: T) => unknown` | Custom accessor; defaults to `row[key]`. Feeds sort, filter and the default cell text. |
941
+ | `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. |
942
+
943
+ Blazor spells the same fields `PascalCase` (`LabelHidden`, `SortType`), makes `Key`/`Label`
944
+ `required`, and adds one field with no TS twin: `Cell`, a `RenderFragment<TRow>` where Vue and React
945
+ use a slot.
946
+
947
+ A column of row controls is worth stating outright, because the obvious move is the wrong one:
948
+
949
+ ```ts
950
+ const columns: FdyTableColumn<User>[] = [
951
+ { key: 'email', label: 'Email', sortable: true, filter: 'text' },
952
+ { key: 'total', label: 'Total', align: 'right', mono: true, sortType: 'number' },
953
+ // Not `label: ''`. A <th> with no text announces as nothing, and the label also names this
954
+ // column's sort button and filter popover, so it has to stay meaningful.
955
+ { key: 'actions', label: 'Actions', labelHidden: true },
956
+ ];
957
+ ```
958
+
959
+ ### Props — `<FdyTable>`
960
+
961
+ Controlled by default: hand it `columns` + `rows` and it filters, sorts and paginates internally;
962
+ provide the state prop for a concern and you own that concern instead. React names the callbacks
963
+ `onXChange`, Vue emits `update:x`, Blazor exposes `XChanged`.
964
+
965
+ | Prop | Type | What it does |
966
+ |---|---|---|
967
+ | `columns` | `ReadonlyArray<FdyTableColumn<Row>>` | The column contract documented above. |
968
+ | `rows` | `ReadonlyArray<Row>` | The current page's rows in server mode, the whole set in client mode. |
969
+ | `rowKey` | `(row: Row) => string \| number` | Row identity: keys, expansion, activation. |
970
+ | `sort?` · `onSortChange?` | `FdySortState \| null` · `(sort) => void` | Pass `sort`, **even as `null`**, to own sorting; omit it for the internal sort. |
971
+ | `filters?` · `onFiltersChange?` | `FdyFilterMap` · `(filters) => void` | The same, for filtering. |
972
+ | `page?` · `onPageChange?` | `FdyPageState` · `(page) => void` | Its **presence switches the table into server mode**: you own paging and get `{ index, size, total }` back. |
973
+ | `pageSize?` | `number` | Client mode: rows per page. Absent or 0 renders every row with no pager. |
974
+ | `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. |
975
+ | `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. |
976
+ | `pager?` | `boolean` | Default true. Turn it off to withhold the table's own footer when the screen renders one. |
977
+ | `loading?` | `boolean` | Show the in-table loading state. |
978
+ | `emptyText?` · `empty?` | `string` · `ReactNode` | The empty state as text, or as markup (React `empty`, Vue's `empty` slot). |
979
+ | `ariaLabel?` | `string` | Names the table for assistive tech. |
980
+ | `rowActivatable?` · `onRowActivate?` | `boolean` · `(row) => void` | Rows become focusable and activate on click, Enter or Space. |
981
+ | `rowClass?` | `(row: Row) => string \| undefined` | Per-row class hook, e.g. marking the selected row. |
982
+ | `expandedKeys?` · `renderRowDetail?` | `ReadonlyArray<string \| number>` · `(row) => ReactNode` | Controlled expansion: these keys get a full-width detail row. Vue uses the `row-detail` slot. |
983
+ | `renderCell?` | `(column, row, value) => ReactNode` | React. Custom cell rendering; Vue uses the `cell` slot. |
984
+ | `toolbar?` | `ReactNode` | React. Content above the table; Vue uses the `toolbar` slot. |
985
+ | `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. |
986
+
987
+ Blazor matches this surface (`LoadingText` and `EmptyContent` in place of `emptyText`/`empty`,
988
+ `Toolbar` and `RowDetail` as `RenderFragment`s) and is the one adapter that also exposes
989
+ `FiltersChanged`.
990
+
775
991
  The interactive table: global search, sort, per-column filters, row selection + bulk bar,
776
992
  pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` + `data-fdy-table`
777
993
  (`data-page-size="N"`).
@@ -849,6 +1065,14 @@ ninety-row list would remove the only way back to twenty.
849
1065
  of one page of rows. A footer inside the table is inside the half a phone hides, so those screens
850
1066
  render it once, outside both, with `pager={false}` on the table.
851
1067
 
1068
+ ### Props — `<FdyTableFooter>`
1069
+
1070
+ | Prop | Type | What it does |
1071
+ |---|---|---|
1072
+ | `page` | `FdyPageState` = `{ index, size, total }` | The page being shown. `size` drives both the range text and the rows-per-page control's value. |
1073
+ | `onPageChange?` | `(page: FdyPageState) => void` | React. A page click or a size change. Vue emits `update:page`; Blazor `PageChanged`. |
1074
+ | `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. |
1075
+
852
1076
  **Who draws the pager.** The table renders its own footer (range + pager) whenever there is more
853
1077
  than one page. Two ways to take it over:
854
1078
 
@@ -879,6 +1103,25 @@ replaces.
879
1103
  ## Charts — `data-fdy-chart`
880
1104
  > **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
1105
 
1106
+ ### Props — `<FdyChart>`
1107
+
1108
+ | Prop | Type | What it does |
1109
+ |---|---|---|
1110
+ | `type` | `'line' \| 'area' \| 'bar' \| 'sparkline' \| 'donut'` | Which chart to draw. |
1111
+ | `values?` | `ReadonlyArray<number>` | One series. |
1112
+ | `series?` | `ReadonlyArray<FdyChartSeries>` | Several: each `{ label, values }`. Use one or the other, not both. |
1113
+ | `labels?` | `ReadonlyArray<string>` | Category labels: the x axis, or the donut's slices. |
1114
+ | `format?` | `'number' \| 'percent' \| 'currency'` | How values are written in labels and the donut centre. |
1115
+ | `stacked?` | `boolean` | Bar charts: stack the series instead of grouping them side by side. |
1116
+ | `legend?` | `'auto' \| 'always' \| 'none'` | `auto` shows one when there is more than one series. |
1117
+ | `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. |
1118
+ | `color?` | `string` | The same, for a single-series chart (sparkline, simple bar or line). |
1119
+ | `center?` | `string \| number` | Donut only: what to print in the middle. |
1120
+ | `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`). |
1121
+ | `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. |
1122
+
1123
+ Blazor adds `ChildContent` for the same placeholder role.
1124
+
882
1125
  Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`; call
883
1126
  `FreedayChart.update(el)` after changing data (or use `FdyChart` in Vue/React/Blazor).
884
1127
 
@@ -1076,6 +1319,22 @@ so the same look serves routed sub-navigation built from plain links. See the no
1076
1319
  ## Modal — `.fdy-modal`
1077
1320
  > **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
1321
 
1322
+ ### Props — `<FdyModal>`
1323
+
1324
+ | Prop | Type | What it does |
1325
+ |---|---|---|
1326
+ | `open` | `boolean` | Controlled: your state decides. The wrapper cancels the native `cancel` event so the DOM can never disagree with it. |
1327
+ | `title` | `string` (Vue) · `ReactNode` | Names the dialog through `aria-labelledby`. Vue also offers a `title` slot when the heading needs markup. |
1328
+ | `onClose` | `() => void` | React. Vue emits `close`. Fires from Escape, a backdrop click and the ×, all three of which exist only under `dismissible`. |
1329
+ | `size?` | `'sm' \| 'md' \| 'lg' \| 'wide'` | Dialog width. |
1330
+ | `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. |
1331
+ | `footer?` | `ReactNode` | React. Vue: the `footer` slot. |
1332
+ | `children?` | `ReactNode` | React. The body. Vue: the default slot. |
1333
+
1334
+ Blazor binds `@bind-Open` and takes `TitleContent` / `ChildContent` / `FooterContent`, plus two
1335
+ things Vue and React do not have: `OnClose` and **`CloseLabel`**. The × in Vue and React is labelled
1336
+ `Close` and cannot be renamed.
1337
+
1079
1338
  Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
1080
1339
  `--wide`; `--cfl` for the choose-from-list dialog.
1081
1340
 
@@ -1111,6 +1370,22 @@ that is not the kit. Both rules measured on Chromium 133 and 151, and pinned as
1111
1370
  ## Drawer — `.fdy-drawer`
1112
1371
  > **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
1372
 
1373
+ ### Props — `<FdyDrawer>`
1374
+
1375
+ The modal's props with `side` in place of `size`; the behaviour notes there apply here too.
1376
+
1377
+ | Prop | Type | What it does |
1378
+ |---|---|---|
1379
+ | `open` | `boolean` | Controlled, as with the modal. |
1380
+ | `title` | `string` (Vue) · `ReactNode` | Names the drawer (`aria-labelledby`). |
1381
+ | `onClose` | `() => void` | React. Vue emits `close`. |
1382
+ | `side?` | `'left' \| 'right'` | Which edge it slides from. |
1383
+ | `dismissible?` | `boolean` | Escape, backdrop and × on or off. |
1384
+ | `footer?` | `ReactNode` | React. Vue: the `footer` slot. |
1385
+ | `children?` | `ReactNode` | React. The body. Vue: the default slot. |
1386
+
1387
+ Blazor: `@bind-Open`, `TitleContent` / `ChildContent` / `FooterContent`, `OnClose`, `CloseLabel`.
1388
+
1114
1389
  Temporary side panel on native `<dialog>`, left by default, `--right` to flip. Parts `__header`
1115
1390
  `__title` `__body` `__footer` `__close`. Open it from any
1116
1391
  `<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.1.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v2.1.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.1.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v2.1.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`.
@@ -0,0 +1,11 @@
1
+ // Types for the choose-from-list value helper (adapters/core/cfl-value.js).
2
+
3
+ /**
4
+ * Narrow an `FdyCfl` value to the row a single-select can actually produce.
5
+ *
6
+ * `Row[]` is reachable only under `multiple` and `null` only under `clearable`, but the emitted
7
+ * type cannot say which of those props a given field sets, so every single-select handler has to
8
+ * prove it. Throws on an array rather than taking its first row: quietly picking one is how a
9
+ * `multiple` added later goes unnoticed.
10
+ */
11
+ export declare function singleRow<Row>(value: Row | Row[] | null): Row | null;
@@ -0,0 +1,31 @@
1
+ // Freeday, choose-from-list value narrowing (pure, zero dependencies).
2
+ //
3
+ // `FdyCfl` in Vue and React declares one model type, `Row | Row[] | null`, covering three prop
4
+ // combinations at once: the array is reachable only under `multiple`, the null only under
5
+ // `clearable`. A single-select screen therefore has to narrow a union that its own props make
6
+ // impossible, and the widening arrived across two releases that were additive from the kit's side
7
+ // (1.29.0 `clearable`, 1.42.0 `multiple`) and a broken build from the consumer's. Apps were each
8
+ // inventing the guard differently: a cast that hides the day `clearable` is added, a silent
9
+ // ignore, or a throw. The kit is what knows the invariant, so the guard is written once, here.
10
+ //
11
+ // Blazor needs none of this: multi-select there is a separate `Values` / `ValuesChanged` pair,
12
+ // because a nullable union is not a C# binding shape.
13
+
14
+ /**
15
+ * Narrow an `FdyCfl` value to the row a single-select can actually produce.
16
+ *
17
+ * @template Row
18
+ * @param {Row | Row[] | null} value the value the component emitted
19
+ * @returns {Row | null} the picked row, or null when nothing is picked
20
+ * @throws {TypeError} when handed an array, which only a `multiple` CFL emits
21
+ */
22
+ export function singleRow(value) {
23
+ if (Array.isArray(value)) {
24
+ throw new TypeError(
25
+ `singleRow() got an array of ${value.length} row(s). Only an FdyCfl with \`multiple\` emits ` +
26
+ 'one, and a set of rows is not a single selection: read the array directly on that field, ' +
27
+ 'or drop `multiple`.',
28
+ );
29
+ }
30
+ return value == null ? null : value;
31
+ }
@@ -34,6 +34,9 @@ export { FdyDateRange, type FdyDateRangeProps, type DateRangeValue } from './com
34
34
  export { FdyAutocomplete, type FdyAutocompleteProps } from './components/FdyAutocomplete';
35
35
  export { FdyCascade, type FdyCascadeProps, type CascadeNode } from './components/FdyCascade';
36
36
  export { FdyCfl, type FdyCflProps, type CflColumn, type CflPage } from './components/FdyCfl';
37
+ /** Narrow `FdyCfl`'s `Row | Row[] | null` to the `Row | null` a single-select can produce; throws
38
+ * on the array only a `multiple` field emits. */
39
+ export { singleRow } from '../core/cfl-value';
37
40
  export { FdyChart, type FdyChartProps, type FdyChartSeries } from './components/FdyChart';
38
41
  export { FdyTable, type FdyTableProps } from './components/FdyTable';
39
42
  /** The table's own footer, standalone, for a responsive list whose table is hidden at some
@@ -6,6 +6,7 @@ export { FdyDateRange } from './components/FdyDateRange.tsx';
6
6
  export { FdyAutocomplete } from './components/FdyAutocomplete.tsx';
7
7
  export { FdyCascade } from './components/FdyCascade.tsx';
8
8
  export { FdyCfl } from './components/FdyCfl.tsx';
9
+ export { singleRow } from '../core/cfl-value.js';
9
10
  export { FdyChart } from './components/FdyChart.tsx';
10
11
  export { FdyTable } from './components/FdyTable.tsx';
11
12
  export { FdyTableFooter } from './components/FdyTableFooter.tsx';
@@ -33,6 +33,9 @@ export { default as FdyAutocomplete } from './components/FdyAutocomplete.vue';
33
33
  export { default as FdyCascade } from './components/FdyCascade.vue';
34
34
  export type { CascadeNode } from './components/FdyCascade.vue';
35
35
  export { default as FdyCfl } from './components/FdyCfl.vue';
36
+ /** Narrow `FdyCfl`'s `Row | Row[] | null` to the `Row | null` a single-select can produce; throws
37
+ * on the array only a `multiple` field emits. */
38
+ export { singleRow } from '../core/cfl-value';
36
39
  export { default as FdyChart } from './components/FdyChart.vue';
37
40
  export { default as FdyTable } from './components/FdyTable.vue';
38
41
  /** The table's own footer, standalone, for a responsive list whose table is hidden at some
@@ -6,6 +6,7 @@ export { default as FdyDateRange } from './components/FdyDateRange.vue';
6
6
  export { default as FdyAutocomplete } from './components/FdyAutocomplete.vue';
7
7
  export { default as FdyCascade } from './components/FdyCascade.vue';
8
8
  export { default as FdyCfl } from './components/FdyCfl.vue';
9
+ export { singleRow } from '../core/cfl-value.js';
9
10
  export { default as FdyChart } from './components/FdyChart.vue';
10
11
  export { default as FdyTable } from './components/FdyTable.vue';
11
12
  export { default as FdyTableFooter } from './components/FdyTableFooter.vue';
@@ -18,10 +18,11 @@
18
18
  'use strict';
19
19
 
20
20
 
21
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
22
- * path, and every one overridable per element, so a host that speaks another language (the
23
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
24
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
21
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
22
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
23
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
24
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
25
+ * hard-coded further down. */
25
26
  var TEXT = {
26
27
  position: '{n} of {total}',
27
28
  slide: 'Slide {n}'
@@ -54,10 +54,11 @@
54
54
  }
55
55
 
56
56
 
57
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
58
- * path, and every one overridable per element, so a host that speaks another language (the
59
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
60
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
57
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
58
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
59
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
60
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
61
+ * hard-coded further down. */
61
62
  var TEXT = {
62
63
  back: 'Back one level',
63
64
  submenu: '{label}, submenu'
@@ -34,10 +34,11 @@
34
34
  }
35
35
 
36
36
 
37
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
38
- * path, and every one overridable per element, so a host that speaks another language (the
39
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
40
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
37
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
38
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
39
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
40
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
41
+ * hard-coded further down. */
41
42
  var TEXT = {
42
43
  selected: '{n} selected'
43
44
  };
@@ -38,10 +38,11 @@
38
38
  badInput: 'type',
39
39
  customError: 'mismatch'
40
40
  };
41
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
42
- * path, and overridable at three levels, narrowest first: `data-fdy-msg-<alias>` on the field,
43
- * `data-fdy-msg` on the field, then `data-fdy-text-<alias>` on the FORM. The form level is what
44
- * a host in another language needs: it sets nine messages once instead of on every input.
41
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
42
+ * enhancer path, and overridable at three levels, narrowest first: `data-fdy-msg-<alias>` on
43
+ * the field, `data-fdy-msg` on the field, then `data-fdy-text-<alias>` on the FORM. The form
44
+ * level is what a host in another language needs: it sets nine messages once instead of on
45
+ * every input.
45
46
  * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
46
47
  var TEXT = {
47
48
  required: 'Required.',
@@ -51,10 +51,11 @@
51
51
  }
52
52
 
53
53
 
54
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
55
- * path, and every one overridable per element, so a host that speaks another language (the
56
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
57
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
54
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
55
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
56
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
57
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
58
+ * hard-coded further down. */
58
59
  var TEXT = {
59
60
  show: 'Show password',
60
61
  hide: 'Hide password'
@@ -19,10 +19,11 @@
19
19
  var CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>';
20
20
 
21
21
 
22
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
23
- * path, and every one overridable per element, so a host that speaks another language (the
24
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
25
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
22
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
23
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
24
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
25
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
26
+ * hard-coded further down. */
26
27
  var TEXT = {
27
28
  done: 'Done',
28
29
  next: 'Next'
@@ -34,10 +34,11 @@
34
34
  }
35
35
 
36
36
 
37
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
38
- * path, and every one overridable per element, so a host that speaks another language (the
39
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
40
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
37
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
38
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
39
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
40
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
41
+ * hard-coded further down. */
41
42
  var TEXT = {
42
43
  prev: 'Previous',
43
44
  next: 'Next',
@@ -77,10 +77,11 @@
77
77
  }
78
78
 
79
79
 
80
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
81
- * path, and every one overridable per element, so a host that speaks another language (the
82
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
83
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
80
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
81
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
82
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
83
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
84
+ * hard-coded further down. */
84
85
  var TEXT = {
85
86
  close: 'Close'
86
87
  };
@@ -51,10 +51,11 @@
51
51
  }
52
52
 
53
53
 
54
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
55
- * path, and every one overridable per element, so a host that speaks another language (the
56
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
57
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
54
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
55
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
56
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
57
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
58
+ * hard-coded further down. */
58
59
  var TEXT = {
59
60
  remove: 'Remove {name}',
60
61
  progress: 'Upload progress for {name}',
package/dist/freeday.js CHANGED
@@ -419,10 +419,11 @@
419
419
  'use strict';
420
420
 
421
421
 
422
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
423
- * path, and every one overridable per element, so a host that speaks another language (the
424
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
425
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
422
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
423
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
424
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
425
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
426
+ * hard-coded further down. */
426
427
  var TEXT = {
427
428
  position: '{n} of {total}',
428
429
  slide: 'Slide {n}'
@@ -596,10 +597,11 @@
596
597
  }
597
598
 
598
599
 
599
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
600
- * path, and every one overridable per element, so a host that speaks another language (the
601
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
602
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
600
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
601
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
602
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
603
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
604
+ * hard-coded further down. */
603
605
  var TEXT = {
604
606
  back: 'Back one level',
605
607
  submenu: '{label}, submenu'
@@ -862,10 +864,11 @@
862
864
  }
863
865
 
864
866
 
865
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
866
- * path, and every one overridable per element, so a host that speaks another language (the
867
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
868
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
867
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
868
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
869
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
870
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
871
+ * hard-coded further down. */
869
872
  var TEXT = {
870
873
  selected: '{n} selected'
871
874
  };
@@ -2367,10 +2370,11 @@
2367
2370
  badInput: 'type',
2368
2371
  customError: 'mismatch'
2369
2372
  };
2370
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
2371
- * path, and overridable at three levels, narrowest first: `data-fdy-msg-<alias>` on the field,
2372
- * `data-fdy-msg` on the field, then `data-fdy-text-<alias>` on the FORM. The form level is what
2373
- * a host in another language needs: it sets nine messages once instead of on every input.
2373
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
2374
+ * enhancer path, and overridable at three levels, narrowest first: `data-fdy-msg-<alias>` on
2375
+ * the field, `data-fdy-msg` on the field, then `data-fdy-text-<alias>` on the FORM. The form
2376
+ * level is what a host in another language needs: it sets nine messages once instead of on
2377
+ * every input.
2374
2378
  * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
2375
2379
  var TEXT = {
2376
2380
  required: 'Required.',
@@ -2581,10 +2585,11 @@
2581
2585
  }
2582
2586
 
2583
2587
 
2584
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
2585
- * path, and every one overridable per element, so a host that speaks another language (the
2586
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
2587
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
2588
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
2589
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
2590
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
2591
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
2592
+ * hard-coded further down. */
2588
2593
  var TEXT = {
2589
2594
  show: 'Show password',
2590
2595
  hide: 'Hide password'
@@ -3272,10 +3277,11 @@
3272
3277
  var CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>';
3273
3278
 
3274
3279
 
3275
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
3276
- * path, and every one overridable per element, so a host that speaks another language (the
3277
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
3278
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
3280
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
3281
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
3282
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
3283
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
3284
+ * hard-coded further down. */
3279
3285
  var TEXT = {
3280
3286
  done: 'Done',
3281
3287
  next: 'Next'
@@ -3407,10 +3413,11 @@
3407
3413
  }
3408
3414
 
3409
3415
 
3410
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
3411
- * path, and every one overridable per element, so a host that speaks another language (the
3412
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
3413
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
3416
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
3417
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
3418
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
3419
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
3420
+ * hard-coded further down. */
3414
3421
  var TEXT = {
3415
3422
  prev: 'Previous',
3416
3423
  next: 'Next',
@@ -4238,10 +4245,11 @@
4238
4245
  }
4239
4246
 
4240
4247
 
4241
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
4242
- * path, and every one overridable per element, so a host that speaks another language (the
4243
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
4244
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
4248
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
4249
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
4250
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
4251
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
4252
+ * hard-coded further down. */
4245
4253
  var TEXT = {
4246
4254
  close: 'Close'
4247
4255
  };
@@ -4472,10 +4480,11 @@
4472
4480
  }
4473
4481
 
4474
4482
 
4475
- /* User-facing strings. Indonesian by default, documented and deliberate for the raw enhancer
4476
- * path, and every one overridable per element, so a host that speaks another language (the
4477
- * Blazor adapters, an English app on the raw path) supplies its own without forking this file.
4478
- * Keeping them in ONE table is also what lets a guard prove none is hard-coded further down. */
4483
+ /* User-facing strings. English by default since 2.0.0, documented and deliberate for the raw
4484
+ * enhancer path, and every one overridable per element with `data-fdy-text-<key>`, so a host
4485
+ * that speaks another language (an Indonesian app on the raw path) supplies its own without
4486
+ * forking this file. Keeping them in ONE table is also what lets a guard prove none is
4487
+ * hard-coded further down. */
4479
4488
  var TEXT = {
4480
4489
  remove: 'Remove {name}',
4481
4490
  progress: 'Upload progress for {name}',
@@ -20,21 +20,21 @@ the **root of the consuming project**:
20
20
 
21
21
  All UI in this project is built from Freeday: a **token-driven CSS kit** (`fdy-*` classes on plain
22
22
  markup) with **typed components for Vue, React and Blazor** layered on top. Most of the kit is
23
- markup + classes; ten interactive components also ship a typed wrapper, and in those three stacks
23
+ markup + classes; eleven interactive components also ship a typed wrapper, and in those three stacks
24
24
  the wrapper is the correct way to use them.
25
25
 
26
26
  **0. First decide which entry point this project uses. This is not an optimisation. Get it wrong
27
27
  and the code looks correct and fails later.**
28
28
 
29
- | This project's stack | Import the ten components from | Binding |
29
+ | This project's stack | Import the eleven components from | Binding |
30
30
  |---|---|---|
31
31
  | Vue 3 | `@cahyo-dimas/freeday/vue` | `v-model` |
32
32
  | React 18/19 | `@cahyo-dimas/freeday/react` | `value` + `onChange` |
33
33
  | Blazor (net8.0) | `@using Freeday.Blazor` (RCL) | `@bind-Value` |
34
34
  | Static HTML, Svelte, server-rendered templates… | no wrapper: raw markup + the enhancer script | `fdy-*` DOM events |
35
35
 
36
- The ten: **FdyCombo · FdyDatepicker · FdyDateRange · FdyAutocomplete · FdyCascade · FdyCfl ·
37
- FdyChart · FdyTable · FdyModal · FdyDrawer**. In Vue/React/Blazor, **never hand-write the raw
36
+ The eleven: **FdyCombo · FdyDatepicker · FdyDateRange · FdyAutocomplete · FdyCascade · FdyCfl ·
37
+ FdyChart · FdyTable · FdyModal · FdyDrawer · FdyAppShell**. In Vue/React/Blazor, **never hand-write the raw
38
38
  markup + enhancer for these eleven.** The raw path *appears* to work: the enhancer auto-initialises
39
39
  once on `DOMContentLoaded` and the first render is correct. Then it fails quietly: DOM your framework
40
40
  renders later is never hydrated, and the widget's state lives in the DOM instead of in your
@@ -75,6 +75,9 @@ wrapper, use the raw markup and hydrate it:
75
75
  components **without** a typed wrapper. For the eleven in step 0, use the wrapper instead.
76
76
  8. Freeday owns components + tokens, **not layout**. Grids/stacks/one-off gaps come from our own
77
77
  layout layer. Build its theme on `var(--space-N)` so both systems stay in step.
78
+ 9. The enhancers render their own English UI strings (pager, filter dialog, validation). Never
79
+ hand-translate one by rewriting the enhancer's nodes — set `data-fdy-text-<key>` on the
80
+ component root, and `<html lang>` for date names. Keys are in COMPONENTS.md.
78
81
  ```
79
82
 
80
83
  Adjust the paths if the package lives somewhere else (a workspace, a vendored copy, `wwwroot/` for
@@ -85,7 +88,7 @@ Blazor). Then verify the agent can actually read those files. An agent that can'
85
88
 
86
89
  | File | What it answers |
87
90
  |---|---|
88
- | `COMPONENTS.md` | The complete class surface: what exists, its modifiers, minimal markup, a11y. |
91
+ | `COMPONENTS.md` | The complete class surface: what exists, its modifiers, minimal markup, a11y. Also the typed wrappers' props — one `### Props — <FdyX>` table each, and the `FdyTableColumn` fields. |
89
92
  | `USAGE.md` | The doctrine: which token/role/shadow/emphasis to use when. |
90
93
  | `docs/getting-started.md` | Install + import + theme, per stack (Static HTML · Vue · React · Blazor). |
91
94
  | `docs/integrations.md` | How to bridge third-party libraries (validation, charts, dates, i18n…). |
@@ -95,7 +98,7 @@ Blazor). Then verify the agent can actually read those files. An agent that can'
95
98
  | `dist/` | Built CSS + enhancers. **`freeday.bundle.css` = tokens + components** (what `@cahyo-dimas/freeday/css` resolves to); `freeday.css` is components **only**, `freeday.tokens.css` tokens only, so linking `freeday.css` alone leaves every `var(--…)` unresolved. Plus `freeday-*.js` and the `.d.ts` files. |
96
99
  | `src/components/*.css` | The authoritative source for every class, when a doc is ambiguous. |
97
100
  | `tokens/tokens.json` | Every token in W3C DTCG format, machine-readable. |
98
- | `adapters/vue` · `adapters/react` · `adapters/blazor` | Typed wrappers, 10 components each. |
101
+ | `adapters/vue` · `adapters/react` · `adapters/blazor` | Typed wrappers, 11 components each (plus `FdyTableFooter`). |
99
102
 
100
103
  The live docs (with an interactive playground) are at
101
104
  <https://cahyo-dimas.github.io/freeday-ui-kit/>, and the repo (including three complete example
@@ -23,7 +23,7 @@ Freeday = **CSS** (semantic tokens + `fdy-*` classes) + **zero-dependency JS enh
23
23
  table: [`integrations.md` §Event & API contract](integrations.md).
24
24
  **On Vue, React or Blazor this is not the path to take for eleven of the components.** `FdyCombo`,
25
25
  `FdyDatepicker`, `FdyDateRange`, `FdyAutocomplete`, `FdyCascade`, `FdyCfl`, `FdyChart`,
26
- `FdyTable`, `FdyModal`, `FdyDrawer` ship typed wrappers that own the state properly (Vue and
26
+ `FdyTable`, `FdyModal`, `FdyDrawer`, `FdyAppShell` ship typed wrappers that own the state properly (Vue and
27
27
  React re-implement the interaction natively; Blazor wraps the enhancer over interop). Use them;
28
28
  the raw path is for the components without a wrapper, and for stacks without an adapter.
29
29
  3. **Hydrate dynamic DOM.** Enhancers auto-init once on `DOMContentLoaded`. DOM an SPA renders
@@ -65,6 +65,13 @@ Freeday = **CSS** (semantic tokens + `fdy-*` classes) + **zero-dependency JS enh
65
65
  `.fdy-page-section`, `.fdy-toolbar`, `.fdy-stats`/`.fdy-stat`) and the type roles (`.fdy-title-page`
66
66
  / `-section` / `-card`), not by re-using `.fdy-card__title` for everything. **Which token/role/shadow
67
67
  to use when lives in [`USAGE.md`](../USAGE.md)**. Read it once; it's what makes screens cohere.
68
+ 9. **The enhancers' UI strings are English, and replaceable per element.** Pager labels, column
69
+ filter dialogs, upload and validation messages come from the enhancer, not from your markup.
70
+ Override any of them with **`data-fdy-text-<key>`** on the component's root — key list and the
71
+ `{n}`/`{from}`/`{total}` placeholders are in [`COMPONENTS.md`](../COMPONENTS.md) §Data table.
72
+ Dates need no attribute: the pickers format through `Intl` from the page's `<html lang>`, so
73
+ `lang="id"` gives Indonesian month and weekday names. **Coming from 1.x**, where those defaults
74
+ were Indonesian: 2.0.0 flipped them to English, and this attribute is the whole migration.
68
75
 
69
76
  ---
70
77
 
@@ -184,7 +191,7 @@ live docs also have a copy button per component.
184
191
  ```bash
185
192
  npm i @cahyo-dimas/freeday
186
193
  ```
187
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^2.0.0"` (public npm package). `dist/` is
194
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^2.1.0"` (public npm package). `dist/` is
188
195
  committed and published → no build step; `npm ci` runs without auth.
189
196
 
190
197
  ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
@@ -300,10 +307,11 @@ export function Panel() {
300
307
  value from `event.detail` in state/ref; don't set the DOM `value` back. `StrictMode` mounts twice in
301
308
  dev; `useFreeday` is idempotent, so it's safe.
302
309
 
303
- ### 5. Alternative: typed controlled components (`FdyCombo` · `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart`)
310
+ ### 5. Alternative: typed controlled components
304
311
  For fields you'd normally write as a native `<select>`/`<input type="date">`,
305
312
  `@cahyo-dimas/freeday/react` also exports typed **controlled** components with plain `value`/`onChange`,
306
- no manual event bubbling (parity with the Vue `v-model` components above):
313
+ no manual event bubbling — the same eleven listed in §Core concepts #2, at parity with the Vue
314
+ `v-model` components above:
307
315
  ```tsx
308
316
  import { FdyCombo } from '@cahyo-dimas/freeday/react';
309
317
  import type { FdyComboOption } from '@cahyo-dimas/freeday/react';
@@ -436,11 +444,12 @@ kit's CSS/enhancers), then bind:
436
444
  <FdyChart Type="donut" Values="_byCity" Labels="_cityLabels" AriaLabel="Revenue by city" />
437
445
  <FdyDrawer @bind-Open="_drawerOpen" Title="Detail" Side="right">…</FdyDrawer>
438
446
  ```
439
- Ten components at parity with the Vue/React adapters: **`FdyModal`** · **`FdyDrawer`** (`@bind-Open`,
447
+ Eleven components at parity with the Vue/React adapters: **`FdyModal`** · **`FdyDrawer`** (`@bind-Open`,
440
448
  `Title`, `Size`/`Side`, `Dismissible`) · **`FdyCombo<TValue>`** · **`FdyDatepicker`** ·
441
449
  **`FdyAutocomplete`** · **`FdyCascade`** · **`FdyDateRange`** (`@bind-From`/`@bind-To`) ·
442
450
  **`FdyCfl<TRow>`** (async `LoadPage`) · **`FdyChart`** · **`FdyTable<TRow>`** (client sort/filter/page,
443
- or controlled `Sort`/`Filters`/`Page` for a server-paged table; `RowActivatable`, `RowDetail`). Each
451
+ or controlled `Sort`/`Filters`/`Page` for a server-paged table; `RowActivatable`, `RowDetail`) ·
452
+ **`FdyAppShell`** (`@bind-NavOpen`). Each
444
453
  `select`-type control also takes `Disabled`/`Readonly`/`Invalid`. The RCL targets **net8.0** and is
445
454
  consumed as source (`<ProjectReference>`); `.NET bin/obj` never ships in the npm tarball.
446
455
 
@@ -154,7 +154,7 @@ function Panel(): JSX.Element {
154
154
  ```
155
155
  > **Controlled alternative (parity with Vue's `v-model`):** `@cahyo-dimas/freeday/react` also exports
156
156
  > the typed components `FdyCombo` / `FdyDatepicker` / `FdyDateRange` / `FdyAutocomplete` /
157
- > `FdyCascade` / `FdyCfl` / `FdyChart` / `FdyTable` / `FdyModal` / `FdyDrawer`, with plain
157
+ > `FdyCascade` / `FdyCfl` / `FdyChart` / `FdyTable` / `FdyModal` / `FdyDrawer` / `FdyAppShell`, with plain
158
158
  > `value`/`onChange` (props in, events out), no `data-fdy-*` + manual event listener:
159
159
  > ```tsx
160
160
  > import { FdyCombo } from '@cahyo-dimas/freeday/react';
@@ -200,7 +200,7 @@ document.addEventListener('fdy-form-invalid', (e) =>
200
200
  > **Native components (parity with Vue's `v-model` / React's `value`/`onChange`):** the
201
201
  > **`Freeday.Blazor`** RCL (net8.0, `adapters/blazor/`) ships the typed components `FdyCombo<TValue>` /
202
202
  > `FdyDatepicker` / `FdyDateRange` / `FdyAutocomplete` / `FdyCascade` / `FdyCfl<TRow>` / `FdyChart` /
203
- > `FdyTable<TRow>` / `FdyModal` / `FdyDrawer` with `@bind`, no `@ref` / manual `initAll` / `[JSInvokable]`:
203
+ > `FdyTable<TRow>` / `FdyModal` / `FdyDrawer` / `FdyAppShell` with `@bind`, no `@ref` / manual `initAll` / `[JSInvokable]`:
204
204
  > ```razor
205
205
  > @using Freeday.Blazor
206
206
  > <FdyCombo TValue="string" @bind-Value="_status" Options="_statusOptions" AriaLabelledby="lbl-status" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",