@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/dist/freeday.js CHANGED
@@ -124,7 +124,10 @@
124
124
  }
125
125
 
126
126
  function initAll(context) {
127
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-autocomplete]'), initAutocomplete);
127
+ var root = context || document;
128
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
129
+ if (root.matches && root.matches('[data-fdy-autocomplete]')) initAutocomplete(root);
130
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-autocomplete]'), initAutocomplete);
128
131
  }
129
132
 
130
133
  if (document.readyState === 'loading') {
@@ -288,7 +291,10 @@
288
291
  }
289
292
 
290
293
  function initAll(context) {
291
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-carousel]'), initCarousel);
294
+ var root = context || document;
295
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
296
+ if (root.matches && root.matches('[data-fdy-carousel]')) initCarousel(root);
297
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-carousel]'), initCarousel);
292
298
  }
293
299
 
294
300
  if (document.readyState === 'loading') {
@@ -545,7 +551,10 @@
545
551
  }
546
552
 
547
553
  function initAll(context) {
548
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-cascade]'), initCascade);
554
+ var root = context || document;
555
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
556
+ if (root.matches && root.matches('[data-fdy-cascade]')) initCascade(root);
557
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-cascade]'), initCascade);
549
558
  }
550
559
 
551
560
  if (document.readyState === 'loading') {
@@ -738,28 +747,33 @@
738
747
  };
739
748
  }
740
749
 
741
- function initTriggers(context) {
742
- var ctx = context || document;
750
+ function bindGroup(group) {
751
+ if (group.dataset.fdyCflBound === '1') return;
752
+ var dialog = document.getElementById(group.getAttribute('data-fdy-cfl'));
753
+ if (!dialog) return;
754
+ initDialog(dialog);
755
+ var opener = group.querySelector('[data-fdy-cfl-trigger], .fdy-input-group__btn');
756
+ if (!opener) return;
757
+ group.dataset.fdyCflBound = '1';
758
+ opener.addEventListener('click', function () { dialog.fdyCflOpen(group, opener); });
759
+ }
743
760
 
744
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl]'), function (group) {
745
- if (group.dataset.fdyCflBound === '1') return;
746
- var dialog = document.getElementById(group.getAttribute('data-fdy-cfl'));
747
- if (!dialog) return;
748
- initDialog(dialog);
749
- var opener = group.querySelector('[data-fdy-cfl-trigger], .fdy-input-group__btn');
750
- if (!opener) return;
751
- group.dataset.fdyCflBound = '1';
752
- opener.addEventListener('click', function () { dialog.fdyCflOpen(group, opener); });
753
- });
761
+ function bindOpener(opener) {
762
+ if (opener.dataset.fdyCflBound === '1') return;
763
+ var dialog = document.getElementById(opener.getAttribute('data-fdy-cfl-open'));
764
+ if (!dialog) return;
765
+ initDialog(dialog);
766
+ opener.dataset.fdyCflBound = '1';
767
+ opener.addEventListener('click', function () { dialog.fdyCflOpen(null, opener); });
768
+ }
754
769
 
755
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl-open]'), function (opener) {
756
- if (opener.dataset.fdyCflBound === '1') return;
757
- var dialog = document.getElementById(opener.getAttribute('data-fdy-cfl-open'));
758
- if (!dialog) return;
759
- initDialog(dialog);
760
- opener.dataset.fdyCflBound = '1';
761
- opener.addEventListener('click', function () { dialog.fdyCflOpen(null, opener); });
762
- });
770
+ function initTriggers(context) {
771
+ var ctx = context || document;
772
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
773
+ if (ctx.matches && ctx.matches('[data-fdy-cfl]')) bindGroup(ctx);
774
+ if (ctx.matches && ctx.matches('[data-fdy-cfl-open]')) bindOpener(ctx);
775
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl]'), bindGroup);
776
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl-open]'), bindOpener);
763
777
  }
764
778
 
