@cahyo-dimas/freeday 1.49.0 → 1.50.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,39 @@
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.50.0] — 2026-08-20
7
+ ### Fixed
8
+ - **A toast raised from inside a modal was painted behind it** (#027). `.fdy-toast-region` carried
9
+ `z-index: 200`, which can never win: `.fdy-modal` is a native `<dialog>` opened with `showModal()`
10
+ and therefore lives in the **top layer**, above the whole z-index universe. An error raised by an
11
+ action taken inside a dialog was dimmed by that dialog's backdrop and mostly hidden under the
12
+ dialog — so the reader saw a confirmation still asking a question they had already answered, and
13
+ no reason why. The region is now `popover="manual"` and `freeday-toast.js` calls `showPopover()`
14
+ as each toast lands (per toast, not once — the top layer is ordered by when you joined, so a
15
+ dialog opened after the last toast would otherwise sit above it). Guarded: where `showPopover` is
16
+ unavailable or throws, the region stays exactly what it was.
17
+ - **A checkbox shrank when its label was long** (#027). `.fdy-check` is an inline-flex row and its
18
+ box declared a width — but a flex item with a width is still shrinkable, so a label that wrapped
19
+ to three lines squeezed the box from 18px to 13px while leaving it 18px tall. A group of five
20
+ rendered three different sizes, none of them square. `flex: none` on `.fdy-check input`,
21
+ `.fdy-radio input`, the `.fdy-switch` track and the standalone `.fdy-checkbox` — all four are
22
+ flex children somewhere in the kit.
23
+ ### Added — guards
24
+ - **`pixelAt(x, y)` in `browser/harness.mjs`** — a 1×1 `Page.captureScreenshot`, decoded in-process.
25
+ Stacking above a modal cannot be asserted with `elementFromPoint`: a modal dialog makes the rest
26
+ of the document inert, so a hit test outside it returns the dialog whatever the paint order is,
27
+ reporting a working fix as broken. The new guard measures the colour actually composited at the
28
+ toast's centre. It waits for both fade-ins to finish first — a screenshot taken mid-animation
29
+ returns a blend of the two elements and flakes in both directions.
30
+ - **Selection controls are asserted square and equal** across a group whose labels run one to three
31
+ lines, so a box that shrank in both axes cannot pass a width-only comparison. All four controls
32
+ the fix touched are measured, each in the container it actually ships in: the standalone
33
+ `.fdy-checkbox` inside `.fdy-filter__check` — the flex row all three typed adapters render in a
34
+ filter popover and a CFL option list — and the switch track against a switch of its own, since it
35
+ is the one control that is not square. The first version of this guard held the standalone
36
+ checkbox in a plain block, where it cannot shrink at all, so removing `flex: none` from the one
37
+ box every adapter puts on screen left it passing.
38
+
6
39
  ## [1.49.0] — 2026-08-20
7
40
  ### Added
8
41
  - **`FdyTableColumn.labelHidden`** (#026). A column of row CONTROLS — an edit button, a row menu —
package/COMPONENTS.md CHANGED
@@ -935,6 +935,13 @@ Every field is optional; it returns the toast element. Classes (rendered for you
935
935
  `.fdy-toast-region`, `.fdy-toast` (+`--info` `--success` `--warning` `--danger`) · `__accent`
936
936
  `__body` `__title` `__text` `__close`.
937
937
 
938
+ **Above modals, and not by z-index.** The region is a `popover` and re-enters the **top layer** as
939
+ each toast lands, because `.fdy-modal` is a native `<dialog>` — see the note under Modal below. If
940
+ a toast of yours is hidden behind something, a bigger `z-index` is not the fix; check whether that
941
+ something is in the top layer too. Where the Popover API is unavailable (before Chrome 114 /
942
+ Safari 17 / Firefox 125) the region falls back to a fixed block at `z-index: 200`, which cannot
943
+ clear an open modal — the message is still announced to a screen reader.
944
+
938
945
  ## Tooltip — `.fdy-tooltip`
939
946
  Hover/focus only, never the sole carrier of information. Wrap in `.fdy-tooltip-wrap`; the trigger
940
947
  gets `aria-describedby` → the `.fdy-tooltip[role="tooltip"]` `id`.
@@ -1019,7 +1026,13 @@ so the same look serves routed sub-navigation built from plain links — see the
1019
1026
  > **Typed wrapper: `<FdyModal>`** — Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
1020
1027
 
1021
1028
  Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
1022
- `--wide`; `--cfl` for the choose-from-list dialog. Parts `__header` `__title` `__body` `__footer`
1029
+ `--wide`; `--cfl` for the choose-from-list dialog.
1030
+
1031
+ **It is in the TOP LAYER.** `showModal()` paints the dialog above every stacking context on the
1032
+ page, so no `z-index` anywhere can put anything over it — only another top-layer element (a
1033
+ `popover`, or another modal) can. It also makes the rest of the document inert, which means
1034
+ `elementFromPoint` outside the dialog returns the dialog: do not use a hit test to check what is
1035
+ painted on top of a modal, because it will tell you the dialog is, even when it visibly is not. Parts `__header` `__title` `__body` `__footer`
1023
1036
  `__close`. The body scrolls; the footer never clips. `aria-labelledby` → the title's `id`;
1024
1037
  `data-close` on any button that should close it.
1025
1038
 
@@ -28,11 +28,39 @@
28
28
  region.className = 'fdy-toast-region';
29
29
  region.setAttribute('aria-live', 'polite');
30
30
  region.setAttribute('aria-atomic', 'false');
31
+ /* `manual`, not `auto`: an auto popover light-dismisses on an outside click and closes
32
+ itself the moment another popover opens. A toast is dismissed by its timer or its own
33
+ close button, by nobody else. */
34
+ region.setAttribute('popover', 'manual');
31
35
  document.body.appendChild(region);
32
36
  }
33
37
  return region;
34
38
  }
35
39
 
40
+ /* Put the region in the TOP LAYER, above any open <dialog>.
41
+ *
42
+ * A modal dialog lives in the top layer, where z-index does not reach — so a toast raised by an
43
+ * action taken inside a modal was painted behind that modal's backdrop, which is where an error
44
+ * message is least useful. showPopover() joins the same layer, and membership there is ordered by
45
+ * when you joined: hide-then-show re-joins at the top, so a toast shown while a dialog is open
46
+ * lands above it rather than under the dialog that was opened after the last toast.
47
+ *
48
+ * Guarded twice on purpose. `showPopover` is absent before Chrome 114 / Safari 17 / Firefox 125,
49
+ * and the call throws if the element is disconnected or already open — in every one of those
50
+ * cases the region stays an ordinary fixed block at z-index 200, which is what it has always
51
+ * been. A notification must never be the thing that throws.
52
+ */
53
+ function raise(node) {
54
+ if (!node || typeof node.showPopover !== 'function') return;
55
+ try {
56
+ if (node.matches(':popover-open')) node.hidePopover();
57
+ node.showPopover();
58
+ } catch (e) {
59
+ /* Not reachable through the guards above, but a toast is on the error path itself: if
60
+ raising it fails the message still has to appear, one layer down. */
61
+ }
62
+ }
63
+
36
64
  function dismiss(toast) {
37
65
  if (typeof toast === 'string') toast = keyed[toast]; // dismiss by key
38
66
  if (!toast || toast.dataset.leaving === '1') return;
@@ -110,6 +138,9 @@
110
138
  keyed[opts.key] = node;
111
139
  }
112
140
  region.appendChild(node);
141
+ // Raised per toast, not once at startup: a dialog opened AFTER the region joined the top layer
142
+ // would otherwise sit above it, which is the exact case this fixes.
143
+ raise(region);
113
144
 
114
145
  var timeout = opts.timeout == null ? 4000 : opts.timeout;
115
146
  if (timeout > 0) setTimeout(function () { dismiss(node); }, timeout);
@@ -1579,8 +1579,15 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1579
1579
  .fdy-rating--sm .fdy-rating__star svg{width:1.1rem;height:1.1rem;}
1580
1580
 
1581
1581
  /* Freeday — Selection controls */
1582
+ /* `flex:none` on every box, track and standalone checkbox is load-bearing, not tidiness.
1583
+ `.fdy-check` is an inline-flex row of [box, label], and a flex item with a WIDTH is still
1584
+ shrinkable — so as soon as the label wrapped to a second line the box gave up horizontal space
1585
+ to it and rendered an 18px-tall rectangle 13px wide. Squashed, off-square, and different from
1586
+ every sibling whose label happened to be short: a form of five checkboxes showed three sizes.
1587
+ Nothing in the declarations looked wrong, because the width was never overridden — it was
1588
+ negotiated away. */
1582
1589
  .fdy-check,.fdy-radio,.fdy-switch{display:inline-flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm);color:var(--color-text);cursor:pointer;}
1583
- .fdy-check input,.fdy-radio input{appearance:none;-webkit-appearance:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
1590
+ .fdy-check input,.fdy-radio input{appearance:none;-webkit-appearance:none;flex:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
1584
1591
  .fdy-check input{border-radius:var(--radius-sm);}
1585
1592
  .fdy-radio input{border-radius:50%;}
1586
1593
  .fdy-check input::before{content:"";width:.62rem;height:.62rem;transform:scale(0);transition:transform var(--dur-fast) var(--ease-spring);box-shadow:inset 1rem 1rem var(--color-on-primary);clip-path:polygon(14% 44%,0 65%,50% 100%,100% 16%,80% 0,43% 62%);}
@@ -1590,13 +1597,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1590
1597
  .fdy-check input:focus-visible,.fdy-radio input:focus-visible,.fdy-switch input:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
1591
1598
  .fdy-check input:disabled,.fdy-radio input:disabled,.fdy-switch input:disabled{opacity:.5;cursor:not-allowed;}
1592
1599
  .fdy-check:has(input:disabled),.fdy-radio:has(input:disabled),.fdy-switch:has(input:disabled){cursor:not-allowed;}
1593
- .fdy-switch input{appearance:none;-webkit-appearance:none;position:relative;width:var(--control-switch-w);height:var(--control-switch-h);margin:0;border-radius:var(--radius-full);background:var(--color-control-border);cursor:pointer;transition:background var(--dur-base) var(--ease-standard);}
1600
+ .fdy-switch input{appearance:none;-webkit-appearance:none;flex:none;position:relative;width:var(--control-switch-w);height:var(--control-switch-h);margin:0;border-radius:var(--radius-full);background:var(--color-control-border);cursor:pointer;transition:background var(--dur-base) var(--ease-standard);}
1594
1601
  .fdy-switch input::before{content:"";position:absolute;top:2px;left:2px;width:calc(var(--control-switch-h) - 4px);height:calc(var(--control-switch-h) - 4px);border-radius:50%;background:var(--color-switch-thumb);box-shadow:var(--shadow-1);transition:transform var(--dur-base) var(--ease-spring);}
1595
1602
  .fdy-switch input:checked{background:var(--color-primary);}
1596
1603
  .fdy-switch input:checked::before{transform:translateX(calc(var(--control-switch-w) - var(--control-switch-h)));}
1597
1604
 
1598
1605
  /* Standalone checkbox (e.g. table row/select-all) — supports :indeterminate */
1599
- .fdy-checkbox{appearance:none;-webkit-appearance:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);border-radius:var(--radius-sm);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
1606
+ .fdy-checkbox{appearance:none;-webkit-appearance:none;flex:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);border-radius:var(--radius-sm);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
1600
1607
  .fdy-checkbox::before{content:"";width:.62rem;height:.62rem;transform:scale(0);transition:transform var(--dur-fast) var(--ease-spring);box-shadow:inset 1rem 1rem var(--color-on-primary);clip-path:polygon(14% 44%,0 65%,50% 100%,100% 16%,80% 0,43% 62%);}
1601
1608
  .fdy-checkbox:checked,.fdy-checkbox:indeterminate{background:var(--color-primary);border-color:var(--color-primary);}
1602
1609
  .fdy-checkbox:checked::before{transform:scale(1);}
@@ -1869,7 +1876,19 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1869
1876
  @media (prefers-reduced-motion:no-preference){.fdy-timepicker__panel:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
1870
1877
 
1871
1878
  /* Freeday — Toast (transient notification) */
1872
- .fdy-toast-region{position:fixed;z-index:200;bottom:var(--space-5);right:var(--space-5);display:flex;flex-direction:column;gap:var(--space-3);width:min(24rem,calc(100vw - var(--space-8)));pointer-events:none;}
1879
+ /* The region is a POPOVER, not merely a high z-index.
1880
+ `.fdy-modal` is a native <dialog> opened with showModal(), which puts it in the TOP LAYER — a
1881
+ layer above the whole z-index universe, so no number here could ever beat it. A toast raised by
1882
+ an action taken inside a modal was painted behind that modal's backdrop: dimmed, unreadable, and
1883
+ most of it under the dialog itself. `freeday-toast.js` calls showPopover() as each toast is
1884
+ added, which puts the region in the top layer too, and last-in wins there.
1885
+ Everything after `pointer-events` is undoing the UA's [popover] defaults — it arrives with
1886
+ inset:0, auto margins, a border, padding, a Canvas background and overflow:auto, none of which a
1887
+ toast stack wants. Where showPopover() is unavailable the element is still an ordinary fixed
1888
+ block at z-index 200, which is exactly what it was before. */
1889
+ .fdy-toast-region{position:fixed;z-index:200;inset:auto;bottom:var(--space-5);right:var(--space-5);display:flex;flex-direction:column;gap:var(--space-3);width:min(24rem,calc(100vw - var(--space-8)));height:auto;max-height:none;margin:0;padding:0;border:0;background:transparent;color:inherit;overflow:visible;pointer-events:none;}
1890
+ /* A popover gets a ::backdrop as well; a toast must not dim the page behind it. */
1891
+ .fdy-toast-region::backdrop{background:transparent;}
1873
1892
  .fdy-toast{pointer-events:auto;display:flex;gap:var(--space-3);align-items:flex-start;padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);background:var(--color-inverse-surface);color:var(--color-inverse-text);box-shadow:var(--shadow-3);}
1874
1893
  .fdy-toast__accent{flex:none;width:.25rem;align-self:stretch;border-radius:var(--radius-full);background:var(--color-primary);}
1875
1894
  .fdy-toast--success .fdy-toast__accent{background:var(--color-success);}
package/dist/freeday.css CHANGED
@@ -1196,8 +1196,15 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1196
1196
  .fdy-rating--sm .fdy-rating__star svg{width:1.1rem;height:1.1rem;}
1197
1197
 
1198
1198
  /* Freeday — Selection controls */
1199
+ /* `flex:none` on every box, track and standalone checkbox is load-bearing, not tidiness.
1200
+ `.fdy-check` is an inline-flex row of [box, label], and a flex item with a WIDTH is still
1201
+ shrinkable — so as soon as the label wrapped to a second line the box gave up horizontal space
1202
+ to it and rendered an 18px-tall rectangle 13px wide. Squashed, off-square, and different from
1203
+ every sibling whose label happened to be short: a form of five checkboxes showed three sizes.
1204
+ Nothing in the declarations looked wrong, because the width was never overridden — it was
1205
+ negotiated away. */
1199
1206
  .fdy-check,.fdy-radio,.fdy-switch{display:inline-flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm);color:var(--color-text);cursor:pointer;}
1200
- .fdy-check input,.fdy-radio input{appearance:none;-webkit-appearance:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
1207
+ .fdy-check input,.fdy-radio input{appearance:none;-webkit-appearance:none;flex:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
1201
1208
  .fdy-check input{border-radius:var(--radius-sm);}
1202
1209
  .fdy-radio input{border-radius:50%;}
1203
1210
  .fdy-check input::before{content:"";width:.62rem;height:.62rem;transform:scale(0);transition:transform var(--dur-fast) var(--ease-spring);box-shadow:inset 1rem 1rem var(--color-on-primary);clip-path:polygon(14% 44%,0 65%,50% 100%,100% 16%,80% 0,43% 62%);}
@@ -1207,13 +1214,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1207
1214
  .fdy-check input:focus-visible,.fdy-radio input:focus-visible,.fdy-switch input:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
1208
1215
  .fdy-check input:disabled,.fdy-radio input:disabled,.fdy-switch input:disabled{opacity:.5;cursor:not-allowed;}
1209
1216
  .fdy-check:has(input:disabled),.fdy-radio:has(input:disabled),.fdy-switch:has(input:disabled){cursor:not-allowed;}
1210
- .fdy-switch input{appearance:none;-webkit-appearance:none;position:relative;width:var(--control-switch-w);height:var(--control-switch-h);margin:0;border-radius:var(--radius-full);background:var(--color-control-border);cursor:pointer;transition:background var(--dur-base) var(--ease-standard);}
1217
+ .fdy-switch input{appearance:none;-webkit-appearance:none;flex:none;position:relative;width:var(--control-switch-w);height:var(--control-switch-h);margin:0;border-radius:var(--radius-full);background:var(--color-control-border);cursor:pointer;transition:background var(--dur-base) var(--ease-standard);}
1211
1218
  .fdy-switch input::before{content:"";position:absolute;top:2px;left:2px;width:calc(var(--control-switch-h) - 4px);height:calc(var(--control-switch-h) - 4px);border-radius:50%;background:var(--color-switch-thumb);box-shadow:var(--shadow-1);transition:transform var(--dur-base) var(--ease-spring);}
1212
1219
  .fdy-switch input:checked{background:var(--color-primary);}
1213
1220
  .fdy-switch input:checked::before{transform:translateX(calc(var(--control-switch-w) - var(--control-switch-h)));}
1214
1221
 
1215
1222
  /* Standalone checkbox (e.g. table row/select-all) — supports :indeterminate */
1216
- .fdy-checkbox{appearance:none;-webkit-appearance:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);border-radius:var(--radius-sm);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
1223
+ .fdy-checkbox{appearance:none;-webkit-appearance:none;flex:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);border-radius:var(--radius-sm);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
1217
1224
  .fdy-checkbox::before{content:"";width:.62rem;height:.62rem;transform:scale(0);transition:transform var(--dur-fast) var(--ease-spring);box-shadow:inset 1rem 1rem var(--color-on-primary);clip-path:polygon(14% 44%,0 65%,50% 100%,100% 16%,80% 0,43% 62%);}
1218
1225
  .fdy-checkbox:checked,.fdy-checkbox:indeterminate{background:var(--color-primary);border-color:var(--color-primary);}
1219
1226
  .fdy-checkbox:checked::before{transform:scale(1);}
@@ -1486,7 +1493,19 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1486
1493
  @media (prefers-reduced-motion:no-preference){.fdy-timepicker__panel:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
1487
1494
 
1488
1495
  /* Freeday — Toast (transient notification) */
1489
- .fdy-toast-region{position:fixed;z-index:200;bottom:var(--space-5);right:var(--space-5);display:flex;flex-direction:column;gap:var(--space-3);width:min(24rem,calc(100vw - var(--space-8)));pointer-events:none;}
1496
+ /* The region is a POPOVER, not merely a high z-index.
1497
+ `.fdy-modal` is a native <dialog> opened with showModal(), which puts it in the TOP LAYER — a
1498
+ layer above the whole z-index universe, so no number here could ever beat it. A toast raised by
1499
+ an action taken inside a modal was painted behind that modal's backdrop: dimmed, unreadable, and
1500
+ most of it under the dialog itself. `freeday-toast.js` calls showPopover() as each toast is
1501
+ added, which puts the region in the top layer too, and last-in wins there.
1502
+ Everything after `pointer-events` is undoing the UA's [popover] defaults — it arrives with
1503
+ inset:0, auto margins, a border, padding, a Canvas background and overflow:auto, none of which a
1504
+ toast stack wants. Where showPopover() is unavailable the element is still an ordinary fixed
1505
+ block at z-index 200, which is exactly what it was before. */
1506
+ .fdy-toast-region{position:fixed;z-index:200;inset:auto;bottom:var(--space-5);right:var(--space-5);display:flex;flex-direction:column;gap:var(--space-3);width:min(24rem,calc(100vw - var(--space-8)));height:auto;max-height:none;margin:0;padding:0;border:0;background:transparent;color:inherit;overflow:visible;pointer-events:none;}
1507
+ /* A popover gets a ::backdrop as well; a toast must not dim the page behind it. */
1508
+ .fdy-toast-region::backdrop{background:transparent;}
1490
1509
  .fdy-toast{pointer-events:auto;display:flex;gap:var(--space-3);align-items:flex-start;padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);background:var(--color-inverse-surface);color:var(--color-inverse-text);box-shadow:var(--shadow-3);}
1491
1510
  .fdy-toast__accent{flex:none;width:.25rem;align-self:stretch;border-radius:var(--radius-full);background:var(--color-primary);}
1492
1511
  .fdy-toast--success .fdy-toast__accent{background:var(--color-success);}
package/dist/freeday.js CHANGED
@@ -3929,11 +3929,39 @@
3929
3929
  region.className = 'fdy-toast-region';
3930
3930
  region.setAttribute('aria-live', 'polite');
3931
3931
  region.setAttribute('aria-atomic', 'false');
3932
+ /* `manual`, not `auto`: an auto popover light-dismisses on an outside click and closes
3933
+ itself the moment another popover opens. A toast is dismissed by its timer or its own
3934
+ close button, by nobody else. */
3935
+ region.setAttribute('popover', 'manual');
3932
3936
  document.body.appendChild(region);
3933
3937
  }
3934
3938
  return region;
3935
3939
  }
3936
3940
 
3941
+ /* Put the region in the TOP LAYER, above any open <dialog>.
3942
+ *
3943
+ * A modal dialog lives in the top layer, where z-index does not reach — so a toast raised by an
3944
+ * action taken inside a modal was painted behind that modal's backdrop, which is where an error
3945
+ * message is least useful. showPopover() joins the same layer, and membership there is ordered by
3946
+ * when you joined: hide-then-show re-joins at the top, so a toast shown while a dialog is open
3947
+ * lands above it rather than under the dialog that was opened after the last toast.
3948
+ *
3949
+ * Guarded twice on purpose. `showPopover` is absent before Chrome 114 / Safari 17 / Firefox 125,
3950
+ * and the call throws if the element is disconnected or already open — in every one of those
3951
+ * cases the region stays an ordinary fixed block at z-index 200, which is what it has always
3952
+ * been. A notification must never be the thing that throws.
3953
+ */
3954
+ function raise(node) {
3955
+ if (!node || typeof node.showPopover !== 'function') return;
3956
+ try {
3957
+ if (node.matches(':popover-open')) node.hidePopover();
3958
+ node.showPopover();
3959
+ } catch (e) {
3960
+ /* Not reachable through the guards above, but a toast is on the error path itself: if
3961
+ raising it fails the message still has to appear, one layer down. */
3962
+ }
3963
+ }
3964
+
3937
3965
  function dismiss(toast) {
3938
3966
  if (typeof toast === 'string') toast = keyed[toast]; // dismiss by key
3939
3967
  if (!toast || toast.dataset.leaving === '1') return;
@@ -4011,6 +4039,9 @@
4011
4039
  keyed[opts.key] = node;
4012
4040
  }
4013
4041
  region.appendChild(node);
4042
+ // Raised per toast, not once at startup: a dialog opened AFTER the region joined the top layer
4043
+ // would otherwise sit above it, which is the exact case this fixes.
4044
+ raise(region);
4014
4045
 
4015
4046
  var timeout = opts.timeout == null ? 4000 : opts.timeout;
4016
4047
  if (timeout > 0) setTimeout(function () { dismiss(node); }, timeout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.49.0",
3
+ "version": "1.50.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -92,7 +92,7 @@
92
92
  "scripts": {
93
93
  "build": "node tokens/build.mjs",
94
94
  "test": "node --test",
95
- "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 browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs",
95
+ "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 browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs",
96
96
  "prepack": "node tokens/build.mjs",
97
97
  "version": "node tokens/build.mjs && git add dist",
98
98
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"
@@ -1,6 +1,13 @@
1
1
  /* Freeday — Selection controls */
2
+ /* `flex:none` on every box, track and standalone checkbox is load-bearing, not tidiness.
3
+ `.fdy-check` is an inline-flex row of [box, label], and a flex item with a WIDTH is still
4
+ shrinkable — so as soon as the label wrapped to a second line the box gave up horizontal space
5
+ to it and rendered an 18px-tall rectangle 13px wide. Squashed, off-square, and different from
6
+ every sibling whose label happened to be short: a form of five checkboxes showed three sizes.
7
+ Nothing in the declarations looked wrong, because the width was never overridden — it was
8
+ negotiated away. */
2
9
  .fdy-check,.fdy-radio,.fdy-switch{display:inline-flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm);color:var(--color-text);cursor:pointer;}
3
- .fdy-check input,.fdy-radio input{appearance:none;-webkit-appearance:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
10
+ .fdy-check input,.fdy-radio input{appearance:none;-webkit-appearance:none;flex:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
4
11
  .fdy-check input{border-radius:var(--radius-sm);}
5
12
  .fdy-radio input{border-radius:50%;}
6
13
  .fdy-check input::before{content:"";width:.62rem;height:.62rem;transform:scale(0);transition:transform var(--dur-fast) var(--ease-spring);box-shadow:inset 1rem 1rem var(--color-on-primary);clip-path:polygon(14% 44%,0 65%,50% 100%,100% 16%,80% 0,43% 62%);}
@@ -10,13 +17,13 @@
10
17
  .fdy-check input:focus-visible,.fdy-radio input:focus-visible,.fdy-switch input:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
11
18
  .fdy-check input:disabled,.fdy-radio input:disabled,.fdy-switch input:disabled{opacity:.5;cursor:not-allowed;}
12
19
  .fdy-check:has(input:disabled),.fdy-radio:has(input:disabled),.fdy-switch:has(input:disabled){cursor:not-allowed;}
13
- .fdy-switch input{appearance:none;-webkit-appearance:none;position:relative;width:var(--control-switch-w);height:var(--control-switch-h);margin:0;border-radius:var(--radius-full);background:var(--color-control-border);cursor:pointer;transition:background var(--dur-base) var(--ease-standard);}
20
+ .fdy-switch input{appearance:none;-webkit-appearance:none;flex:none;position:relative;width:var(--control-switch-w);height:var(--control-switch-h);margin:0;border-radius:var(--radius-full);background:var(--color-control-border);cursor:pointer;transition:background var(--dur-base) var(--ease-standard);}
14
21
  .fdy-switch input::before{content:"";position:absolute;top:2px;left:2px;width:calc(var(--control-switch-h) - 4px);height:calc(var(--control-switch-h) - 4px);border-radius:50%;background:var(--color-switch-thumb);box-shadow:var(--shadow-1);transition:transform var(--dur-base) var(--ease-spring);}
15
22
  .fdy-switch input:checked{background:var(--color-primary);}
16
23
  .fdy-switch input:checked::before{transform:translateX(calc(var(--control-switch-w) - var(--control-switch-h)));}
17
24
 
18
25
  /* Standalone checkbox (e.g. table row/select-all) — supports :indeterminate */
19
- .fdy-checkbox{appearance:none;-webkit-appearance:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);border-radius:var(--radius-sm);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
26
+ .fdy-checkbox{appearance:none;-webkit-appearance:none;flex:none;width:var(--control-box);height:var(--control-box);margin:0;border:1.5px solid var(--color-control-border);border-radius:var(--radius-sm);background:var(--color-surface);box-shadow:inset 0 1px 2px var(--color-shadow-inset);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
20
27
  .fdy-checkbox::before{content:"";width:.62rem;height:.62rem;transform:scale(0);transition:transform var(--dur-fast) var(--ease-spring);box-shadow:inset 1rem 1rem var(--color-on-primary);clip-path:polygon(14% 44%,0 65%,50% 100%,100% 16%,80% 0,43% 62%);}
21
28
  .fdy-checkbox:checked,.fdy-checkbox:indeterminate{background:var(--color-primary);border-color:var(--color-primary);}
22
29
  .fdy-checkbox:checked::before{transform:scale(1);}
@@ -1,5 +1,17 @@
1
1
  /* Freeday — Toast (transient notification) */
2
- .fdy-toast-region{position:fixed;z-index:200;bottom:var(--space-5);right:var(--space-5);display:flex;flex-direction:column;gap:var(--space-3);width:min(24rem,calc(100vw - var(--space-8)));pointer-events:none;}
2
+ /* The region is a POPOVER, not merely a high z-index.
3
+ `.fdy-modal` is a native <dialog> opened with showModal(), which puts it in the TOP LAYER — a
4
+ layer above the whole z-index universe, so no number here could ever beat it. A toast raised by
5
+ an action taken inside a modal was painted behind that modal's backdrop: dimmed, unreadable, and
6
+ most of it under the dialog itself. `freeday-toast.js` calls showPopover() as each toast is
7
+ added, which puts the region in the top layer too, and last-in wins there.
8
+ Everything after `pointer-events` is undoing the UA's [popover] defaults — it arrives with
9
+ inset:0, auto margins, a border, padding, a Canvas background and overflow:auto, none of which a
10
+ toast stack wants. Where showPopover() is unavailable the element is still an ordinary fixed
11
+ block at z-index 200, which is exactly what it was before. */
12
+ .fdy-toast-region{position:fixed;z-index:200;inset:auto;bottom:var(--space-5);right:var(--space-5);display:flex;flex-direction:column;gap:var(--space-3);width:min(24rem,calc(100vw - var(--space-8)));height:auto;max-height:none;margin:0;padding:0;border:0;background:transparent;color:inherit;overflow:visible;pointer-events:none;}
13
+ /* A popover gets a ::backdrop as well; a toast must not dim the page behind it. */
14
+ .fdy-toast-region::backdrop{background:transparent;}
3
15
  .fdy-toast{pointer-events:auto;display:flex;gap:var(--space-3);align-items:flex-start;padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);background:var(--color-inverse-surface);color:var(--color-inverse-text);box-shadow:var(--shadow-3);}
4
16
  .fdy-toast__accent{flex:none;width:.25rem;align-self:stretch;border-radius:var(--radius-full);background:var(--color-primary);}
5
17
  .fdy-toast--success .fdy-toast__accent{background:var(--color-success);}