@cahyo-dimas/freeday 1.20.0 → 1.22.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,139 @@
3
3
  Semua perubahan penting dicatat di sini. Format longgar mengikuti
4
4
  [Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
5
5
 
6
+ ## [1.22.0] — 2026-08-12
7
+ Two bodies of work. **(a)** three more findings from consumption round 5 — the report grew §5-§7
8
+ after 1.21.0 was cut; **(b)** a **routing** failure found in the same adopted project, which had been
9
+ built on raw markup + enhancers inside a framework that has typed wrappers because nothing in the
10
+ package ever told it otherwise. (b) was prepared as 1.21.1 and folded in here rather than shipped as
11
+ a separate patch minutes earlier.
12
+ ### Added — from consumption round 5 (§5, §6)
13
+ - **`.fdy-nav--horizontal`** — the same navigation links laid out as a **row**, for a top-nav
14
+ application that puts its primary nav in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar.
15
+ Deliberately a modifier, not a new block: the item, its states and `aria-current="page"` are
16
+ unchanged. On `.fdy-appbar--primary` the links go on-colour automatically (full on-colour ink in
17
+ every state; the current page is carried by the background wash plus the semibold weight the base
18
+ rule already applies, so no dimmed variant trades away the bar's guaranteed contrast).
19
+ Reported as "there is no horizontal navigation component": `.fdy-nav` was the sidebar's vertical
20
+ menu and `.fdy-appbar` had `__brand`/`__spacer`/`__actions` but nothing to put *links* in — so a
21
+ top-nav app had a component for the bar and none for what the bar is mostly made of.
22
+ - **`.fdy-tabs__tab` also honours `aria-current="page"`**, so the tab *look* legitimately serves
23
+ **routed** sub-navigation (`/settings/profile` · `/settings/billing`) built from real links. A link
24
+ cannot be `aria-selected` — that is invalid ARIA on an anchor — so the app that borrowed the class
25
+ had to restate the active style for a second attribute. Keep `role="tab"` + `freeday-tabs.js` for
26
+ **in-page** tabs, where the roving-tabindex / one-panel-per-tab contract is real.
27
+ - **Pressed state for toggle buttons** — `aria-pressed="true"` now styles: soft primary fill + strong
28
+ ink on `--ghost`/`--text`, an inverted-gradient sunken look on the solid button, and the same
29
+ treatment in the danger hue. No new class or markup contract, since `aria-pressed` is already the
30
+ correct attribute — and it is what turns `.fdy-btn-group` from a joined row into a **complete**
31
+ segmented control. Previously "which one am I on" was left to each app to hand-tint, which is a
32
+ colour decision the kit should own.
33
+ *Found while building it:* one shared `.fdy-btn[aria-pressed="true"]` rule **cannot** work. A
34
+ gradient is a background-**image**, so the `background` shorthand resets `background-color` to
35
+ transparent — at equal specificity the later rule won and the ghost segment lost its fill entirely.
36
+ Each fill variant now defines its own pressed look. The CSS read correctly in the file; only a real
37
+ engine showed it, which is why the new guard lives in `browser/`.
38
+ ### Fixed — from consumption round 5 (§7)
39
+ - **Hard rule 1 in `COMPONENTS.md` now records its one exception.** The rule says a modifier is
40
+ always written beside its block class, but `.fdy-input-group__addon--icon` is standalone by design
41
+ — adding the base `__addon` gives a search glyph the grey fill and divider of a `Rp` / `%` prefix.
42
+ An agent following the rule literally produced the wrong control; one following the CSS comment
43
+ broke the stated rule. The exception is now written down, and it is the only one.
44
+ ### Fixed — the package now routes by stack
45
+ - **The paste-block in `docs/agent-onboarding.md` never mentioned the adapters.** That block is the
46
+ only text that lands in a consuming project's agent instructions, so it is the only text an agent
47
+ re-reads on every task — and it opened with "a CSS kit + enhancers, **not a component framework**",
48
+ which actively steers a Vue/React project away from the typed components. It now opens with a
49
+ **step 0 routing table** (Vue → `/vue` · React → `/react` · Blazor → RCL · everything else → raw
50
+ markup), names the ten wrapped components, and states the failure mode: the raw path renders
51
+ correctly the first time and then silently stops hydrating.
52
+ - **`COMPONENTS.md` flags the wrapper at each of the ten sections**, so the choice is visible at the
53
+ point of use rather than only on an onboarding page read once — with each component's *real*
54
+ binding (`v-model` / `value`+`onChange` / `@bind-Value` for the six value-bound ones; `open`+`close`
55
+ for modal and drawer; data props for chart; `columns`+`rows`+events for table). Hard rule 7 states
56
+ it up front.
57
+ - **Both READMEs lead with the routing table**, above the first import block. Previously the first
58
+ runnable snippet was the vanilla one and the adapter table sat ~130 lines below it.
59
+ - **`docs/getting-started.md`** core concept 2 ("the enhancer is the source of truth") is now scoped
60
+ to the raw path, and notes that Vue/React re-implement the ten natively while Blazor wraps the
61
+ enhancer over interop.
62
+ ### Notes
63
+ - Root cause was structural, not editorial: at **v1.18.0** `docs/getting-started.md` — the only
64
+ per-stack router the kit had — was **not in `files`**, so `npm i` delivered a README whose
65
+ "Starting a project? per stack" link pointed at a path that does not exist inside `node_modules`.
66
+ `adapters/` *was* shipped, so the wrappers sat in the install, unused, with nothing pointing at
67
+ them. v1.20.0 started shipping the docs; this release makes them route.
68
+
69
+ ## [1.21.0] — 2026-08-12
70
+ Fifth round of real-app consumption feedback, written while that app adopted 1.20.0. Two findings —
71
+ and a **withdrawal**: the reporter retracted 1.20's rejected §A themselves after isolating the real
72
+ mechanism, which turned out to be a genuine kit bug hiding behind it. Both halves of that rejection
73
+ reproduce here, so the rejection stands and the withdrawal is recorded rather than re-litigated.
74
+ ### Fixed
75
+ - **Hidden labels could scroll the whole page.** `.fdy-visually-hidden` is `position:absolute`, and
76
+ `clip` hides *painting*, not *layout*. With no positioned ancestor its containing block is the
77
+ document, and `overflow` clips only what is contained inside the overflow box — so a hidden label in
78
+ a horizontally scrolling table (the kit's own recommended way to name an icon button) parks at its
79
+ static position and drags the document sideways. Measured in Chrome: **1351px** of phantom page
80
+ scroll from 11 spans in one table; `overflow-x:hidden` on the scroller, the shell, `body` and `html`
81
+ each changed it by **0px**; removing the spans took it to 0. Every clipping/scrolling container that
82
+ holds consumer markup now declares `position:relative` — `.fdy-table-scroll`, `.fdy-table-wrap`,
83
+ `.fdy-list`, `.fdy-card`, `.fdy-tabs__list`, `.fdy-carousel__viewport`, `.fdy-accordion` — each
84
+ measured to take its own case to 0 with the scroller still scrolling internally. The rest are
85
+ already inside something the kit positions (`<dialog>`, the sticky sidebar, a fixed popover) and are
86
+ listed with that reason in the new test.
87
+ **`.fdy-accordion` is the one to note:** it was contained only by its panel's reveal *animation*
88
+ (a transform makes a containing block), and that animation sits behind
89
+ `prefers-reduced-motion: no-preference` — so the bug was reachable **only** by readers who asked for
90
+ reduced motion. Measured with the animation off: 2906px → 0.
91
+ *Possible migration:* if you absolutely positioned something inside a card, list or table scroller
92
+ and relied on it escaping, it now anchors to that container. Anchoring is the reason for the change.
93
+ - **`--button` rows and cards ignored `:disabled`.** `.fdy-list__row--button` adopts the UA button box
94
+ but not its disabled state, so a row disabled mid-flight kept its hover tint and pointer cursor — a
95
+ control answering the pointer while refusing input. `:disabled` and `[aria-disabled="true"]` now dim
96
+ it and withdraw hover, for `.fdy-list__row--button`, `.fdy-list__row--interactive`,
97
+ `.fdy-card--button` and `.fdy-card--interactive`. The report suggested `cursor:default`; the kit uses
98
+ `opacity:.5` + `cursor:not-allowed` on every other disabled control, and consistency wins.
99
+ ### Changed
100
+ - **`data-theme` is no longer root-scoped** — the same move density made in 1.20.0, and for the same
101
+ reason. The two *explicit* selectors are now bare `[data-theme="dark"]` / `[data-theme="light"]`, so
102
+ a `<section data-theme="dark">` inverts that region and **every component inside it follows** —
103
+ card surfaces, inputs, and text roles like `.fdy-title-page` that set `color: var(--color-text)`
104
+ explicitly and therefore never saw a consumer's hand-rolled override. A dark brand panel beside a
105
+ light sign-in form is an ordinary layout; it should not cost a re-colouring pass. Setting the
106
+ attribute on `<html>` is unchanged, and a `[data-theme="light"]` island nested inside a dark region
107
+ wins in turn.
108
+ **The system default stays root-scoped, deliberately** — the report asked for "the two generated
109
+ selectors", but there are three. Un-rooting `@media (prefers-color-scheme: dark) { :root:not(...) }`
110
+ would make it match every element that does not itself carry `data-theme="light"`, including the
111
+ *children* of a light island, dragging them back to dark. Measured: with the un-rooted variant that
112
+ island renders light ink on a light surface; root-scoped, it stays correct.
113
+ ### Added — guards
114
+ - **`test/css.test.mjs`** — the containment invariant, CI-gated: a rule that declares `overflow` must
115
+ also be positioned, or be listed with the ancestor that already contains it. Single-line truncation
116
+ (`text-overflow:ellipsis` on a label) is excluded by shape, not by name, so new truncating labels
117
+ don't accumulate in an allowlist. A new clipping container fails the test until someone decides
118
+ which case it is.
119
+ - **`browser/layout.mjs`** — the same bug end-to-end in real Chrome (`npm run test:browser`): ten
120
+ hidden labels in a wide table, asserting the page cannot scroll horizontally *and* that the scroller
121
+ still scrolls. A static test cannot see this failure; only a layout engine can. Both guards
122
+ mutation-checked — reverting any single `position:relative` fails them.
123
+ - **`browser/state.mjs`** — pressed toggles and nav orientation in a real engine: the pressed ghost
124
+ segment must keep a flat background-colour (the mutation that reproduces the shorthand bug fails
125
+ it), the solid toggle must read as inset, `--horizontal` must actually be a row with the current
126
+ link marked, on-colour nav ink must stay on-colour in both states, and a routed tab must take its
127
+ underline from `aria-current`. Mutation-checked against all three fixes.
128
+ - **`browser/theme.mjs`** — subtree theming end-to-end: a `.fdy-title-page` and a `.fdy-card` inside
129
+ `<section data-theme="dark">` take the dark tokens, a nested light island goes back, and
130
+ `data-theme` on `<html>` still themes everything. `test/build.test.mjs` guards the selector shape
131
+ (not root-scoped, media block still is, and the block order the cascade depends on); this guards the
132
+ behaviour that shape exists for. Mutation-checked: re-rooting the selectors fails it.
133
+ ### Notes
134
+ - Not adopted from the report, deliberately: `.fdy-datatable`, `.fdy-modal__body`, `.fdy-drawer__body`
135
+ were listed as needing the same fix. Measured: they do not escape — `<dialog>` is `position:fixed`
136
+ and is already their containing block, and the datatable's own scrolling child now carries it. They
137
+ are in the test's allowlist with that reason instead of carrying a declaration that does nothing.
138
+
6
139
  ## [1.20.0] — 2026-08-12
7
140
  Two bodies of work in one release (1.19.0 was prepared but never committed, tagged or published, so
8
141
  it is folded in here rather than left as a phantom version):
package/COMPONENTS.md CHANGED
@@ -15,6 +15,10 @@ live docs.
15
15
  1. **Prefix is `fdy-`, pattern is compact BEM**: block `fdy-card`, element `fdy-card__title`,
16
16
  modifier `fdy-card--elevated`. A modifier is **always** written alongside its block class:
17
17
  `class="fdy-btn fdy-btn--ghost"`, never `fdy-btn--ghost` alone.
18
+ **One documented exception:** `.fdy-input-group__addon--icon` is **standalone** — it is a
19
+ borderless leading glyph, and adding the base `.fdy-input-group__addon` would give it the grey
20
+ fill and divider of a `Rp` / `%` prefix, which is not what a search icon is. Written
21
+ `class="fdy-input-group__addon--icon"`, alone. It is the only one; everywhere else the rule holds.
18
22
  2. **Never invent a class.** If it is not in this file, it does not exist. Freeday has no
19
23
  `fdy-grid`, no `fdy-flex`, no `fdy-mt-4`, no `fdy-primary`.
20
24
  3. **Never write a raw hex or px value** in app CSS. Use the tokens — `var(--color-primary)`,
@@ -30,6 +34,33 @@ live docs.
30
34
  invalid state does not belong to the control itself.
31
35
  6. **Interactive components need their enhancer script.** Static ones (button, card, badge, table,
32
36
  alert, breadcrumb, timeline, accordion, tree-without-cascade…) are CSS-only.
37
+ 7. **On Vue, React or Blazor, ten components have a typed wrapper — use it.** `FdyCombo` ·
38
+ `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` ·
39
+ `FdyTable` · `FdyModal` · `FdyDrawer`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
40
+ `Freeday.Blazor` RCL. Each is flagged at its own section below. Hand-writing their raw markup in
41
+ those stacks *looks* right — the enhancer initialises once and the first render is correct — then
42
+ breaks quietly: later-rendered DOM is never hydrated and the widget's state sits in the DOM
43
+ instead of your framework's. Everything else is the same markup everywhere; hydrate the other
44
+ interactive components with `useFreeday` (Vue/React) or `FreedayBlazor.initAll` (Blazor).
45
+
46
+ ### Containment — why the kit's containers are `position:relative`
47
+
48
+ `.fdy-visually-hidden` is `position:absolute`, and `clip` hides **painting**, not **layout**. An
49
+ absolutely positioned box resolves against its nearest *positioned* ancestor, and `overflow` clips
50
+ only what is contained that way — so in an unpositioned scroller, a hidden label parks at its static
51
+ position (possibly thousands of px to the right) and drags the **whole document** sideways. It is
52
+ invisible in the DOM and immune to `overflow-x: hidden` on every wrapper.
53
+
54
+ Every kit container that clips or scrolls therefore declares `position: relative` —
55
+ `.fdy-table-scroll`, `.fdy-table-wrap`, `.fdy-list`, `.fdy-card`, `.fdy-tabs__list`,
56
+ `.fdy-carousel__viewport`, `.fdy-accordion` (the rest are already inside a positioned ancestor).
57
+ Two consequences for you:
58
+
59
+ - **Do the same in your own scrollers.** A container with `overflow` that holds arbitrary markup
60
+ needs `position: relative`, or the hidden labels *you* write will escape it.
61
+ - **Diagnose it correctly.** `document.documentElement.scrollWidth` sees the escaped box;
62
+ `document.body.scrollWidth` does not. The honest check is `window.scrollTo(9999, 0)` then reading
63
+ `window.scrollX`.
33
64
 
34
65
  ## Enhancers — hook, script, global, events
35
66
 
@@ -208,6 +239,10 @@ rest of the app is not.
208
239
  - Modifiers: `--ghost` `--danger` `--text` `--icon` (square, icon-only) `--sm` `--lg`
209
240
  - Parts: `.fdy-btn__icon` (wraps the `<svg>`) · `.fdy-btn-group` (segmented row, `role="group"` +
210
241
  `aria-label`)
242
+ - **Toggle / segmented:** `aria-pressed="true"` gives a real pressed state (soft primary fill on
243
+ `--ghost`/`--text`, an inset-sunk gradient on the solid button). No extra class — `aria-pressed`
244
+ is already the right attribute, and it is what turns `.fdy-btn-group` from a joined row into a
245
+ complete segmented control.
211
246
  - A11y: `--icon` **requires** `aria-label`. Use `<button type="button">` unless it submits.
212
247
 
213
248
  ```html
@@ -293,6 +328,8 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
293
328
  ```
294
329
 
295
330
  ## Select / combobox — `.fdy-combo`
331
+ > **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).
332
+
296
333
  Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
297
334
 
298
335
  - `.fdy-combo` (+`--error`, `--no-icon`) · `__button` `__value` (+`--placeholder`) `__listbox`
@@ -315,6 +352,8 @@ Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
315
352
  ```
316
353
 
317
354
  ## Autocomplete — `.fdy-autocomplete`
355
+ > **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).
356
+
318
357
  Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
319
358
 
320
359
  - `.fdy-autocomplete` · `__listbox` `__option` `__empty`
@@ -322,6 +361,8 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
322
361
  `aria-controls` → the listbox `id`, `autocomplete="off"`.
323
362
 
324
363
  ## Cascade select — `.fdy-cascade`
364
+ > **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).
365
+
325
366
  Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
326
367
  child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
327
368
 
@@ -341,6 +382,8 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
341
382
  ```
342
383
 
343
384
  ## Choose-from-list (CFL) — `data-fdy-cfl`
385
+ > **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).
386
+
344
387
  A read-only field backed by master data: the button opens a searchable dialog, the picked row fills
345
388
  the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
346
389
 
@@ -391,6 +434,8 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
391
434
  ```
392
435
 
393
436
  ## Date picker — `data-fdy-datepicker`
437
+ > **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).
438
+
394
439
  Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author an empty `<div>`**
395
440
  — the enhancer builds everything.
396
441
 
@@ -398,6 +443,7 @@ Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author
398
443
  `__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` `__dow` `__day`
399
444
  - Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
400
445
  get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start.
446
+ **Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor) — use it in those stacks.
401
447
  - Attributes: `data-value="YYYY-MM-DD"`, `data-placeholder`, `data-label`, `data-fdy-no-icon`
402
448
 
403
449
  ```html
@@ -498,6 +544,8 @@ Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-tab
498
544
  ```
499
545
 
500
546
  ## Data table — `.fdy-datatable`
547
+ > **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.
548
+
501
549
  The interactive table: global search, sort, per-column filters, row selection + bulk bar,
502
550
  pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` + `data-fdy-table`
503
551
  (`data-page-size="N"`).
@@ -537,6 +585,8 @@ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__ic
537
585
  replaces.
538
586
 
539
587
  ## Charts — `data-fdy-chart`
588
+ > **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.
589
+
540
590
  Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`; call
541
591
  `FreedayChart.update(el)` after changing data (or use `FdyChart` in Vue/React/Blazor).
542
592
 
@@ -572,6 +622,10 @@ responsive `.fdy-datatable` should become below `md` — not a stack of `.fdy-ca
572
622
  control (render it as a real `<button>`/`<a>`; the UA box is reset without losing the list surface)
573
623
  - Row internals: `.fdy-list__main` (truncating stack) → `.fdy-list__title` + `.fdy-list__meta`, and
574
624
  `.fdy-list__aside` pinned right
625
+ - **Disabled** — `disabled` on the `<button>` (or `aria-disabled="true"` when the row is an `<a>`/
626
+ `<div>`) dims the row and withdraws the hover tint and the pointer cursor, same as every other
627
+ control in the kit. Do not hand-roll it: an undimmed row that still lights up under the pointer
628
+ reads as clickable while it is refusing input.
575
629
  - Not `.fdy-list-reset` — that utility only strips UA bullets/indent from a semantic list.
576
630
 
577
631
  ```html
@@ -639,18 +693,43 @@ Size-matched placeholders so nothing shifts when data lands: `--title` `--text`
639
693
 
640
694
  # Navigation
641
695
 
642
- ## Nav (sidebar menu) — `.fdy-nav`
643
- Vertical navigation, used by the app shell. Items are `<a class="fdy-nav__item">` with
696
+ ## Nav (menu) — `.fdy-nav`
697
+ Navigation links **vertical by default** (the app shell sidebar), horizontal with `--horizontal`. Items are `<a class="fdy-nav__item">` with
644
698
  `__icon` / `__label` / `__badge`; the current one gets `aria-current="page"`.
645
699
  `--flat` drops the surface. Nested groups are native `<details>`:
646
700
 
647
701
  - `.fdy-nav__tree` + `<summary class="fdy-nav__item">` + `.fdy-nav__caret` → children in
648
702
  `.fdy-nav__sub`
649
703
  - `.fdy-nav__group` + `<summary class="fdy-nav__grouplabel">` → a collapsible section
704
+ - **`--horizontal`** lays the same links out as a row, for a **top-nav application** (primary
705
+ navigation in `.fdy-appbar` or `.fdy-app__topbar`, no sidebar). Same item, same states, same
706
+ `aria-current="page"`; the row scrolls if it runs out of width. On `.fdy-appbar--primary` the
707
+ links go on-colour automatically.
708
+
709
+ ```html
710
+ <header class="fdy-appbar">
711
+ <a class="fdy-appbar__brand" href="/">Acme</a>
712
+ <nav class="fdy-nav fdy-nav--horizontal" aria-label="Main">
713
+ <a class="fdy-nav__item" href="/invoices" aria-current="page">Invoices</a>
714
+ <a class="fdy-nav__item" href="/customers">Customers</a>
715
+ </nav>
716
+ <span class="fdy-appbar__spacer"></span>
717
+ <div class="fdy-appbar__actions"><!-- buttons --></div>
718
+ </header>
719
+ ```
720
+
721
+ > **Which one for navigation?** A nav link is a link marked `aria-current="page"` — never
722
+ > `aria-selected` (invalid ARIA on an anchor) and never `role="tab"`. Use `.fdy-nav--horizontal`
723
+ > for an application's **primary** navigation. For **routed sub-navigation** that should look like
724
+ > tabs (`/settings/profile` · `/settings/billing`), put `.fdy-tabs__list` / `.fdy-tabs__tab` on
725
+ > plain `<a>`s — those classes honour `aria-current="page"` too — and do **not** add the tab roles
726
+ > or `freeday-tabs.js`, which promise a keyboard contract routes do not have.
650
727
 
651
728
  ## App bar — `.fdy-appbar`
652
729
  Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--sticky`
653
730
  `--elevated` `--dense` `--primary` (on-colour controls). Parts `__brand` `__spacer` `__actions`.
731
+ It ships **no link class of its own** — put `<nav class="fdy-nav fdy-nav--horizontal">` in it
732
+ for primary navigation (see Nav above).
654
733
 
655
734
  ## Breadcrumb — `.fdy-breadcrumb`
656
735
  `<nav class="fdy-breadcrumb" aria-label="Breadcrumb">` → `<ol class="fdy-breadcrumb__list">` with
@@ -659,6 +738,8 @@ Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--
659
738
 
660
739
  ## Tabs — `.fdy-tabs`
661
740
  APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
741
+ `.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
742
+ so the same look serves routed sub-navigation built from plain links — see the note under Nav.
662
743
 
663
744
  ```html
664
745
  <div class="fdy-tabs" data-fdy-tabs>
@@ -676,12 +757,16 @@ APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
676
757
  # Overlays
677
758
 
678
759
  ## Modal — `.fdy-modal`
760
+ > **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).
761
+
679
762
  Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