765
779
  if (document.readyState === 'loading') {
@@ -1189,7 +1203,10 @@
1189
1203
  }
1190
1204
 
1191
1205
  function initAll(context) {
1192
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-chart]'), initChart);
1206
+ var root = context || document;
1207
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
1208
+ if (root.matches && root.matches('[data-fdy-chart]')) initChart(root);
1209
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-chart]'), initChart);
1193
1210
  }
1194
1211
 
1195
1212
  if (document.readyState === 'loading') {
@@ -1272,6 +1289,9 @@
1272
1289
 
1273
1290
  function initAll(context) {
1274
1291
  var root = context || document;
1292
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
1293
+ if (root.matches && root.matches('[data-fdy-chips]')) initGroup(root);
1294
+ if (root.matches && root.matches('.fdy-chip__remove')) initRemove(root);
1275
1295
  Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-chips]'), initGroup);
1276
1296
  Array.prototype.forEach.call(root.querySelectorAll('.fdy-chip__remove'), initRemove);
1277
1297
  }
@@ -1593,9 +1613,12 @@
1593
1613
  }
1594
1614
 
1595
1615
  function initAll(context) {
1596
- var ctx = context || document;
1597
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-daterange]'), initRange);
1598
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-datepicker]'), initPicker);
1616
+ var root = context || document;
1617
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
1618
+ if (root.matches && root.matches('[data-fdy-daterange]')) initRange(root);
1619
+ if (root.matches && root.matches('[data-fdy-datepicker]')) initPicker(root);
1620
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-daterange]'), initRange);
1621
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-datepicker]'), initPicker);
1599
1622
  }
1600
1623
 
1601
1624
  if (document.readyState === 'loading') {
@@ -1679,7 +1702,10 @@
1679
1702
  }
1680
1703
 
1681
1704
  function initAll(context) {
1682
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-datetimepicker]'), initDatetime);
1705
+ var root = context || document;
1706
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
1707
+ if (root.matches && root.matches('[data-fdy-datetimepicker]')) initDatetime(root);
1708
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-datetimepicker]'), initDatetime);
1683
1709
  }
1684
1710
 
1685
1711
  if (document.readyState === 'loading') {
@@ -1709,19 +1735,24 @@
1709
1735
  });
1710
1736
  }
1711
1737
 
1738
+ function bindTrigger(btn) {
1739
+ if (btn.dataset.fdyDrawerBound === '1') return;
1740
+ var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
1741
+ if (!dialog) return;
1742
+ initDrawer(dialog);
1743
+ btn.dataset.fdyDrawerBound = '1';
1744
+ btn.addEventListener('click', function () {
1745
+ if (typeof dialog.showModal === 'function') dialog.showModal();
1746
+ else dialog.setAttribute('open', '');
1747
+ });
1748
+ }
1749
+
1712
1750
  function initTriggers(context) {
1713
1751
  var ctx = context || document;
1714
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-drawer]'), function (btn) {
1715
- if (btn.dataset.fdyDrawerBound === '1') return;
1716
- var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
1717
- if (!dialog) return;
1718
- initDrawer(dialog);
1719
- btn.dataset.fdyDrawerBound = '1';
1720
- btn.addEventListener('click', function () {
1721
- if (typeof dialog.showModal === 'function') dialog.showModal();
1722
- else dialog.setAttribute('open', '');
1723
- });
1724
- });
1752
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
1753
+ if (ctx.matches && ctx.matches('[data-fdy-drawer]')) bindTrigger(ctx);
1754
+ if (ctx.matches && ctx.matches('.fdy-drawer')) initDrawer(ctx);
1755
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-drawer]'), bindTrigger);
1725
1756
  // Init any drawer dialogs even without a wired trigger (backdrop/close still work).
1726
1757
  Array.prototype.forEach.call(ctx.querySelectorAll('.fdy-drawer'), initDrawer);
1727
1758
  }
