@cahyo-dimas/freeday 1.22.0 → 1.24.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,92 @@
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.24.0] — 2026-08-13
7
+ Improvement note #41: the upload row had no "chosen, not yet sent" state, so every consumer-driven
8
+ integration showed a transfer that had not started.
9
+ ### Fixed
10
+ - **A dropped file now rests until the consumer starts the transfer.** `handleFiles` called
11
+ `row.uploading()` unconditionally — and *before* dispatching `fdy-upload-add`, so a consumer could
12
+ not pre-empt it. Between the drop and the app's own submit button (which may be a minute, while the
13
+ user fills in the rest of the form) the row claimed to be uploading, with a progress bar that never
14
+ moved. A user watching that reasonably concludes the upload has hung and reports a bug against a
15
+ transfer that was never started. The state machine was missing its start state: `done()` claims
16
+ success, `fail()` claims an error, and `uploading()` was where it already was.
17
+ The demo path is unchanged — with `data-fdy-upload-simulate` the kit *is* performing a transfer, so
18
+ showing one stays correct. That attribute already marks the only place the old default was right,
19
+ which is why this changes the default rather than adding an opt-in: making correct integrations opt
20
+ in to correctness is backwards.
21
+ - **`fdy-upload-add` no longer depends on rendering a row.** The guard was `if (!list || !fileList)
22
+ return;`, so a dropzone with no file list lost the event that tells the app a file arrived —
23
+ "bring your own row" silently cost you the notification. Rendering and announcing are now separate:
24
+ no list means no row is attached, and the event still fires with a working `detail.row`.
25
+ Deliberately **no new attribute** for this. The reporter proposed `data-rows="off"`; `list` is used
26
+ in exactly two places, so decoupling covers the same case without growing the API surface, and no
27
+ page can be relying on "no event".
28
+ ### Added
29
+ - **`row.ready()`** — the rest state, and the way back to it (a failed attempt the user will retry).
30
+ It reuses the existing `dropProgress()`, so the bar is removed exactly as `done()`/`fail()` do it.
31
+ ### Docs
32
+ - **`COMPONENTS.md` documents the row state machine at all.** `uploading()` / `setProgress()` /
33
+ `done()` / `fail()` had **zero** mentions anywhere in the shipped docs — a consumer holding
34
+ `detail.row` had no supported way to know they existed, which is a fair part of why the old default
35
+ went unquestioned. The section now carries the state table, a worked example, the
36
+ bring-your-own-row position, and `data-fdy-upload-simulate` marked demo-only.
37
+ Two corrections to the report while transcribing it: the function is `handleFiles` (not `addFiles`)
38
+ and the attribute is `data-fdy-upload-simulate` (not `data-simulate`) — the latter matters, since
39
+ documenting the wrong name would have consumers set an attribute that does nothing.
40
+ ### Added — guards
41
+ - **`browser/upload-states.mjs`** — drops a real `File` and asserts the rest state shows no progress
42
+ bar, that `uploading()` → `setProgress()` → `done()` still chains, that the simulate path is
43
+ untouched, and that a listless dropzone still dispatches. Mutation-checked on both halves.
44
+ The fixture wraps the listless dropzone in its own container **on purpose**: with no
45
+ `data-filelist` the enhancer falls back to `parentNode.querySelector('.fdy-filelist')`, so a bare
46
+ dropzone sharing a parent with another list adopts it — the first version of this guard was
47
+ testing nothing, and the mutation run is what exposed that.
48
+
49
+ ## [1.23.0] — 2026-08-13
50
+ Consumption round 6 (`improvement-notes/006`). The reporter filed two of the three as **their own**
51
+ bugs rather than the kit's — and they were right about the code, but in both cases the kit had a way
52
+ to make the mistake impossible and had not taken it. Those are the two most valuable findings here.
53
+ ### Fixed
54
+ - **`initAll(ctx)` now enhances `ctx` itself, not just its descendants.** `querySelectorAll` never
55
+ matches its own root, so a framework ref placed **on** the widget — `<div ref="menu" data-fdy-menu>`,
56
+ which is the ordinary shape when a component's root element *is* the widget — meant the one element
57
+ that needed enhancing was the only one that could not be found. It failed with **no error, no
58
+ warning, and a UI that looked finished**: the markup rendered and simply never opened. Measured
59
+ before the fix: `data-fdy-menu-ready` stayed `null` and `aria-expanded` stayed `"false"` while
60
+ `initAll` was present and callable the whole time; `initAll(parent)` worked. One `matches()` line
61
+ per selector, in **all 21 enhancers** (`drawer` and `cfl` needed their inline trigger callbacks
62
+ hoisted first). Init was already idempotent via each widget's `-ready` flag, so a root that is also
63
+ matched by the descendant query costs nothing. `useFreeday` (Vue/React) and `FreedayBlazor.initAll`
64
+ delegate to these, so they inherit the fix with no change of their own.
65
+ - **The stepper connector no longer draws through the markers when `__btn` is omitted.** The lift now
66
+ lives on `.fdy-step__marker` — the part that must always exist — instead of only on
67
+ `.fdy-step__btn`. The connector is `position:absolute; z-index:0`, and a positioned box paints
68
+ *after* in-flow inline content in the same stacking context, so a marker that was merely inline got
69
+ painted over: not for lack of a z-index, but for lack of being positioned at all. An
70
+ indicator-only stepper (no navigation, so no button) is a reasonable thing to write and it produced
71
+ a visible defect in shipped UI. Measured: hit-testing the marker's centre returned the connector's
72
+ own `.fdy-step` before, the marker after. `__btn` keeps its z-index for the navigable case.
73
+ ### Added
74
+ - **`.fdy-avatar--xs`** (1.5rem, `--text-xs`) — an avatar inside a control had nothing to reach for:
75
+ `.fdy-btn--sm` is `calc(var(--control-h) - var(--space-2))` = **2rem**, exactly `--avatar--sm`'s
76
+ size, so the monogram filled a small button edge to edge and the ghost border crossed the circle
77
+ (measured: trigger 32px tall, avatar 32×32). 1.5rem inside 2rem leaves the ~4px the kit's other
78
+ controls give their icons. The report suggested `--text-2xs`; that token does not exist — `--text-xs`
79
+ is the smallest the kit has, and inventing a type step for one avatar size is not worth it.
80
+ ### Added — guards
81
+ - **`browser/root-init.mjs`** — mounts a widget *after* load, initialises it through its own root, and
82
+ drives a **real click** to prove it opens; repeated for a second enhancer so the fix reads as the
83
+ shared pattern rather than a one-off. Mutation-checked on both. Being marked `-ready` is explicitly
84
+ not accepted as passing: an early version of this check reported success against markup the enhancer
85
+ had actually bailed on.
86
+ ### Docs
87
+ - `COMPONENTS.md`: `initAll(root)` documents that the root may be the widget itself; the stepper
88
+ section states that `__btn` is **optional** and shows the read-only indicator markup (the parts list
89
+ never said which parts were optional); avatar gains its size table with the in-control guidance.
90
+ - Both `useFreeday` docstrings now show the ref-on-the-widget shape alongside the wrapping one.
91
+
6
92
  ## [1.22.0] — 2026-08-12
