@cahyo-dimas/freeday 1.29.0 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,82 @@
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.31.0] — 2026-08-18
7
+ Improvement note 002: density was a one-way door.
8
+ ### Added
9
+ - **`[data-density="comfortable"]` is now a real rule.** The kit shipped only the `compact` block,
10
+ and its own comment promised per-subtree density — true in one direction only. These are
11
+ inheriting custom properties, so once `<html>` is compact *every* subtree is compact and a
12
+ `comfortable` wrapper matched nothing at all. An app that is dense overall (a back office where
13
+ most screens are data grids) could not opt its shared chrome back out: measured against a sibling
14
+ product, the logo sat 12px from the edge instead of 16 and the avatar 12 instead of 20 — four
15
+ pixels in two places, from `--space-4` and `--control-h`, which reads as sloppiness rather than as
16
+ a token. The workaround, restating the five defaults on a local class, is exactly the copy that
17
+ goes stale when the kit retunes a step.
18
+ Note the kit's own README has been telling people to write `data-density="comfortable"` on `<html>`
19
+ since long before a rule existed for it — harmless there, because it matched the defaults by
20
+ accident, and broken the moment anyone needed it on a wrapper.
21
+ ### Notes on the shape of the fix
22
+ - The five values are **derived from the tokens' own defaults** at build time, not written out a
23
+ second time. Same key set by construction, so a token that gains a `$compact` value automatically
24
+ gains its way back out, and a retuned default can never leave the two blocks disagreeing. The
25
+ report's hand-written block was correct in all five values — this only removes the chance for it
26
+ to stop being correct.
27
+ ### Added — guards
28
+ - `test/build.test.mjs` asserts the two density blocks cover the *same* tokens, that every
29
+ comfortable value equals the `:root` default, and that none of them equals its compact counterpart
30
+ (a block that resets nothing is as green as one that works).
31
+ - `browser/theme.mjs` measures it in a real engine: a compact root, a `comfortable` island that
32
+ returns to a 40px control and `--space-4: 1rem`, and a `compact` island nested back inside that —
33
+ because the point of the rule is the box that comes out, not the declaration going in.
34
+
35
+ ## [1.30.0] — 2026-08-18
36
+ Note 008 (adopting 1.29.0) plus a direct design report: *"the input border looks too dark next to the
37
+ card and button-group borders."*
38
+ ### Fixed
39
+ - **The dark theme's contrast has not actually been tested since v1.21.0.** `contrast.test.mjs` looked
40
+ for `:root[data-theme="dark"]`; un-rooting the theme selectors in 1.21.0 changed the emitted
41
+ selector to `[data-theme="dark"]`, so the regex matched nothing, `dark` fell back to the **light**
42
+ values, and every `WCAG contrast — DARK` assertion re-tested the light theme under a dark label —
43
+ 54 declarations ignored, all green. Fixed, and the scope now has a guard of its own: a dark scope
44
+ that parses to nothing, or that resolves to the light surface, fails loudly. *No latent dark-theme
45
+ failures were hiding behind it — the theme passes on its own merits.*
46
+ - **`--color-control-border` retuned** to a new ramp step `--slate-450` (`#798295`). It was
47
+ `--slate-500`, the same ink as `--color-text-subtle`: a control **boundary** carrying text-level
48
+ contrast (4.69:1 on surface where WCAG 1.4.11 asks 3:1), which is what makes a form look heavier
49
+ than the cards around it. Now ≈3.9/3.6/3.4 across the three light surfaces. The same step
50
+ *strengthens* the dark theme, which was sitting at **3.02:1** on `surface-3` — 0.02 above the
51
+ floor, and unguarded because of the bug above; it is now 3.66:1.
52
+ - **`.fdy-stat__label` uses `--color-text-muted`.** It spent `--color-text-subtle`, which is gated at
53
+ 3.0 *on purpose* (placeholders, dividers, decorative glyphs) and measures 4.41:1 on `surface-2` —
54
+ under AA for text, on the only thing naming the number above it. Reported in 006 §6 and still
55
+ carried by the app at 1.29.0.
56
+ ### Added
57
+ - **`.fdy-btn--stretch` now works in a `.fdy-list__row`** (note 008 §1). The pattern existed for
58
+ "one card, one primary action, one escape hatch" and had no equivalent for the same sentence with
59
+ *row* in it — an app rendering both grid and list from one component had to hand-roll the row half.
60
+ The row anchors the overlay itself, opt-in through `:has(.fdy-btn--stretch)` so a plain row is
61
+ untouched and nothing an app pinned inside one starts resolving against a new containing block.
62
+ Measured before the fix: **every** row's overlay covered the whole list and the last one in the DOM
63
+ won, so a click on row one opened row two — under row one's name.
64
+ ### Notes on the shape of the fix
65
+ - **Note 008 §2 is not reproducible** and no code shipped for it. With a positive control to prove
66
+ the measurement works (pointing at the card body *does* put the stretched target in `:hover`),
67
+ pointing at the escape hatch leaves it at rest: the hover chain is `HTML > BODY > card > footer >
68
+ hatch`, and the stretched button is a sibling, not an ancestor. The `pointer-events:none` rule the
69
+ report carries fixes something the kit's own composition does not do.
70
+ - The report's contrast figures (4.41 / 4.69) were re-derived from the 1.29 ramp and are exact.
71
+ ### Added — guards
72
+ - `contrast.test.mjs`: the control border is now asserted on **all three** surfaces (`surface-3` was
73
+ missing, and it is the worst case that pinned the dark theme to the floor) and at **3.25**, not
74
+ 3.0 — a boundary that clears the floor by 0.02 has no headroom, and the margin is what catches a
75
+ revert to the old ink.
76
+ - `css.test.mjs`: a stat label must spend a *text* ink, and a list row hosting a stretched target
77
+ must anchor it — both are class↔token pairings that every token-level assertion passes straight
78
+ through.
79
+ - `browser/card-stretch.mjs`: the list case, asserting **row one** (the point that hides the bug is
80
+ row two, which passes even when the overlay belongs to another row).
81
+
6
82
  ## [1.29.0] — 2026-08-18
