@cahyo-dimas/freeday 1.21.0 → 1.23.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,112 @@
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.23.0] — 2026-08-13
7
+ Consumption round 6 (`improvement-notes/006`). The reporter filed two of the three as **their own**
8
+ bugs rather than the kit's — and they were right about the code, but in both cases the kit had a way
9
+ to make the mistake impossible and had not taken it. Those are the two most valuable findings here.
10
+ ### Fixed
11
+ - **`initAll(ctx)` now enhances `ctx` itself, not just its descendants.** `querySelectorAll` never
12
+ matches its own root, so a framework ref placed **on** the widget — `<div ref="menu" data-fdy-menu>`,
13
+ which is the ordinary shape when a component's root element *is* the widget — meant the one element
14
+ that needed enhancing was the only one that could not be found. It failed with **no error, no
15
+ warning, and a UI that looked finished**: the markup rendered and simply never opened. Measured
16
+ before the fix: `data-fdy-menu-ready` stayed `null` and `aria-expanded` stayed `"false"` while
17
+ `initAll` was present and callable the whole time; `initAll(parent)` worked. One `matches()` line
18
+ per selector, in **all 21 enhancers** (`drawer` and `cfl` needed their inline trigger callbacks
19
+ hoisted first). Init was already idempotent via each widget's `-ready` flag, so a root that is also
20
+ matched by the descendant query costs nothing. `useFreeday` (Vue/React) and `FreedayBlazor.initAll`
21
+ delegate to these, so they inherit the fix with no change of their own.
22
+ - **The stepper connector no longer draws through the markers when `__btn` is omitted.** The lift now
23
+ lives on `.fdy-step__marker` — the part that must always exist — instead of only on
24
+ `.fdy-step__btn`. The connector is `position:absolute; z-index:0`, and a positioned box paints
25
+ *after* in-flow inline content in the same stacking context, so a marker that was merely inline got
26
+ painted over: not for lack of a z-index, but for lack of being positioned at all. An
27
+ indicator-only stepper (no navigation, so no button) is a reasonable thing to write and it produced
28
+ a visible defect in shipped UI. Measured: hit-testing the marker's centre returned the connector's
29
+ own `.fdy-step` before, the marker after. `__btn` keeps its z-index for the navigable case.
30
+ ### Added
31
+ - **`.fdy-avatar--xs`** (1.5rem, `--text-xs`) — an avatar inside a control had nothing to reach for:
32
+ `.fdy-btn--sm` is `calc(var(--control-h) - var(--space-2))` = **2rem**, exactly `--avatar--sm`'s
33
+ size, so the monogram filled a small button edge to edge and the ghost border crossed the circle
34
+ (measured: trigger 32px tall, avatar 32×32). 1.5rem inside 2rem leaves the ~4px the kit's other
35
+ controls give their icons. The report suggested `--text-2xs`; that token does not exist — `--text-xs`
36
+ is the smallest the kit has, and inventing a type step for one avatar size is not worth it.
37
+ ### Added — guards
38
+ - **`browser/root-init.mjs`** — mounts a widget *after* load, initialises it through its own root, and
39
+ drives a **real click** to prove it opens; repeated for a second enhancer so the fix reads as the
40
+ shared pattern rather than a one-off. Mutation-checked on both. Being marked `-ready` is explicitly
41
+ not accepted as passing: an early version of this check reported success against markup the enhancer
42
+ had actually bailed on.
43
+ ### Docs
44
+ - `COMPONENTS.md`: `initAll(root)` documents that the root may be the widget itself; the stepper
45
+ section states that `__btn` is **optional** and shows the read-only indicator markup (the parts list
46
+ never said which parts were optional); avatar gains its size table with the in-control guidance.
47
+ - Both `useFreeday` docstrings now show the ref-on-the-widget shape alongside the wrapping one.
48
+
49
+ ## [1.22.0] — 2026-08-12
50
+ Two bodies of work. **(a)** three more findings from consumption round 5 — the report grew §5-§7
51
+ after 1.21.0 was cut; **(b)** a **routing** failure found in the same adopted project, which had been
52
+ built on raw markup + enhancers inside a framework that has typed wrappers because nothing in the
53
+ package ever told it otherwise. (b) was prepared as 1.21.1 and folded in here rather than shipped as
54
+ a separate patch minutes earlier.
55
+ ### Added — from consumption round 5 (§5, §6)
56
+ - **`.fdy-nav--horizontal`** — the same navigation links laid out as a **row**, for a top-nav
57
+ application that puts its primary nav in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar.
58
+ Deliberately a modifier, not a new block: the item, its states and `aria-current="page"` are
59
+ unchanged. On `.fdy-appbar--primary` the links go on-colour automatically (full on-colour ink in
60
+ every state; the current page is carried by the background wash plus the semibold weight the base
61
+ rule already applies, so no dimmed variant trades away the bar's guaranteed contrast).
62
+ Reported as "there is no horizontal navigation component": `.fdy-nav` was the sidebar's vertical
63
+ menu and `.fdy-appbar` had `__brand`/`__spacer`/`__actions` but nothing to put *links* in — so a
64
+ top-nav app had a component for the bar and none for what the bar is mostly made of.
65
+ - **`.fdy-tabs__tab` also honours `aria-current="page"`**, so the tab *look* legitimately serves
66
+ **routed** sub-navigation (`/settings/profile` · `/settings/billing`) built from real links. A link
67
+ cannot be `aria-selected` — that is invalid ARIA on an anchor — so the app that borrowed the class
68
+ had to restate the active style for a second attribute. Keep `role="tab"` + `freeday-tabs.js` for
69
+ **in-page** tabs, where the roving-tabindex / one-panel-per-tab contract is real.
70
+ - **Pressed state for toggle buttons** — `aria-pressed="true"` now styles: soft primary fill + strong
71
+ ink on `--ghost`/`--text`, an inverted-gradient sunken look on the solid button, and the same
72
+ treatment in the danger hue. No new class or markup contract, since `aria-pressed` is already the
73
+ correct attribute — and it is what turns `.fdy-btn-group` from a joined row into a **complete**
74
+ segmented control. Previously "which one am I on" was left to each app to hand-tint, which is a
75
+ colour decision the kit should own.
76
+ *Found while building it:* one shared `.fdy-btn[aria-pressed="true"]` rule **cannot** work. A
77
+ gradient is a background-**image**, so the `background` shorthand resets `background-color` to
78
+ transparent — at equal specificity the later rule won and the ghost segment lost its fill entirely.
79
+ Each fill variant now defines its own pressed look. The CSS read correctly in the file; only a real
80
+ engine showed it, which is why the new guard lives in `browser/`.
81
+ ### Fixed — from consumption round 5 (§7)
82
+ - **Hard rule 1 in `COMPONENTS.md` now records its one exception.** The rule says a modifier is
83
+ always written beside its block class, but `.fdy-input-group__addon--icon` is standalone by design
84
+ — adding the base `__addon` gives a search glyph the grey fill and divider of a `Rp` / `%` prefix.
85
+ An agent following the rule literally produced the wrong control; one following the CSS comment
86
+ broke the stated rule. The exception is now written down, and it is the only one.
87
+ ### Fixed — the package now routes by stack
88
+ - **The paste-block in `docs/agent-onboarding.md` never mentioned the adapters.** That block is the
89
+ only text that lands in a consuming project's agent instructions, so it is the only text an agent
90
+ re-reads on every task — and it opened with "a CSS kit + enhancers, **not a component framework**",
91
+ which actively steers a Vue/React project away from the typed components. It now opens with a
92
+ **step 0 routing table** (Vue → `/vue` · React → `/react` · Blazor → RCL · everything else → raw
93
+ markup), names the ten wrapped components, and states the failure mode: the raw path renders
94
+ correctly the first time and then silently stops hydrating.
95
+ - **`COMPONENTS.md` flags the wrapper at each of the ten sections**, so the choice is visible at the
96
+ point of use rather than only on an onboarding page read once — with each component's *real*
97
+ binding (`v-model` / `value`+`onChange` / `@bind-Value` for the six value-bound ones; `open`+`close`
98
+ for modal and drawer; data props for chart; `columns`+`rows`+events for table). Hard rule 7 states
99
+ it up front.
100
+ - **Both READMEs lead with the routing table**, above the first import block. Previously the first
101
+ runnable snippet was the vanilla one and the adapter table sat ~130 lines below it.
102
+ - **`docs/getting-started.md`** core concept 2 ("the enhancer is the source of truth") is now scoped
103
+ to the raw path, and notes that Vue/React re-implement the ten natively while Blazor wraps the
104
+ enhancer over interop.
105
+ ### Notes
106
+ - Root cause was structural, not editorial: at **v1.18.0** `docs/getting-started.md` — the only
107
+ per-stack router the kit had — was **not in `files`**, so `npm i` delivered a README whose
108
+ "Starting a project? per stack" link pointed at a path that does not exist inside `node_modules`.
109
+ `adapters/` *was* shipped, so the wrappers sat in the install, unused, with nothing pointing at
110
+ them. v1.20.0 started shipping the docs; this release makes them route.
111
+
6
112
  ## [1.21.0] — 2026-08-12
7
113
  Fifth round of real-app consumption feedback, written while that app adopted 1.20.0. Two findings —
8
114
  and a **withdrawal**: the reporter retracted 1.20's rejected §A themselves after isolating the real
@@ -57,6 +163,11 @@ reproduce here, so the rejection stands and the withdrawal is recorded rather th
57
163
  hidden labels in a wide table, asserting the page cannot scroll horizontally *and* that the scroller
58
164
  still scrolls. A static test cannot see this failure; only a layout engine can. Both guards
59
165
  mutation-checked — reverting any single `position:relative` fails them.
166
+ - **`browser/state.mjs`** — pressed toggles and nav orientation in a real engine: the pressed ghost
167
+ segment must keep a flat background-colour (the mutation that reproduces the shorthand bug fails
168
+ it), the solid toggle must read as inset, `--horizontal` must actually be a row with the current
169
+ link marked, on-colour nav ink must stay on-colour in both states, and a routed tab must take its
170
+ underline from `aria-current`. Mutation-checked against all three fixes.
60
171
  - **`browser/theme.mjs`** — subtree theming end-to-end: a `.fdy-title-page` and a `.fdy-card` inside
61
172
  `<section data-theme="dark">` take the dark tokens, a nested light island goes back, and
62
173
  `data-theme` on `<html>` still themes everything. `test/build.test.mjs` guards the selector shape
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,14 @@ 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).
33
45
 