7
93
  Two bodies of work. **(a)** three more findings from consumption round 5 — the report grew §5-§7
8
94
  after 1.21.0 was cut; **(b)** a **routing** failure found in the same adopted project, which had been
package/COMPONENTS.md CHANGED
@@ -65,7 +65,9 @@ Two consequences for you:
65
65
  ## Enhancers — hook, script, global, events
66
66
 
67
67
  Every enhancer is zero-dependency, auto-initialises once on `DOMContentLoaded`, and is idempotent:
68
- 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
69
71
  `CustomEvent`s; the payload is in `event.detail`.
70
72
 
71
73
  | Markup hook | Script | Global | Emits |
@@ -476,9 +478,41 @@ Click/Enter opens the file dialog; drop works too. Needs `freeday-upload.js`.
476
478
  `<input type="file">`
477
479
  - List: `.fdy-filelist` (+`--grid`) of `.fdy-file` (+`--success`, `--error`) · `__icon` `__meta`
478
480
  `__name` `__sub` `__progress` `__remove`
479
- - Attributes: `data-max-size` (bytes), `data-filelist="#id"`
481
+ - Attributes: `data-max-size` (bytes), `data-filelist="#id"`, `data-fdy-upload-simulate` (demo only
482
+ — the kit fakes a transfer to `done()`; never set it in an app)
480
483
  - A11y: the dropzone is `role="button" tabindex="0"` + `aria-label`.