7
83
  Improvement note 001 from a second consuming app (a 40-screen back office). Eight findings; six
8
84
  executed, one already documented, one left as an owner decision.
package/COMPONENTS.md CHANGED
@@ -232,7 +232,8 @@ utility variant that has to agree with the shell must use `nav`; using `md` leav
232
232
 
233
233
  Density: `data-density="compact"` works on `<html>` **or any wrapper** — the selector is a bare
234
234
  `[data-density="compact"]` over inheriting custom properties, so one screen can be dense while the
235
- rest of the app is not.
235
+ rest of the app is not. `data-density="comfortable"` is a real rule too, so the reverse also works:
236
+ a compact root with one region opted back out (shared chrome that must match a sibling product).
236
237
 
237
238
  ---
238
239
 
@@ -962,9 +963,13 @@ while it stays a real `<button>`/`<a>` with real keyboard semantics.
962
963
  - **Do not add `.fdy-btn--stretch` to more than one control per card** — the last one wins the
963
964
  overlap, and which one that is depends on source order.
964
965
  - Text under the overlay is no longer selectable; that is the cost of the pattern, not a bug.
965
- - **Cards only.** The overlay anchors to the nearest *positioned* ancestor, and in the kit that is
966
- `.fdy-card`. A `.fdy-list__row` is **not** positioned (`.fdy-list` is), so a stretched target in a
967
- list row would cover the **whole list** for clickable rows use `.fdy-list__row--button`.
966
+ - **Cards and list rows.** The overlay anchors to the nearest *positioned* ancestor, and both hosts
967
+ supply one: `.fdy-card` always, `.fdy-list__row` when it contains a stretched target. Everything
968
+ above applies to a row unchanged same markup, same automatic raise. In any *other* container,
969
+ make the element `position:relative` yourself, or the overlay resolves against whatever is
970
+ positioned further up (in a list that used to be `.fdy-list` itself, so every row's target covered
971
+ the whole list and the last one in the DOM won — a click on row one opened row two).
972
+ A row whose *whole* surface is one control is still better as `.fdy-list__row--button`.
968
973
  - `.fdy-btn` nudges itself on `:hover`/`:active`, and a transformed element becomes the containing