680
763
  `--wide`; `--cfl` for the choose-from-list dialog. Parts `__header` `__title` `__body` `__footer`
681
764
  `__close`. The body scrolls; the footer never clips. `aria-labelledby` → the title's `id`;
682
765
  `data-close` on any button that should close it.
683
766
 
684
767
  ## Drawer — `.fdy-drawer`
768
+ > **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).
769
+
685
770
  Temporary side panel on native `<dialog>` — left by default, `--right` to flip. Parts `__header`
686
771
  `__title` `__body` `__footer` `__close`. Open it from any
687
772
  `<button data-fdy-drawer="<dialog id>">`. Needs `freeday-drawer.js`.
@@ -709,6 +794,10 @@ Parts `__body` `__title` `__desc` `__footer`. Modifiers:
709
794
  the UA button box **without** losing the card surface/border
710
795
  - `.fdy-card--button` never replaces keyboard semantics — a clickable card must be a real
711
796
  `<button>` or `<a>`.
797
+ - **Disabled** — `disabled` (or `aria-disabled="true"`) dims the card and withdraws the pointer
798
+ cursor and the `--interactive` hover-lift.
799
+ - The card is `position:relative`, so a badge or ribbon you absolutely position inside it anchors
800
+ to the card. That is also what keeps hidden labels inside it from escaping — see *Containment*.
712
801
 