@@ -1905,7 +1936,10 @@
1905
1936
  }
1906
1937
 
1907
1938
  function initAll(context) {
1908
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-validate]'), initForm);
1939
+ var root = context || document;
1940
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
1941
+ if (root.matches && root.matches('[data-fdy-validate]')) initForm(root);
1942
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-validate]'), initForm);
1909
1943
  }
1910
1944
 
1911
1945
  if (document.readyState === 'loading') {
@@ -2019,6 +2053,9 @@
2019
2053
 
2020
2054
  function initAll(context) {
2021
2055
  var root = context || document;
2056
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
2057
+ if (root.matches && root.matches('[data-fdy-mask]')) initMask(root);
2058
+ if (root.matches && root.matches('[data-fdy-password]')) initReveal(root);
2022
2059
  Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-mask]'), initMask);
2023
2060
  Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-password]'), initReveal);
2024
2061
  }
@@ -2114,7 +2151,10 @@
2114
2151
  }
2115
2152
 
2116
2153
  function initAll(ctx) {
2117
- Array.prototype.forEach.call((ctx || document).querySelectorAll('[data-fdy-menu]'), initMenu);
2154
+ var root = ctx || document;
2155
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
2156
+ if (root.matches && root.matches('[data-fdy-menu]')) initMenu(root);
2157
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-menu]'), initMenu);
2118
2158
  }
2119
2159
 
2120
2160
  if (document.readyState === 'loading') {
@@ -2235,7 +2275,10 @@
2235
2275
  }
2236
2276
 
2237
2277
  function initAll(context) {
2238
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-rating]'), initRating);
2278
+ var root = context || document;
2279
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
2280
+ if (root.matches && root.matches('[data-fdy-rating]')) initRating(root);
2281
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-rating]'), initRating);
2239
2282
  }
2240
2283
 
2241
2284
  if (document.readyState === 'loading') {
@@ -2438,11 +2481,10 @@
2438
2481
  }
2439
2482
 
2440
2483
  function initAll(context) {
2441
- var scope = context || document;
2442
- Array.prototype.forEach.call(
2443
- scope.querySelectorAll('[data-fdy-combo]'),
2444
- initCombo
2445
- );
2484
+ var root = context || document;
2485
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
2486
+ if (root.matches && root.matches('[data-fdy-combo]')) initCombo(root);
2487
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-combo]'), initCombo);
2446
2488
  }
2447
2489
 
2448
2490
  // One global handler closes the open combo on an outside click.
@@ -2489,7 +2531,10 @@
2489
2531
  }
2490
2532
 
2491
2533
  function initAll(context) {
2492
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-slider]'), initSlider);
2534
+ var root = context || document;
2535
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
2536
+ if (root.matches && root.matches('[data-fdy-slider]')) initSlider(root);
2537
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-slider]'), initSlider);
2493
2538
  }
2494
2539
 
2495
2540
  if (document.readyState === 'loading') {
@@ -2580,7 +2625,10 @@
2580
2625
  }
2581
2626
 
2582
2627
  function initAll(context) {
2583
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-stepper]'), initStepper);
2628
+ var root = context || document;
2629
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
2630
+ if (root.matches && root.matches('[data-fdy-stepper]')) initStepper(root);
2631
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-stepper]'), initStepper);
2584
2632
  }
2585
2633
 
2586
2634
  if (document.readyState === 'loading') {
@@ -3053,10 +3101,10 @@
3053
3101
  }
3054
3102
 
3055
3103
  function initAll(context) {
3056
- Array.prototype.forEach.call(
3057
- (context || document).querySelectorAll('[data-fdy-table]'),
3058
- initTable
3059
- );
3104
+ var root = context || document;
3105
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
3106
+ if (root.matches && root.matches('[data-fdy-table]')) initTable(root);
3107
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-table]'), initTable);
3060
3108
  }
3061
3109
 