969
974
  block for its own absolutely positioned descendants — so a hand-rolled `::after{inset:0}` overlay
970
975
  **re-anchors to the button mid-gesture** and the click never lands. `--stretch` neutralises those
package/README.id.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.29.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.29.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.31.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.31.0)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic — satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs — the UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.29.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.29.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.31.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.31.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`.
package/USAGE.md CHANGED
@@ -27,6 +27,21 @@ the single biggest cause of "flat grey mush". Pick the role, not the size:
27
27
  Everything else is body. If you reach for a fourth title size, you probably need a section, not a
28
28
  font size.
29
29
 
30
+
31
+ ## Borders: two roles, two contrast contracts
32
+
33
+ `--color-border` (and `-muted` / `-strong`) is **decoration** — a card edge, a table rule, a
34
+ separator. Nothing in WCAG requires it to be visible, and it is deliberately faint (≈1.3:1).
35
+
36
+ `--color-control-border` is the **boundary of a control** — input, textarea, checkbox, radio,
37
+ switch, dropzone. WCAG 1.4.11 requires 3:1 against every surface it can sit on, so it is
38
+ necessarily darker than a card edge; that difference is the contract, not an inconsistency.
39
+
40
+ It is tuned to clear that floor with margin (≈3.4–3.9 depending on surface) rather than to be as
41
+ dark as it can be: a boundary that reads as loud as body text makes a form feel heavier than it is.
42
+ Do not "harmonise" a control border down to `--color-border` — that drops a required 3:1 boundary to
43
+ 1.3:1.
44
+
30
45
  ## 2. Spacing rhythm — three gaps, always from the scale
31
46
 
32
47
  Never a loose value; always `var(--space-N)` (a 4px scale). Three rhythms carry most layouts, and the
@@ -115,6 +130,13 @@ container, a single `<section>`. An app whose two list screens are dense and who
115
130
  are not should scope it per screen rather than densifying everything. Set it at one level per screen,
116
131
  never per component.
117
132
 
133
+ **And it goes both ways.** `data-density="comfortable"` is a real rule, not just the absence of
134
+ `compact`, so an app that is dense *at the root* can opt one region back out — a header, a toolbar,
135
+ a footer shared with a sibling product that has to line up pixel for pixel. Inheritance only runs
136
+ downhill; without that rule a compact root left every subtree compact and a `comfortable` wrapper
137
+ changed nothing. Do not restate the five values by hand: that copy goes stale the moment the kit
138
+ retunes a step.
139
+
118
140
  ## 6b. Navigation: one component, two orientations — and never a tab role
119
141
 
120
142
  A navigation link is an `<a class="fdy-nav__item">` marked **`aria-current="page"`**. That does not
@@ -18,6 +18,7 @@
18
18
  --slate-200: #e0e3ea;
19
19
  --slate-300: #cbd0da;
20
20
  --slate-400: #99a1b3;
21
+ --slate-450: #798295;
21
22
  --slate-500: #6b7488;
22
23
  --slate-600: #4d5568;
23
24
  --slate-700: #3a4152;
@@ -138,7 +139,7 @@
138
139
  --color-border: var(--slate-200);
139
140
  --color-border-strong: var(--slate-300);
140
141
  --color-border-muted: var(--slate-100);
141
- --color-control-border: var(--slate-500);
142
+ --color-control-border: var(--slate-450);
142
143
  --color-switch-thumb: var(--slate-0);
143
144
  --color-shadow-inset: rgba(16,14,30,.06);
144
145
  --color-inverse-surface: var(--slate-900);
@@ -209,7 +210,7 @@
209
210
  --color-border: var(--slate-800);
210
211
  --color-border-strong: var(--slate-700);
211
212
  --color-border-muted: #1c2130;
212
- --color-control-border: var(--slate-500);
213
+ --color-control-border: var(--slate-450);
213
214
  --color-inverse-surface: var(--slate-100);
214
215
  --color-inverse-text: var(--slate-900);
215
216
  --color-success: var(--green-500);
@@ -272,7 +273,7 @@
272
273
  --color-border: var(--slate-800);
273
274
  --color-border-strong: var(--slate-700);
274
275
  --color-border-muted: #1c2130;
275
- --color-control-border: var(--slate-500);
276
+ --color-control-border: var(--slate-450);
276
277
  --color-inverse-surface: var(--slate-100);
277
278
  --color-inverse-text: var(--slate-900);
278
279
  --color-success: var(--green-500);
@@ -328,7 +329,7 @@
328
329
  --color-border: var(--slate-200);
329
330
  --color-border-strong: var(--slate-300);
330
331
  --color-border-muted: var(--slate-100);
331
- --color-control-border: var(--slate-500);
332
+ --color-control-border: var(--slate-450);
332
333
  --color-inverse-surface: var(--slate-900);
333
334
  --color-inverse-text: var(--slate-0);
334
335
  --color-success: var(--green-600);
@@ -367,6 +368,18 @@
367
368
  --space-6: 1.25rem;
368
369
  --control-h: 2rem;
369
370
  }
371
+ /* ...and the way back out. Inheritance only runs downhill: once the root is compact every subtree is
372
+ * compact, and before this rule existed a data-density="comfortable" wrapper matched nothing — so the
373
+ * comment above was true in one direction only. An app that is dense overall and carries one region
374
+ * of shared chrome (a header, a toolbar, a footer shared with a sibling product) had to restate the
375
+ * defaults by hand, which is exactly the local copy that goes stale when the kit retunes them. */
376
+ [data-density="comfortable"] {
377
+ --space-3: 0.75rem;
378
+ --space-4: 1rem;
379
+ --space-5: 1.25rem;
380
+ --space-6: 1.5rem;
381
+ --control-h: 2.5rem;
382
+ }
370
383
 
371
384
  /* Freeday components — GENERATED by tokens/build.mjs. Do not edit by hand. */
372
385
  /* Freeday base layer — reset ringan + default via token */
@@ -779,13 +792,13 @@ a { color: var(--color-primary); }
779
792
  because forgetting it fails SILENTLY: the secondary control looks and hovers exactly as before and
780
793
  simply never receives the click, which is the very symptom the pattern exists to cure. Matched by
781
794
  shape (anything focusable) so a control the kit does not ship yet is covered too. */
782
- .fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch){z-index:1;}
795
+ :is(.fdy-card,.fdy-list__row):has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch){z-index:1;}
783
796
  /* The position is a DEFAULT, not an override: z-index needs a positioned box (or a flex/grid item),
784
797
  but an app that deliberately pins a control inside the card — a corner dismiss, a favourite star —
785
798
  must keep its own `position:absolute`. Wrapped in :where() so this whole selector weighs nothing
786
799
  and ANY app rule wins it; measured, because at normal specificity the kit silently dragged an
787
800
  absolutely positioned dismiss button back into the flow. Do not merge these two rules. */
788
- :where(.fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
801
+ :where(:is(.fdy-card,.fdy-list__row):has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
789
802
  .fdy-card__body{padding:var(--space-5);}
790
803
  .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);}
791
804
  .fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
@@ -1043,7 +1056,10 @@ a { color: var(--color-primary); }
1043
1056
  * single boxed strip, or use .fdy-stats--boxed for shared surface + dividers. */
1044
1057
  .fdy-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:var(--space-5);}
1045
1058
  .fdy-stat{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;}
1046
- .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
1059
+ /* --color-text-muted, not -subtle: this is a LABEL, i.e. text, and -subtle is deliberately gated at
1060
+ 3.0 (placeholders, dividers, decorative glyphs). On surface-2 -subtle measures 4.41 — under AA
1061
+ for text, and a stat label is often the only thing naming the number above it. */
1062
+ .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-muted);}
1047
1063
  .fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
1048
1064
  .fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
1049
1065
  .fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
@@ -1343,6 +1359,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1343
1359
  * Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
1344
1360
  /* position:relative — containing block for out-of-flow row content (see base.css). */
1345
1361
  .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;}
1362
+ /* A row that hosts a stretched target has to be the anchor itself (#008 §1). Without this the
1363
+ overlay resolves against .fdy-list — which IS positioned — so EVERY row's target covers the whole
1364
+ list and the last one in the DOM wins: measured, a click on row one opens row two, under row one's
1365
+ name, which looks exactly like working software. Opt-in via :has(), so a row that doesn't use the
1366
+ pattern is untouched and nothing an app absolutely positions inside a plain row starts resolving
1367
+ against a new containing block. The escape-hatch raise lives in card.css beside the card's. */
1368
+ .fdy-list__row:has(.fdy-btn--stretch){position:relative;}
1346
1369
  .fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
1347
1370
  /* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
1348
1371
  * of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
package/dist/freeday.css CHANGED
@@ -409,13 +409,13 @@ a { color: var(--color-primary); }
409
409
  because forgetting it fails SILENTLY: the secondary control looks and hovers exactly as before and
410
410
  simply never receives the click, which is the very symptom the pattern exists to cure. Matched by
411
411
  shape (anything focusable) so a control the kit does not ship yet is covered too. */
412
- .fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch){z-index:1;}
412
+ :is(.fdy-card,.fdy-list__row):has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch){z-index:1;}
413
413
  /* The position is a DEFAULT, not an override: z-index needs a positioned box (or a flex/grid item),
414
414
  but an app that deliberately pins a control inside the card — a corner dismiss, a favourite star —
415
415
  must keep its own `position:absolute`. Wrapped in :where() so this whole selector weighs nothing
416
416
  and ANY app rule wins it; measured, because at normal specificity the kit silently dragged an
417
417
  absolutely positioned dismiss button back into the flow. Do not merge these two rules. */
418
- :where(.fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
418
+ :where(:is(.fdy-card,.fdy-list__row):has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
419
419
  .fdy-card__body{padding:var(--space-5);}
420
420
  .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);}
421
421
  .fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
@@ -673,7 +673,10 @@ a { color: var(--color-primary); }
673
673
  * single boxed strip, or use .fdy-stats--boxed for shared surface + dividers. */
674
674
  .fdy-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:var(--space-5);}
675
675
  .fdy-stat{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;}
676
- .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
676
+ /* --color-text-muted, not -subtle: this is a LABEL, i.e. text, and -subtle is deliberately gated at
677
+ 3.0 (placeholders, dividers, decorative glyphs). On surface-2 -subtle measures 4.41 — under AA
678
+ for text, and a stat label is often the only thing naming the number above it. */
679
+ .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-muted);}
677
680
  .fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
678
681
  .fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
679
682
  .fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
@@ -973,6 +976,13 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
973
976
  * Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
974
977
  /* position:relative — containing block for out-of-flow row content (see base.css). */
975
978
  .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;}
979
+ /* A row that hosts a stretched target has to be the anchor itself (#008 §1). Without this the
980
+ overlay resolves against .fdy-list — which IS positioned — so EVERY row's target covers the whole
981
+ list and the last one in the DOM wins: measured, a click on row one opens row two, under row one's
982
+ name, which looks exactly like working software. Opt-in via :has(), so a row that doesn't use the
983
+ pattern is untouched and nothing an app absolutely positions inside a plain row starts resolving
984
+ against a new containing block. The escape-hatch raise lives in card.css beside the card's. */
985
+ .fdy-list__row:has(.fdy-btn--stretch){position:relative;}
976
986
  .fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
977
987
  /* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
978
988
  * of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
@@ -17,6 +17,7 @@
17
17
  --slate-200: #e0e3ea;
18
18
  --slate-300: #cbd0da;
19
19
  --slate-400: #99a1b3;
20
+ --slate-450: #798295;
20
21
  --slate-500: #6b7488;
21
22
  --slate-600: #4d5568;
22
23
  --slate-700: #3a4152;
@@ -137,7 +138,7 @@
137
138
  --color-border: var(--slate-200);
138
139
  --color-border-strong: var(--slate-300);
139
140
  --color-border-muted: var(--slate-100);
140
- --color-control-border: var(--slate-500);
141
+ --color-control-border: var(--slate-450);
141
142
  --color-switch-thumb: var(--slate-0);
142
143
  --color-shadow-inset: rgba(16,14,30,.06);
143
144
  --color-inverse-surface: var(--slate-900);
@@ -208,7 +209,7 @@
208
209
  --color-border: var(--slate-800);
209
210
  --color-border-strong: var(--slate-700);
210
211
  --color-border-muted: #1c2130;
211
- --color-control-border: var(--slate-500);
212
+ --color-control-border: var(--slate-450);
212
213
  --color-inverse-surface: var(--slate-100);
213
214
  --color-inverse-text: var(--slate-900);
214
215
  --color-success: var(--green-500);
@@ -271,7 +272,7 @@
271
272
  --color-border: var(--slate-800);
272
273
  --color-border-strong: var(--slate-700);
273
274
  --color-border-muted: #1c2130;
274
- --color-control-border: var(--slate-500);
275
+ --color-control-border: var(--slate-450);
275
276
  --color-inverse-surface: var(--slate-100);
276
277
  --color-inverse-text: var(--slate-900);
277
278
  --color-success: var(--green-500);
@@ -327,7 +328,7 @@
327
328
  --color-border: var(--slate-200);
328
329
  --color-border-strong: var(--slate-300);
329
330
  --color-border-muted: var(--slate-100);
330
- --color-control-border: var(--slate-500);
331
+ --color-control-border: var(--slate-450);
331
332
  --color-inverse-surface: var(--slate-900);
332
333
  --color-inverse-text: var(--slate-0);
333
334
  --color-success: var(--green-600);
@@ -366,3 +367,15 @@
366
367
  --space-6: 1.25rem;
367
368
  --control-h: 2rem;
368
369
  }
370
+ /* ...and the way back out. Inheritance only runs downhill: once the root is compact every subtree is
371
+ * compact, and before this rule existed a data-density="comfortable" wrapper matched nothing — so the
372
+ * comment above was true in one direction only. An app that is dense overall and carries one region
373
+ * of shared chrome (a header, a toolbar, a footer shared with a sibling product) had to restate the
374
+ * defaults by hand, which is exactly the local copy that goes stale when the kit retunes them. */
375
+ [data-density="comfortable"] {
376
+ --space-3: 0.75rem;
377
+ --space-4: 1rem;
378
+ --space-5: 1.25rem;
379
+ --space-6: 1.5rem;
380
+ --control-h: 2.5rem;
381
+ }
@@ -184,7 +184,7 @@ live docs also have a copy button per component.
184
184
  ```bash