713
802
  ## Badge — `.fdy-badge`
714
803
  Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only —
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-v1.20.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.20.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.22.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.22.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`.
@@ -31,6 +31,28 @@ npm test # tes transformasi build + kontras WCAG (node:test)
31
31
 
32
32
  ## Pakai di project
33
33
 
34
+ ### Pertama: pintu masuk mana yang kamu pakai?
35
+
36
+ Sebagian besar Freeday itu markup + kelas `fdy-*` dan sama di mana pun. **Sepuluh komponen
37
+ interaktif juga punya wrapper typed** — di stack berikut, pakai wrapper-nya, bukan markup mentah +
38
+ enhancer:
39
+
40
+ | Stack-mu | Impor kesepuluhnya dari | Binding |
41
+ |---|---|---|
42
+ | **Vue 3** | `@cahyo-dimas/freeday/vue` | `v-model` |
43
+ | **React 18/19** | `@cahyo-dimas/freeday/react` | `value` + `onChange` |
44
+ | **Blazor (net8.0)** | RCL `Freeday.Blazor` | `@bind-Value` |
45
+ | HTML statis · Svelte · template server-rendered | *(tanpa wrapper)* markup mentah + enhancer, di bawah | event `fdy-*` |
46
+
47
+ Kesepuluhnya: `FdyCombo` · `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` ·
48
+ `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer`. Menulis markup mentah untuk
49
+ kesepuluh ini di dalam Vue/React/Blazor *kelihatan* baik-baik saja — render pertama benar — lalu
50
+ rusak diam-diam: DOM yang di-render framework belakangan tak pernah ter-hydrate, dan state widget
51
+ berakhir di DOM alih-alih di state framework-mu. Selebihnya (button, card, badge, alert, markup
52
+ tabel, layout, dan komponen interaktif yang tak punya wrapper) sama persis di semua stack — untuk
53
+ yang terakhir, hydrate dengan `useFreeday` (Vue/React) atau `FreedayBlazor.initAll` (Blazor).
54
+ Panduan lengkap per stack: [`docs/getting-started.md`](docs/getting-started.md).
55
+
34
56
  **Sebagai paket (project dengan bundler — Vue/React/Blazor/Vite):**
35
57
  ```bash