3062
3110
  if (document.readyState === 'loading') {
@@ -3122,10 +3170,10 @@
3122
3170
  }
3123
3171
 
3124
3172
  function initAll(context) {
3125
- Array.prototype.forEach.call(
3126
- (context || document).querySelectorAll('[data-fdy-tabs]'),
3127
- initTabs
3128
- );
3173
+ var root = context || document;
3174
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
3175
+ if (root.matches && root.matches('[data-fdy-tabs]')) initTabs(root);
3176
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-tabs]'), initTabs);
3129
3177
  }
3130
3178
 
3131
3179
  if (document.readyState === 'loading') {
@@ -3306,7 +3354,10 @@
3306
3354
  }
3307
3355
 
3308
3356
  function initAll(context) {
3309
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-timepicker]'), initTime);
3357
+ var root = context || document;
3358
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
3359
+ if (root.matches && root.matches('[data-fdy-timepicker]')) initTime(root);
3360
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-timepicker]'), initTime);
3310
3361
  }
3311
3362
 
3312
3363
  if (document.readyState === 'loading') {
@@ -3507,7 +3558,10 @@
3507
3558
  }
3508
3559
 
3509
3560
  function initAll(context) {
3510
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-tree]'), initTree);
3561
+ var root = context || document;
3562
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
3563
+ if (root.matches && root.matches('[data-fdy-tree]')) initTree(root);
3564
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-tree]'), initTree);
3511
3565
  }
3512
3566
 
3513
3567
  if (document.readyState === 'loading') {
@@ -3714,7 +3768,10 @@
3714
3768
  }
3715
3769
 
3716
3770
  function initAll(context) {
3717
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-dropzone]'), initDropzone);
3771
+ var root = context || document;
3772
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
3773
+ if (root.matches && root.matches('[data-fdy-dropzone]')) initDropzone(root);
3774
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-dropzone]'), initDropzone);
3718
3775
  }
3719
3776
 