481
- - Wire `fdy-upload-add` to your real upload; render explicit progress/success/error state.
484
+
485
+ **The row is yours to drive.** `fdy-upload-add` carries `detail.row`, a small state machine over the
486
+ rendered `.fdy-file`. A dropped file **rests** — it shows its size and nothing else — until you say a
487
+ transfer started; the kit never claims one it is not performing.
488
+
489
+ | `detail.row` | State it renders |
490
+ |---|---|
491
+ | *(initial)* | **rest** — chosen, not sent. Size only, no progress bar. |
492
+ | `.uploading()` | in flight — adds the progress bar |
493
+ | `.setProgress(pct)` | moves the bar (0–100) |
494
+ | `.done()` | success — drops the bar, `.fdy-file--success` |
495
+ | `.fail(msg)` | error — drops the bar, `.fdy-file--error`, `msg` replaces the sub-line |
496
+ | `.ready()` | back to **rest** (e.g. after a failed attempt the user will retry) |
497
+ | `.el` | the row element |
498
+
499
+ ```js
500
+ zone.addEventListener('fdy-upload-add', (e) => {
501
+ if (e.detail.rejected) return; // the kit already rendered the reason
502
+ const { file, row } = e.detail; // row is at rest — nothing is in flight yet
503
+ submitBtn.onclick = async () => {
504
+ row.uploading();
505
+ await send(file, (pct) => row.setProgress(pct));
506
+ row.done();
507
+ };
508
+ });
509
+ ```
510
+
511
+ **Bring your own row:** omit the file list entirely (no `data-filelist`, and no `.fdy-filelist`
512
+ sibling) and the enhancer renders nothing while still dispatching `fdy-upload-add` — `detail.row`
513
+ still works, its element simply isn't attached. Note the fallback when `data-filelist` is absent is
514
+ `parentNode.querySelector('.fdy-filelist')`, so a bare dropzone will adopt a list that happens to
515
+ share its parent; give the dropzone its own container if you mean "no list".
482
516
 
483
517
  ## Form validation — `data-fdy-validate`
484
518
  Native Constraint Validation wired to accessible inline errors: `aria-invalid` +
@@ -526,6 +560,18 @@ Linear multi-step flow: marker → check, one panel at a time, back/next. Needs
526
560
 
527
561
  # Data
528
562
 
563
+ **`__btn` is optional.** For a **read-only indicator** put `__marker` + `__label` straight in the
564
+ `<li>`; for a **navigable** stepper wrap them in `<button class="fdy-step__btn">` (add `disabled`
565
+ for a step that is not reachable yet). Both render identically — the marker carries its own lift
566
+ over the connector line, so omitting `__btn` cannot make the connector draw through the numbers.
567
+
568
+ ```html
569
+ <ol class="fdy-stepper">
570
+ <li class="fdy-step is-complete"><span class="fdy-step__marker">1</span><span class="fdy-step__label">Email</span></li>
571
+ <li class="fdy-step is-active"><span class="fdy-step__marker">2</span><span class="fdy-step__label">Verify</span></li>
572
+ </ol>
573
+ ```
574
+
529
575
  ## Table — `.fdy-table`