185
185
  npm i @cahyo-dimas/freeday
186
186
  ```
187
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.29.0"` (public npm package). `dist/` is
187
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.31.0"` (public npm package). `dist/` is
188
188
  committed and published → no build step; `npm ci` runs without auth.
189
189
 
190
190
  ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.29.0",
3
+ "version": "1.31.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,13 +24,13 @@
24
24
  because forgetting it fails SILENTLY: the secondary control looks and hovers exactly as before and
25
25
  simply never receives the click, which is the very symptom the pattern exists to cure. Matched by
26
26
  shape (anything focusable) so a control the kit does not ship yet is covered too. */
27
- .fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch){z-index:1;}
27
+ :is(.fdy-card,.fdy-list__row):has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch){z-index:1;}
28
28
  /* The position is a DEFAULT, not an override: z-index needs a positioned box (or a flex/grid item),
29
29
  but an app that deliberately pins a control inside the card — a corner dismiss, a favourite star —
30
30
  must keep its own `position:absolute`. Wrapped in :where() so this whole selector weighs nothing
31
31
  and ANY app rule wins it; measured, because at normal specificity the kit silently dragged an
32
32
  absolutely positioned dismiss button back into the flow. Do not merge these two rules. */
33
- :where(.fdy-card:has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
33
+ :where(:is(.fdy-card,.fdy-list__row):has(.fdy-btn--stretch) :is(a[href],button,input,select,textarea,[tabindex]):not(.fdy-btn--stretch)){position:relative;}
34
34
  .fdy-card__body{padding:var(--space-5);}
35
35
  .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);}