3720
3777
  if (document.readyState === 'loading') {
@@ -18,8 +18,38 @@ the **root of the consuming project**:
18
18
  ```markdown
19
19
  ## UI: Freeday design system (@cahyo-dimas/freeday)
20
20
 
21
- All UI in this project is built from Freeday. It is a **token-driven CSS kit + zero-dependency JS
22
- enhancers**, not a component framework — components are plain markup with `fdy-*` classes.
21
+ All UI in this project is built from Freeday: a **token-driven CSS kit** (`fdy-*` classes on plain
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
24
+ the wrapper is the correct way to use them.
25
+
26
+ **0. First decide which entry point this project uses. This is not an optimisation — get it wrong
27
+ and the code looks correct and fails later.**
28
+
29
+ | This project's stack | Import the ten components from | Binding |
30
+ |---|---|---|
31
+ | Vue 3 | `@cahyo-dimas/freeday/vue` | `v-model` |
32
+ | React 18/19 | `@cahyo-dimas/freeday/react` | `value` + `onChange` |
33
+ | Blazor (net8.0) | `@using Freeday.Blazor` (RCL) | `@bind-Value` |
34
+ | Static HTML, Svelte, server-rendered templates… | no wrapper — raw markup + the enhancer script | `fdy-*` DOM events |
35
+
36
+ The ten: **FdyCombo · FdyDatepicker · FdyDateRange · FdyAutocomplete · FdyCascade · FdyCfl ·
37
+ FdyChart · FdyTable · FdyModal · FdyDrawer**. In Vue/React/Blazor, **never hand-write the raw
38
+ markup + enhancer for these ten.** The raw path *appears* to work — the enhancer auto-initialises
39
+ once on `DOMContentLoaded` and the first render is correct — then fails quietly: DOM your framework
40
+ renders later is never hydrated, and the widget's state lives in the DOM instead of in your
41
+ framework's state.
42
+
43
+ Everything else is the same in every stack: plain `fdy-*` markup (button, card, badge, alert,
44
+ table markup, layout…). For the *other* interactive components (chips, stepper, input mask, file
45
+ upload, tree, tabs, menu, rating, slider, form validation, carousel, timepicker) there is no
46
+ wrapper — use the raw markup and hydrate it:
47
+
48
+ - **Vue / React** — `useFreeday(rootRef)` from the same import path, plus `import '@cahyo-dimas/freeday'`
49
+ once at app entry to register the enhancers. (The ten typed components do **not** need this: they
50
+ are native Vue/React implementations of the same markup, not wrappers over the enhancer.)
51
+ - **Blazor** — `FreedayBlazor.initAll` interop. (Here the typed components *are* thin wrappers over
52
+ the enhancers, so the enhancer script is always required.)
23
53
 
24
54
  **Before writing or editing any markup/CSS, read these (they ship inside the package):**
25
55
  - `node_modules/@cahyo-dimas/freeday/COMPONENTS.md` — every class that exists, with minimal markup
@@ -40,8 +70,9 @@ enhancers**, not a component framework — components are plain markup with `fdy
40
70
  5. Assemble from the frame down: `.fdy-app` → `.fdy-page` → `.fdy-page-section` → components.
41
71
  6. Form errors: `aria-invalid="true"` + `aria-describedby` → a `.fdy-help.fdy-help--error`.
42
72
  Icon-only buttons need `aria-label`. Status is never colour-only.
43
- 7. Interactive components need their enhancer script loaded (see the table in COMPONENTS.md);
44
- in an SPA, re-hydrate dynamic DOM (`useFreeday` in Vue/React does this).
73
+ 7. Interactive components need their enhancer script loaded (see the table in COMPONENTS.md); in an
74
+ SPA, re-hydrate dynamic DOM with `useFreeday` / `FreedayBlazor.initAll`. This applies to the
75
+ components **without** a typed wrapper — for the ten in step 0, use the wrapper instead.
45
76
  8. Freeday owns components + tokens, **not layout**. Grids/stacks/one-off gaps come from our own
46
77
  layout layer — build its theme on `var(--space-N)` so both systems stay in step.
47
78
  ```
@@ -17,10 +17,15 @@ Freeday = **CSS** (semantic tokens + `fdy-*` classes) + **zero-dependency JS enh
17
17
  1. **Static vs interactive.** Static components (button, card, badge, plain input, layout) need
18
18
  only the **`fdy-*` classes** — no JS. Interactive components (select/combo, cascade, date/time
19
19
  picker, table, dropzone, form validation, input mask, chip) need the **JS enhancers**.
20
- 2. **The enhancer is the source of truth.** You don't re-implement components; the enhancer owns
21
- the widget's DOM. You **listen for `fdy-*` events** (all bubbling `CustomEvent`s, data in
22
- `event.detail`) → store them in your framework state. Event/API contract table:
23
- [`integrations.md` §Event & API contract](integrations.md).
20
+ 2. **The enhancer is the source of truth — *on the raw path*.** You don't re-implement components;
21
+ the enhancer owns the widget's DOM. You **listen for `fdy-*` events** (all bubbling
22
+ `CustomEvent`s, data in `event.detail`) → store them in your framework state. Event/API contract
23
+ table: [`integrations.md` §Event & API contract](integrations.md).
24
+ **On Vue, React or Blazor this is not the path to take for ten of the components** — `FdyCombo`,
25
+ `FdyDatepicker`, `FdyDateRange`, `FdyAutocomplete`, `FdyCascade`, `FdyCfl`, `FdyChart`,
26
+ `FdyTable`, `FdyModal`, `FdyDrawer` ship typed wrappers that own the state properly (Vue and
27
+ React re-implement the interaction natively; Blazor wraps the enhancer over interop). Use them;
28
+ the raw path is for the components without a wrapper, and for stacks without an adapter.
24
29
  3. **Hydrate dynamic DOM.** Enhancers auto-init once on `DOMContentLoaded`. DOM an SPA renders
25
30
  **after** that must be re-hydrated: `window.Freeday<X>.initAll(el)` (idempotent, safe to repeat).
26
31
  Each framework's adapter wraps this — you don't call it manually.
@@ -179,7 +184,7 @@ live docs also have a copy button per component.
179
184
  ```bash
180
185
  npm i @cahyo-dimas/freeday
181
186
  ```
182
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.21.0"` (public npm package). `dist/` is
187
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.23.0"` (public npm package). `dist/` is
183
188
  committed and published → no build step; `npm ci` runs without auth.
184
189
 
185
190
  ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -71,7 +71,7 @@
71
71
  "scripts": {
72
72
  "build": "node tokens/build.mjs",
73
73
  "test": "node --test",
74
- "test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs",
74
+ "test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs",
75
75
  "prepack": "node tokens/build.mjs",
76
76
  "version": "node tokens/build.mjs && git add dist",
77
77
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"
@@ -24,6 +24,15 @@
24
24
  .fdy-app__brand-title{display:block;font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-lg);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
25
25
  .fdy-app__brand-subtitle{display:block;font-size:var(--text-xs);font-weight:var(--weight-regular);color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
26
26
  .fdy-nav{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:2px;padding:var(--space-3) var(--space-3) var(--space-6);}
27
+ /* Horizontal variant — the SAME nav links laid out as a row, for a top-nav application (one that
28
+ * puts its primary navigation in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar). Deliberately
29
+ * a modifier and not a new block: the item, its states and `aria-current="page"` are unchanged. That
30
+ * is the point — a navigation link is a link marked `aria-current`, never `aria-selected` (invalid
31
+ * ARIA on an anchor) and never a tab role, so borrowing `.fdy-tabs` for routes half-adopts a contract
32
+ * (roving tabindex, a panel per tab) that route links do not honour. position:relative because the
33
+ * row scrolls; see base.css on why a scroller must be a containing block. */
34
+ .fdy-nav--horizontal{position:relative;flex:0 1 auto;flex-direction:row;align-items:center;gap:var(--space-1);padding:0;min-height:0;overflow-x:auto;}
35
+ .fdy-nav--horizontal .fdy-nav__item{white-space:nowrap;}
27
36
  .fdy-nav__item{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);text-decoration:none;cursor:pointer;transition:background var(--dur-fast) var(--ease-standard),color var(--dur-fast) var(--ease-standard);}
28
37
  .fdy-nav__item:hover{background:var(--color-surface-2);color:var(--color-text);}
29
38
  /* Active indicator stays in the primary ramp so it never clashes with a re-themed accent. */
@@ -10,6 +10,15 @@
10
10
  .fdy-appbar__brand svg{display:block;width:1.5rem;height:1.5rem;}
11
11
  .fdy-appbar__spacer{flex:1;}
12
12
  .fdy-appbar__actions{display:inline-flex;align-items:center;gap:var(--space-1);}
13
+ /* Primary navigation inside the bar: `<nav class="fdy-nav fdy-nav--horizontal">` with
14
+ `.fdy-nav__item` links (see app-shell.css). The bar ships no link class of its own — a nav link
15
+ is the same component whether the nav is a sidebar column or a top row. */
13
16
  /* Controls on a coloured app bar go on-colour (icons stay legible, hover is a light wash) */
14
17
  .fdy-appbar--primary .fdy-btn--ghost{background:transparent;border-color:transparent;color:var(--color-on-primary);box-shadow:none;}
15
18
  .fdy-appbar--primary .fdy-btn--ghost:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);transform:none;}