36
58
  npm i @cahyo-dimas/freeday
@@ -121,6 +143,8 @@ container-nya, bukan scroll sendiri.
121
143
 
122
144
  ### 3. Theming — 3 sumbu lewat `data-*` di root
123
145
  - `data-theme="light|dark"` — redefinisi token semantic (bind ke state tema app-mu).
146
+ Berlaku di **ancestor mana pun**, tak harus root: `<section data-theme="dark">` membalik region itu
147
+ dan semua komponen di dalamnya ikut (lihat [`USAGE.md`](USAGE.md) §5b).
124
148
  - `data-density="comfortable|compact"` — tinggi kontrol (`--control-h`) untuk layar padat data.
125
149
  Ini auto-apply hanya ke kontrol bawaan Freeday (button, input, combo, dst); komponen
126
150
  custom/hand-built harus baca `--control-h` sendiri (mis. `height: var(--control-h)`) supaya ikut
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-v1.20.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.20.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.22.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.22.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`.
@@ -31,6 +31,27 @@ npm test # build-transform tests + WCAG contrast (node:test)
31
31
 
32
32
  ## Use in a project
33
33
 
34
+ ### First: which entry point is yours?
35
+
36
+ Most of Freeday is markup + `fdy-*` classes and is identical everywhere. **Ten interactive
37
+ components also ship a typed wrapper** — in these stacks, use the wrapper, not raw markup + enhancer:
38
+
39
+ | Your stack | Import the ten from | Binding |
40
+ |---|---|---|
41
+ | **Vue 3** | `@cahyo-dimas/freeday/vue` | `v-model` |
42
+ | **React 18/19** | `@cahyo-dimas/freeday/react` | `value` + `onChange` |
43
+ | **Blazor (net8.0)** | `Freeday.Blazor` RCL | `@bind-Value` |
44
+ | Static HTML · Svelte · server-rendered templates | *(no wrapper)* raw markup + enhancers, below | `fdy-*` events |
45
+
46
+ The ten: `FdyCombo` · `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` ·
47
+ `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer`. Hand-writing the raw markup for
48
+ these inside Vue/React/Blazor *looks* fine — the first render is correct — and then breaks quietly:
49
+ DOM the framework renders later is never hydrated, and the widget's state ends up in the DOM instead
50
+ of your framework's. Everything else (button, card, badge, alert, table markup, layout, and the
51
+ interactive components with no wrapper) is the same plain markup in every stack — hydrate the latter
52
+ with `useFreeday` (Vue/React) or `FreedayBlazor.initAll` (Blazor). Full walkthrough per stack:
53
+ [`docs/getting-started.md`](docs/getting-started.md).
54
+
34
55
  **As a package (projects with a bundler — Vue/React/Blazor/Vite):**
35
56
  ```bash