36
36
  .fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
@@ -40,7 +40,10 @@
40
40
  * single boxed strip, or use .fdy-stats--boxed for shared surface + dividers. */
41
41
  .fdy-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:var(--space-5);}
42
42
  .fdy-stat{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;}
43
- .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
43
+ /* --color-text-muted, not -subtle: this is a LABEL, i.e. text, and -subtle is deliberately gated at
44
+ 3.0 (placeholders, dividers, decorative glyphs). On surface-2 -subtle measures 4.41 — under AA
45
+ for text, and a stat label is often the only thing naming the number above it. */
46
+ .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-muted);}
44
47
  .fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
45
48
  .fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
46
49
  .fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
@@ -10,6 +10,13 @@
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
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;}
13
+ /* A row that hosts a stretched target has to be the anchor itself (#008 §1). Without this the
14
+ overlay resolves against .fdy-list — which IS positioned — so EVERY row's target covers the whole
15
+ list and the last one in the DOM wins: measured, a click on row one opens row two, under row one's
16
+ name, which looks exactly like working software. Opt-in via :has(), so a row that doesn't use the
17
+ pattern is untouched and nothing an app absolutely positions inside a plain row starts resolving
18
+ against a new containing block. The escape-hatch raise lives in card.css beside the card's. */
19
+ .fdy-list__row:has(.fdy-btn--stretch){position:relative;}
13
20
  .fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