530
576
  Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-table-scroll`
531
577
  (horizontal scroll). Always a `<caption>` (use `.fdy-visually-hidden` if it shouldn't show) and
@@ -813,6 +859,10 @@ Initials or image. Sizes `--sm` / default / `--lg`; `.fdy-avatar-group` stacks t
813
859
  group). `--tone-1`…`--tone-8` are decorative tints (WCAG AA in both themes) to distinguish
814
860
  same-initial avatars — hash the index off the **full** name, not the initials.
815
861
 
862
+ Sizes: `--xs` 1.5rem · *(base)* 2.5rem · `--sm` 2rem · `--lg` 3.5rem. Use **`--xs` inside a control**
863
+ — `.fdy-btn--sm` is itself 2rem tall, so an `--sm` avatar fills a small button edge to edge and the
864
+ ghost border cuts across the circle.
865
+
816
866
  ## Chip — `.fdy-chip`
817
867
  Three roles inside a `.fdy-chips` row:
818
868
 
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.22.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.22.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.24.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.24.0)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic — satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs — the UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.22.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.22.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.24.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.24.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`.
@@ -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') {
@@ -16,19 +16,24 @@
16
16
  });
17
17
  }
18
18
 
19
+ function bindTrigger(btn) {
20
+ if (btn.dataset.fdyDrawerBound === '1') return;
21
+ var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
22
+ if (!dialog) return;
23
+ initDrawer(dialog);
24
+ btn.dataset.fdyDrawerBound = '1';
25
+ btn.addEventListener('click', function () {
26
+ if (typeof dialog.showModal === 'function') dialog.showModal();
27
+ else dialog.setAttribute('open', '');
28
+ });
29
+ }
30
+
19
31
  function initTriggers(context) {
20
32
  var ctx = context || document;
21
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-drawer]'), function (btn) {
22
- if (btn.dataset.fdyDrawerBound === '1') return;
23
- var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
24
- if (!dialog) return;
25
- initDrawer(dialog);
26
- btn.dataset.fdyDrawerBound = '1';
27
- btn.addEventListener('click', function () {
28
- if (typeof dialog.showModal === 'function') dialog.showModal();
29
- else dialog.setAttribute('open', '');
30
- });
31
- });
33
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
34
+ if (ctx.matches && ctx.matches('[data-fdy-drawer]')) bindTrigger(ctx);
35
+ if (ctx.matches && ctx.matches('.fdy-drawer')) initDrawer(ctx);
36
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-drawer]'), bindTrigger);
32
37
  // Init any drawer dialogs even without a wired trigger (backdrop/close still work).
33
38
  Array.prototype.forEach.call(ctx.querySelectorAll('.fdy-drawer'), initDrawer);
34
39
  }
@@ -168,7 +168,10 @@
168
168
  }
169
169
 
170
170
  function initAll(context) {
171
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-validate]'), initForm);
171
+ var root = context || document;
172
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
173
+ if (root.matches && root.matches('[data-fdy-validate]')) initForm(root);
174
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-validate]'), initForm);
172
175
  }
173
176
 
174
177
  if (document.readyState === 'loading') {
@@ -100,6 +100,9 @@
100
100
 
101
101
  function initAll(context) {
102
102
  var root = context || document;
103
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
104
+ if (root.matches && root.matches('[data-fdy-mask]')) initMask(root);
105
+ if (root.matches && root.matches('[data-fdy-password]')) initReveal(root);
103
106
  Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-mask]'), initMask);
104
107
  Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-password]'), initReveal);
105
108
  }
@@ -80,7 +80,10 @@
80
80
  }
81
81
 