19
+ /* Nav links on a coloured bar: full on-colour ink in every state (a dimmed variant would trade away
20
+ the contrast the bar's own token pair guarantees). The current page is carried by the background
21
+ wash plus the semibold weight the base rule already applies — and by `aria-current` itself. */
22
+ .fdy-appbar--primary .fdy-nav__item{color:var(--color-on-primary);}
23
+ .fdy-appbar--primary .fdy-nav__item:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);}
24
+ .fdy-appbar--primary .fdy-nav__item[aria-current="page"]{background:color-mix(in srgb,var(--color-on-primary) 24%,transparent);color:var(--color-on-primary);}
@@ -1,6 +1,11 @@
1
1
  /* Freeday — Avatar */
2
2
  .fdy-avatar{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.5rem;height:2.5rem;border-radius:var(--radius-full);background:var(--color-primary-soft);color:var(--color-primary-strong);font-family:var(--font-body);font-weight:var(--weight-semibold);font-size:var(--text-sm);line-height:1;overflow:hidden;user-select:none;}
3
3
  .fdy-avatar img{width:100%;height:100%;object-fit:cover;}
4
+ /* --xs exists for an avatar INSIDE a control: `.fdy-btn--sm` is exactly 2rem tall, which is also
5
+ `--sm`'s size, so a small avatar in a small button fills it edge to edge and the ghost border
6
+ crosses the circle. 1.5rem leaves the same ~4px breathing room the kit's controls give their
7
+ icons. Uses `--text-xs` (the smallest type token the kit has — there is no `2xs`). */
8
+ .fdy-avatar--xs{width:1.5rem;height:1.5rem;font-size:var(--text-xs);}
4
9
  .fdy-avatar--sm{width:2rem;height:2rem;font-size:var(--text-xs);}
