@cahyo-dimas/freeday 1.20.0 → 1.21.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 +65 -0
- package/COMPONENTS.md +27 -0
- package/README.id.md +3 -1
- package/README.md +3 -1
- package/USAGE.md +21 -0
- package/dist/freeday.bundle.css +57 -9
- package/dist/freeday.css +46 -7
- package/dist/freeday.tokens.css +11 -2
- package/docs/agent-onboarding.md +4 -2
- package/docs/getting-started.md +6 -4
- package/package.json +2 -2
- package/src/base.css +15 -0
- package/src/components/accordion.css +5 -1
- package/src/components/card.css +7 -1
- package/src/components/carousel.css +4 -1
- package/src/components/list.css +8 -1
- package/src/components/table.css +5 -2
- package/src/components/tabs.css +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,71 @@
|
|
|
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.21.0] — 2026-08-12
|
|
7
|
+
Fifth round of real-app consumption feedback, written while that app adopted 1.20.0. Two findings —
|
|
8
|
+
and a **withdrawal**: the reporter retracted 1.20's rejected §A themselves after isolating the real
|
|
9
|
+
mechanism, which turned out to be a genuine kit bug hiding behind it. Both halves of that rejection
|
|
10
|
+
reproduce here, so the rejection stands and the withdrawal is recorded rather than re-litigated.
|
|
11
|
+
### Fixed
|
|
12
|
+
- **Hidden labels could scroll the whole page.** `.fdy-visually-hidden` is `position:absolute`, and
|
|
13
|
+
`clip` hides *painting*, not *layout*. With no positioned ancestor its containing block is the
|
|
14
|
+
document, and `overflow` clips only what is contained inside the overflow box — so a hidden label in
|
|
15
|
+
a horizontally scrolling table (the kit's own recommended way to name an icon button) parks at its
|
|
16
|
+
static position and drags the document sideways. Measured in Chrome: **1351px** of phantom page
|
|
17
|
+
scroll from 11 spans in one table; `overflow-x:hidden` on the scroller, the shell, `body` and `html`
|
|
18
|
+
each changed it by **0px**; removing the spans took it to 0. Every clipping/scrolling container that
|
|
19
|
+
holds consumer markup now declares `position:relative` — `.fdy-table-scroll`, `.fdy-table-wrap`,
|
|
20
|
+
`.fdy-list`, `.fdy-card`, `.fdy-tabs__list`, `.fdy-carousel__viewport`, `.fdy-accordion` — each
|
|
21
|
+
measured to take its own case to 0 with the scroller still scrolling internally. The rest are
|
|
22
|
+
already inside something the kit positions (`<dialog>`, the sticky sidebar, a fixed popover) and are
|
|
23
|
+
listed with that reason in the new test.
|
|
24
|
+
**`.fdy-accordion` is the one to note:** it was contained only by its panel's reveal *animation*
|
|
25
|
+
(a transform makes a containing block), and that animation sits behind
|
|
26
|
+
`prefers-reduced-motion: no-preference` — so the bug was reachable **only** by readers who asked for
|
|
27
|
+
reduced motion. Measured with the animation off: 2906px → 0.
|
|
28
|
+
*Possible migration:* if you absolutely positioned something inside a card, list or table scroller
|
|
29
|
+
and relied on it escaping, it now anchors to that container. Anchoring is the reason for the change.
|
|
30
|
+
- **`--button` rows and cards ignored `:disabled`.** `.fdy-list__row--button` adopts the UA button box
|
|
31
|
+
but not its disabled state, so a row disabled mid-flight kept its hover tint and pointer cursor — a
|
|
32
|
+
control answering the pointer while refusing input. `:disabled` and `[aria-disabled="true"]` now dim
|
|
33
|
+
it and withdraw hover, for `.fdy-list__row--button`, `.fdy-list__row--interactive`,
|
|
34
|
+
`.fdy-card--button` and `.fdy-card--interactive`. The report suggested `cursor:default`; the kit uses
|
|
35
|
+
`opacity:.5` + `cursor:not-allowed` on every other disabled control, and consistency wins.
|
|
36
|
+
### Changed
|
|
37
|
+
- **`data-theme` is no longer root-scoped** — the same move density made in 1.20.0, and for the same
|
|
38
|
+
reason. The two *explicit* selectors are now bare `[data-theme="dark"]` / `[data-theme="light"]`, so
|
|
39
|
+
a `<section data-theme="dark">` inverts that region and **every component inside it follows** —
|
|
40
|
+
card surfaces, inputs, and text roles like `.fdy-title-page` that set `color: var(--color-text)`
|
|
41
|
+
explicitly and therefore never saw a consumer's hand-rolled override. A dark brand panel beside a
|
|
42
|
+
light sign-in form is an ordinary layout; it should not cost a re-colouring pass. Setting the
|
|
43
|
+
attribute on `<html>` is unchanged, and a `[data-theme="light"]` island nested inside a dark region
|
|
44
|
+
wins in turn.
|
|
45
|
+
**The system default stays root-scoped, deliberately** — the report asked for "the two generated
|
|
46
|
+
selectors", but there are three. Un-rooting `@media (prefers-color-scheme: dark) { :root:not(...) }`
|
|
47
|
+
would make it match every element that does not itself carry `data-theme="light"`, including the
|
|
48
|
+
*children* of a light island, dragging them back to dark. Measured: with the un-rooted variant that
|
|
49
|
+
island renders light ink on a light surface; root-scoped, it stays correct.
|
|
50
|
+
### Added — guards
|
|
51
|
+
- **`test/css.test.mjs`** — the containment invariant, CI-gated: a rule that declares `overflow` must
|
|
52
|
+
also be positioned, or be listed with the ancestor that already contains it. Single-line truncation
|
|
53
|
+
(`text-overflow:ellipsis` on a label) is excluded by shape, not by name, so new truncating labels
|
|
54
|
+
don't accumulate in an allowlist. A new clipping container fails the test until someone decides
|
|
55
|
+
which case it is.
|
|
56
|
+
- **`browser/layout.mjs`** — the same bug end-to-end in real Chrome (`npm run test:browser`): ten
|
|
57
|
+
hidden labels in a wide table, asserting the page cannot scroll horizontally *and* that the scroller
|
|
58
|
+
still scrolls. A static test cannot see this failure; only a layout engine can. Both guards
|
|
59
|
+
mutation-checked — reverting any single `position:relative` fails them.
|
|
60
|
+
- **`browser/theme.mjs`** — subtree theming end-to-end: a `.fdy-title-page` and a `.fdy-card` inside
|
|
61
|
+
`<section data-theme="dark">` take the dark tokens, a nested light island goes back, and
|
|
62
|
+
`data-theme` on `<html>` still themes everything. `test/build.test.mjs` guards the selector shape
|
|
63
|
+
(not root-scoped, media block still is, and the block order the cascade depends on); this guards the
|
|
64
|
+
behaviour that shape exists for. Mutation-checked: re-rooting the selectors fails it.
|
|
65
|
+
### Notes
|
|
66
|
+
- Not adopted from the report, deliberately: `.fdy-datatable`, `.fdy-modal__body`, `.fdy-drawer__body`
|
|
67
|
+
were listed as needing the same fix. Measured: they do not escape — `<dialog>` is `position:fixed`
|
|
68
|
+
and is already their containing block, and the datatable's own scrolling child now carries it. They
|
|
69
|
+
are in the test's allowlist with that reason instead of carrying a declaration that does nothing.
|
|
70
|
+
|
|
6
71
|
## [1.20.0] — 2026-08-12
|
|
7
72
|
Two bodies of work in one release (1.19.0 was prepared but never committed, tagged or published, so
|
|
8
73
|
it is folded in here rather than left as a phantom version):
|
package/COMPONENTS.md
CHANGED
|
@@ -31,6 +31,25 @@ live docs.
|
|
|
31
31
|
6. **Interactive components need their enhancer script.** Static ones (button, card, badge, table,
|
|
32
32
|
alert, breadcrumb, timeline, accordion, tree-without-cascade…) are CSS-only.
|
|
33
33
|
|
|
34
|
+
### Containment — why the kit's containers are `position:relative`
|
|
35
|
+
|
|
36
|
+
`.fdy-visually-hidden` is `position:absolute`, and `clip` hides **painting**, not **layout**. An
|
|
37
|
+
absolutely positioned box resolves against its nearest *positioned* ancestor, and `overflow` clips
|
|
38
|
+
only what is contained that way — so in an unpositioned scroller, a hidden label parks at its static
|
|
39
|
+
position (possibly thousands of px to the right) and drags the **whole document** sideways. It is
|
|
40
|
+
invisible in the DOM and immune to `overflow-x: hidden` on every wrapper.
|
|
41
|
+
|
|
42
|
+
Every kit container that clips or scrolls therefore declares `position: relative` —
|
|
43
|
+
`.fdy-table-scroll`, `.fdy-table-wrap`, `.fdy-list`, `.fdy-card`, `.fdy-tabs__list`,
|
|
44
|
+
`.fdy-carousel__viewport`, `.fdy-accordion` (the rest are already inside a positioned ancestor).
|
|
45
|
+
Two consequences for you:
|
|
46
|
+
|
|
47
|
+
- **Do the same in your own scrollers.** A container with `overflow` that holds arbitrary markup
|
|
48
|
+
needs `position: relative`, or the hidden labels *you* write will escape it.
|
|
49
|
+
- **Diagnose it correctly.** `document.documentElement.scrollWidth` sees the escaped box;
|
|
50
|
+
`document.body.scrollWidth` does not. The honest check is `window.scrollTo(9999, 0)` then reading
|
|
51
|
+
`window.scrollX`.
|
|
52
|
+
|
|
34
53
|
## Enhancers — hook, script, global, events
|
|
35
54
|
|
|
36
55
|
Every enhancer is zero-dependency, auto-initialises once on `DOMContentLoaded`, and is idempotent:
|
|
@@ -572,6 +591,10 @@ responsive `.fdy-datatable` should become below `md` — not a stack of `.fdy-ca
|
|
|
572
591
|
control (render it as a real `<button>`/`<a>`; the UA box is reset without losing the list surface)
|
|
573
592
|
- Row internals: `.fdy-list__main` (truncating stack) → `.fdy-list__title` + `.fdy-list__meta`, and
|
|
574
593
|
`.fdy-list__aside` pinned right
|
|
594
|
+
- **Disabled** — `disabled` on the `<button>` (or `aria-disabled="true"` when the row is an `<a>`/
|
|
595
|
+
`<div>`) dims the row and withdraws the hover tint and the pointer cursor, same as every other
|
|
596
|
+
control in the kit. Do not hand-roll it: an undimmed row that still lights up under the pointer
|
|
597
|
+
reads as clickable while it is refusing input.
|
|
575
598
|
- Not `.fdy-list-reset` — that utility only strips UA bullets/indent from a semantic list.
|
|
576
599
|
|
|
577
600
|
```html
|
|
@@ -709,6 +732,10 @@ Parts `__body` `__title` `__desc` `__footer`. Modifiers:
|
|
|
709
732
|
the UA button box **without** losing the card surface/border
|
|
710
733
|
- `.fdy-card--button` never replaces keyboard semantics — a clickable card must be a real
|
|
711
734
|
`<button>` or `<a>`.
|
|
735
|
+
- **Disabled** — `disabled` (or `aria-disabled="true"`) dims the card and withdraws the pointer
|
|
736
|
+
cursor and the `--interactive` hover-lift.
|
|
737
|
+
- The card is `position:relative`, so a badge or ribbon you absolutely position inside it anchors
|
|
738
|
+
to the card. That is also what keeps hidden labels inside it from escaping — see *Containment*.
|
|
712
739
|
|
|
713
740
|
## Badge — `.fdy-badge`
|
|
714
741
|
Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only —
|
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
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.21.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`.
|
|
@@ -121,6 +121,8 @@ container-nya, bukan scroll sendiri.
|
|
|
121
121
|
|
|
122
122
|
### 3. Theming — 3 sumbu lewat `data-*` di root
|
|
123
123
|
- `data-theme="light|dark"` — redefinisi token semantic (bind ke state tema app-mu).
|
|
124
|
+
Berlaku di **ancestor mana pun**, tak harus root: `<section data-theme="dark">` membalik region itu
|
|
125
|
+
dan semua komponen di dalamnya ikut (lihat [`USAGE.md`](USAGE.md) §5b).
|
|
124
126
|
- `data-density="comfortable|compact"` — tinggi kontrol (`--control-h`) untuk layar padat data.
|
|
125
127
|
Ini auto-apply hanya ke kontrol bawaan Freeday (button, input, combo, dst); komponen
|
|
126
128
|
custom/hand-built harus baca `--control-h` sendiri (mis. `height: var(--control-h)`) supaya ikut
|
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
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.21.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`.
|
|
@@ -121,6 +121,8 @@ of scrolling.
|
|
|
121
121
|
|
|
122
122
|
### 3. Theming — 3 axes via `data-*` on the root
|
|
123
123
|
- `data-theme="light|dark"` — redefines the semantic tokens (bind it to your app's theme state).
|
|
124
|
+
Works on **any ancestor**, not just the root: `<section data-theme="dark">` inverts that region
|
|
125
|
+
and every component inside it follows (see [`USAGE.md`](USAGE.md) §5b).
|
|
124
126
|
- `data-density="comfortable|compact"` — control height (`--control-h`) for data-dense screens.
|
|
125
127
|
This auto-applies only to Freeday's built-in controls (button, input, combo, etc.);
|
|
126
128
|
custom/hand-built components must read `--control-h` themselves (e.g. `height: var(--control-h)`)
|
package/USAGE.md
CHANGED
|
@@ -82,6 +82,27 @@ point, everything around it quiet.
|
|
|
82
82
|
- **Surfaces:** most backgrounds are `--color-surface`; `--color-surface-2`/`-3` for a recessed area;
|
|
83
83
|
`--color-primary-soft` only when you want a tinted callout, not as a default panel colour.
|
|
84
84
|
|
|
85
|
+
## 5b. Theme — global by default, per-subtree when a region is inverted
|
|
86
|
+
|
|
87
|
+
`data-theme="light|dark"` redefines the semantic tokens. Set it on `<html>` and it themes the app;
|
|
88
|
+
that is the normal case and nothing about it has changed.
|
|
89
|
+
|
|
90
|
+
**It is also per-subtree.** The two explicit selectors are bare `[data-theme="dark"]` /
|
|
91
|
+
`[data-theme="light"]`, and semantic tokens are inheriting custom properties — so a
|
|
92
|
+
`<section data-theme="dark">` inverts that region and **every Freeday component inside it follows**:
|
|
93
|
+
card surfaces, inputs, buttons, and text roles like `.fdy-title-page` that set
|
|
94
|
+
`color: var(--color-text)` explicitly. A dark brand panel beside a light sign-in form needs no
|
|
95
|
+
per-element re-colouring, and a `[data-theme="light"]` island nested back inside a dark region wins
|
|
96
|
+
in turn.
|
|
97
|
+
|
|
98
|
+
Do **not** invert a region by hand with `--color-inverse-*` plus `color-mix`. That pair is right for
|
|
99
|
+
a one-off band of your own markup, but it stops scaling the moment the region contains a real
|
|
100
|
+
component — anything that sets its own colour from a token never sees your override, and you end up
|
|
101
|
+
restating colours per element.
|
|
102
|
+
|
|
103
|
+
The **system** default (`@media (prefers-color-scheme: dark)`) stays root-scoped on purpose: that
|
|
104
|
+
rule is about the document, and un-rooting it would re-darken the children of a light island.
|
|
105
|
+
|
|
85
106
|
## 6. Density — `compact` for data-dense screens
|
|
86
107
|
|
|
87
108
|
`data-density="compact"` tightens control height **and** the mid-range spacing scale
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -177,6 +177,9 @@
|
|
|
177
177
|
--focus-ring: var(--azure-600);
|
|
178
178
|
--focus-ring-width: 2px;
|
|
179
179
|
}
|
|
180
|
+
/* The SYSTEM default stays root-scoped: it is a statement about the document, and dropping :root
|
|
181
|
+
* here would match every element that is not itself [data-theme="light"] — which would re-darken
|
|
182
|
+
* the children of a light panel, since they carry no attribute of their own. */
|
|
180
183
|
@media (prefers-color-scheme: dark) {
|
|
181
184
|
:root:not([data-theme="light"]) {
|
|
182
185
|
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
|
|
@@ -235,7 +238,13 @@
|
|
|
235
238
|
--focus-ring: var(--azure-400);
|
|
236
239
|
}
|
|
237
240
|
}
|
|
238
|
-
:root
|
|
241
|
+
/* The two EXPLICIT opt-ins are deliberately NOT scoped to :root, for the same reason as density
|
|
242
|
+
* below: these are inheriting custom properties, so data-theme on any ancestor re-themes just that
|
|
243
|
+
* subtree — a dark brand panel beside a light form is an ordinary layout, and it should not require
|
|
244
|
+
* re-colouring each component by hand. The root still matches, so data-theme on <html> is unchanged.
|
|
245
|
+
* Both keep the same specificity (0,1,0) as :root and come after it, so they still win there; and
|
|
246
|
+
* --light after --dark means a light island inside a dark region wins in turn. */
|
|
247
|
+
[data-theme="dark"] {
|
|
239
248
|
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
|
|
240
249
|
--shadow-2: 0 2px 6px rgba(0,0,0,.45);
|
|
241
250
|
--shadow-3: 0 6px 20px -2px rgba(0,0,0,.6),0 1px 3px rgba(0,0,0,.5);
|
|
@@ -291,7 +300,7 @@
|
|
|
291
300
|
--chart-tick: var(--slate-600);
|
|
292
301
|
--focus-ring: var(--azure-400);
|
|
293
302
|
}
|
|
294
|
-
|
|
303
|
+
[data-theme="light"] {
|
|
295
304
|
--shadow-1: 0 1px 2px rgba(16,14,30,.06);
|
|
296
305
|
--shadow-2: 0 2px 6px rgba(16,14,30,.07),0 1px 2px rgba(16,14,30,.05);
|
|
297
306
|
--shadow-3: 0 4px 16px -2px rgba(16,14,30,.14),0 1px 3px rgba(16,14,30,.10);
|
|
@@ -376,6 +385,21 @@ h1, h2, h3, h4 { font-family: var(--font-display); font-weight: var(--weight-bol
|
|
|
376
385
|
a { color: var(--color-primary); }
|
|
377
386
|
:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-width); border-radius: var(--radius-xs); }
|
|
378
387
|
:where(button, input, select, textarea) { font: inherit; }
|
|
388
|
+
/* Hidden from sight, kept for assistive tech. `clip` hides PAINTING, not LAYOUT: the box still
|
|
389
|
+
* has a position, and because it is position:absolute its containing block is the nearest
|
|
390
|
+
* POSITIONED ancestor — with none, the initial containing block (the document itself).
|
|
391
|
+
*
|
|
392
|
+
* That matters, because `overflow` only clips a descendant whose containing block is inside the
|
|
393
|
+
* overflow box. So a hidden span in a horizontally scrolling table (the kit's own recommended way
|
|
394
|
+
* to name an icon button) parks at its static position — possibly thousands of px to the right —
|
|
395
|
+
* and drags the whole DOCUMENT sideways: invisible, and immune to `overflow-x:hidden` on every
|
|
396
|
+
* wrapper. Measured before the fix: 1351px of phantom page scroll from 11 spans.
|
|
397
|
+
*
|
|
398
|
+
* The fix is not here — it is on the containers: every clipping/scrolling container in the kit
|
|
399
|
+
* that holds consumer markup declares `position:relative` so it becomes the containing block for
|
|
400
|
+
* its own out-of-flow content (see test/css.test.mjs, which will not let a new one skip it).
|
|
401
|
+
* Note the measurement trap: documentElement.scrollWidth sees it, body.scrollWidth does not —
|
|
402
|
+
* the honest check is `window.scrollTo(9999, 0)` then reading `window.scrollX`. */
|
|
379
403
|
.fdy-visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
380
404
|
/* Opt-in list reset. base.css is intentionally a *light* reset — it does NOT strip list/paragraph
|
|
381
405
|
* margins (see the kit's own list components, which each reset themselves). If you run a utility
|
|
@@ -387,7 +411,11 @@ a { color: var(--color-primary); }
|
|
|
387
411
|
}
|
|
388
412
|
|
|
389
413
|
/* Freeday — Accordion (native <details>/<summary>, zero-JS) */
|
|
390
|
-
|
|
414
|
+
/* position:relative — containing block for out-of-flow panel content (see base.css). NOT redundant
|
|
415
|
+
with the panel's reveal animation: that animation gives the panel a transform (which happens to
|
|
416
|
+
make it a containing block too), but it lives behind prefers-reduced-motion:no-preference — so
|
|
417
|
+
without this line the escape bug appears ONLY for readers who asked for reduced motion. */
|
|
418
|
+
.fdy-accordion{position:relative;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;background:var(--color-surface);}
|
|
391
419
|
.fdy-accordion__item + .fdy-accordion__item{border-top:var(--bw) solid var(--color-border);}
|
|
392
420
|
.fdy-accordion__item > summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-4);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
393
421
|
.fdy-accordion__item > summary::-webkit-details-marker{display:none;}
|
|
@@ -673,7 +701,9 @@ a { color: var(--color-primary); }
|
|
|
673
701
|
.fdy-fab--accent{background:linear-gradient(180deg,var(--color-accent),color-mix(in srgb,#000 10%,var(--color-accent)));color:var(--color-on-accent);}
|
|
674
702
|
|
|
675
703
|
/* Freeday — Card */
|
|
676
|
-
|
|
704
|
+
/* position:relative — containing block for out-of-flow card content (see base.css); it also gives
|
|
705
|
+
consumers the anchor a corner ribbon/badge needs. */
|
|
706
|
+
.fdy-card{position:relative;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-lift);transition:box-shadow var(--dur-base) var(--ease-standard),transform var(--dur-base) var(--ease-standard);}
|
|
677
707
|
.fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
|
|
678
708
|
.fdy-card--interactive{cursor:pointer;}
|
|
679
709
|
.fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
|
|
@@ -684,6 +714,10 @@ a { color: var(--color-primary); }
|
|
|
684
714
|
* announced — reset the UA button box. Note: it does NOT reset background/border, because
|
|
685
715
|
* .fdy-card already sets its own surface + border (resetting them here would strip the card). */
|
|
686
716
|
.fdy-card--button{display:block;width:100%;text-align:inherit;color:inherit;appearance:none;-webkit-appearance:none;}
|
|
717
|
+
/* Disabled card-as-control — same contract as .fdy-list__row--button: dim + not-allowed, and the
|
|
718
|
+
--interactive lift is withdrawn so a refusing control stops answering the pointer. */
|
|
719
|
+
.fdy-card--button:disabled,.fdy-card--button[aria-disabled="true"],.fdy-card--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
720
|
+
.fdy-card--interactive:disabled:hover,.fdy-card--interactive[aria-disabled="true"]:hover{box-shadow:var(--shadow-lift);transform:none;}
|
|
687
721
|
.fdy-card__body{padding:var(--space-5);}
|
|
688
722
|
.fdy-card__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0 0 var(--space-1);}
|
|
689
723
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
@@ -691,7 +725,10 @@ a { color: var(--color-primary); }
|
|
|
691
725
|
|
|
692
726
|
/* Freeday — Carousel (scroll-snap track + arrows + dots). Enhanced by freeday-carousel.js. */
|
|
693
727
|
.fdy-carousel{position:relative;}
|
|
694
|
-
.fdy-
|
|
728
|
+
/* position:relative — the arrows are positioned against .fdy-carousel (the parent), so this only
|
|
729
|
+
catches out-of-flow content inside the SLIDES, which would otherwise escape the viewport
|
|
730
|
+
entirely and scroll the page by one slide-offset per slide (see base.css). */
|
|
731
|
+
.fdy-carousel__viewport{position:relative;display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;border-radius:var(--radius-lg);scrollbar-width:none;-ms-overflow-style:none;}
|
|
695
732
|
.fdy-carousel__viewport::-webkit-scrollbar{display:none;}
|
|
696
733
|
.fdy-carousel__slide{flex:0 0 100%;min-width:0;scroll-snap-align:center;}
|
|
697
734
|
.fdy-carousel__arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:2;display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:0;border-radius:var(--radius-full);background:color-mix(in srgb,var(--color-surface) 86%,transparent);color:var(--color-text);box-shadow:var(--shadow-2);cursor:pointer;transition:background var(--dur-fast) var(--ease-standard);}
|
|
@@ -1198,7 +1235,8 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1198
1235
|
*
|
|
1199
1236
|
* Not to be confused with .fdy-list-reset (base.css), which only strips UA bullets/indent.
|
|
1200
1237
|
* Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
|
|
1201
|
-
|
|
1238
|
+
/* position:relative — containing block for out-of-flow row content (see base.css). */
|
|
1239
|
+
.fdy-list{position:relative;list-style:none;margin:0;padding:0;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;}
|
|
1202
1240
|
.fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
|
|
1203
1241
|
/* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
|
|
1204
1242
|
* of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
|
|
@@ -1211,6 +1249,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1211
1249
|
.fdy-list__row--button:hover,.fdy-list__row--interactive:hover{background:var(--color-surface-2);}
|
|
1212
1250
|
.fdy-list__row--button:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--color-primary);}
|
|
1213
1251
|
.fdy-list__row--interactive{cursor:pointer;}
|
|
1252
|
+
/* Disabled row. The reset above adopts the UA button box, but not its disabled state — so a row
|
|
1253
|
+
disabled mid-flight (a redirect in progress, say) kept lighting up and kept a pointer cursor:
|
|
1254
|
+
a control answering the pointer while refusing input. Same contract as every other disabled
|
|
1255
|
+
control in the kit (dim + not-allowed), hover withdrawn, both the native and the aria- form. */
|
|
1256
|
+
.fdy-list__row--button:disabled,.fdy-list__row--button[aria-disabled="true"],.fdy-list__row--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
1257
|
+
.fdy-list__row--button:disabled:hover,.fdy-list__row--button[aria-disabled="true"]:hover,.fdy-list__row--interactive[aria-disabled="true"]:hover{background:none;}
|
|
1214
1258
|
/* Row internals: a title/meta stack that truncates, and a trailing slot pinned right. */
|
|
1215
1259
|
.fdy-list__main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;flex:1;}
|
|
1216
1260
|
.fdy-list__title{font-weight:var(--weight-medium);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
@@ -1395,7 +1439,10 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1395
1439
|
.fdy-step-nav{display:flex;justify-content:space-between;gap:var(--space-2);margin-top:var(--space-5);}
|
|
1396
1440
|
|
|
1397
1441
|
/* Freeday — Table */
|
|
1398
|
-
|
|
1442
|
+
/* position:relative on every scroller/clipper — it makes the box the containing block for its own
|
|
1443
|
+
absolutely positioned content (.fdy-visually-hidden above all), which `overflow` alone does NOT
|
|
1444
|
+
clip. Without it a hidden label in a wide table scrolls the whole page. See base.css. */
|
|
1445
|
+
.fdy-table-wrap{position:relative;overflow-x:auto;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);}
|
|
1399
1446
|
.fdy-table{width:100%;border-collapse:collapse;font-size:var(--text-sm);}
|
|
1400
1447
|
.fdy-table caption{text-align:left;padding:var(--space-3) var(--space-4);font-weight:var(--weight-semibold);color:var(--color-text);}
|
|
1401
1448
|
.fdy-table th{text-align:left;padding:var(--space-3) var(--space-4);font-size:var(--text-xs);font-weight:var(--weight-semibold);text-transform:uppercase;letter-spacing:var(--tracking-wide);color:var(--color-text-muted);background:var(--color-surface-2);border-bottom:var(--bw) solid var(--color-border);white-space:nowrap;}
|
|
@@ -1432,7 +1479,7 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1432
1479
|
|
|
1433
1480
|
/* Data-table shell (toolbar + scroll + footer stay put while the table scrolls) */
|
|
1434
1481
|
.fdy-datatable{border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);overflow:hidden;}
|
|
1435
|
-
.fdy-table-scroll{overflow-x:auto;}
|
|
1482
|
+
.fdy-table-scroll{position:relative;overflow-x:auto;}
|
|
1436
1483
|
.fdy-table-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border);}
|
|
1437
1484
|
.fdy-table-toolbar__search{max-width:18rem;}
|
|
1438
1485
|
.fdy-table-toolbar__spacer{flex:1;}
|
|
@@ -1465,7 +1512,8 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1465
1512
|
.fdy-table-bulkbar__actions{display:flex;gap:var(--space-2);}
|
|
1466
1513
|
|
|
1467
1514
|
/* Freeday — Tabs (WAI-ARIA APG) */
|
|
1468
|
-
|
|
1515
|
+
/* position:relative — containing block for out-of-flow content in the tabs (see base.css). */
|
|
1516
|
+
.fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
|
|
1469
1517
|
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
1470
1518
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
1471
1519
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
package/dist/freeday.css
CHANGED
|
@@ -15,6 +15,21 @@ h1, h2, h3, h4 { font-family: var(--font-display); font-weight: var(--weight-bol
|
|
|
15
15
|
a { color: var(--color-primary); }
|
|
16
16
|
:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-width); border-radius: var(--radius-xs); }
|
|
17
17
|
:where(button, input, select, textarea) { font: inherit; }
|
|
18
|
+
/* Hidden from sight, kept for assistive tech. `clip` hides PAINTING, not LAYOUT: the box still
|
|
19
|
+
* has a position, and because it is position:absolute its containing block is the nearest
|
|
20
|
+
* POSITIONED ancestor — with none, the initial containing block (the document itself).
|
|
21
|
+
*
|
|
22
|
+
* That matters, because `overflow` only clips a descendant whose containing block is inside the
|
|
23
|
+
* overflow box. So a hidden span in a horizontally scrolling table (the kit's own recommended way
|
|
24
|
+
* to name an icon button) parks at its static position — possibly thousands of px to the right —
|
|
25
|
+
* and drags the whole DOCUMENT sideways: invisible, and immune to `overflow-x:hidden` on every
|
|
26
|
+
* wrapper. Measured before the fix: 1351px of phantom page scroll from 11 spans.
|
|
27
|
+
*
|
|
28
|
+
* The fix is not here — it is on the containers: every clipping/scrolling container in the kit
|
|
29
|
+
* that holds consumer markup declares `position:relative` so it becomes the containing block for
|
|
30
|
+
* its own out-of-flow content (see test/css.test.mjs, which will not let a new one skip it).
|
|
31
|
+
* Note the measurement trap: documentElement.scrollWidth sees it, body.scrollWidth does not —
|
|
32
|
+
* the honest check is `window.scrollTo(9999, 0)` then reading `window.scrollX`. */
|
|
18
33
|
.fdy-visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
19
34
|
/* Opt-in list reset. base.css is intentionally a *light* reset — it does NOT strip list/paragraph
|
|
20
35
|
* margins (see the kit's own list components, which each reset themselves). If you run a utility
|
|
@@ -26,7 +41,11 @@ a { color: var(--color-primary); }
|
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
/* Freeday — Accordion (native <details>/<summary>, zero-JS) */
|
|
29
|
-
|
|
44
|
+
/* position:relative — containing block for out-of-flow panel content (see base.css). NOT redundant
|
|
45
|
+
with the panel's reveal animation: that animation gives the panel a transform (which happens to
|
|
46
|
+
make it a containing block too), but it lives behind prefers-reduced-motion:no-preference — so
|
|
47
|
+
without this line the escape bug appears ONLY for readers who asked for reduced motion. */
|
|
48
|
+
.fdy-accordion{position:relative;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;background:var(--color-surface);}
|
|
30
49
|
.fdy-accordion__item + .fdy-accordion__item{border-top:var(--bw) solid var(--color-border);}
|
|
31
50
|
.fdy-accordion__item > summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-4);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
32
51
|
.fdy-accordion__item > summary::-webkit-details-marker{display:none;}
|
|
@@ -312,7 +331,9 @@ a { color: var(--color-primary); }
|
|
|
312
331
|
.fdy-fab--accent{background:linear-gradient(180deg,var(--color-accent),color-mix(in srgb,#000 10%,var(--color-accent)));color:var(--color-on-accent);}
|
|
313
332
|
|
|
314
333
|
/* Freeday — Card */
|
|
315
|
-
|
|
334
|
+
/* position:relative — containing block for out-of-flow card content (see base.css); it also gives
|
|
335
|
+
consumers the anchor a corner ribbon/badge needs. */
|
|
336
|
+
.fdy-card{position:relative;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-lift);transition:box-shadow var(--dur-base) var(--ease-standard),transform var(--dur-base) var(--ease-standard);}
|
|
316
337
|
.fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
|
|
317
338
|
.fdy-card--interactive{cursor:pointer;}
|
|
318
339
|
.fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
|
|
@@ -323,6 +344,10 @@ a { color: var(--color-primary); }
|
|
|
323
344
|
* announced — reset the UA button box. Note: it does NOT reset background/border, because
|
|
324
345
|
* .fdy-card already sets its own surface + border (resetting them here would strip the card). */
|
|
325
346
|
.fdy-card--button{display:block;width:100%;text-align:inherit;color:inherit;appearance:none;-webkit-appearance:none;}
|
|
347
|
+
/* Disabled card-as-control — same contract as .fdy-list__row--button: dim + not-allowed, and the
|
|
348
|
+
--interactive lift is withdrawn so a refusing control stops answering the pointer. */
|
|
349
|
+
.fdy-card--button:disabled,.fdy-card--button[aria-disabled="true"],.fdy-card--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
350
|
+
.fdy-card--interactive:disabled:hover,.fdy-card--interactive[aria-disabled="true"]:hover{box-shadow:var(--shadow-lift);transform:none;}
|
|
326
351
|
.fdy-card__body{padding:var(--space-5);}
|
|
327
352
|
.fdy-card__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0 0 var(--space-1);}
|
|
328
353
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
@@ -330,7 +355,10 @@ a { color: var(--color-primary); }
|
|
|
330
355
|
|
|
331
356
|
/* Freeday — Carousel (scroll-snap track + arrows + dots). Enhanced by freeday-carousel.js. */
|
|
332
357
|
.fdy-carousel{position:relative;}
|
|
333
|
-
.fdy-
|
|
358
|
+
/* position:relative — the arrows are positioned against .fdy-carousel (the parent), so this only
|
|
359
|
+
catches out-of-flow content inside the SLIDES, which would otherwise escape the viewport
|
|
360
|
+
entirely and scroll the page by one slide-offset per slide (see base.css). */
|
|
361
|
+
.fdy-carousel__viewport{position:relative;display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;border-radius:var(--radius-lg);scrollbar-width:none;-ms-overflow-style:none;}
|
|
334
362
|
.fdy-carousel__viewport::-webkit-scrollbar{display:none;}
|
|
335
363
|
.fdy-carousel__slide{flex:0 0 100%;min-width:0;scroll-snap-align:center;}
|
|
336
364
|
.fdy-carousel__arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:2;display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:0;border-radius:var(--radius-full);background:color-mix(in srgb,var(--color-surface) 86%,transparent);color:var(--color-text);box-shadow:var(--shadow-2);cursor:pointer;transition:background var(--dur-fast) var(--ease-standard);}
|
|
@@ -837,7 +865,8 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
837
865
|
*
|
|
838
866
|
* Not to be confused with .fdy-list-reset (base.css), which only strips UA bullets/indent.
|
|
839
867
|
* Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
|
|
840
|
-
|
|
868
|
+
/* position:relative — containing block for out-of-flow row content (see base.css). */
|
|
869
|
+
.fdy-list{position:relative;list-style:none;margin:0;padding:0;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;}
|
|
841
870
|
.fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
|
|
842
871
|
/* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
|
|
843
872
|
* of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
|
|
@@ -850,6 +879,12 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
850
879
|
.fdy-list__row--button:hover,.fdy-list__row--interactive:hover{background:var(--color-surface-2);}
|
|
851
880
|
.fdy-list__row--button:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--color-primary);}
|
|
852
881
|
.fdy-list__row--interactive{cursor:pointer;}
|
|
882
|
+
/* Disabled row. The reset above adopts the UA button box, but not its disabled state — so a row
|
|
883
|
+
disabled mid-flight (a redirect in progress, say) kept lighting up and kept a pointer cursor:
|
|
884
|
+
a control answering the pointer while refusing input. Same contract as every other disabled
|
|
885
|
+
control in the kit (dim + not-allowed), hover withdrawn, both the native and the aria- form. */
|
|
886
|
+
.fdy-list__row--button:disabled,.fdy-list__row--button[aria-disabled="true"],.fdy-list__row--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
887
|
+
.fdy-list__row--button:disabled:hover,.fdy-list__row--button[aria-disabled="true"]:hover,.fdy-list__row--interactive[aria-disabled="true"]:hover{background:none;}
|
|
853
888
|
/* Row internals: a title/meta stack that truncates, and a trailing slot pinned right. */
|
|
854
889
|
.fdy-list__main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;flex:1;}
|
|
855
890
|
.fdy-list__title{font-weight:var(--weight-medium);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
@@ -1034,7 +1069,10 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1034
1069
|
.fdy-step-nav{display:flex;justify-content:space-between;gap:var(--space-2);margin-top:var(--space-5);}
|
|
1035
1070
|
|
|
1036
1071
|
/* Freeday — Table */
|
|
1037
|
-
|
|
1072
|
+
/* position:relative on every scroller/clipper — it makes the box the containing block for its own
|
|
1073
|
+
absolutely positioned content (.fdy-visually-hidden above all), which `overflow` alone does NOT
|
|
1074
|
+
clip. Without it a hidden label in a wide table scrolls the whole page. See base.css. */
|
|
1075
|
+
.fdy-table-wrap{position:relative;overflow-x:auto;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);}
|
|
1038
1076
|
.fdy-table{width:100%;border-collapse:collapse;font-size:var(--text-sm);}
|
|
1039
1077
|
.fdy-table caption{text-align:left;padding:var(--space-3) var(--space-4);font-weight:var(--weight-semibold);color:var(--color-text);}
|
|
1040
1078
|
.fdy-table th{text-align:left;padding:var(--space-3) var(--space-4);font-size:var(--text-xs);font-weight:var(--weight-semibold);text-transform:uppercase;letter-spacing:var(--tracking-wide);color:var(--color-text-muted);background:var(--color-surface-2);border-bottom:var(--bw) solid var(--color-border);white-space:nowrap;}
|
|
@@ -1071,7 +1109,7 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1071
1109
|
|
|
1072
1110
|
/* Data-table shell (toolbar + scroll + footer stay put while the table scrolls) */
|
|
1073
1111
|
.fdy-datatable{border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);overflow:hidden;}
|
|
1074
|
-
.fdy-table-scroll{overflow-x:auto;}
|
|
1112
|
+
.fdy-table-scroll{position:relative;overflow-x:auto;}
|
|
1075
1113
|
.fdy-table-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border);}
|
|
1076
1114
|
.fdy-table-toolbar__search{max-width:18rem;}
|
|
1077
1115
|
.fdy-table-toolbar__spacer{flex:1;}
|
|
@@ -1104,7 +1142,8 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1104
1142
|
.fdy-table-bulkbar__actions{display:flex;gap:var(--space-2);}
|
|
1105
1143
|
|
|
1106
1144
|
/* Freeday — Tabs (WAI-ARIA APG) */
|
|
1107
|
-
|
|
1145
|
+
/* position:relative — containing block for out-of-flow content in the tabs (see base.css). */
|
|
1146
|
+
.fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
|
|
1108
1147
|
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
1109
1148
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
1110
1149
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
package/dist/freeday.tokens.css
CHANGED
|
@@ -176,6 +176,9 @@
|
|
|
176
176
|
--focus-ring: var(--azure-600);
|
|
177
177
|
--focus-ring-width: 2px;
|
|
178
178
|
}
|
|
179
|
+
/* The SYSTEM default stays root-scoped: it is a statement about the document, and dropping :root
|
|
180
|
+
* here would match every element that is not itself [data-theme="light"] — which would re-darken
|
|
181
|
+
* the children of a light panel, since they carry no attribute of their own. */
|
|
179
182
|
@media (prefers-color-scheme: dark) {
|
|
180
183
|
:root:not([data-theme="light"]) {
|
|
181
184
|
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
|
|
@@ -234,7 +237,13 @@
|
|
|
234
237
|
--focus-ring: var(--azure-400);
|
|
235
238
|
}
|
|
236
239
|
}
|
|
237
|
-
:root
|
|
240
|
+
/* The two EXPLICIT opt-ins are deliberately NOT scoped to :root, for the same reason as density
|
|
241
|
+
* below: these are inheriting custom properties, so data-theme on any ancestor re-themes just that
|
|
242
|
+
* subtree — a dark brand panel beside a light form is an ordinary layout, and it should not require
|
|
243
|
+
* re-colouring each component by hand. The root still matches, so data-theme on <html> is unchanged.
|
|
244
|
+
* Both keep the same specificity (0,1,0) as :root and come after it, so they still win there; and
|
|
245
|
+
* --light after --dark means a light island inside a dark region wins in turn. */
|
|
246
|
+
[data-theme="dark"] {
|
|
238
247
|
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
|
|
239
248
|
--shadow-2: 0 2px 6px rgba(0,0,0,.45);
|
|
240
249
|
--shadow-3: 0 6px 20px -2px rgba(0,0,0,.6),0 1px 3px rgba(0,0,0,.5);
|
|
@@ -290,7 +299,7 @@
|
|
|
290
299
|
--chart-tick: var(--slate-600);
|
|
291
300
|
--focus-ring: var(--azure-400);
|
|
292
301
|
}
|
|
293
|
-
|
|
302
|
+
[data-theme="light"] {
|
|
294
303
|
--shadow-1: 0 1px 2px rgba(16,14,30,.06);
|
|
295
304
|
--shadow-2: 0 2px 6px rgba(16,14,30,.07),0 1px 2px rgba(16,14,30,.05);
|
|
296
305
|
--shadow-3: 0 4px 16px -2px rgba(16,14,30,.14),0 1px 3px rgba(16,14,30,.10);
|
package/docs/agent-onboarding.md
CHANGED
|
@@ -82,8 +82,10 @@ The order matters; skipping to components is what produces flat, identical-card
|
|
|
82
82
|
you build the frame instead of inventing a class. Not in the npm package — read it on GitHub.
|
|
83
83
|
1. **Shell** — is `.fdy-app` already in place (usually once, in the app layout)? If not, copy it
|
|
84
84
|
from `docs/getting-started.md` §The app shell.
|
|
85
|
-
2. **Theme** — `data-theme="light|dark"` + `data-density="comfortable|compact"
|
|
86
|
-
once at the root. Use `compact` for table-heavy back-office screens.
|
|
85
|
+
2. **Theme** — `data-theme="light|dark"` + `data-density="comfortable|compact"`, normally on
|
|
86
|
+
`<html>`, set once at the root. Use `compact` for table-heavy back-office screens. Both attributes
|
|
87
|
+
also work on **any ancestor**: `<section data-theme="dark">` inverts that region and every
|
|
88
|
+
component inside it follows, so never hand-colour an inverted panel.
|
|
87
89
|
3. **Fonts** — the package ships **no** `@font-face`. Load Sora / IBM Plex Sans / JetBrains Mono
|
|
88
90
|
yourself, or override `--font-display`/`--font-body`/`--font-mono`. Skipping this reads as
|
|
89
91
|
"unfinished design", not "missing dependency".
|
package/docs/getting-started.md
CHANGED
|
@@ -24,9 +24,11 @@ Freeday = **CSS** (semantic tokens + `fdy-*` classes) + **zero-dependency JS enh
|
|
|
24
24
|
3. **Hydrate dynamic DOM.** Enhancers auto-init once on `DOMContentLoaded`. DOM an SPA renders
|
|
25
25
|
**after** that must be re-hydrated: `window.Freeday<X>.initAll(el)` (idempotent, safe to repeat).
|
|
26
26
|
Each framework's adapter wraps this — you don't call it manually.
|
|
27
|
-
4. **Theme via `data
|
|
28
|
-
`data-density="comfortable|compact"` (control height, for data-dense screens).
|
|
29
|
-
`document.documentElement.dataset.theme = 'dark'`.
|
|
27
|
+
4. **Theme via `data-*`.** `data-theme="light|dark"` (all semantic tokens switch) +
|
|
28
|
+
`data-density="comfortable|compact"` (control height, for data-dense screens). Normally on
|
|
29
|
+
`<html>`; change at runtime with `document.documentElement.dataset.theme = 'dark'`. Both also
|
|
30
|
+
work on **any ancestor** — these are inheriting custom properties, so `<section data-theme="dark">`
|
|
31
|
+
inverts just that region and every component inside it follows. See [`USAGE.md`](../USAGE.md) §5b.
|
|
30
32
|
5. **3-tier token rule.** Components only touch **Tier 2/3** (`var(--color-primary)`,
|
|
31
33
|
`var(--space-4)`, `var(--radius-md)`…). **Never** write raw hex/px.
|
|
32
34
|
6. **Scope: components + tokens, deliberately *not* layout.** Freeday ships components and tokens;
|
|
@@ -177,7 +179,7 @@ live docs also have a copy button per component.
|
|
|
177
179
|
```bash
|
|
178
180
|
npm i @cahyo-dimas/freeday
|
|
179
181
|
```
|
|
180
|
-
Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.
|
|
182
|
+
Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.21.0"` (public npm package). `dist/` is
|
|
181
183
|
committed and published → no build step; `npm ci` runs without auth.
|
|
182
184
|
|
|
183
185
|
### 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.
|
|
3
|
+
"version": "1.21.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",
|
|
74
|
+
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.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"
|
package/src/base.css
CHANGED
|
@@ -14,6 +14,21 @@ h1, h2, h3, h4 { font-family: var(--font-display); font-weight: var(--weight-bol
|
|
|
14
14
|
a { color: var(--color-primary); }
|
|
15
15
|
:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-width); border-radius: var(--radius-xs); }
|
|
16
16
|
:where(button, input, select, textarea) { font: inherit; }
|
|
17
|
+
/* Hidden from sight, kept for assistive tech. `clip` hides PAINTING, not LAYOUT: the box still
|
|
18
|
+
* has a position, and because it is position:absolute its containing block is the nearest
|
|
19
|
+
* POSITIONED ancestor — with none, the initial containing block (the document itself).
|
|
20
|
+
*
|
|
21
|
+
* That matters, because `overflow` only clips a descendant whose containing block is inside the
|
|
22
|
+
* overflow box. So a hidden span in a horizontally scrolling table (the kit's own recommended way
|
|
23
|
+
* to name an icon button) parks at its static position — possibly thousands of px to the right —
|
|
24
|
+
* and drags the whole DOCUMENT sideways: invisible, and immune to `overflow-x:hidden` on every
|
|
25
|
+
* wrapper. Measured before the fix: 1351px of phantom page scroll from 11 spans.
|
|
26
|
+
*
|
|
27
|
+
* The fix is not here — it is on the containers: every clipping/scrolling container in the kit
|
|
28
|
+
* that holds consumer markup declares `position:relative` so it becomes the containing block for
|
|
29
|
+
* its own out-of-flow content (see test/css.test.mjs, which will not let a new one skip it).
|
|
30
|
+
* Note the measurement trap: documentElement.scrollWidth sees it, body.scrollWidth does not —
|
|
31
|
+
* the honest check is `window.scrollTo(9999, 0)` then reading `window.scrollX`. */
|
|
17
32
|
.fdy-visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
18
33
|
/* Opt-in list reset. base.css is intentionally a *light* reset — it does NOT strip list/paragraph
|
|
19
34
|
* margins (see the kit's own list components, which each reset themselves). If you run a utility
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/* Freeday — Accordion (native <details>/<summary>, zero-JS) */
|
|
2
|
-
|
|
2
|
+
/* position:relative — containing block for out-of-flow panel content (see base.css). NOT redundant
|
|
3
|
+
with the panel's reveal animation: that animation gives the panel a transform (which happens to
|
|
4
|
+
make it a containing block too), but it lives behind prefers-reduced-motion:no-preference — so
|
|
5
|
+
without this line the escape bug appears ONLY for readers who asked for reduced motion. */
|
|
6
|
+
.fdy-accordion{position:relative;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;background:var(--color-surface);}
|
|
3
7
|
.fdy-accordion__item + .fdy-accordion__item{border-top:var(--bw) solid var(--color-border);}
|
|
4
8
|
.fdy-accordion__item > summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-4);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
5
9
|
.fdy-accordion__item > summary::-webkit-details-marker{display:none;}
|
package/src/components/card.css
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* Freeday — Card */
|
|
2
|
-
|
|
2
|
+
/* position:relative — containing block for out-of-flow card content (see base.css); it also gives
|
|
3
|
+
consumers the anchor a corner ribbon/badge needs. */
|
|
4
|
+
.fdy-card{position:relative;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-lift);transition:box-shadow var(--dur-base) var(--ease-standard),transform var(--dur-base) var(--ease-standard);}
|
|
3
5
|
.fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
|
|
4
6
|
.fdy-card--interactive{cursor:pointer;}
|
|
5
7
|
.fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
|
|
@@ -10,6 +12,10 @@
|
|
|
10
12
|
* announced — reset the UA button box. Note: it does NOT reset background/border, because
|
|
11
13
|
* .fdy-card already sets its own surface + border (resetting them here would strip the card). */
|
|
12
14
|
.fdy-card--button{display:block;width:100%;text-align:inherit;color:inherit;appearance:none;-webkit-appearance:none;}
|
|
15
|
+
/* Disabled card-as-control — same contract as .fdy-list__row--button: dim + not-allowed, and the
|
|
16
|
+
--interactive lift is withdrawn so a refusing control stops answering the pointer. */
|
|
17
|
+
.fdy-card--button:disabled,.fdy-card--button[aria-disabled="true"],.fdy-card--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
18
|
+
.fdy-card--interactive:disabled:hover,.fdy-card--interactive[aria-disabled="true"]:hover{box-shadow:var(--shadow-lift);transform:none;}
|
|
13
19
|
.fdy-card__body{padding:var(--space-5);}
|
|
14
20
|
.fdy-card__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0 0 var(--space-1);}
|
|
15
21
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/* Freeday — Carousel (scroll-snap track + arrows + dots). Enhanced by freeday-carousel.js. */
|
|
2
2
|
.fdy-carousel{position:relative;}
|
|
3
|
-
.fdy-
|
|
3
|
+
/* position:relative — the arrows are positioned against .fdy-carousel (the parent), so this only
|
|
4
|
+
catches out-of-flow content inside the SLIDES, which would otherwise escape the viewport
|
|
5
|
+
entirely and scroll the page by one slide-offset per slide (see base.css). */
|
|
6
|
+
.fdy-carousel__viewport{position:relative;display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;border-radius:var(--radius-lg);scrollbar-width:none;-ms-overflow-style:none;}
|
|
4
7
|
.fdy-carousel__viewport::-webkit-scrollbar{display:none;}
|
|
5
8
|
.fdy-carousel__slide{flex:0 0 100%;min-width:0;scroll-snap-align:center;}
|
|
6
9
|
.fdy-carousel__arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:2;display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:0;border-radius:var(--radius-full);background:color-mix(in srgb,var(--color-surface) 86%,transparent);color:var(--color-text);box-shadow:var(--shadow-2);cursor:pointer;transition:background var(--dur-fast) var(--ease-standard);}
|
package/src/components/list.css
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Not to be confused with .fdy-list-reset (base.css), which only strips UA bullets/indent.
|
|
10
10
|
* Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
|
|
11
|
-
|
|
11
|
+
/* position:relative — containing block for out-of-flow row content (see base.css). */
|
|
12
|
+
.fdy-list{position:relative;list-style:none;margin:0;padding:0;background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;}
|
|
12
13
|
.fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
|
|
13
14
|
/* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
|
|
14
15
|
* of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
|
|
@@ -21,6 +22,12 @@
|
|
|
21
22
|
.fdy-list__row--button:hover,.fdy-list__row--interactive:hover{background:var(--color-surface-2);}
|
|
22
23
|
.fdy-list__row--button:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--color-primary);}
|
|
23
24
|
.fdy-list__row--interactive{cursor:pointer;}
|
|
25
|
+
/* Disabled row. The reset above adopts the UA button box, but not its disabled state — so a row
|
|
26
|
+
disabled mid-flight (a redirect in progress, say) kept lighting up and kept a pointer cursor:
|
|
27
|
+
a control answering the pointer while refusing input. Same contract as every other disabled
|
|
28
|
+
control in the kit (dim + not-allowed), hover withdrawn, both the native and the aria- form. */
|
|
29
|
+
.fdy-list__row--button:disabled,.fdy-list__row--button[aria-disabled="true"],.fdy-list__row--interactive[aria-disabled="true"]{opacity:.5;cursor:not-allowed;}
|
|
30
|
+
.fdy-list__row--button:disabled:hover,.fdy-list__row--button[aria-disabled="true"]:hover,.fdy-list__row--interactive[aria-disabled="true"]:hover{background:none;}
|
|
24
31
|
/* Row internals: a title/meta stack that truncates, and a trailing slot pinned right. */
|
|
25
32
|
.fdy-list__main{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;flex:1;}
|
|
26
33
|
.fdy-list__title{font-weight:var(--weight-medium);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
package/src/components/table.css
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/* Freeday — Table */
|
|
2
|
-
|
|
2
|
+
/* position:relative on every scroller/clipper — it makes the box the containing block for its own
|
|
3
|
+
absolutely positioned content (.fdy-visually-hidden above all), which `overflow` alone does NOT
|
|
4
|
+
clip. Without it a hidden label in a wide table scrolls the whole page. See base.css. */
|
|
5
|
+
.fdy-table-wrap{position:relative;overflow-x:auto;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);}
|
|
3
6
|
.fdy-table{width:100%;border-collapse:collapse;font-size:var(--text-sm);}
|
|
4
7
|
.fdy-table caption{text-align:left;padding:var(--space-3) var(--space-4);font-weight:var(--weight-semibold);color:var(--color-text);}
|
|
5
8
|
.fdy-table th{text-align:left;padding:var(--space-3) var(--space-4);font-size:var(--text-xs);font-weight:var(--weight-semibold);text-transform:uppercase;letter-spacing:var(--tracking-wide);color:var(--color-text-muted);background:var(--color-surface-2);border-bottom:var(--bw) solid var(--color-border);white-space:nowrap;}
|
|
@@ -36,7 +39,7 @@
|
|
|
36
39
|
|
|
37
40
|
/* Data-table shell (toolbar + scroll + footer stay put while the table scrolls) */
|
|
38
41
|
.fdy-datatable{border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);overflow:hidden;}
|
|
39
|
-
.fdy-table-scroll{overflow-x:auto;}
|
|
42
|
+
.fdy-table-scroll{position:relative;overflow-x:auto;}
|
|
40
43
|
.fdy-table-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border);}
|
|
41
44
|
.fdy-table-toolbar__search{max-width:18rem;}
|
|
42
45
|
.fdy-table-toolbar__spacer{flex:1;}
|
package/src/components/tabs.css
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Freeday — Tabs (WAI-ARIA APG) */
|
|
2
|
-
|
|
2
|
+
/* position:relative — containing block for out-of-flow content in the tabs (see base.css). */
|
|
3
|
+
.fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
|
|
3
4
|
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
4
5
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
5
6
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|