82
82
  function initAll(ctx) {
83
- Array.prototype.forEach.call((ctx || document).querySelectorAll('[data-fdy-menu]'), initMenu);
83
+ var root = ctx || document;
84
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
85
+ if (root.matches && root.matches('[data-fdy-menu]')) initMenu(root);
86
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-menu]'), initMenu);
84
87
  }
85
88
 
86
89
  if (document.readyState === 'loading') {
@@ -37,7 +37,10 @@
37
37
  }
38
38
 
39
39
  function initAll(context) {
40
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-rating]'), initRating);
40
+ var root = context || document;
41
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
42
+ if (root.matches && root.matches('[data-fdy-rating]')) initRating(root);
43
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-rating]'), initRating);
41
44
  }
42
45
 
43
46
  if (document.readyState === 'loading') {
@@ -189,11 +189,10 @@
189
189
  }
190
190
 
191
191
  function initAll(context) {
192
- var scope = context || document;
193
- Array.prototype.forEach.call(
194
- scope.querySelectorAll('[data-fdy-combo]'),
195
- initCombo
196
- );
192
+ var root = context || document;
193
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
194
+ if (root.matches && root.matches('[data-fdy-combo]')) initCombo(root);
195
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-combo]'), initCombo);
197
196
  }
198
197
 
199
198
  // One global handler closes the open combo on an outside click.
@@ -21,7 +21,10 @@
21
21
  }
22
22
 
23
23
  function initAll(context) {
24
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-slider]'), initSlider);
24
+ var root = context || document;
25
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
26
+ if (root.matches && root.matches('[data-fdy-slider]')) initSlider(root);
27
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-slider]'), initSlider);
25
28
  }
26
29
 
27
30
  if (document.readyState === 'loading') {
@@ -77,7 +77,10 @@
77
77
  }
78
78
 
79
79
  function initAll(context) {
80
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-stepper]'), initStepper);
80
+ var root = context || document;
81
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
82
+ if (root.matches && root.matches('[data-fdy-stepper]')) initStepper(root);
83
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-stepper]'), initStepper);
81
84
  }
82
85
 
83
86
  if (document.readyState === 'loading') {
@@ -459,10 +459,10 @@
459
459
  }
460
460
 
461
461
  function initAll(context) {
462
- Array.prototype.forEach.call(
463
- (context || document).querySelectorAll('[data-fdy-table]'),
464
- initTable
465
- );
462
+ var root = context || document;
463
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
464
+ if (root.matches && root.matches('[data-fdy-table]')) initTable(root);
465
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-table]'), initTable);
466
466
  }
467
467
 
468
468
  if (document.readyState === 'loading') {
@@ -52,10 +52,10 @@
52
52
  }
53
53
 
54
54
  function initAll(context) {
55
- Array.prototype.forEach.call(
56
- (context || document).querySelectorAll('[data-fdy-tabs]'),
57
- initTabs
58
- );
55
+ var root = context || document;
56
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
57
+ if (root.matches && root.matches('[data-fdy-tabs]')) initTabs(root);
58
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-tabs]'), initTabs);
59
59
  }
60
60
 
61
61
  if (document.readyState === 'loading') {
@@ -167,7 +167,10 @@
167
167
  }
168
168
 
169
169
  function initAll(context) {
170
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-timepicker]'), initTime);
170
+ var root = context || document;
171
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
172
+ if (root.matches && root.matches('[data-fdy-timepicker]')) initTime(root);
173
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-timepicker]'), initTime);
171
174
  }
172
175
 
173
176
  if (document.readyState === 'loading') {
@@ -81,7 +81,10 @@
81
81
  }
82
82
 
83
83
  function initAll(context) {
84
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-tree]'), initTree);
84
+ var root = context || document;
85
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
86
+ if (root.matches && root.matches('[data-fdy-tree]')) initTree(root);
87
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-tree]'), initTree);
85
88
  }
