@cahyo-dimas/freeday 1.48.1 → 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 +52 -0
- package/COMPONENTS.md +14 -1
- package/adapters/blazor/FdyTable.razor +2 -2
- package/adapters/blazor/TableTypes.cs +7 -0
- package/adapters/core/table-model.d.ts +10 -0
- package/adapters/react/components/FdyTable.tsx +4 -2
- package/adapters/vue/components/FdyTable.vue +2 -2
- package/dist/freeday-toast.js +31 -0
- package/dist/freeday.bundle.css +23 -4
- package/dist/freeday.css +23 -4
- package/dist/freeday.js +31 -0
- package/package.json +2 -2
- package/src/components/selection.css +10 -3
- package/src/components/toast.css +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,58 @@
|
|
|
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
|
+
|
|
39
|
+
## [1.49.0] — 2026-08-20
|
|
40
|
+
### Added
|
|
41
|
+
- **`FdyTableColumn.labelHidden`** (#026). A column of row CONTROLS — an edit button, a row menu —
|
|
42
|
+
could be named or quiet, not both: `label` renders as bare text in the `<th>`, so a designer who
|
|
43
|
+
does not want a word above a column of icons was left with an empty header, which assistive tech
|
|
44
|
+
announces as nothing. With `labelHidden: true` the label renders inside `.fdy-visually-hidden`:
|
|
45
|
+
the cell looks empty and the column is still named. The label keeps naming the column's filter
|
|
46
|
+
popover and sort button, so it stays meaningful either way.
|
|
47
|
+
Contract and ALL THREE typed adapters — Vue, React and Blazor. Each renders its own header, so
|
|
48
|
+
each spends the flag itself; Blazor takes `LabelHidden` on `FdyTableColumn<TRow>`.
|
|
49
|
+
### Added — guards
|
|
50
|
+
- **The column contract now has to reach all three adapters** (#026). Vue and React consume
|
|
51
|
+
`table-model.d.ts` and TypeScript keeps them honest, but Blazor RE-DECLARES the column in C# — a
|
|
52
|
+
hand copy, and the surface that can fall behind silently. A test asserts every contract property
|
|
53
|
+
exists on Blazor's `FdyTableColumn<TRow>`; coverage, not equality, so it needs no exemption list.
|
|
54
|
+
- **A hidden label is clipped, not dropped** — asserted in a real browser for both JS adapters
|
|
55
|
+
(`browser/adapter.mjs`), because the claim is about computed geometry: a span carrying the class
|
|
56
|
+
with no CSS behind it passes any string match and still prints the word.
|
|
57
|
+
|
|
6
58
|
## [1.48.1] — 2026-08-20
|
|
7
59
|
### Fixed
|
|
8
60
|
- **A date column sorted correctly and filtered nothing** (#025). `FdyTable`'s client-mode date
|
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.
|
|
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
|
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
<th scope="col" style="@AlignStyle(col)" aria-sort="@AriaSortOf(col)">
|
|
27
27
|
@if (col.Sortable)
|
|
28
28
|
{
|
|
29
|
-
<button type="button" class="fdy-table__sortbtn" @onclick="() => OnSortClick(col)">@col.Label</button>
|
|
29
|
+
<button type="button" class="fdy-table__sortbtn" @onclick="() => OnSortClick(col)"><span class="@(col.LabelHidden ? "fdy-visually-hidden" : null)">@col.Label</span></button>
|
|
30
30
|
}
|
|
31
31
|
else
|
|
32
32
|
{
|
|
33
|
-
@col.Label
|
|
33
|
+
<span class="@(col.LabelHidden ? "fdy-visually-hidden" : null)">@col.Label</span>
|
|
34
34
|
}
|
|
35
35
|
@if (col.Filter is { } ft)
|
|
36
36
|
{
|
|
@@ -47,6 +47,13 @@ public sealed class FdyTableColumn<TRow>
|
|
|
47
47
|
/// <summary>Header label.</summary>
|
|
48
48
|
public required string Label { get; init; }
|
|
49
49
|
|
|
50
|
+
/// <summary>Render the label for assistive tech only — the header cell looks empty.
|
|
51
|
+
/// For a column of row CONTROLS (an edit button, a row menu), where a visible heading is noise
|
|
52
|
+
/// above a column of icons but the column still has to be named: a <c>th</c> with no text is
|
|
53
|
+
/// announced as nothing, and a reader tabbing the header row cannot tell what it is. The label
|
|
54
|
+
/// still names the column's filter popover and its sort button, so it must stay meaningful.</summary>
|
|
55
|
+
public bool LabelHidden { get; init; }
|
|
56
|
+
|
|
50
57
|
/// <summary>Show a sort toggle in the header.</summary>
|
|
51
58
|
public bool Sortable { get; init; }
|
|
52
59
|
|
|
@@ -24,6 +24,16 @@ export interface FdyTableColumn<T> {
|
|
|
24
24
|
key: string;
|
|
25
25
|
/** Header label. */
|
|
26
26
|
label: string;
|
|
27
|
+
/**
|
|
28
|
+
* Render the label for assistive tech only — the header cell looks empty.
|
|
29
|
+
*
|
|
30
|
+
* For a column of row CONTROLS (an edit button, a row menu), where a visible
|
|
31
|
+
* heading is noise above a column of icons but the column still has to be
|
|
32
|
+
* named: a `<th>` with no text is announced as nothing, and a reader tabbing
|
|
33
|
+
* the header row cannot tell what it is. The label still names the column's
|
|
34
|
+
* filter popover and its sort button, so it must stay meaningful.
|
|
35
|
+
*/
|
|
36
|
+
labelHidden?: boolean;
|
|
27
37
|
/** Show a sort toggle in the header. */
|
|
28
38
|
sortable?: boolean;
|
|
29
39
|
/** Offer a column filter of this type. */
|
|
@@ -271,9 +271,11 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
271
271
|
{props.columns.map((col: FdyTableColumn<Row>): JSX.Element => (
|
|
272
272
|
<th key={col.key} scope="col" style={alignStyle(col)} aria-sort={ariaSortOf(col)}>
|
|
273
273
|
{col.sortable ? (
|
|
274
|
-
<button type="button" className="fdy-table__sortbtn" onClick={(): void => onSort(col)}>
|
|
274
|
+
<button type="button" className="fdy-table__sortbtn" onClick={(): void => onSort(col)}>
|
|
275
|
+
<span className={col.labelHidden ? 'fdy-visually-hidden' : undefined}>{col.label}</span>
|
|
276
|
+
</button>
|
|
275
277
|
) : (
|
|
276
|
-
col.label
|
|
278
|
+
<span className={col.labelHidden ? 'fdy-visually-hidden' : undefined}>{col.label}</span>
|
|
277
279
|
)}
|
|
278
280
|
{col.filter && (
|
|
279
281
|
<FdyTableFilter
|
|
@@ -280,8 +280,8 @@ function isExpanded(row: Row): boolean {
|
|
|
280
280
|
type="button"
|
|
281
281
|
class="fdy-table__sortbtn"
|
|
282
282
|
@click="onSort(col)"
|
|
283
|
-
>{{ col.label }}</button>
|
|
284
|
-
<
|
|
283
|
+
><span :class="col.labelHidden ? 'fdy-visually-hidden' : undefined">{{ col.label }}</span></button>
|
|
284
|
+
<span v-else :class="col.labelHidden ? 'fdy-visually-hidden' : undefined">{{ col.label }}</span>
|
|
285
285
|
<FdyTableFilter
|
|
286
286
|
v-if="col.filter"
|
|
287
287
|
:label="col.label"
|
package/dist/freeday-toast.js
CHANGED
|
@@ -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);
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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);}
|
package/src/components/toast.css
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/* Freeday — Toast (transient notification) */
|
|
2
|
-
|
|
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);}
|