34
46
  ### Containment — why the kit's containers are `position:relative`
35
47
 
@@ -53,7 +65,9 @@ Two consequences for you:
53
65
  ## Enhancers — hook, script, global, events
54
66
 
55
67
  Every enhancer is zero-dependency, auto-initialises once on `DOMContentLoaded`, and is idempotent:
56
- re-hydrate SPA-rendered DOM with `window.Freeday<X>.initAll(root)`. Events are bubbling
68
+ re-hydrate SPA-rendered DOM with `window.Freeday<X>.initAll(root)`. **`root` may be the widget
69
+ itself or an ancestor of it** — both work, so a framework ref placed directly on the enhanced
70
+ element is fine. Events are bubbling
57
71
  `CustomEvent`s; the payload is in `event.detail`.
58
72
 
59
73
  | Markup hook | Script | Global | Emits |
@@ -227,6 +241,10 @@ rest of the app is not.
227
241
  - Modifiers: `--ghost` `--danger` `--text` `--icon` (square, icon-only) `--sm` `--lg`
228
242
  - Parts: `.fdy-btn__icon` (wraps the `<svg>`) · `.fdy-btn-group` (segmented row, `role="group"` +
229
243
  `aria-label`)
244
+ - **Toggle / segmented:** `aria-pressed="true"` gives a real pressed state (soft primary fill on
245
+ `--ghost`/`--text`, an inset-sunk gradient on the solid button). No extra class — `aria-pressed`
246
+ is already the right attribute, and it is what turns `.fdy-btn-group` from a joined row into a
247
+ complete segmented control.
230
248
  - A11y: `--icon` **requires** `aria-label`. Use `<button type="button">` unless it submits.