86
89
 
87
90
  if (document.readyState === 'loading') {
@@ -95,6 +95,16 @@
95
95
  }
96
96
  return {
97
97
  el: el,
98
+ /* The state a row starts in: chosen, not yet sent. The size alone — it makes no claim about
99
+ a transfer, and no progress bar is shown, because nothing is in flight. Without this the
100
+ state machine had no start state: done() claims success, fail() claims an error, and
101
+ uploading() is a lie until the consumer actually sends the file. */
102
+ ready: function () {
103
+ el.classList.remove('fdy-file--error', 'fdy-file--success');
104
+ icon.innerHTML = FILE_ICON;
105
+ sub.textContent = fmtSize(file.size);
106
+ dropProgress();
107
+ },
98
108
  uploading: function () {
99
109
  el.classList.remove('fdy-file--error', 'fdy-file--success');
100
110
  icon.innerHTML = FILE_ICON;
@@ -171,19 +181,30 @@
171
181
  });
172
182
  if (input) input.addEventListener('change', function () { handleFiles(input.files); input.value = ''; });
173
183
 
184
+ /* The list is optional: rendering a row and announcing the file are separate jobs. A consumer
185
+ that wants its own markup simply provides no list — it still gets `fdy-upload-add`, and
186
+ `detail.row` still works (its element is just never attached). Gating the EVENT on the list
187
+ meant "bring your own row" silently cost you the notification that a file had arrived. */
174
188
  function handleFiles(fileList) {
175
- if (!list || !fileList) return;
189
+ if (!fileList) return;
176
190
  Array.prototype.slice.call(fileList).forEach(function (file) {
177
191
  var reason = null;
178
192
  if (!accepts(file, acceptAttr)) reason = 'Tipe berkas tidak didukung.';
179
193
  else if (maxSize && file.size > maxSize) reason = 'Ukuran melebihi batas (' + fmtSize(maxSize) + ').';
180
194
  var row = makeRow(file);
181
- list.appendChild(row.el);
195
+ if (list) list.appendChild(row.el);
182
196
  if (reason) {
183
197
  row.fail(reason);
184
- } else {
198
+ } else if (simulate) {
199
+ /* The kit is driving (demo path): keep showing a transfer, because there is one. */
185
200
  row.uploading();
186
- if (simulate) simulateUpload(row);
201
+ simulateUpload(row);
202
+ } else {
203
+ /* The CONSUMER owns the transfer. Rest until it says otherwise by calling row.uploading():
204
+ a file that has only been chosen must not claim to be uploading. A progress bar that
205
+ never moves reads as a hung upload, and gets reported as a bug against a transfer that
206
+ was never started. */
207
+ row.ready();
187
208
  }
188
209
  zone.dispatchEvent(new CustomEvent('fdy-upload-add', {
189
210
  bubbles: true, detail: { file: file, rejected: !!reason, reason: reason, row: row }
@@ -193,7 +214,10 @@
193
214
  }
194
215
 
195
216
  function initAll(context) {
196
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-dropzone]'), initDropzone);
217
+ var root = context || document;
218
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
219
+ if (root.matches && root.matches('[data-fdy-dropzone]')) initDropzone(root);
220
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-dropzone]'), initDropzone);
197
221
  }
198
222
 
199
223
  if (document.readyState === 'loading') {
@@ -586,6 +586,11 @@ a { color: var(--color-primary); }
586
586
  /* Freeday — Avatar */
587
587
  .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;}
588
588
  .fdy-avatar img{width:100%;height:100%;object-fit:cover;}
589
+ /* --xs exists for an avatar INSIDE a control: `.fdy-btn--sm` is exactly 2rem tall, which is also
590
+ `--sm`'s size, so a small avatar in a small button fills it edge to edge and the ghost border
591
+ crosses the circle. 1.5rem leaves the same ~4px breathing room the kit's controls give their
592
+ icons. Uses `--text-xs` (the smallest type token the kit has — there is no `2xs`). */
593
+ .fdy-avatar--xs{width:1.5rem;height:1.5rem;font-size:var(--text-xs);}
589
594
  .fdy-avatar--sm{width:2rem;height:2rem;font-size:var(--text-xs);}
590
595
  .fdy-avatar--lg{width:3.5rem;height:3.5rem;font-size:var(--text-lg);}
591
596
  .fdy-avatar-group{display:inline-flex;}
@@ -1457,7 +1462,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
1457
1462
  .fdy-step.is-active::before,.fdy-step.is-complete::before{background:var(--color-primary);}
1458
1463
  .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%;}
1459
1464
  .fdy-step__btn:disabled{cursor:default;}
1460
- .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;}
1465
+ /* The marker carries the lift itself, NOT `__btn`. The connector above is position:absolute with
1466
+ z-index:0, and a positioned box paints after in-flow inline content in the same stacking context —
1467
+ so a marker that is merely inline (an indicator-only stepper with no `__btn`) gets painted THROUGH
1468
+ by the connector line. Lifting the part that must always exist makes that assembly impossible;
1469
+ `__btn`'s own z-index stays, harmlessly, for the navigable case. */
1470
+ .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;}
1461
1471
  .fdy-step__marker svg{display:block;width:1rem;height:1rem;}