5
10
  .fdy-avatar--lg{width:3.5rem;height:3.5rem;font-size:var(--text-lg);}
6
11
  .fdy-avatar-group{display:inline-flex;}
@@ -10,6 +10,14 @@
10
10
  .fdy-btn:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-primary) 60%,transparent),inset 0 1px 0 rgba(255,255,255,.26);}
11
11
  .fdy-btn:active{transform:translateY(1px);box-shadow:0 1px 4px -1px color-mix(in srgb,var(--color-primary) 46%,transparent);}
12
12
  .fdy-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
13
+ /* Pressed / toggle. `aria-pressed` is already the right attribute for a toggle button, so this adds
14
+ no class and no new markup contract — and it is what makes .fdy-btn-group a complete segmented
15
+ control: the group joins the borders, this says which segment you are on. Each fill variant
16
+ defines its OWN pressed look below; a single shared rule cannot work, because a gradient is a
17
+ background-IMAGE and would blank the ghost variant's background-color. Solid: invert the gradient
18
+ and sink the shadow inward, so a held button reads as depressed, not merely primary-coloured. */
19
+ .fdy-btn[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 12%,var(--color-primary)),var(--color-primary));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
20
+ .fdy-btn[aria-pressed="true"]:hover{transform:none;box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
13
21
 
14
22
  .fdy-btn--ghost{background:var(--color-surface);color:var(--color-primary-strong);border-color:var(--color-border-strong);box-shadow:var(--shadow-1);}
15
23
  .fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
@@ -18,6 +26,7 @@
18
26
  .fdy-btn--danger{background:linear-gradient(180deg,var(--color-danger-btn),color-mix(in srgb,#000 12%,var(--color-danger-btn)));color:var(--color-on-danger);box-shadow:0 2px 9px -1px color-mix(in srgb,var(--color-danger-btn) 46%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
19
27
  .fdy-btn--danger:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-danger-btn) 54%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
20
28
  .fdy-btn--danger:active{transform:translateY(1px);}
29
+ .fdy-btn--danger[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 14%,var(--color-danger-btn)),var(--color-danger-btn));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 24%,transparent);}
21
30
 
22
31
  .fdy-btn--sm{height:calc(var(--control-h) - var(--space-2));padding:0 var(--space-3);font-size:var(--text-xs);border-radius:var(--radius-sm);}