231
249
 
232
250
  ```html
@@ -312,6 +330,8 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
312
330
  ```
313
331
 
314
332
  ## Select / combobox — `.fdy-combo`
333
+ > **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).
334
+
315
335
  Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
316
336
 
317
337
  - `.fdy-combo` (+`--error`, `--no-icon`) · `__button` `__value` (+`--placeholder`) `__listbox`
@@ -334,6 +354,8 @@ Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
334
354
  ```
335
355
 
336
356
  ## Autocomplete — `.fdy-autocomplete`
357
+ > **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).
358
+
337
359
  Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
338
360
 
339
361
  - `.fdy-autocomplete` · `__listbox` `__option` `__empty`
@@ -341,6 +363,8 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
341
363
  `aria-controls` → the listbox `id`, `autocomplete="off"`.
342
364
 
343
365
  ## Cascade select — `.fdy-cascade`
366
+ > **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).
367
+
344
368
  Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
345
369
  child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
346
370
 
@@ -360,6 +384,8 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
360
384
  ```
361
385
 
362
386
  ## Choose-from-list (CFL) — `data-fdy-cfl`
387
+ > **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).
388
+
363
389
  A read-only field backed by master data: the button opens a searchable dialog, the picked row fills
364
390
  the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
365
391
 
@@ -410,6 +436,8 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
410
436
  ```
411
437
 
412
438
  ## Date picker — `data-fdy-datepicker`
439
+ > **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).
440
+
413
441
  Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author an empty `<div>`**
414
442
  — the enhancer builds everything.
415
443
 
@@ -417,6 +445,7 @@ Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author
417
445
  `__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` `__dow` `__day`
418
446
  - Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
419
447
  get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start.
448
+ **Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor) — use it in those stacks.
420
449
  - Attributes: `data-value="YYYY-MM-DD"`, `data-placeholder`, `data-label`, `data-fdy-no-icon`
421
450
 
422
451
  ```html