1462
1472
  .fdy-step.is-active .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary-strong);}
1463
1473
  .fdy-step.is-complete .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary);color:var(--color-on-primary);}
package/dist/freeday.css CHANGED
@@ -216,6 +216,11 @@ a { color: var(--color-primary); }
216
216
  /* Freeday — Avatar */
217
217
  .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;}
218
218
  .fdy-avatar img{width:100%;height:100%;object-fit:cover;}
219
+ /* --xs exists for an avatar INSIDE a control: `.fdy-btn--sm` is exactly 2rem tall, which is also
220
+ `--sm`'s size, so a small avatar in a small button fills it edge to edge and the ghost border
221
+ crosses the circle. 1.5rem leaves the same ~4px breathing room the kit's controls give their
222
+ icons. Uses `--text-xs` (the smallest type token the kit has — there is no `2xs`). */
223
+ .fdy-avatar--xs{width:1.5rem;height:1.5rem;font-size:var(--text-xs);}
219
224
  .fdy-avatar--sm{width:2rem;height:2rem;font-size:var(--text-xs);}
220
225
  .fdy-avatar--lg{width:3.5rem;height:3.5rem;font-size:var(--text-lg);}
221
226
  .fdy-avatar-group{display:inline-flex;}
@@ -1087,7 +1092,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
1087
1092
  .fdy-step.is-active::before,.fdy-step.is-complete::before{background:var(--color-primary);}
1088
1093
  .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%;}
1089
1094
  .fdy-step__btn:disabled{cursor:default;}
1090
- .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;}
1095
+ /* The marker carries the lift itself, NOT `__btn`. The connector above is position:absolute with
1096
+ z-index:0, and a positioned box paints after in-flow inline content in the same stacking context —
1097
+ so a marker that is merely inline (an indicator-only stepper with no `__btn`) gets painted THROUGH
1098
+ by the connector line. Lifting the part that must always exist makes that assembly impossible;
1099
+ `__btn`'s own z-index stays, harmlessly, for the navigable case. */
1100
+ .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;}
1091
1101
  .fdy-step__marker svg{display:block;width:1rem;height:1rem;}
1092
1102
  .fdy-step.is-active .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary-strong);}
1093
1103
  .fdy-step.is-complete .fdy-step__marker{border-color:var(--color-primary);background:var(--color-primary);color:var(--color-on-primary);}
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') {
@@ -3616,6 +3670,16 @@
3616
3670
  }