36
57
  npm i @cahyo-dimas/freeday
@@ -121,6 +142,8 @@ of scrolling.
121
142
 
122
143
  ### 3. Theming — 3 axes via `data-*` on the root
123
144
  - `data-theme="light|dark"` — redefines the semantic tokens (bind it to your app's theme state).
145
+ Works on **any ancestor**, not just the root: `<section data-theme="dark">` inverts that region
146
+ and every component inside it follows (see [`USAGE.md`](USAGE.md) §5b).
124
147
  - `data-density="comfortable|compact"` — control height (`--control-h`) for data-dense screens.
125
148
  This auto-applies only to Freeday's built-in controls (button, input, combo, etc.);
126
149
  custom/hand-built components must read `--control-h` themselves (e.g. `height: var(--control-h)`)
package/USAGE.md CHANGED
@@ -82,6 +82,27 @@ point, everything around it quiet.
82
82
  - **Surfaces:** most backgrounds are `--color-surface`; `--color-surface-2`/`-3` for a recessed area;
83
83
  `--color-primary-soft` only when you want a tinted callout, not as a default panel colour.
84
84
 
85
+ ## 5b. Theme — global by default, per-subtree when a region is inverted
86
+
87
+ `data-theme="light|dark"` redefines the semantic tokens. Set it on `<html>` and it themes the app;
88
+ that is the normal case and nothing about it has changed.
89
+
90
+ **It is also per-subtree.** The two explicit selectors are bare `[data-theme="dark"]` /
91
+ `[data-theme="light"]`, and semantic tokens are inheriting custom properties — so a
92
+ `<section data-theme="dark">` inverts that region and **every Freeday component inside it follows**:
93
+ card surfaces, inputs, buttons, and text roles like `.fdy-title-page` that set
94
+ `color: var(--color-text)` explicitly. A dark brand panel beside a light sign-in form needs no
95
+ per-element re-colouring, and a `[data-theme="light"]` island nested back inside a dark region wins
96
+ in turn.
97
+
98
+ Do **not** invert a region by hand with `--color-inverse-*` plus `color-mix`. That pair is right for
99
+ a one-off band of your own markup, but it stops scaling the moment the region contains a real
100
+ component — anything that sets its own colour from a token never sees your override, and you end up
101
+ restating colours per element.
102
+
103
+ The **system** default (`@media (prefers-color-scheme: dark)`) stays root-scoped on purpose: that
104
+ rule is about the document, and un-rooting it would re-darken the children of a light island.
105
+
85
106
  ## 6. Density — `compact` for data-dense screens
86
107
 
87
108
  `data-density="compact"` tightens control height **and** the mid-range spacing scale
@@ -94,6 +115,27 @@ container, a single `<section>`. An app whose two list screens are dense and who
94
115
  are not should scope it per screen rather than densifying everything. Set it at one level per screen,
95
116
  never per component.
96
117
 
118
+ ## 6b. Navigation: one component, two orientations — and never a tab role
119
+
120
+ A navigation link is an `<a class="fdy-nav__item">` marked **`aria-current="page"`**. That does not
121
+ change with the layout:
122
+
123
+ - **Sidebar app** — `.fdy-nav` inside `.fdy-app__sidebar` (the default).
124
+ - **Top-nav app** — `.fdy-nav.fdy-nav--horizontal` inside `.fdy-appbar` or `.fdy-app__topbar`, with
125
+ no sidebar. Same items, same states; on `.fdy-appbar--primary` the links go on-colour for you.
126
+ - **Routed sub-navigation that should look like tabs** (`/settings/profile` · `/settings/billing`) —
127
+ put `.fdy-tabs__list` / `.fdy-tabs__tab` on plain `<a>`s. Those classes honour `aria-current="page"`
128
+ as well as `aria-selected="true"` precisely for this. Do **not** add `role="tab"`/`role="tablist"`
129
+ or `freeday-tabs.js`: those promise a roving-tabindex, arrow-key, one-panel-per-tab contract that
130
+ route links do not honour, and `aria-selected` is invalid ARIA on an anchor in the first place.
131
+
132
+ Reserve the full `.fdy-tabs` component (with its roles and its enhancer) for **in-page** tabs, where
133
+ nothing navigates.
134
+
135
+ **Toggles:** a button that is on/off carries `aria-pressed`, and the kit styles it — soft primary
136
+ fill on `--ghost`/`--text`, a sunken gradient on the solid button. A `.fdy-btn-group` of `--ghost`
137
+ buttons with exactly one `aria-pressed="true"` **is** the segmented control; don't hand-tint it.
138
+
97
139
  ## 7. Assemble the page from the frame down
98
140
 
99
141
  1. **Shell:** every application starts inside **`.fdy-app`** — a flex row of `__sidebar` (with