@@ -499,6 +528,18 @@ Linear multi-step flow: marker → check, one panel at a time, back/next. Needs
499
528
 
500
529
  # Data
501
530
 
531
+ **`__btn` is optional.** For a **read-only indicator** put `__marker` + `__label` straight in the
532
+ `<li>`; for a **navigable** stepper wrap them in `<button class="fdy-step__btn">` (add `disabled`
533
+ for a step that is not reachable yet). Both render identically — the marker carries its own lift
534
+ over the connector line, so omitting `__btn` cannot make the connector draw through the numbers.
535
+
536
+ ```html
537
+ <ol class="fdy-stepper">
538
+ <li class="fdy-step is-complete"><span class="fdy-step__marker">1</span><span class="fdy-step__label">Email</span></li>
539
+ <li class="fdy-step is-active"><span class="fdy-step__marker">2</span><span class="fdy-step__label">Verify</span></li>
540
+ </ol>
541
+ ```
542
+
502
543
  ## Table — `.fdy-table`
503
544
  Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-table-scroll`
504
545
  (horizontal scroll). Always a `<caption>` (use `.fdy-visually-hidden` if it shouldn't show) and
@@ -517,6 +558,8 @@ Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-tab
517
558
  ```
518
559
 
519
560
  ## Data table — `.fdy-datatable`
561
+ > **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.
562
+
520
563
  The interactive table: global search, sort, per-column filters, row selection + bulk bar,
521
564
  pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` + `data-fdy-table`
522
565
  (`data-page-size="N"`).
@@ -556,6 +599,8 @@ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__ic
556
599
  replaces.
557
600
 
558
601
  ## Charts — `data-fdy-chart`
602
+ > **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.
603
+
559
604
  Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`; call
560
605
  `FreedayChart.update(el)` after changing data (or use `FdyChart` in Vue/React/Blazor).
561
606
 
@@ -662,18 +707,43 @@ Size-matched placeholders so nothing shifts when data lands: `--title` `--text`
662
707
 
663
708
  # Navigation
664
709
 
665
- ## Nav (sidebar menu) — `.fdy-nav`
666
- Vertical navigation, used by the app shell. Items are `<a class="fdy-nav__item">` with
710
+ ## Nav (menu) — `.fdy-nav`
711
+ Navigation links **vertical by default** (the app shell sidebar), horizontal with `--horizontal`. Items are `<a class="fdy-nav__item">` with
667
712
  `__icon` / `__label` / `__badge`; the current one gets `aria-current="page"`.