23
32
  .fdy-btn--lg{height:calc(var(--control-h) + var(--space-2));padding:0 var(--space-6);font-size:var(--text-base);border-radius:var(--radius-lg);}
@@ -30,6 +39,11 @@
30
39
  .fdy-btn--text{background:none;color:var(--color-primary-strong);border-color:transparent;box-shadow:none;}
31
40
  .fdy-btn--text:hover{transform:none;background:var(--color-primary-soft);box-shadow:none;}
32
41
  .fdy-btn--text:active{transform:translateY(1px);box-shadow:none;}
42
+ /* Ghost/text pressed: the kit's selected-surface pair (soft fill + strong ink) — the same pairing
43
+ .fdy-nav__item uses for the current page, and one the AA contrast gate already covers. This is
44
+ the realistic segmented control: a row of --ghost buttons with exactly one aria-pressed. */
45
+ .fdy-btn--ghost[aria-pressed="true"],.fdy-btn--text[aria-pressed="true"]{background:var(--color-primary-soft);color:var(--color-primary-strong);border-color:var(--color-primary-border);box-shadow:none;}
46
+ .fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
33
47
 
34
48
  /* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
35
49
  .fdy-btn--icon{width:var(--control-h);padding:0;}
@@ -7,7 +7,12 @@
7
7
  .fdy-step.is-active::before,.fdy-step.is-complete::before{background:var(--color-primary);}
8
8
  .fdy-step__btn{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;gap:var(--space-2);padding:0 var(--space-2);background:none;border:0;font:inherit;color:inherit;cursor:pointer;max-width:100%;}
9
9
  .fdy-step__btn:disabled{cursor:default;}
10
- .fdy-step__marker{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;flex:none;border-radius:var(--radius-full);background:var(--color-surface);border:2px solid var(--color-border-strong);color:var(--color-text-muted);font-size:var(--text-sm);font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;}
10
+ /* The marker carries the lift itself, NOT `__btn`. The connector above is position:absolute with
11
+ z-index:0, and a positioned box paints after in-flow inline content in the same stacking context —
12
+ so a marker that is merely inline (an indicator-only stepper with no `__btn`) gets painted THROUGH
13
+ by the connector line. Lifting the part that must always exist makes that assembly impossible;
14
+ `__btn`'s own z-index stays, harmlessly, for the navigable case. */
15
+ .fdy-step__marker{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;flex:none;border-radius:var(--radius-full);background:var(--color-surface);border:2px solid var(--color-border-strong);color:var(--color-text-muted);font-size:var(--text-sm);font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;}
11
16
  .fdy-step__marker svg{display:block;width:1rem;height:1rem;}
12
17
  .fdy-step.is-active .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary-strong);}
13
18
  .fdy-step.is-complete .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary);color:var(--color-on-primary);}
@@ -5,7 +5,13 @@
5
5
  .fdy-tabs__tab:hover{color:var(--color-text);}
6
6
  .fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
7
7
  .fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
8
- .fdy-tabs__tab[aria-selected="true"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
8
+ /* `aria-current="page"` is honoured alongside `aria-selected="true"` so the tab look can be used for
9
+ ROUTE-driven sub-navigation (/settings/profile · /settings/billing), where the tabs are real links.
10
+ A link cannot be `aria-selected` — that is invalid ARIA on an anchor; `aria-current` is the only
11
+ correct marker, and it is what a router sets. Use `.fdy-tabs` + `freeday-tabs.js` (roving tabindex,
12
+ a panel per tab) for in-page tabs; use these classes on plain links for routed ones, and
13
+ `.fdy-nav--horizontal` for an application's PRIMARY navigation. */
14
+ .fdy-tabs__tab[aria-selected="true"],.fdy-tabs__tab[aria-current="page"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
9
15
  .fdy-tabs__tab:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
10
16
  .fdy-tabs__panel{padding:var(--space-5) 0;}
11
17
  .fdy-tabs__panel[hidden]{display:none;}