3617
3671
  return {
3618
3672
  el: el,
3673
+ /* The state a row starts in: chosen, not yet sent. The size alone — it makes no claim about
3674
+ a transfer, and no progress bar is shown, because nothing is in flight. Without this the
3675
+ state machine had no start state: done() claims success, fail() claims an error, and
3676
+ uploading() is a lie until the consumer actually sends the file. */
3677
+ ready: function () {
3678
+ el.classList.remove('fdy-file--error', 'fdy-file--success');
3679
+ icon.innerHTML = FILE_ICON;
3680
+ sub.textContent = fmtSize(file.size);
3681
+ dropProgress();
3682
+ },
3619
3683
  uploading: function () {
3620
3684
  el.classList.remove('fdy-file--error', 'fdy-file--success');
3621
3685
  icon.innerHTML = FILE_ICON;
@@ -3692,19 +3756,30 @@
3692
3756
  });
3693
3757
  if (input) input.addEventListener('change', function () { handleFiles(input.files); input.value = ''; });
3694
3758
 
3759
+ /* The list is optional: rendering a row and announcing the file are separate jobs. A consumer
3760
+ that wants its own markup simply provides no list — it still gets `fdy-upload-add`, and
3761
+ `detail.row` still works (its element is just never attached). Gating the EVENT on the list
3762
+ meant "bring your own row" silently cost you the notification that a file had arrived. */
3695
3763
  function handleFiles(fileList) {
3696
- if (!list || !fileList) return;
3764
+ if (!fileList) return;
3697
3765
  Array.prototype.slice.call(fileList).forEach(function (file) {
3698
3766
  var reason = null;
3699
3767
  if (!accepts(file, acceptAttr)) reason = 'Tipe berkas tidak didukung.';
3700
3768
  else if (maxSize && file.size > maxSize) reason = 'Ukuran melebihi batas (' + fmtSize(maxSize) + ').';
3701
3769
  var row = makeRow(file);
3702
- list.appendChild(row.el);
3770
+ if (list) list.appendChild(row.el);
3703
3771
  if (reason) {
3704
3772
  row.fail(reason);
3705
- } else {
3773
+ } else if (simulate) {
3774
+ /* The kit is driving (demo path): keep showing a transfer, because there is one. */
3706
3775
  row.uploading();
3707
- if (simulate) simulateUpload(row);
3776
+ simulateUpload(row);
3777
+ } else {
3778
+ /* The CONSUMER owns the transfer. Rest until it says otherwise by calling row.uploading():
3779
+ a file that has only been chosen must not claim to be uploading. A progress bar that
3780
+ never moves reads as a hung upload, and gets reported as a bug against a transfer that
3781
+ was never started. */
3782
+ row.ready();
3708
3783
  }
3709
3784
  zone.dispatchEvent(new CustomEvent('fdy-upload-add', {
3710
3785
  bubbles: true, detail: { file: file, rejected: !!reason, reason: reason, row: row }
@@ -3714,7 +3789,10 @@
3714
3789
  }
3715
3790
 
3716
3791
  function initAll(context) {
3717
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-dropzone]'), initDropzone);
3792
+ var root = context || document;
3793
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
3794
+ if (root.matches && root.matches('[data-fdy-dropzone]')) initDropzone(root);
3795
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-dropzone]'), initDropzone);
3718
3796
  }
3719
3797
 
3720
3798
  if (document.readyState === 'loading') {
@@ -184,7 +184,7 @@ live docs also have a copy button per component.
184
184
  ```bash
185
185
  npm i @cahyo-dimas/freeday
186
186
  ```
187
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.22.0"` (public npm package). `dist/` is
187
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.24.0"` (public npm package). `dist/` is
188
188
  committed and published → no build step; `npm ci` runs without auth.
189
189
 
190
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.22.0",
3
+ "version": "1.24.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 browser/state.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 browser/upload-states.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"
@@ -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;}
@@ -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);}