@cahyo-dimas/freeday 1.28.0 → 1.30.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 +103 -0
- package/COMPONENTS.md +46 -8
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/USAGE.md +15 -0
- package/adapters/blazor/FdyCfl.razor +9 -0
- package/adapters/blazor/FdyCfl.razor.cs +13 -0
- package/adapters/react/components/FdyCfl.tsx +28 -1
- package/adapters/vue/components/FdyCfl.vue +33 -3
- package/dist/freeday.bundle.css +47 -9
- package/dist/freeday.css +42 -5
- package/dist/freeday.tokens.css +5 -4
- package/docs/getting-started.md +1 -1
- package/package.json +1 -1
- package/src/components/card.css +2 -2
- package/src/components/composition.css +20 -1
- package/src/components/input.css +9 -1
- package/src/components/list.css +7 -0
- package/src/components/menu.css +4 -1
- package/tokens/tokens.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,109 @@
|
|
|
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.30.0] — 2026-08-18
|
|
7
|
+
Note 008 (adopting 1.29.0) plus a direct design report: *"the input border looks too dark next to the
|
|
8
|
+
card and button-group borders."*
|
|
9
|
+
### Fixed
|
|
10
|
+
- **The dark theme's contrast has not actually been tested since v1.21.0.** `contrast.test.mjs` looked
|
|
11
|
+
for `:root[data-theme="dark"]`; un-rooting the theme selectors in 1.21.0 changed the emitted
|
|
12
|
+
selector to `[data-theme="dark"]`, so the regex matched nothing, `dark` fell back to the **light**
|
|
13
|
+
values, and every `WCAG contrast — DARK` assertion re-tested the light theme under a dark label —
|
|
14
|
+
54 declarations ignored, all green. Fixed, and the scope now has a guard of its own: a dark scope
|
|
15
|
+
that parses to nothing, or that resolves to the light surface, fails loudly. *No latent dark-theme
|
|
16
|
+
failures were hiding behind it — the theme passes on its own merits.*
|
|
17
|
+
- **`--color-control-border` retuned** to a new ramp step `--slate-450` (`#798295`). It was
|
|
18
|
+
`--slate-500`, the same ink as `--color-text-subtle`: a control **boundary** carrying text-level
|
|
19
|
+
contrast (4.69:1 on surface where WCAG 1.4.11 asks 3:1), which is what makes a form look heavier
|
|
20
|
+
than the cards around it. Now ≈3.9/3.6/3.4 across the three light surfaces. The same step
|
|
21
|
+
*strengthens* the dark theme, which was sitting at **3.02:1** on `surface-3` — 0.02 above the
|
|
22
|
+
floor, and unguarded because of the bug above; it is now 3.66:1.
|
|
23
|
+
- **`.fdy-stat__label` uses `--color-text-muted`.** It spent `--color-text-subtle`, which is gated at
|
|
24
|
+
3.0 *on purpose* (placeholders, dividers, decorative glyphs) and measures 4.41:1 on `surface-2` —
|
|
25
|
+
under AA for text, on the only thing naming the number above it. Reported in 006 §6 and still
|
|
26
|
+
carried by the app at 1.29.0.
|
|
27
|
+
### Added
|
|
28
|
+
- **`.fdy-btn--stretch` now works in a `.fdy-list__row`** (note 008 §1). The pattern existed for
|
|
29
|
+
"one card, one primary action, one escape hatch" and had no equivalent for the same sentence with
|
|
30
|
+
*row* in it — an app rendering both grid and list from one component had to hand-roll the row half.
|
|
31
|
+
The row anchors the overlay itself, opt-in through `:has(.fdy-btn--stretch)` so a plain row is
|
|
32
|
+
untouched and nothing an app pinned inside one starts resolving against a new containing block.
|
|
33
|
+
Measured before the fix: **every** row's overlay covered the whole list and the last one in the DOM
|
|
34
|
+
won, so a click on row one opened row two — under row one's name.
|
|
35
|
+
### Notes on the shape of the fix
|
|
36
|
+
- **Note 008 §2 is not reproducible** and no code shipped for it. With a positive control to prove
|
|
37
|
+
the measurement works (pointing at the card body *does* put the stretched target in `:hover`),
|
|
38
|
+
pointing at the escape hatch leaves it at rest: the hover chain is `HTML > BODY > card > footer >
|
|
39
|
+
hatch`, and the stretched button is a sibling, not an ancestor. The `pointer-events:none` rule the
|
|
40
|
+
report carries fixes something the kit's own composition does not do.
|
|
41
|
+
- The report's contrast figures (4.41 / 4.69) were re-derived from the 1.29 ramp and are exact.
|
|
42
|
+
### Added — guards
|
|
43
|
+
- `contrast.test.mjs`: the control border is now asserted on **all three** surfaces (`surface-3` was
|
|
44
|
+
missing, and it is the worst case that pinned the dark theme to the floor) and at **3.25**, not
|
|
45
|
+
3.0 — a boundary that clears the floor by 0.02 has no headroom, and the margin is what catches a
|
|
46
|
+
revert to the old ink.
|
|
47
|
+
- `css.test.mjs`: a stat label must spend a *text* ink, and a list row hosting a stretched target
|
|
48
|
+
must anchor it — both are class↔token pairings that every token-level assertion passes straight
|
|
49
|
+
through.
|
|
50
|
+
- `browser/card-stretch.mjs`: the list case, asserting **row one** (the point that hides the bug is
|
|
51
|
+
row two, which passes even when the overlay belongs to another row).
|
|
52
|
+
|
|
53
|
+
## [1.29.0] — 2026-08-18
|
|
54
|
+
Improvement note 001 from a second consuming app (a 40-screen back office). Eight findings; six
|
|
55
|
+
executed, one already documented, one left as an owner decision.
|
|
56
|
+
### Added
|
|
57
|
+
- **`clearable` on `FdyCfl`** (Vue · React · Blazor). The value type already said `Row | null`, but
|
|
58
|
+
the emit was `Row` only — a choose-from-list could be **set and never unset**, which breaks every
|
|
59
|
+
*optional* foreign key (a device with no project, an expense with no workflow, a top-level record
|
|
60
|
+
with no parent). A user who picked the wrong row had to reload the form. The clear control is a
|
|
61
|
+
second `.fdy-input-group__btn`, emits `null`, returns focus to the trigger, and never touches the
|
|
62
|
+
dialog.
|
|
63
|
+
- **`.fdy-label--required`** — the marker is painted through `::after` with **CSS alt text**
|
|
64
|
+
(`content:"*" / ""`), so the glyph appears and the accessibility tree gets nothing. The control
|
|
65
|
+
already carries `required`; a `<span>` an app has to remember to mark `aria-hidden` can be
|
|
66
|
+
forgotten, this cannot.
|
|
67
|
+
- **`.fdy-icon`** — a `1em` square, `flex:none` box for the icons the kit deliberately does not
|
|
68
|
+
ship. Every icon slot it *does* ship (`.fdy-btn__icon`, `.fdy-nav__icon`, `.fdy-state__icon`)
|
|
69
|
+
already assumed something sensible inside; a standalone icon had no contract.
|
|
70
|
+
- **`.fdy-text-success` · `.fdy-text-warning` · `.fdy-text-danger`** — the kit had three
|
|
71
|
+
*de-emphasis* text roles and no *state* role, so a consequential sentence ("Amount changed from X
|
|
72
|
+
to Y", "No approver assigned") had to fall back to `.fdy-text-caption`, which de-emphasises
|
|
73
|
+
exactly the line that should stand out. A badge is wrong (prose, not a status) and an alert is
|
|
74
|
+
wrong (a block, not one line in a row).
|
|
75
|
+
### Fixed
|
|
76
|
+
- **`.fdy-menu__item:focus-visible` gets a real ring.** It marked focus with the same fill as
|
|
77
|
+
`:hover`, and `freeday-menu.js` moves real DOM focus — so arrowing through a menu was invisible,
|
|
78
|
+
and hover and focus were identical to everyone else. `.fdy-nav__item`, in the same kit, has always
|
|
79
|
+
done it correctly. Reported by two different apps before it was fixed.
|
|
80
|
+
- **A grouped `<fieldset>` now has its spacing.** `fieldset.fdy-field` already reset the UA border,
|
|
81
|
+
but the **rendered legend is laid out outside the flex flow**, so `gap` never reached it —
|
|
82
|
+
measured 0px between legend and first control. One declaration on the reset the kit already
|
|
83
|
+
ships, not a new block; `COMPONENTS.md` documents the compose (`.fdy-field` on the fieldset,
|
|
84
|
+
`.fdy-label` on the legend), which is what was actually missing.
|
|
85
|
+
### Notes on the shape of the fix
|
|
86
|
+
- **§7 (an Indonesian `aria-label`) is not what it looked like.** The report assumed the rest of the
|
|
87
|
+
kit's output is English; it is not — *every* user-visible string the vanilla enhancers write is
|
|
88
|
+
Indonesian (`Sebelumnya`, `Berikutnya`, `Berisi teks`, `Reset`, `Tutup`, `Menampilkan …`,
|
|
89
|
+
`Bulan berikutnya`, `Format tidak valid.`). Translating one would **create** the mixed interface
|
|
90
|
+
the note objects to. The real choice — an override hook, or switching the defaults and breaking
|
|
91
|
+
every Indonesian consumer — is recorded in `NEXT-UP.md` #6 with its trigger; `COMPONENTS.md` now
|
|
92
|
+
states the caveat in full and points English apps at the typed wrappers.
|
|
93
|
+
- **§8 needed no code**, only the line it asked for: the filter button and its dialog share an
|
|
94
|
+
accessible name on purpose, so a test suite wants `getByRole`, not `getByLabel`.
|
|
95
|
+
- Two traps found by measuring rather than reading: `.fdy-icon` as a bare inline element **ignored
|
|
96
|
+
`width` entirely** (it does not apply to non-replaced inline boxes) and rendered 936px wide until
|
|
97
|
+
it got a `display`; and stacking two colour roles (`.fdy-help.fdy-text-warning`) silently loses to
|
|
98
|
+
whichever rule the bundle happens to emit later — documented, and the docs page no longer does it.
|
|
99
|
+
### Added — guards
|
|
100
|
+
- Adapter specs for `clearable` in both Vue and React (real clicks: emits `null`, empties the field,
|
|
101
|
+
the control disappears with the value it cleared, focus lands on the trigger, dialog stays shut).
|
|
102
|
+
- CSS gate: menu focus must render as more than the hover fill; the required marker must keep its
|
|
103
|
+
alt text; `.fdy-icon` must keep a display that accepts a width; the state roles must spend the
|
|
104
|
+
exact inks the contrast gate proves readable — that last one closes the seam between the two test
|
|
105
|
+
files, where a class could drift to a weaker token while every token assertion stayed green.
|
|
106
|
+
- `contrast.test.mjs` now also asserts the state inks on **plain** surfaces, not just over their own
|
|
107
|
+
`-soft` fills.
|
|
108
|
+
|
|
6
109
|
## [1.28.0] — 2026-08-18
|
|
7
110
|
Improvement note 007, written while fixing a workspace picker whose cards showed a pointer cursor
|
|
8
111
|
and swallowed every click.
|
package/COMPONENTS.md
CHANGED
|
@@ -212,6 +212,8 @@ One role per level of hierarchy — never re-use a card title for a page title.
|
|
|
212
212
|
| `.fdy-title-card` | `<h3>` | A title inside a card or row (`.fdy-card__title` is equivalent). |
|
|
213
213
|
| `.fdy-text-muted` · `.fdy-text-subtle` | any | Secondary / tertiary text colour. |
|
|
214
214
|
| `.fdy-text-caption` | `<p>` | Small muted text: timestamps, help. |
|
|
215
|
+
| `.fdy-text-success` · `.fdy-text-warning` · `.fdy-text-danger` | any | Inline text that carries **state** — a sentence, not a status. Use when a badge is wrong (it is prose) and an alert is wrong (it is one line inside a row): "Amount changed from X to Y", "No approver assigned". **Do not stack two colour roles** (`.fdy-help.fdy-text-warning`): both are single classes, so the one that happens to come later in the stylesheet wins — put the state class on its own element, and use `.fdy-help--error` for help text. |
|
|
216
|
+
| `.fdy-icon` | `<svg>` or a wrapper | Icon box: `1em` square, `flex:none`, so the glyph tracks the text beside it at every scale. The kit ships no paths — bring your own, use the box. |
|
|
215
217
|
| `.fdy-mono` | any | Tabular/monospace data (codes, amounts, ids). |
|
|
216
218
|
|
|
217
219
|
## Utilities
|
|
@@ -291,7 +293,23 @@ native control, otherwise a `<div>` + explicitly associated label.
|
|
|
291
293
|
|
|
292
294
|
- `.fdy-field` (+ `--full` inside `.fdy-form-grid`; widths `--w-sm` `--w-lg` `--w-xl` `--w-2xl`
|
|
293
295
|
`--w-grow` inside `.fdy-filterbar`)
|
|
294
|
-
- `.fdy-label` · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`)
|
|
296
|
+
- `.fdy-label` (+`--required`) · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`)
|
|
297
|
+
|
|
298
|
+
**Grouped controls are a `<fieldset>`, not a new block.** Put `.fdy-field` on the fieldset and
|
|
299
|
+
`.fdy-label` on the legend — the kit already resets the UA border/padding and supplies the spacing:
|
|
300
|
+
|
|
301
|
+
```html
|
|
302
|
+
<fieldset class="fdy-field">
|
|
303
|
+
<legend class="fdy-label">Working week</legend>
|
|
304
|
+
<label class="fdy-check"><input type="checkbox"> Monday</label>
|
|
305
|
+
<label class="fdy-check"><input type="checkbox"> Tuesday</label>
|
|
306
|
+
</fieldset>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**`--required` marks the label, not the accessibility tree.** The control already carries
|
|
310
|
+
`required`; the asterisk is painted through `::after` with CSS alt text, so a screen reader never
|
|
311
|
+
reads "star" after the label — something a `<span>` you have to remember to mark `aria-hidden`
|
|
312
|
+
cannot guarantee.
|
|
295
313
|
- `[readonly]` is styled on input/textarea: full contrast, focusable, copyable.
|
|
296
314
|
|
|
297
315
|
```html
|
|
@@ -472,6 +490,13 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
|
|
|
472
490
|
</dialog>
|
|
473
491
|
```
|
|
474
492
|
|
|
493
|
+
**`clearable` makes the value removable.** Without it a choose-from-list can be set but never unset,
|
|
494
|
+
which breaks every *optional* foreign key — the value type already allows null, only the component
|
|
495
|
+
could not produce it. With it, a clear button appears beside the trigger whenever a row is picked
|
|
496
|
+
and the field is editable; it emits `null` (Vue `update:modelValue`/`change`, React `onChange`,
|
|
497
|
+
Blazor `ValueChanged`) and returns focus to the trigger. It is a second `.fdy-input-group__btn`, not
|
|
498
|
+
a new class.
|
|
499
|
+
|
|
475
500
|
## Date picker — `data-fdy-datepicker`
|
|
476
501
|
> **Typed wrapper: `<FdyDatepicker>`** — Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
477
502
|
|
|
@@ -660,10 +685,19 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
|
|
|
660
685
|
- Footer: `.fdy-table-footer` · `__info` (`data-fdy-table-info`) + `<nav class="fdy-pagination"
|
|
661
686
|
data-fdy-table-pagination>`
|
|
662
687
|
- Sort values: put the raw value in `data-sort-value` when the cell text is formatted.
|
|
663
|
-
- **Language caveat:**
|
|
664
|
-
(`Menampilkan 1–5 dari 7`, `N dipilih`)
|
|
665
|
-
|
|
666
|
-
`
|
|
688
|
+
- **Language caveat:** every user-visible string the **vanilla enhancers** write is Indonesian —
|
|
689
|
+
the table's footer and bulk count (`Menampilkan 1–5 dari 7`, `N dipilih`), its pager and filter
|
|
690
|
+
UI (`Sebelumnya`, `Berikutnya`, `Filter kolom`, `Berisi teks`, `Reset`, `Tutup`), and the same
|
|
691
|
+
applies elsewhere (`Bulan berikutnya`, `Format tidak valid.`). There is no override hook. The
|
|
692
|
+
Vue/React/Blazor components are English throughout, so **an English app should use the typed
|
|
693
|
+
wrapper**, not the enhancer. For an English static page, render the footer/bulk nodes yourself
|
|
694
|
+
from the `fdy-table-change` event instead of using `data-fdy-table-info` /
|
|
695
|
+
`data-fdy-table-bulk-count`; the filter button's `aria-label` has no such escape today.
|
|
696
|
+
|
|
697
|
+
**Testing note:** a column's filter button and the dialog it opens deliberately share one
|
|
698
|
+
accessible name (`Filter <column>`) — a dialog named after its trigger is the normal pattern. In a
|
|
699
|
+
Playwright/Testing-Library suite that means `getByLabel('Filter Name')` resolves to two elements;
|
|
700
|
+
reach for `getByRole('button', { name: 'Filter Name' })` instead.
|
|
667
701
|
|
|
668
702
|
## Pagination — `.fdy-pagination`
|
|
669
703
|
The block class on the `<nav>` is a **structural hook only** — it carries no rule of its own; the
|
|
@@ -928,9 +962,13 @@ while it stays a real `<button>`/`<a>` with real keyboard semantics.
|
|
|
928
962
|
- **Do not add `.fdy-btn--stretch` to more than one control per card** — the last one wins the
|
|
929
963
|
overlap, and which one that is depends on source order.
|
|
930
964
|
- Text under the overlay is no longer selectable; that is the cost of the pattern, not a bug.
|
|
931
|
-
- **Cards
|
|
932
|
-
`.fdy-card
|
|
933
|
-
|
|
965
|
+
- **Cards and list rows.** The overlay anchors to the nearest *positioned* ancestor, and both hosts
|
|
966
|
+
supply one: `.fdy-card` always, `.fdy-list__row` when it contains a stretched target. Everything
|
|
967
|
+
above applies to a row unchanged — same markup, same automatic raise. In any *other* container,
|
|
968
|
+
make the element `position:relative` yourself, or the overlay resolves against whatever is
|
|
969
|
+
positioned further up (in a list that used to be `.fdy-list` itself, so every row's target covered
|
|
970
|
+
the whole list and the last one in the DOM won — a click on row one opened row two).
|
|
971
|
+
A row whose *whole* surface is one control is still better as `.fdy-list__row--button`.
|
|
934
972
|
- `.fdy-btn` nudges itself on `:hover`/`:active`, and a transformed element becomes the containing
|
|
935
973
|
block for its own absolutely positioned descendants — so a hand-rolled `::after{inset:0}` overlay
|
|
936
974
|
**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
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.30.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
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.30.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
|
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
<input class="fdy-input" type="text" readonly value="@DisplayValue"
|
|
12
12
|
placeholder="@Placeholder" aria-labelledby="@AriaLabelledby"
|
|
13
13
|
aria-invalid="@(Invalid ? "true" : null)" aria-readonly="@(Readonly ? "true" : null)" />
|
|
14
|
+
@if (Clearable && Value is not null && !Disabled && !Readonly)
|
|
15
|
+
{
|
|
16
|
+
<button class="fdy-input-group__btn" type="button" @onclick="ClearAsync" aria-label="@ClearLabel">
|
|
17
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
|
|
18
|
+
stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
|
19
|
+
<path d="M6 6l12 12M18 6L6 18"></path>
|
|
20
|
+
</svg>
|
|
21
|
+
</button>
|
|
22
|
+
}
|
|
14
23
|
<button class="fdy-input-group__btn" type="button" @onclick="OpenAsync"
|
|
15
24
|
disabled="@(Disabled || Readonly)" aria-label="@SearchPlaceholder">
|
|
16
25
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
|
|
@@ -37,6 +37,11 @@ public partial class FdyCfl<TRow>
|
|
|
37
37
|
/// <summary>Locked/view mode: shows the value but the search dialog can't be opened.</summary>
|
|
38
38
|
[Parameter] public bool Readonly { get; set; }
|
|
39
39
|
[Parameter] public bool Invalid { get; set; }
|
|
40
|
+
/// <summary>Render a clear button when a row is picked, so an OPTIONAL foreign key can be unset.
|
|
41
|
+
/// <c>Value</c> is already <c>TRow?</c>; without this the component can never produce null.</summary>
|
|
42
|
+
[Parameter] public bool Clearable { get; set; }
|
|
43
|
+
/// <summary>aria-label for the clear button (when <see cref="Clearable"/>).</summary>
|
|
44
|
+
[Parameter] public string ClearLabel { get; set; } = "Clear selection";
|
|
40
45
|
[Parameter] public string? AriaLabelledby { get; set; }
|
|
41
46
|
|
|
42
47
|
/// <summary>Debounce (ms) between a keystroke and the search request.</summary>
|
|
@@ -129,6 +134,14 @@ public partial class FdyCfl<TRow>
|
|
|
129
134
|
|
|
130
135
|
private Task RetryAsync() => LoadAsync(reset: true);
|
|
131
136
|
|
|
137
|
+
/// <summary>Unset the value. Not "choosing nothing": the dialog is not involved, so this neither
|
|
138
|
+
/// opens nor closes it.</summary>
|
|
139
|
+
private async Task ClearAsync()
|
|
140
|
+
{
|
|
141
|
+
Value = default;
|
|
142
|
+
await ValueChanged.InvokeAsync(default);
|
|
143
|
+
}
|
|
144
|
+
|
|
132
145
|
private async Task ChooseAsync(TRow row)
|
|
133
146
|
{
|
|
134
147
|
Value = row;
|
|
@@ -22,7 +22,7 @@ export interface CflPage<Row> {
|
|
|
22
22
|
|
|
23
23
|
export interface FdyCflProps<Row extends Record<string, unknown>> {
|
|
24
24
|
value: Row | null;
|
|
25
|
-
onChange: (value: Row) => void;
|
|
25
|
+
onChange: (value: Row | null) => void;
|
|
26
26
|
fetchPage: (query: string, page: number) => Promise<CflPage<Row>>;
|
|
27
27
|
columns: ReadonlyArray<CflColumn<Row>>;
|
|
28
28
|
display: (row: Row) => string;
|
|
@@ -34,6 +34,11 @@ export interface FdyCflProps<Row extends Record<string, unknown>> {
|
|
|
34
34
|
/** Locked/view mode: shows the picked value (focusable, copyable), but the search dialog can't be opened. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
35
35
|
readonly?: boolean;
|
|
36
36
|
invalid?: boolean;
|
|
37
|
+
/** Render a clear button when a row is picked, so an OPTIONAL foreign key can be unset. Without it
|
|
38
|
+
* `value` accepts `Row | null` but the component can only ever produce a `Row`. */
|
|
39
|
+
clearable?: boolean;
|
|
40
|
+
/** aria-label for the clear button (when `clearable`). Default 'Clear selection'. */
|
|
41
|
+
clearLabel?: string;
|
|
37
42
|
describedby?: string;
|
|
38
43
|
id?: string;
|
|
39
44
|
ariaLabelledby?: string;
|
|
@@ -46,6 +51,14 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
46
51
|
const resultsId: string = `${baseId}-results`;
|
|
47
52
|
const rowId = (index: number): string => `${baseId}-row-${index}`;
|
|
48
53
|
|
|
54
|
+
/* Unsetting is not "picking nothing": it must not touch the dialog, and focus must land on a
|
|
55
|
+
control that still exists — the trigger beside it, since this button disappears with the value. */
|
|
56
|
+
const clearLabelText: string = props.clearLabel ?? 'Clear selection';
|
|
57
|
+
const clearValue = (): void => {
|
|
58
|
+
props.onChange(null);
|
|
59
|
+
triggerRef.current?.focus();
|
|
60
|
+
};
|
|
61
|
+
|
|
49
62
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
50
63
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
51
64
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
@@ -71,6 +84,8 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
71
84
|
|
|
72
85
|
const isDisabled: boolean = props.disabled === true;
|
|
73
86
|
const isReadonly: boolean = props.readonly === true;
|
|
87
|
+
const showClear: boolean =
|
|
88
|
+
props.clearable === true && props.value !== null && isDisabled === false && isReadonly === false;
|
|
74
89
|
const isInvalid: boolean = props.invalid === true;
|
|
75
90
|
const displayValue: string = props.value !== null ? props.display(props.value) : '';
|
|
76
91
|
// The results <table> (owner of `resultsId`) only renders in the rows branch, so gate the
|
|
@@ -253,6 +268,18 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
253
268
|
aria-describedby={props.describedby}
|
|
254
269
|
disabled={isDisabled}
|
|
255
270
|
/>
|
|
271
|
+
{showClear ? (
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
className="fdy-input-group__btn"
|
|
275
|
+
aria-label={clearLabelText}
|
|
276
|
+
onClick={clearValue}
|
|
277
|
+
>
|
|
278
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
|
279
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
280
|
+
</svg>
|
|
281
|
+
</button>
|
|
282
|
+
) : null}
|
|
256
283
|
<button
|
|
257
284
|
ref={triggerRef}
|
|
258
285
|
type="button"
|
|
@@ -35,14 +35,19 @@ const props = defineProps<{
|
|
|
35
35
|
/** Locked/view mode: shows the picked value (focusable, copyable), but the search dialog can't be opened. Unlike `disabled`, it keeps tab order and isn't greyed. */
|
|
36
36
|
readonly?: boolean;
|
|
37
37
|
invalid?: boolean;
|
|
38
|
+
/** Render a clear button when a row is picked, so an OPTIONAL foreign key can be unset. Without it
|
|
39
|
+
* `modelValue` accepts `Row | null` but the component can only ever produce a `Row`. */
|
|
40
|
+
clearable?: boolean;
|
|
41
|
+
/** aria-label for the clear button (when `clearable`). Default 'Clear selection'. */
|
|
42
|
+
clearLabel?: string;
|
|
38
43
|
describedby?: string;
|
|
39
44
|
id?: string;
|
|
40
45
|
ariaLabelledby?: string;
|
|
41
46
|
}>();
|
|
42
47
|
|
|
43
48
|
const emit = defineEmits<{
|
|
44
|
-
'update:modelValue': [value: Row];
|
|
45
|
-
change: [value: Row];
|
|
49
|
+
'update:modelValue': [value: Row | null];
|
|
50
|
+
change: [value: Row | null];
|
|
46
51
|
}>();
|
|
47
52
|
|
|
48
53
|
const baseId: string = useId();
|
|
@@ -67,6 +72,12 @@ const activeIndex: Ref<number> = ref(-1);
|
|
|
67
72
|
|
|
68
73
|
const isDisabled: ComputedRef<boolean> = computed((): boolean => props.disabled === true);
|
|
69
74
|
const isReadonly: ComputedRef<boolean> = computed((): boolean => props.readonly === true);
|
|
75
|
+
|
|
76
|
+
const showClear: ComputedRef<boolean> = computed(
|
|
77
|
+
(): boolean =>
|
|
78
|
+
props.clearable === true && props.modelValue != null && isDisabled.value === false && isReadonly.value === false,
|
|
79
|
+
);
|
|
80
|
+
const clearLabelText: ComputedRef<string> = computed((): string => props.clearLabel ?? 'Clear selection');
|
|
70
81
|
const isInvalid: ComputedRef<boolean> = computed((): boolean => props.invalid === true);
|
|
71
82
|
const displayValue: ComputedRef<string> = computed((): string =>
|
|
72
83
|
props.modelValue !== null ? props.display(props.modelValue) : '',
|
|
@@ -154,12 +165,20 @@ function setActive(index: number): void {
|
|
|
154
165
|
});
|
|
155
166
|
}
|
|
156
167
|
|
|
157
|
-
function commit(row: Row): void {
|
|
168
|
+
function commit(row: Row | null): void {
|
|
158
169
|
emit('update:modelValue', row);
|
|
159
170
|
emit('change', row);
|
|
160
171
|
closeDialog();
|
|
161
172
|
}
|
|
162
173
|
|
|
174
|
+
/* Unsetting is not "picking nothing" — it must not open or close the dialog, and it must leave focus
|
|
175
|
+
on a control that still exists, so focus returns to the trigger beside it. */
|
|
176
|
+
function clearValue(): void {
|
|
177
|
+
emit('update:modelValue', null);
|
|
178
|
+
emit('change', null);
|
|
179
|
+
triggerEl.value?.focus();
|
|
180
|
+
}
|
|
181
|
+
|
|
163
182
|
function onKeydown(e: KeyboardEvent): void {
|
|
164
183
|
switch (e.key) {
|
|
165
184
|
case 'ArrowDown':
|
|
@@ -256,6 +275,17 @@ onBeforeUnmount((): void => {
|
|
|
256
275
|
:aria-describedby="describedby"
|
|
257
276
|
:disabled="isDisabled"
|
|
258
277
|
/>
|
|
278
|
+
<button
|
|
279
|
+
v-if="showClear"
|
|
280
|
+
type="button"
|
|
281
|
+
class="fdy-input-group__btn"
|
|
282
|
+
:aria-label="clearLabelText"
|
|
283
|
+
@click="clearValue"
|
|
284
|
+
>
|
|
285
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
|
286
|
+
<path d="M6 6l12 12M18 6L6 18"></path>
|
|
287
|
+
</svg>
|
|
288
|
+
</button>
|
|
259
289
|
<button
|
|
260
290
|
ref="triggerEl"
|
|
261
291
|
type="button"
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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);
|
|
@@ -779,13 +780,13 @@ a { color: var(--color-primary); }
|
|
|
779
780
|
because forgetting it fails SILENTLY: the secondary control looks and hovers exactly as before and
|
|
780
781
|
simply never receives the click, which is the very symptom the pattern exists to cure. Matched by
|
|
781
782
|
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;}
|
|
783
|
+
: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
784
|
/* The position is a DEFAULT, not an override: z-index needs a positioned box (or a flex/grid item),
|
|
784
785
|
but an app that deliberately pins a control inside the card — a corner dismiss, a favourite star —
|
|
785
786
|
must keep its own `position:absolute`. Wrapped in :where() so this whole selector weighs nothing
|
|
786
787
|
and ANY app rule wins it; measured, because at normal specificity the kit silently dragged an
|
|
787
788
|
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;}
|
|
789
|
+
: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
790
|
.fdy-card__body{padding:var(--space-5);}
|
|
790
791
|
.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
792
|
.fdy-card__desc{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;line-height:var(--leading-normal);}
|
|
@@ -1043,7 +1044,10 @@ a { color: var(--color-primary); }
|
|
|
1043
1044
|
* single boxed strip, or use .fdy-stats--boxed for shared surface + dividers. */
|
|
1044
1045
|
.fdy-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:var(--space-5);}
|
|
1045
1046
|
.fdy-stat{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;}
|
|
1046
|
-
|
|
1047
|
+
/* --color-text-muted, not -subtle: this is a LABEL, i.e. text, and -subtle is deliberately gated at
|
|
1048
|
+
3.0 (placeholders, dividers, decorative glyphs). On surface-2 -subtle measures 4.41 — under AA
|
|
1049
|
+
for text, and a stat label is often the only thing naming the number above it. */
|
|
1050
|
+
.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
1051
|
.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
1052
|
.fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
|
|
1049
1053
|
.fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
|
|
@@ -1051,6 +1055,22 @@ a { color: var(--color-primary); }
|
|
|
1051
1055
|
.fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
|
|
1052
1056
|
.fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
|
|
1053
1057
|
|
|
1058
|
+
/* Icon box (#001 §4). The kit ships no icons on purpose, but every icon slot it DOES ship
|
|
1059
|
+
(.fdy-btn__icon, .fdy-nav__icon, .fdy-state__icon) assumes something sensible inside, so a
|
|
1060
|
+
standalone icon had no contract and every app invented one. 1em keeps the glyph matched to the
|
|
1061
|
+
text beside it at every scale — bring your own paths, use our box. Works on the <svg> itself or
|
|
1062
|
+
on a wrapper around it. */
|
|
1063
|
+
.fdy-icon{display:inline-block;width:1em;height:1em;flex:none;vertical-align:-0.125em;}
|
|
1064
|
+
.fdy-icon>svg{display:block;width:100%;height:100%;}
|
|
1065
|
+
|
|
1066
|
+
/* Inline text that carries STATE (#001 §5). The three roles above all de-emphasise; a sentence like
|
|
1067
|
+
"Amount changed from X to Y" or "No approver assigned" needs the opposite, and is neither a badge
|
|
1068
|
+
(those are statuses, not sentences) nor an .fdy-alert (those are blocks, not one line inside a
|
|
1069
|
+
row). Same -strong inks the badges use, gated by contrast.test.mjs on plain surfaces too. */
|
|
1070
|
+
.fdy-text-success{color:var(--color-success-strong);}
|
|
1071
|
+
.fdy-text-warning{color:var(--color-warning-strong);}
|
|
1072
|
+
.fdy-text-danger{color:var(--color-danger-strong);}
|
|
1073
|
+
|
|
1054
1074
|
/* Freeday — Date picker (input-styled trigger + calendar popover).
|
|
1055
1075
|
* Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
|
|
1056
1076
|
.fdy-datepicker{position:relative;display:inline-block;width:100%;max-width:14rem;}
|
|
@@ -1277,7 +1297,10 @@ a { color: var(--color-primary); }
|
|
|
1277
1297
|
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
1278
1298
|
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
1279
1299
|
fieldset.fdy-field{border:0;margin:0;padding:0;min-inline-size:0;}
|
|
1280
|
-
fieldset
|
|
1300
|
+
/* The rendered legend is laid out outside the fieldset's flex flow, so `gap` never reaches it —
|
|
1301
|
+
measured 0px between legend and first control. This is the spacing contract a grouped field
|
|
1302
|
+
needs; it matches .fdy-field's own gap so the group reads as one rhythm. */
|
|
1303
|
+
fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
1281
1304
|
.fdy-label{font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
1282
1305
|
.fdy-input,.fdy-textarea{width:100%;height:var(--control-h);padding:0 var(--space-3);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1.5px solid var(--color-control-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 2px rgba(16,14,30,.06);transition:border-color var(--dur-fast) var(--ease-standard),box-shadow var(--dur-fast) var(--ease-standard);}
|
|
1283
1306
|
.fdy-input::placeholder,.fdy-textarea::placeholder{color:var(--color-text-subtle);}
|
|
@@ -1301,6 +1324,11 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1301
1324
|
.fdy-input[type="number"]::-webkit-outer-spin-button,.fdy-input[type="number"]::-webkit-inner-spin-button{appearance:none;-webkit-appearance:none;margin:0;}
|
|
1302
1325
|
.fdy-input--error,.fdy-input[aria-invalid="true"],.fdy-textarea[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
1303
1326
|
.fdy-input--error:focus,.fdy-input[aria-invalid="true"]:focus,.fdy-textarea[aria-invalid="true"]:focus{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
1327
|
+
/* Required marker. The control already carries `required`, so the asterisk is decoration: the
|
|
1328
|
+
`/ ""` is CSS alt text — the glyph is painted but exposes NOTHING to the accessibility tree, so a
|
|
1329
|
+
screen reader does not read "star" after every label. That is stricter than a <span> an app has to
|
|
1330
|
+
remember to mark aria-hidden, and it cannot be forgotten. */
|
|
1331
|
+
.fdy-label--required::after{content:"*" / "";margin-left:var(--space-1);color:var(--color-danger);}
|
|
1304
1332
|
.fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
|
|
1305
1333
|
.fdy-help--error{color:var(--color-danger);}
|
|
1306
1334
|
|
|
@@ -1319,6 +1347,13 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1319
1347
|
* Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
|
|
1320
1348
|
/* position:relative — containing block for out-of-flow row content (see base.css). */
|
|
1321
1349
|
.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;}
|
|
1350
|
+
/* A row that hosts a stretched target has to be the anchor itself (#008 §1). Without this the
|
|
1351
|
+
overlay resolves against .fdy-list — which IS positioned — so EVERY row's target covers the whole
|
|
1352
|
+
list and the last one in the DOM wins: measured, a click on row one opens row two, under row one's
|
|
1353
|
+
name, which looks exactly like working software. Opt-in via :has(), so a row that doesn't use the
|
|
1354
|
+
pattern is untouched and nothing an app absolutely positions inside a plain row starts resolving
|
|
1355
|
+
against a new containing block. The escape-hatch raise lives in card.css beside the card's. */
|
|
1356
|
+
.fdy-list__row:has(.fdy-btn--stretch){position:relative;}
|
|
1322
1357
|
.fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
|
|
1323
1358
|
/* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
|
|
1324
1359
|
* of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
|
|
@@ -1351,7 +1386,10 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1351
1386
|
.fdy-menu--end{left:auto;right:0;}
|
|
1352
1387
|
.fdy-menu__item{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);border:0;background:transparent;border-radius:var(--radius-sm);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);text-align:left;cursor:pointer;white-space:nowrap;}
|
|
1353
1388
|
.fdy-menu__item:hover,.fdy-menu__item.is-highlighted{background:var(--color-surface-3);}
|
|
1354
|
-
.
|
|
1389
|
+
/* Focus is not hover. The enhancer moves real DOM focus (freeday-menu.js `.focus()`), so this is
|
|
1390
|
+
the only thing a keyboard user has to go on — with the hover fill alone, arrowing through the
|
|
1391
|
+
menu is invisible. Same ring as .fdy-nav__item, which got this right. Reported twice. */
|
|
1392
|
+
.fdy-menu__item:focus-visible{outline:none;background:var(--color-surface-3);box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
|
|
1355
1393
|
.fdy-menu__item:disabled{opacity:.5;cursor:not-allowed;background:transparent;}
|
|
1356
1394
|
.fdy-menu__item--danger{color:var(--color-danger-strong);}
|
|
1357
1395
|
.fdy-menu__item svg{display:block;width:1rem;height:1rem;flex:none;}
|
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
|
-
|
|
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);}
|
|
@@ -681,6 +684,22 @@ a { color: var(--color-primary); }
|
|
|
681
684
|
.fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
|
|
682
685
|
.fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
|
|
683
686
|
|
|
687
|
+
/* Icon box (#001 §4). The kit ships no icons on purpose, but every icon slot it DOES ship
|
|
688
|
+
(.fdy-btn__icon, .fdy-nav__icon, .fdy-state__icon) assumes something sensible inside, so a
|
|
689
|
+
standalone icon had no contract and every app invented one. 1em keeps the glyph matched to the
|
|
690
|
+
text beside it at every scale — bring your own paths, use our box. Works on the <svg> itself or
|
|
691
|
+
on a wrapper around it. */
|
|
692
|
+
.fdy-icon{display:inline-block;width:1em;height:1em;flex:none;vertical-align:-0.125em;}
|
|
693
|
+
.fdy-icon>svg{display:block;width:100%;height:100%;}
|
|
694
|
+
|
|
695
|
+
/* Inline text that carries STATE (#001 §5). The three roles above all de-emphasise; a sentence like
|
|
696
|
+
"Amount changed from X to Y" or "No approver assigned" needs the opposite, and is neither a badge
|
|
697
|
+
(those are statuses, not sentences) nor an .fdy-alert (those are blocks, not one line inside a
|
|
698
|
+
row). Same -strong inks the badges use, gated by contrast.test.mjs on plain surfaces too. */
|
|
699
|
+
.fdy-text-success{color:var(--color-success-strong);}
|
|
700
|
+
.fdy-text-warning{color:var(--color-warning-strong);}
|
|
701
|
+
.fdy-text-danger{color:var(--color-danger-strong);}
|
|
702
|
+
|
|
684
703
|
/* Freeday — Date picker (input-styled trigger + calendar popover).
|
|
685
704
|
* Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
|
|
686
705
|
.fdy-datepicker{position:relative;display:inline-block;width:100%;max-width:14rem;}
|
|
@@ -907,7 +926,10 @@ a { color: var(--color-primary); }
|
|
|
907
926
|
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
908
927
|
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
909
928
|
fieldset.fdy-field{border:0;margin:0;padding:0;min-inline-size:0;}
|
|
910
|
-
fieldset
|
|
929
|
+
/* The rendered legend is laid out outside the fieldset's flex flow, so `gap` never reaches it —
|
|
930
|
+
measured 0px between legend and first control. This is the spacing contract a grouped field
|
|
931
|
+
needs; it matches .fdy-field's own gap so the group reads as one rhythm. */
|
|
932
|
+
fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
911
933
|
.fdy-label{font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
912
934
|
.fdy-input,.fdy-textarea{width:100%;height:var(--control-h);padding:0 var(--space-3);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1.5px solid var(--color-control-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 2px rgba(16,14,30,.06);transition:border-color var(--dur-fast) var(--ease-standard),box-shadow var(--dur-fast) var(--ease-standard);}
|
|
913
935
|
.fdy-input::placeholder,.fdy-textarea::placeholder{color:var(--color-text-subtle);}
|
|
@@ -931,6 +953,11 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
931
953
|
.fdy-input[type="number"]::-webkit-outer-spin-button,.fdy-input[type="number"]::-webkit-inner-spin-button{appearance:none;-webkit-appearance:none;margin:0;}
|
|
932
954
|
.fdy-input--error,.fdy-input[aria-invalid="true"],.fdy-textarea[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
933
955
|
.fdy-input--error:focus,.fdy-input[aria-invalid="true"]:focus,.fdy-textarea[aria-invalid="true"]:focus{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
956
|
+
/* Required marker. The control already carries `required`, so the asterisk is decoration: the
|
|
957
|
+
`/ ""` is CSS alt text — the glyph is painted but exposes NOTHING to the accessibility tree, so a
|
|
958
|
+
screen reader does not read "star" after every label. That is stricter than a <span> an app has to
|
|
959
|
+
remember to mark aria-hidden, and it cannot be forgotten. */
|
|
960
|
+
.fdy-label--required::after{content:"*" / "";margin-left:var(--space-1);color:var(--color-danger);}
|
|
934
961
|
.fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
|
|
935
962
|
.fdy-help--error{color:var(--color-danger);}
|
|
936
963
|
|
|
@@ -949,6 +976,13 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
949
976
|
* Works on <ul>/<ol> (list-style is reset here) or on plain <div>s. */
|
|
950
977
|
/* position:relative — containing block for out-of-flow row content (see base.css). */
|
|
951
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;}
|
|
952
986
|
.fdy-list__row{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);min-width:0;}
|
|
953
987
|
/* Divider between rows. Two shapes are supported and both are load-bearing: rows as direct children
|
|
954
988
|
* of the list, and rows wrapped in <li> (the semantic shape, where the ADJACENT siblings are the
|
|
@@ -981,7 +1015,10 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
981
1015
|
.fdy-menu--end{left:auto;right:0;}
|
|
982
1016
|
.fdy-menu__item{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);border:0;background:transparent;border-radius:var(--radius-sm);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);text-align:left;cursor:pointer;white-space:nowrap;}
|
|
983
1017
|
.fdy-menu__item:hover,.fdy-menu__item.is-highlighted{background:var(--color-surface-3);}
|
|
984
|
-
.
|
|
1018
|
+
/* Focus is not hover. The enhancer moves real DOM focus (freeday-menu.js `.focus()`), so this is
|
|
1019
|
+
the only thing a keyboard user has to go on — with the hover fill alone, arrowing through the
|
|
1020
|
+
menu is invisible. Same ring as .fdy-nav__item, which got this right. Reported twice. */
|
|
1021
|
+
.fdy-menu__item:focus-visible{outline:none;background:var(--color-surface-3);box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
|
|
985
1022
|
.fdy-menu__item:disabled{opacity:.5;cursor:not-allowed;background:transparent;}
|
|
986
1023
|
.fdy-menu__item--danger{color:var(--color-danger-strong);}
|
|
987
1024
|
.fdy-menu__item svg{display:block;width:1rem;height:1rem;flex:none;}
|
package/dist/freeday.tokens.css
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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);
|
package/docs/getting-started.md
CHANGED
|
@@ -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.
|
|
187
|
+
Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.30.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
package/src/components/card.css
CHANGED
|
@@ -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,10 +40,29 @@
|
|
|
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
|
-
|
|
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);}
|
|
47
50
|
.fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
|
|
48
51
|
.fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
|
|
49
52
|
.fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
|
|
53
|
+
|
|
54
|
+
/* Icon box (#001 §4). The kit ships no icons on purpose, but every icon slot it DOES ship
|
|
55
|
+
(.fdy-btn__icon, .fdy-nav__icon, .fdy-state__icon) assumes something sensible inside, so a
|
|
56
|
+
standalone icon had no contract and every app invented one. 1em keeps the glyph matched to the
|
|
57
|
+
text beside it at every scale — bring your own paths, use our box. Works on the <svg> itself or
|
|
58
|
+
on a wrapper around it. */
|
|
59
|
+
.fdy-icon{display:inline-block;width:1em;height:1em;flex:none;vertical-align:-0.125em;}
|
|
60
|
+
.fdy-icon>svg{display:block;width:100%;height:100%;}
|
|
61
|
+
|
|
62
|
+
/* Inline text that carries STATE (#001 §5). The three roles above all de-emphasise; a sentence like
|
|
63
|
+
"Amount changed from X to Y" or "No approver assigned" needs the opposite, and is neither a badge
|
|
64
|
+
(those are statuses, not sentences) nor an .fdy-alert (those are blocks, not one line inside a
|
|
65
|
+
row). Same -strong inks the badges use, gated by contrast.test.mjs on plain surfaces too. */
|
|
66
|
+
.fdy-text-success{color:var(--color-success-strong);}
|
|
67
|
+
.fdy-text-warning{color:var(--color-warning-strong);}
|
|
68
|
+
.fdy-text-danger{color:var(--color-danger-strong);}
|
package/src/components/input.css
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
5
5
|
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
6
6
|
fieldset.fdy-field{border:0;margin:0;padding:0;min-inline-size:0;}
|
|
7
|
-
fieldset
|
|
7
|
+
/* The rendered legend is laid out outside the fieldset's flex flow, so `gap` never reaches it —
|
|
8
|
+
measured 0px between legend and first control. This is the spacing contract a grouped field
|
|
9
|
+
needs; it matches .fdy-field's own gap so the group reads as one rhythm. */
|
|
10
|
+
fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
8
11
|
.fdy-label{font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);}
|
|
9
12
|
.fdy-input,.fdy-textarea{width:100%;height:var(--control-h);padding:0 var(--space-3);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1.5px solid var(--color-control-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 2px rgba(16,14,30,.06);transition:border-color var(--dur-fast) var(--ease-standard),box-shadow var(--dur-fast) var(--ease-standard);}
|
|
10
13
|
.fdy-input::placeholder,.fdy-textarea::placeholder{color:var(--color-text-subtle);}
|
|
@@ -28,5 +31,10 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
28
31
|
.fdy-input[type="number"]::-webkit-outer-spin-button,.fdy-input[type="number"]::-webkit-inner-spin-button{appearance:none;-webkit-appearance:none;margin:0;}
|
|
29
32
|
.fdy-input--error,.fdy-input[aria-invalid="true"],.fdy-textarea[aria-invalid="true"]{border-color:var(--color-danger);}
|
|
30
33
|
.fdy-input--error:focus,.fdy-input[aria-invalid="true"]:focus,.fdy-textarea[aria-invalid="true"]:focus{border-color:var(--color-danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-danger) 26%,transparent);}
|
|
34
|
+
/* Required marker. The control already carries `required`, so the asterisk is decoration: the
|
|
35
|
+
`/ ""` is CSS alt text — the glyph is painted but exposes NOTHING to the accessibility tree, so a
|
|
36
|
+
screen reader does not read "star" after every label. That is stricter than a <span> an app has to
|
|
37
|
+
remember to mark aria-hidden, and it cannot be forgotten. */
|
|
38
|
+
.fdy-label--required::after{content:"*" / "";margin-left:var(--space-1);color:var(--color-danger);}
|
|
31
39
|
.fdy-help{font-size:var(--text-xs);color:var(--color-text-muted);}
|
|
32
40
|
.fdy-help--error{color:var(--color-danger);}
|
package/src/components/list.css
CHANGED
|
@@ -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
|
package/src/components/menu.css
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
.fdy-menu--end{left:auto;right:0;}
|
|
6
6
|
.fdy-menu__item{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);border:0;background:transparent;border-radius:var(--radius-sm);font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text);text-align:left;cursor:pointer;white-space:nowrap;}
|
|
7
7
|
.fdy-menu__item:hover,.fdy-menu__item.is-highlighted{background:var(--color-surface-3);}
|
|
8
|
-
.
|
|
8
|
+
/* Focus is not hover. The enhancer moves real DOM focus (freeday-menu.js `.focus()`), so this is
|
|
9
|
+
the only thing a keyboard user has to go on — with the hover fill alone, arrowing through the
|
|
10
|
+
menu is invisible. Same ring as .fdy-nav__item, which got this right. Reported twice. */
|
|
11
|
+
.fdy-menu__item:focus-visible{outline:none;background:var(--color-surface-3);box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
|
|
9
12
|
.fdy-menu__item:disabled{opacity:.5;cursor:not-allowed;background:transparent;}
|
|
10
13
|
.fdy-menu__item--danger{color:var(--color-danger-strong);}
|
|
11
14
|
.fdy-menu__item svg{display:block;width:1rem;height:1rem;flex:none;}
|
package/tokens/tokens.json
CHANGED
|
@@ -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.
|
|
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
|
|