14
21
  /* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
15
22
  * of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
@@ -10,7 +10,7 @@
10
10
  "slate": {
11
11
  "0": { "$value": "#ffffff" }, "50": { "$value": "#f7f8fa" },
12
12
  "100": { "$value": "#eef0f4" }, "200": { "$value": "#e0e3ea" },
13
- "300": { "$value": "#cbd0da" }, "400": { "$value": "#99a1b3" },
13
+ "300": { "$value": "#cbd0da" }, "400": { "$value": "#99a1b3" }, "450": { "$value": "#798295" },
14
14
  "500": { "$value": "#6b7488" }, "600": { "$value": "#4d5568" },
15
15
  "700": { "$value": "#3a4152" }, "800": { "$value": "#262b38" },
16
16
  "900": { "$value": "#171b26" }, "950": { "$value": "#0e111a" }
@@ -86,7 +86,7 @@
86
86
  "border": { "$value": "{slate.200}", "$dark": "{slate.800}" },
87
87
  "border-strong": { "$value": "{slate.300}", "$dark": "{slate.700}" },
88
88
  "border-muted": { "$value": "{slate.100}", "$dark": "#1c2130" },
89
- "control-border": { "$value": "{slate.500}", "$dark": "{slate.500}" },
89
+ "control-border": { "$value": "{slate.450}", "$dark": "{slate.450}" },
90
90
  "switch-thumb": { "$value": "{slate.0}" },
91
91
  "shadow-inset": { "$value": "rgba(16,14,30,.06)" },
92
92