668
713
  `--flat` drops the surface. Nested groups are native `<details>`:
669
714
 
670
715
  - `.fdy-nav__tree` + `<summary class="fdy-nav__item">` + `.fdy-nav__caret` → children in
671
716
  `.fdy-nav__sub`
672
717
  - `.fdy-nav__group` + `<summary class="fdy-nav__grouplabel">` → a collapsible section
718
+ - **`--horizontal`** lays the same links out as a row, for a **top-nav application** (primary
719
+ navigation in `.fdy-appbar` or `.fdy-app__topbar`, no sidebar). Same item, same states, same
720
+ `aria-current="page"`; the row scrolls if it runs out of width. On `.fdy-appbar--primary` the
721
+ links go on-colour automatically.
722
+
723
+ ```html
724
+ <header class="fdy-appbar">
725
+ <a class="fdy-appbar__brand" href="/">Acme</a>
726
+ <nav class="fdy-nav fdy-nav--horizontal" aria-label="Main">
727
+ <a class="fdy-nav__item" href="/invoices" aria-current="page">Invoices</a>
728
+ <a class="fdy-nav__item" href="/customers">Customers</a>
729
+ </nav>
730
+ <span class="fdy-appbar__spacer"></span>
731
+ <div class="fdy-appbar__actions"><!-- buttons --></div>
732
+ </header>
733
+ ```
734
+
735
+ > **Which one for navigation?** A nav link is a link marked `aria-current="page"` — never
736
+ > `aria-selected` (invalid ARIA on an anchor) and never `role="tab"`. Use `.fdy-nav--horizontal`
737
+ > for an application's **primary** navigation. For **routed sub-navigation** that should look like
738
+ > tabs (`/settings/profile` · `/settings/billing`), put `.fdy-tabs__list` / `.fdy-tabs__tab` on
739
+ > plain `<a>`s — those classes honour `aria-current="page"` too — and do **not** add the tab roles
740
+ > or `freeday-tabs.js`, which promise a keyboard contract routes do not have.
673
741
 
674
742
  ## App bar — `.fdy-appbar`
675
743
  Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--sticky`
676
744
  `--elevated` `--dense` `--primary` (on-colour controls). Parts `__brand` `__spacer` `__actions`.
745
+ It ships **no link class of its own** — put `<nav class="fdy-nav fdy-nav--horizontal">` in it
746
+ for primary navigation (see Nav above).
677
747
 
678
748
  ## Breadcrumb — `.fdy-breadcrumb`
679
749
  `<nav class="fdy-breadcrumb" aria-label="Breadcrumb">` → `<ol class="fdy-breadcrumb__list">` with
@@ -682,6 +752,8 @@ Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--
682
752
 
683
753
  ## Tabs — `.fdy-tabs`
684
754
  APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
755
+ `.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
756
+ so the same look serves routed sub-navigation built from plain links — see the note under Nav.
685
757
 
686
758
  ```html
687
759
  <div class="fdy-tabs" data-fdy-tabs>
@@ -699,12 +771,16 @@ APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
699
771
  # Overlays
700
772
 
701
773
  ## Modal — `.fdy-modal`
774
+ > **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).
775
+
702
776
  Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
703
777
  `--wide`; `--cfl` for the choose-from-list dialog. Parts `__header` `__title` `__body` `__footer`
704
778
  `__close`. The body scrolls; the footer never clips. `aria-labelledby` → the title's `id`;
705
779
  `data-close` on any button that should close it.
706
780
 
707
781
  ## Drawer — `.fdy-drawer`
782
+ > **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).
783
+
708
784
  Temporary side panel on native `<dialog>` — left by default, `--right` to flip. Parts `__header`
709
785
  `__title` `__body` `__footer` `__close`. Open it from any
710
786
  `<button data-fdy-drawer="<dialog id>">`. Needs `freeday-drawer.js`.
@@ -751,6 +827,10 @@ Initials or image. Sizes `--sm` / default / `--lg`; `.fdy-avatar-group` stacks t
751
827
  group). `--tone-1`…`--tone-8` are decorative tints (WCAG AA in both themes) to distinguish
752
828
  same-initial avatars — hash the index off the **full** name, not the initials.
753
829
 
830
+ Sizes: `--xs` 1.5rem · *(base)* 2.5rem · `--sm` 2rem · `--lg` 3.5rem. Use **`--xs` inside a control**
831
+ — `.fdy-btn--sm` is itself 2rem tall, so an `--sm` avatar fills a small button edge to edge and the
832
+ ghost border cuts across the circle.
833
+
754
834
  ## Chip — `.fdy-chip`
755
835
  Three roles inside a `.fdy-chips` row:
756
836
 
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.21.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.21.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.23.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.23.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
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.21.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.21.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.23.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.23.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
package/USAGE.md CHANGED
@@ -115,6 +115,27 @@ container, a single `<section>`. An app whose two list screens are dense and who
115
115
  are not should scope it per screen rather than densifying everything. Set it at one level per screen,
116
116
  never per component.
117
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
+
118
139
  ## 7. Assemble the page from the frame down
119
140
 
120
141
  1. **Shell:** every application starts inside **`.fdy-app`** — a flex row of `__sidebar` (with
@@ -9,6 +9,11 @@
9
9
  * useFreeday(root); // scopes init to this component's subtree
10
10
  * // <div ref={root}> ...[data-fdy-*] markup... </div>
11
11
  *
12
+ * The ref may also sit ON the widget itself — <div ref={root} data-fdy-menu> — which is what you
13
+ * write when the component's root element IS the widget. initAll() matches its own root as
14
+ * well as descendants, so both shapes work. (Before 1.23.0 only the wrapping shape did, and
15
+ * the other failed silently: querySelectorAll never matches its root.)
16
+ *
12
17
  * Events are native bubbling CustomEvents — add listeners on the root (they
13
18
  * bubble) and read event.detail. See index.d.ts for the detail types.
14
19
  */
@@ -10,6 +10,11 @@
10
10
  * useFreeday(root); // scopes init to this component's subtree
11
11
  * // <div ref="root"> ...[data-fdy-*] markup... </div>
12
12
  *
13
+ * The ref may also sit ON the widget itself — <div ref="root" data-fdy-menu> — which is what you
14
+ * write when the component's root element IS the widget. initAll() matches its own root as
15
+ * well as descendants, so both shapes work. (Before 1.23.0 only the wrapping shape did, and
16
+ * the other failed silently: querySelectorAll never matches its root.)
17
+ *
13
18
  * Events come straight through as native bubbling CustomEvents — bind them with
14
19
  * plain v-on (e.g. @fdy-cascade-change) and read event.detail. See index.d.ts
15
20
  * for the detail types.
@@ -122,7 +122,10 @@
122
122
  }
123
123
 
124
124
  function initAll(context) {
125
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-autocomplete]'), initAutocomplete);
125
+ var root = context || document;
126
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
127
+ if (root.matches && root.matches('[data-fdy-autocomplete]')) initAutocomplete(root);
128
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-autocomplete]'), initAutocomplete);
126
129
  }
127
130
 
128
131
  if (document.readyState === 'loading') {
@@ -98,7 +98,10 @@
98
98
  }
99
99
 
100
100
  function initAll(context) {
101
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-carousel]'), initCarousel);
101
+ var root = context || document;
102
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
103
+ if (root.matches && root.matches('[data-fdy-carousel]')) initCarousel(root);
104
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-carousel]'), initCarousel);
102
105
  }
103
106
 
104
107
  if (document.readyState === 'loading') {
@@ -243,7 +243,10 @@
243
243
  }
244
244
 
245
245
  function initAll(context) {
246
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-cascade]'), initCascade);
246
+ var root = context || document;
247
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
248
+ if (root.matches && root.matches('[data-fdy-cascade]')) initCascade(root);
249
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-cascade]'), initCascade);
247
250
  }
248
251
 
249
252
  if (document.readyState === 'loading') {
@@ -179,28 +179,33 @@
179
179
  };
180
180
  }
181
181
 
182
- function initTriggers(context) {
183
- var ctx = context || document;
182
+ function bindGroup(group) {
183
+ if (group.dataset.fdyCflBound === '1') return;
184
+ var dialog = document.getElementById(group.getAttribute('data-fdy-cfl'));
185
+ if (!dialog) return;
186
+ initDialog(dialog);
187
+ var opener = group.querySelector('[data-fdy-cfl-trigger], .fdy-input-group__btn');
188
+ if (!opener) return;
189
+ group.dataset.fdyCflBound = '1';
190
+ opener.addEventListener('click', function () { dialog.fdyCflOpen(group, opener); });
191
+ }
184
192
 
185
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl]'), function (group) {
186
- if (group.dataset.fdyCflBound === '1') return;
187
- var dialog = document.getElementById(group.getAttribute('data-fdy-cfl'));
188
- if (!dialog) return;
189
- initDialog(dialog);
190
- var opener = group.querySelector('[data-fdy-cfl-trigger], .fdy-input-group__btn');
191
- if (!opener) return;
192
- group.dataset.fdyCflBound = '1';
193
- opener.addEventListener('click', function () { dialog.fdyCflOpen(group, opener); });
194
- });
193
+ function bindOpener(opener) {
194
+ if (opener.dataset.fdyCflBound === '1') return;
195
+ var dialog = document.getElementById(opener.getAttribute('data-fdy-cfl-open'));
196
+ if (!dialog) return;
197
+ initDialog(dialog);
198
+ opener.dataset.fdyCflBound = '1';
199
+ opener.addEventListener('click', function () { dialog.fdyCflOpen(null, opener); });
200
+ }
195
201
 
196
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl-open]'), function (opener) {
197
- if (opener.dataset.fdyCflBound === '1') return;
198
- var dialog = document.getElementById(opener.getAttribute('data-fdy-cfl-open'));
199
- if (!dialog) return;
200
- initDialog(dialog);
201
- opener.dataset.fdyCflBound = '1';
202
- opener.addEventListener('click', function () { dialog.fdyCflOpen(null, opener); });
203
- });
202
+ function initTriggers(context) {
203
+ var ctx = context || document;
204
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
205
+ if (ctx.matches && ctx.matches('[data-fdy-cfl]')) bindGroup(ctx);
206
+ if (ctx.matches && ctx.matches('[data-fdy-cfl-open]')) bindOpener(ctx);
207
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl]'), bindGroup);
208
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl-open]'), bindOpener);
204
209
  }
205
210
 
206
211
  if (document.readyState === 'loading') {
@@ -416,7 +416,10 @@
416
416
  }
417
417
 
418
418
  function initAll(context) {
419
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-chart]'), initChart);
419
+ var root = context || document;
420
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
421
+ if (root.matches && root.matches('[data-fdy-chart]')) initChart(root);
422
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-chart]'), initChart);
420
423
  }
421
424
 
422
425
  if (document.readyState === 'loading') {
@@ -69,6 +69,9 @@
69
69
 
70
70
  function initAll(context) {
71
71
  var root = context || document;
72
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
73
+ if (root.matches && root.matches('[data-fdy-chips]')) initGroup(root);
74
+ if (root.matches && root.matches('.fdy-chip__remove')) initRemove(root);
72
75
  Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-chips]'), initGroup);
73
76
  Array.prototype.forEach.call(root.querySelectorAll('.fdy-chip__remove'), initRemove);
74
77
  }
@@ -306,9 +306,12 @@
306
306
  }
307
307
 
308
308
  function initAll(context) {
309
- var ctx = context || document;
310
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-daterange]'), initRange);
311
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-datepicker]'), initPicker);
309
+ var root = context || document;
310
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
311
+ if (root.matches && root.matches('[data-fdy-daterange]')) initRange(root);
312
+ if (root.matches && root.matches('[data-fdy-datepicker]')) initPicker(root);
313
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-daterange]'), initRange);
314
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-datepicker]'), initPicker);
312
315
  }
313
316
 
314
317
  if (document.readyState === 'loading') {
@@ -70,7 +70,10 @@
70
70
  }
71
71
 
72
72
  function initAll(context) {
73
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-datetimepicker]'), initDatetime);
73
+ var root = context || document;
74
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
75
+ if (root.matches && root.matches('[data-fdy-datetimepicker]')) initDatetime(root);
76
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-datetimepicker]'), initDatetime);
74
77
  }
75
78
 
76
79
  if (document.readyState === 'loading') {