@cahyo-dimas/freeday 1.21.0 → 1.22.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 +68 -0
- package/COMPONENTS.md +64 -2
- package/README.id.md +23 -1
- package/README.md +22 -1
- package/USAGE.md +21 -0
- package/dist/freeday.bundle.css +39 -1
- package/dist/freeday.css +39 -1
- package/docs/agent-onboarding.md +35 -4
- package/docs/getting-started.md +10 -5
- package/package.json +2 -2
- package/src/components/app-shell.css +9 -0
- package/src/components/appbar.css +9 -0
- package/src/components/button.css +14 -0
- package/src/components/tabs.css +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,69 @@
|
|
|
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.22.0] — 2026-08-12
|
|
7
|
+
Two bodies of work. **(a)** three more findings from consumption round 5 — the report grew §5-§7
|
|
8
|
+
after 1.21.0 was cut; **(b)** a **routing** failure found in the same adopted project, which had been
|
|
9
|
+
built on raw markup + enhancers inside a framework that has typed wrappers because nothing in the
|
|
10
|
+
package ever told it otherwise. (b) was prepared as 1.21.1 and folded in here rather than shipped as
|
|
11
|
+
a separate patch minutes earlier.
|
|
12
|
+
### Added — from consumption round 5 (§5, §6)
|
|
13
|
+
- **`.fdy-nav--horizontal`** — the same navigation links laid out as a **row**, for a top-nav
|
|
14
|
+
application that puts its primary nav in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar.
|
|
15
|
+
Deliberately a modifier, not a new block: the item, its states and `aria-current="page"` are
|
|
16
|
+
unchanged. On `.fdy-appbar--primary` the links go on-colour automatically (full on-colour ink in
|
|
17
|
+
every state; the current page is carried by the background wash plus the semibold weight the base
|
|
18
|
+
rule already applies, so no dimmed variant trades away the bar's guaranteed contrast).
|
|
19
|
+
Reported as "there is no horizontal navigation component": `.fdy-nav` was the sidebar's vertical
|
|
20
|
+
menu and `.fdy-appbar` had `__brand`/`__spacer`/`__actions` but nothing to put *links* in — so a
|
|
21
|
+
top-nav app had a component for the bar and none for what the bar is mostly made of.
|
|
22
|
+
- **`.fdy-tabs__tab` also honours `aria-current="page"`**, so the tab *look* legitimately serves
|
|
23
|
+
**routed** sub-navigation (`/settings/profile` · `/settings/billing`) built from real links. A link
|
|
24
|
+
cannot be `aria-selected` — that is invalid ARIA on an anchor — so the app that borrowed the class
|
|
25
|
+
had to restate the active style for a second attribute. Keep `role="tab"` + `freeday-tabs.js` for
|
|
26
|
+
**in-page** tabs, where the roving-tabindex / one-panel-per-tab contract is real.
|
|
27
|
+
- **Pressed state for toggle buttons** — `aria-pressed="true"` now styles: soft primary fill + strong
|
|
28
|
+
ink on `--ghost`/`--text`, an inverted-gradient sunken look on the solid button, and the same
|
|
29
|
+
treatment in the danger hue. No new class or markup contract, since `aria-pressed` is already the
|
|
30
|
+
correct attribute — and it is what turns `.fdy-btn-group` from a joined row into a **complete**
|
|
31
|
+
segmented control. Previously "which one am I on" was left to each app to hand-tint, which is a
|
|
32
|
+
colour decision the kit should own.
|
|
33
|
+
*Found while building it:* one shared `.fdy-btn[aria-pressed="true"]` rule **cannot** work. A
|
|
34
|
+
gradient is a background-**image**, so the `background` shorthand resets `background-color` to
|
|
35
|
+
transparent — at equal specificity the later rule won and the ghost segment lost its fill entirely.
|
|
36
|
+
Each fill variant now defines its own pressed look. The CSS read correctly in the file; only a real
|
|
37
|
+
engine showed it, which is why the new guard lives in `browser/`.
|
|
38
|
+
### Fixed — from consumption round 5 (§7)
|
|
39
|
+
- **Hard rule 1 in `COMPONENTS.md` now records its one exception.** The rule says a modifier is
|
|
40
|
+
always written beside its block class, but `.fdy-input-group__addon--icon` is standalone by design
|
|
41
|
+
— adding the base `__addon` gives a search glyph the grey fill and divider of a `Rp` / `%` prefix.
|
|
42
|
+
An agent following the rule literally produced the wrong control; one following the CSS comment
|
|
43
|
+
broke the stated rule. The exception is now written down, and it is the only one.
|
|
44
|
+
### Fixed — the package now routes by stack
|
|
45
|
+
- **The paste-block in `docs/agent-onboarding.md` never mentioned the adapters.** That block is the
|
|
46
|
+
only text that lands in a consuming project's agent instructions, so it is the only text an agent
|
|
47
|
+
re-reads on every task — and it opened with "a CSS kit + enhancers, **not a component framework**",
|
|
48
|
+
which actively steers a Vue/React project away from the typed components. It now opens with a
|
|
49
|
+
**step 0 routing table** (Vue → `/vue` · React → `/react` · Blazor → RCL · everything else → raw
|
|
50
|
+
markup), names the ten wrapped components, and states the failure mode: the raw path renders
|
|
51
|
+
correctly the first time and then silently stops hydrating.
|
|
52
|
+
- **`COMPONENTS.md` flags the wrapper at each of the ten sections**, so the choice is visible at the
|
|
53
|
+
point of use rather than only on an onboarding page read once — with each component's *real*
|
|
54
|
+
binding (`v-model` / `value`+`onChange` / `@bind-Value` for the six value-bound ones; `open`+`close`
|
|
55
|
+
for modal and drawer; data props for chart; `columns`+`rows`+events for table). Hard rule 7 states
|
|
56
|
+
it up front.
|
|
57
|
+
- **Both READMEs lead with the routing table**, above the first import block. Previously the first
|
|
58
|
+
runnable snippet was the vanilla one and the adapter table sat ~130 lines below it.
|
|
59
|
+
- **`docs/getting-started.md`** core concept 2 ("the enhancer is the source of truth") is now scoped
|
|
60
|
+
to the raw path, and notes that Vue/React re-implement the ten natively while Blazor wraps the
|
|
61
|
+
enhancer over interop.
|
|
62
|
+
### Notes
|
|
63
|
+
- Root cause was structural, not editorial: at **v1.18.0** `docs/getting-started.md` — the only
|
|
64
|
+
per-stack router the kit had — was **not in `files`**, so `npm i` delivered a README whose
|
|
65
|
+
"Starting a project? per stack" link pointed at a path that does not exist inside `node_modules`.
|
|
66
|
+
`adapters/` *was* shipped, so the wrappers sat in the install, unused, with nothing pointing at
|
|
67
|
+
them. v1.20.0 started shipping the docs; this release makes them route.
|
|
68
|
+
|
|
6
69
|
## [1.21.0] — 2026-08-12
|
|
7
70
|
Fifth round of real-app consumption feedback, written while that app adopted 1.20.0. Two findings —
|
|
8
71
|
and a **withdrawal**: the reporter retracted 1.20's rejected §A themselves after isolating the real
|
|
@@ -57,6 +120,11 @@ reproduce here, so the rejection stands and the withdrawal is recorded rather th
|
|
|
57
120
|
hidden labels in a wide table, asserting the page cannot scroll horizontally *and* that the scroller
|
|
58
121
|
still scrolls. A static test cannot see this failure; only a layout engine can. Both guards
|
|
59
122
|
mutation-checked — reverting any single `position:relative` fails them.
|
|
123
|
+
- **`browser/state.mjs`** — pressed toggles and nav orientation in a real engine: the pressed ghost
|
|
124
|
+
segment must keep a flat background-colour (the mutation that reproduces the shorthand bug fails
|
|
125
|
+
it), the solid toggle must read as inset, `--horizontal` must actually be a row with the current
|
|
126
|
+
link marked, on-colour nav ink must stay on-colour in both states, and a routed tab must take its
|
|
127
|
+
underline from `aria-current`. Mutation-checked against all three fixes.
|
|
60
128
|
- **`browser/theme.mjs`** — subtree theming end-to-end: a `.fdy-title-page` and a `.fdy-card` inside
|
|
61
129
|
`<section data-theme="dark">` take the dark tokens, a nested light island goes back, and
|
|
62
130
|
`data-theme` on `<html>` still themes everything. `test/build.test.mjs` guards the selector shape
|
package/COMPONENTS.md
CHANGED
|
@@ -15,6 +15,10 @@ live docs.
|
|
|
15
15
|
1. **Prefix is `fdy-`, pattern is compact BEM**: block `fdy-card`, element `fdy-card__title`,
|
|
16
16
|
modifier `fdy-card--elevated`. A modifier is **always** written alongside its block class:
|
|
17
17
|
`class="fdy-btn fdy-btn--ghost"`, never `fdy-btn--ghost` alone.
|
|
18
|
+
**One documented exception:** `.fdy-input-group__addon--icon` is **standalone** — it is a
|
|
19
|
+
borderless leading glyph, and adding the base `.fdy-input-group__addon` would give it the grey
|
|
20
|
+
fill and divider of a `Rp` / `%` prefix, which is not what a search icon is. Written
|
|
21
|
+
`class="fdy-input-group__addon--icon"`, alone. It is the only one; everywhere else the rule holds.
|
|
18
22
|
2. **Never invent a class.** If it is not in this file, it does not exist. Freeday has no
|
|
19
23
|
`fdy-grid`, no `fdy-flex`, no `fdy-mt-4`, no `fdy-primary`.
|
|
20
24
|
3. **Never write a raw hex or px value** in app CSS. Use the tokens — `var(--color-primary)`,
|
|
@@ -30,6 +34,14 @@ live docs.
|
|
|
30
34
|
invalid state does not belong to the control itself.
|
|
31
35
|
6. **Interactive components need their enhancer script.** Static ones (button, card, badge, table,
|
|
32
36
|
alert, breadcrumb, timeline, accordion, tree-without-cascade…) are CSS-only.
|
|
37
|
+
7. **On Vue, React or Blazor, ten components have a typed wrapper — use it.** `FdyCombo` ·
|
|
38
|
+
`FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` ·
|
|
39
|
+
`FdyTable` · `FdyModal` · `FdyDrawer`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
|
|
40
|
+
`Freeday.Blazor` RCL. Each is flagged at its own section below. Hand-writing their raw markup in
|
|
41
|
+
those stacks *looks* right — the enhancer initialises once and the first render is correct — then
|
|
42
|
+
breaks quietly: later-rendered DOM is never hydrated and the widget's state sits in the DOM
|
|
43
|
+
instead of your framework's. Everything else is the same markup everywhere; hydrate the other
|
|
44
|
+
interactive components with `useFreeday` (Vue/React) or `FreedayBlazor.initAll` (Blazor).
|
|
33
45
|
|
|
34
46
|
### Containment — why the kit's containers are `position:relative`
|
|
35
47
|
|
|
@@ -227,6 +239,10 @@ rest of the app is not.
|
|
|
227
239
|
- Modifiers: `--ghost` `--danger` `--text` `--icon` (square, icon-only) `--sm` `--lg`
|
|
228
240
|
- Parts: `.fdy-btn__icon` (wraps the `<svg>`) · `.fdy-btn-group` (segmented row, `role="group"` +
|
|
229
241
|
`aria-label`)
|
|
242
|
+
- **Toggle / segmented:** `aria-pressed="true"` gives a real pressed state (soft primary fill on
|
|
243
|
+
`--ghost`/`--text`, an inset-sunk gradient on the solid button). No extra class — `aria-pressed`
|
|
244
|
+
is already the right attribute, and it is what turns `.fdy-btn-group` from a joined row into a
|
|
245
|
+
complete segmented control.
|
|
230
246
|
- A11y: `--icon` **requires** `aria-label`. Use `<button type="button">` unless it submits.
|
|
231
247
|
|
|
232
248
|
```html
|
|
@@ -312,6 +328,8 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
|
|
|
312
328
|
```
|
|
313
329
|
|
|
314
330
|
## Select / combobox — `.fdy-combo`
|
|
331
|
+
> **Typed wrapper: `<FdyCombo>`** — 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).
|
|
332
|
+
|
|
315
333
|
Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
|
|
316
334
|
|
|
317
335
|
- `.fdy-combo` (+`--error`, `--no-icon`) · `__button` `__value` (+`--placeholder`) `__listbox`
|
|
@@ -334,6 +352,8 @@ Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
|
|
|
334
352
|
```
|
|
335
353
|
|
|
336
354
|
## Autocomplete — `.fdy-autocomplete`
|
|
355
|
+
> **Typed wrapper: `<FdyAutocomplete>`** — 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).
|
|
356
|
+
|
|
337
357
|
Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
|
|
338
358
|
|
|
339
359
|
- `.fdy-autocomplete` · `__listbox` `__option` `__empty`
|
|
@@ -341,6 +361,8 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
|
|
|
341
361
|
`aria-controls` → the listbox `id`, `autocomplete="off"`.
|
|
342
362
|
|
|
343
363
|
## Cascade select — `.fdy-cascade`
|
|
364
|
+
> **Typed wrapper: `<FdyCascade>`** — 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).
|
|
365
|
+
|
|
344
366
|
Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
|
|
345
367
|
child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
|
|
346
368
|
|
|
@@ -360,6 +382,8 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
|
|
|
360
382
|
```
|
|
361
383
|
|
|
362
384
|
## Choose-from-list (CFL) — `data-fdy-cfl`
|
|
385
|
+
> **Typed wrapper: `<FdyCfl>`** — 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).
|
|
386
|
+
|
|
363
387
|
A read-only field backed by master data: the button opens a searchable dialog, the picked row fills
|
|
364
388
|
the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
|
|
365
389
|
|
|
@@ -410,6 +434,8 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
|
|
|
410
434
|
```
|
|
411
435
|
|
|
412
436
|
## Date picker — `data-fdy-datepicker`
|
|
437
|
+
> **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).
|
|
438
|
+
|
|
413
439
|
Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author an empty `<div>`**
|
|
414
440
|
— the enhancer builds everything.
|
|
415
441
|
|
|
@@ -417,6 +443,7 @@ Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author
|
|
|
417
443
|
`__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` `__dow` `__day`
|
|
418
444
|
- Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
|
|
419
445
|
get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start.
|
|
446
|
+
**Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor) — use it in those stacks.
|
|
420
447
|
- Attributes: `data-value="YYYY-MM-DD"`, `data-placeholder`, `data-label`, `data-fdy-no-icon`
|
|
421
448
|
|
|
422
449
|
```html
|
|
@@ -517,6 +544,8 @@ Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-tab
|
|
|
517
544
|
```
|
|
518
545
|
|
|
519
546
|
## Data table — `.fdy-datatable`
|
|
547
|
+
> **Typed wrapper: `<FdyTable>`** — controlled: `columns` + `rows`, with sort/filter/page events (`update:pageIndex` · `onPageIndexChange` · `PageIndexChanged`) and `process` for driving a card list off the same processed set. The markup below is the raw enhancer path.
|
|
548
|
+
|
|
520
549
|
The interactive table: global search, sort, per-column filters, row selection + bulk bar,
|
|
521
550
|
pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` + `data-fdy-table`
|
|
522
551
|
(`data-page-size="N"`).
|
|
@@ -556,6 +585,8 @@ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__ic
|
|
|
556
585
|
replaces.
|
|
557
586
|
|
|
558
587
|
## Charts — `data-fdy-chart`
|
|
588
|
+
> **Typed wrapper: `<FdyChart>`** — data props in all three (`type` + `values` / `series`); it repaints on data change, so `FreedayChart.update(el)` is only for the raw path below.
|
|
589
|
+
|
|
559
590
|
Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`; call
|
|
560
591
|
`FreedayChart.update(el)` after changing data (or use `FdyChart` in Vue/React/Blazor).
|
|
561
592
|
|
|
@@ -662,18 +693,43 @@ Size-matched placeholders so nothing shifts when data lands: `--title` `--text`
|
|
|
662
693
|
|
|
663
694
|
# Navigation
|
|
664
695
|
|
|
665
|
-
## Nav (
|
|
666
|
-
|
|
696
|
+
## Nav (menu) — `.fdy-nav`
|
|
697
|
+
Navigation links — **vertical by default** (the app shell sidebar), horizontal with `--horizontal`. Items are `<a class="fdy-nav__item">` with
|
|
667
698
|
`__icon` / `__label` / `__badge`; the current one gets `aria-current="page"`.
|
|
668
699
|
`--flat` drops the surface. Nested groups are native `<details>`:
|
|
669
700
|
|
|
670
701
|
- `.fdy-nav__tree` + `<summary class="fdy-nav__item">` + `.fdy-nav__caret` → children in
|
|
671
702
|
`.fdy-nav__sub`
|
|
672
703
|
- `.fdy-nav__group` + `<summary class="fdy-nav__grouplabel">` → a collapsible section
|
|
704
|
+
- **`--horizontal`** lays the same links out as a row, for a **top-nav application** (primary
|
|
705
|
+
navigation in `.fdy-appbar` or `.fdy-app__topbar`, no sidebar). Same item, same states, same
|
|
706
|
+
`aria-current="page"`; the row scrolls if it runs out of width. On `.fdy-appbar--primary` the
|
|
707
|
+
links go on-colour automatically.
|
|
708
|
+
|
|
709
|
+
```html
|
|
710
|
+
<header class="fdy-appbar">
|
|
711
|
+
<a class="fdy-appbar__brand" href="/">Acme</a>
|
|
712
|
+
<nav class="fdy-nav fdy-nav--horizontal" aria-label="Main">
|
|
713
|
+
<a class="fdy-nav__item" href="/invoices" aria-current="page">Invoices</a>
|
|
714
|
+
<a class="fdy-nav__item" href="/customers">Customers</a>
|
|
715
|
+
</nav>
|
|
716
|
+
<span class="fdy-appbar__spacer"></span>
|
|
717
|
+
<div class="fdy-appbar__actions"><!-- buttons --></div>
|
|
718
|
+
</header>
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
> **Which one for navigation?** A nav link is a link marked `aria-current="page"` — never
|
|
722
|
+
> `aria-selected` (invalid ARIA on an anchor) and never `role="tab"`. Use `.fdy-nav--horizontal`
|
|
723
|
+
> for an application's **primary** navigation. For **routed sub-navigation** that should look like
|
|
724
|
+
> tabs (`/settings/profile` · `/settings/billing`), put `.fdy-tabs__list` / `.fdy-tabs__tab` on
|
|
725
|
+
> plain `<a>`s — those classes honour `aria-current="page"` too — and do **not** add the tab roles
|
|
726
|
+
> or `freeday-tabs.js`, which promise a keyboard contract routes do not have.
|
|
673
727
|
|
|
674
728
|
## App bar — `.fdy-appbar`
|
|
675
729
|
Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--sticky`
|
|
676
730
|
`--elevated` `--dense` `--primary` (on-colour controls). Parts `__brand` `__spacer` `__actions`.
|
|
731
|
+
It ships **no link class of its own** — put `<nav class="fdy-nav fdy-nav--horizontal">` in it
|
|
732
|
+
for primary navigation (see Nav above).
|
|
677
733
|
|
|
678
734
|
## Breadcrumb — `.fdy-breadcrumb`
|
|
679
735
|
`<nav class="fdy-breadcrumb" aria-label="Breadcrumb">` → `<ol class="fdy-breadcrumb__list">` with
|
|
@@ -682,6 +738,8 @@ Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--
|
|
|
682
738
|
|
|
683
739
|
## Tabs — `.fdy-tabs`
|
|
684
740
|
APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
|
|
741
|
+
`.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
|
|
742
|
+
so the same look serves routed sub-navigation built from plain links — see the note under Nav.
|
|
685
743
|
|
|
686
744
|
```html
|
|
687
745
|
<div class="fdy-tabs" data-fdy-tabs>
|
|
@@ -699,12 +757,16 @@ APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
|
|
|
699
757
|
# Overlays
|
|
700
758
|
|
|
701
759
|
## Modal — `.fdy-modal`
|
|
760
|
+
> **Typed wrapper: `<FdyModal>`** — Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
761
|
+
|
|
702
762
|
Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
|
|
703
763
|
`--wide`; `--cfl` for the choose-from-list dialog. Parts `__header` `__title` `__body` `__footer`
|
|
704
764
|
`__close`. The body scrolls; the footer never clips. `aria-labelledby` → the title's `id`;
|
|
705
765
|
`data-close` on any button that should close it.
|
|
706
766
|
|
|
707
767
|
## Drawer — `.fdy-drawer`
|
|
768
|
+
> **Typed wrapper: `<FdyDrawer>`** — Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
769
|
+
|
|
708
770
|
Temporary side panel on native `<dialog>` — left by default, `--right` to flip. Parts `__header`
|
|
709
771
|
`__title` `__body` `__footer` `__close`. Open it from any
|
|
710
772
|
`<button data-fdy-drawer="<dialog id>">`. Needs `freeday-drawer.js`.
|
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.22.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`.
|
|
@@ -31,6 +31,28 @@ npm test # tes transformasi build + kontras WCAG (node:test)
|
|
|
31
31
|
|
|
32
32
|
## Pakai di project
|
|
33
33
|
|
|
34
|
+
### Pertama: pintu masuk mana yang kamu pakai?
|
|
35
|
+
|
|
36
|
+
Sebagian besar Freeday itu markup + kelas `fdy-*` dan sama di mana pun. **Sepuluh komponen
|
|
37
|
+
interaktif juga punya wrapper typed** — di stack berikut, pakai wrapper-nya, bukan markup mentah +
|
|
38
|
+
enhancer:
|
|
39
|
+
|
|
40
|
+
| Stack-mu | Impor kesepuluhnya dari | Binding |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| **Vue 3** | `@cahyo-dimas/freeday/vue` | `v-model` |
|
|
43
|
+
| **React 18/19** | `@cahyo-dimas/freeday/react` | `value` + `onChange` |
|
|
44
|
+
| **Blazor (net8.0)** | RCL `Freeday.Blazor` | `@bind-Value` |
|
|
45
|
+
| HTML statis · Svelte · template server-rendered | *(tanpa wrapper)* markup mentah + enhancer, di bawah | event `fdy-*` |
|
|
46
|
+
|
|
47
|
+
Kesepuluhnya: `FdyCombo` · `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` ·
|
|
48
|
+
`FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer`. Menulis markup mentah untuk
|
|
49
|
+
kesepuluh ini di dalam Vue/React/Blazor *kelihatan* baik-baik saja — render pertama benar — lalu
|
|
50
|
+
rusak diam-diam: DOM yang di-render framework belakangan tak pernah ter-hydrate, dan state widget
|
|
51
|
+
berakhir di DOM alih-alih di state framework-mu. Selebihnya (button, card, badge, alert, markup
|
|
52
|
+
tabel, layout, dan komponen interaktif yang tak punya wrapper) sama persis di semua stack — untuk
|
|
53
|
+
yang terakhir, hydrate dengan `useFreeday` (Vue/React) atau `FreedayBlazor.initAll` (Blazor).
|
|
54
|
+
Panduan lengkap per stack: [`docs/getting-started.md`](docs/getting-started.md).
|
|
55
|
+
|
|
34
56
|
**Sebagai paket (project dengan bundler — Vue/React/Blazor/Vite):**
|
|
35
57
|
```bash
|
|
36
58
|
npm i @cahyo-dimas/freeday
|
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.22.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`.
|
|
@@ -31,6 +31,27 @@ npm test # build-transform tests + WCAG contrast (node:test)
|
|
|
31
31
|
|
|
32
32
|
## Use in a project
|
|
33
33
|
|
|
34
|
+
### First: which entry point is yours?
|
|
35
|
+
|
|
36
|
+
Most of Freeday is markup + `fdy-*` classes and is identical everywhere. **Ten interactive
|
|
37
|
+
components also ship a typed wrapper** — in these stacks, use the wrapper, not raw markup + enhancer:
|
|
38
|
+
|
|
39
|
+
| Your stack | Import the ten from | Binding |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| **Vue 3** | `@cahyo-dimas/freeday/vue` | `v-model` |
|
|
42
|
+
| **React 18/19** | `@cahyo-dimas/freeday/react` | `value` + `onChange` |
|
|
43
|
+
| **Blazor (net8.0)** | `Freeday.Blazor` RCL | `@bind-Value` |
|
|
44
|
+
| Static HTML · Svelte · server-rendered templates | *(no wrapper)* raw markup + enhancers, below | `fdy-*` events |
|
|
45
|
+
|
|
46
|
+
The ten: `FdyCombo` · `FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` ·
|
|
47
|
+
`FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer`. Hand-writing the raw markup for
|
|
48
|
+
these inside Vue/React/Blazor *looks* fine — the first render is correct — and then breaks quietly:
|
|
49
|
+
DOM the framework renders later is never hydrated, and the widget's state ends up in the DOM instead
|
|
50
|
+
of your framework's. Everything else (button, card, badge, alert, table markup, layout, and the
|
|
51
|
+
interactive components with no wrapper) is the same plain markup in every stack — hydrate the latter
|
|
52
|
+
with `useFreeday` (Vue/React) or `FreedayBlazor.initAll` (Blazor). Full walkthrough per stack:
|
|
53
|
+
[`docs/getting-started.md`](docs/getting-started.md).
|
|
54
|
+
|
|
34
55
|
**As a package (projects with a bundler — Vue/React/Blazor/Vite):**
|
|
35
56
|
```bash
|
|
36
57
|
npm i @cahyo-dimas/freeday
|
package/USAGE.md
CHANGED
|
@@ -115,6 +115,27 @@ container, a single `<section>`. An app whose two list screens are dense and who
|
|
|
115
115
|
are not should scope it per screen rather than densifying everything. Set it at one level per screen,
|
|
116
116
|
never per component.
|
|
117
117
|
|
|
118
|
+
## 6b. Navigation: one component, two orientations — and never a tab role
|
|
119
|
+
|
|
120
|
+
A navigation link is an `<a class="fdy-nav__item">` marked **`aria-current="page"`**. That does not
|
|
121
|
+
change with the layout:
|
|
122
|
+
|
|
123
|
+
- **Sidebar app** — `.fdy-nav` inside `.fdy-app__sidebar` (the default).
|
|
124
|
+
- **Top-nav app** — `.fdy-nav.fdy-nav--horizontal` inside `.fdy-appbar` or `.fdy-app__topbar`, with
|
|
125
|
+
no sidebar. Same items, same states; on `.fdy-appbar--primary` the links go on-colour for you.
|
|
126
|
+
- **Routed sub-navigation that should look like tabs** (`/settings/profile` · `/settings/billing`) —
|
|
127
|
+
put `.fdy-tabs__list` / `.fdy-tabs__tab` on plain `<a>`s. Those classes honour `aria-current="page"`
|
|
128
|
+
as well as `aria-selected="true"` precisely for this. Do **not** add `role="tab"`/`role="tablist"`
|
|
129
|
+
or `freeday-tabs.js`: those promise a roving-tabindex, arrow-key, one-panel-per-tab contract that
|
|
130
|
+
route links do not honour, and `aria-selected` is invalid ARIA on an anchor in the first place.
|
|
131
|
+
|
|
132
|
+
Reserve the full `.fdy-tabs` component (with its roles and its enhancer) for **in-page** tabs, where
|
|
133
|
+
nothing navigates.
|
|
134
|
+
|
|
135
|
+
**Toggles:** a button that is on/off carries `aria-pressed`, and the kit styles it — soft primary
|
|
136
|
+
fill on `--ghost`/`--text`, a sunken gradient on the solid button. A `.fdy-btn-group` of `--ghost`
|
|
137
|
+
buttons with exactly one `aria-pressed="true"` **is** the segmented control; don't hand-tint it.
|
|
138
|
+
|
|
118
139
|
## 7. Assemble the page from the frame down
|
|
119
140
|
|
|
120
141
|
1. **Shell:** every application starts inside **`.fdy-app`** — a flex row of `__sidebar` (with
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -473,6 +473,15 @@ a { color: var(--color-primary); }
|
|
|
473
473
|
.fdy-app__brand-title{display:block;font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-lg);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
474
474
|
.fdy-app__brand-subtitle{display:block;font-size:var(--text-xs);font-weight:var(--weight-regular);color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
475
475
|
.fdy-nav{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:2px;padding:var(--space-3) var(--space-3) var(--space-6);}
|
|
476
|
+
/* Horizontal variant — the SAME nav links laid out as a row, for a top-nav application (one that
|
|
477
|
+
* puts its primary navigation in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar). Deliberately
|
|
478
|
+
* a modifier and not a new block: the item, its states and `aria-current="page"` are unchanged. That
|
|
479
|
+
* is the point — a navigation link is a link marked `aria-current`, never `aria-selected` (invalid
|
|
480
|
+
* ARIA on an anchor) and never a tab role, so borrowing `.fdy-tabs` for routes half-adopts a contract
|
|
481
|
+
* (roving tabindex, a panel per tab) that route links do not honour. position:relative because the
|
|
482
|
+
* row scrolls; see base.css on why a scroller must be a containing block. */
|
|
483
|
+
.fdy-nav--horizontal{position:relative;flex:0 1 auto;flex-direction:row;align-items:center;gap:var(--space-1);padding:0;min-height:0;overflow-x:auto;}
|
|
484
|
+
.fdy-nav--horizontal .fdy-nav__item{white-space:nowrap;}
|
|
476
485
|
.fdy-nav__item{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);text-decoration:none;cursor:pointer;transition:background var(--dur-fast) var(--ease-standard),color var(--dur-fast) var(--ease-standard);}
|
|
477
486
|
.fdy-nav__item:hover{background:var(--color-surface-2);color:var(--color-text);}
|
|
478
487
|
/* Active indicator stays in the primary ramp so it never clashes with a re-themed accent. */
|
|
@@ -548,9 +557,18 @@ a { color: var(--color-primary); }
|
|
|
548
557
|
.fdy-appbar__brand svg{display:block;width:1.5rem;height:1.5rem;}
|
|
549
558
|
.fdy-appbar__spacer{flex:1;}
|
|
550
559
|
.fdy-appbar__actions{display:inline-flex;align-items:center;gap:var(--space-1);}
|
|
560
|
+
/* Primary navigation inside the bar: `<nav class="fdy-nav fdy-nav--horizontal">` with
|
|
561
|
+
`.fdy-nav__item` links (see app-shell.css). The bar ships no link class of its own — a nav link
|
|
562
|
+
is the same component whether the nav is a sidebar column or a top row. */
|
|
551
563
|
/* Controls on a coloured app bar go on-colour (icons stay legible, hover is a light wash) */
|
|
552
564
|
.fdy-appbar--primary .fdy-btn--ghost{background:transparent;border-color:transparent;color:var(--color-on-primary);box-shadow:none;}
|
|
553
565
|
.fdy-appbar--primary .fdy-btn--ghost:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);transform:none;}
|
|
566
|
+
/* Nav links on a coloured bar: full on-colour ink in every state (a dimmed variant would trade away
|
|
567
|
+
the contrast the bar's own token pair guarantees). The current page is carried by the background
|
|
568
|
+
wash plus the semibold weight the base rule already applies — and by `aria-current` itself. */
|
|
569
|
+
.fdy-appbar--primary .fdy-nav__item{color:var(--color-on-primary);}
|
|
570
|
+
.fdy-appbar--primary .fdy-nav__item:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);}
|
|
571
|
+
.fdy-appbar--primary .fdy-nav__item[aria-current="page"]{background:color-mix(in srgb,var(--color-on-primary) 24%,transparent);color:var(--color-on-primary);}
|
|
554
572
|
|
|
555
573
|
/* Freeday — Autocomplete (text input + filtered suggestion listbox, APG editable combobox).
|
|
556
574
|
* Enhanced by freeday-autocomplete.js. */
|
|
@@ -653,6 +671,14 @@ a { color: var(--color-primary); }
|
|
|
653
671
|
.fdy-btn:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-primary) 60%,transparent),inset 0 1px 0 rgba(255,255,255,.26);}
|
|
654
672
|
.fdy-btn:active{transform:translateY(1px);box-shadow:0 1px 4px -1px color-mix(in srgb,var(--color-primary) 46%,transparent);}
|
|
655
673
|
.fdy-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
|
|
674
|
+
/* Pressed / toggle. `aria-pressed` is already the right attribute for a toggle button, so this adds
|
|
675
|
+
no class and no new markup contract — and it is what makes .fdy-btn-group a complete segmented
|
|
676
|
+
control: the group joins the borders, this says which segment you are on. Each fill variant
|
|
677
|
+
defines its OWN pressed look below; a single shared rule cannot work, because a gradient is a
|
|
678
|
+
background-IMAGE and would blank the ghost variant's background-color. Solid: invert the gradient
|
|
679
|
+
and sink the shadow inward, so a held button reads as depressed, not merely primary-coloured. */
|
|
680
|
+
.fdy-btn[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 12%,var(--color-primary)),var(--color-primary));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
|
|
681
|
+
.fdy-btn[aria-pressed="true"]:hover{transform:none;box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
|
|
656
682
|
|
|
657
683
|
.fdy-btn--ghost{background:var(--color-surface);color:var(--color-primary-strong);border-color:var(--color-border-strong);box-shadow:var(--shadow-1);}
|
|
658
684
|
.fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
|
|
@@ -661,6 +687,7 @@ a { color: var(--color-primary); }
|
|
|
661
687
|
.fdy-btn--danger{background:linear-gradient(180deg,var(--color-danger-btn),color-mix(in srgb,#000 12%,var(--color-danger-btn)));color:var(--color-on-danger);box-shadow:0 2px 9px -1px color-mix(in srgb,var(--color-danger-btn) 46%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
|
|
662
688
|
.fdy-btn--danger:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-danger-btn) 54%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
|
|
663
689
|
.fdy-btn--danger:active{transform:translateY(1px);}
|
|
690
|
+
.fdy-btn--danger[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 14%,var(--color-danger-btn)),var(--color-danger-btn));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 24%,transparent);}
|
|
664
691
|
|
|
665
692
|
.fdy-btn--sm{height:calc(var(--control-h) - var(--space-2));padding:0 var(--space-3);font-size:var(--text-xs);border-radius:var(--radius-sm);}
|
|
666
693
|
.fdy-btn--lg{height:calc(var(--control-h) + var(--space-2));padding:0 var(--space-6);font-size:var(--text-base);border-radius:var(--radius-lg);}
|
|
@@ -673,6 +700,11 @@ a { color: var(--color-primary); }
|
|
|
673
700
|
.fdy-btn--text{background:none;color:var(--color-primary-strong);border-color:transparent;box-shadow:none;}
|
|
674
701
|
.fdy-btn--text:hover{transform:none;background:var(--color-primary-soft);box-shadow:none;}
|
|
675
702
|
.fdy-btn--text:active{transform:translateY(1px);box-shadow:none;}
|
|
703
|
+
/* Ghost/text pressed: the kit's selected-surface pair (soft fill + strong ink) — the same pairing
|
|
704
|
+
.fdy-nav__item uses for the current page, and one the AA contrast gate already covers. This is
|
|
705
|
+
the realistic segmented control: a row of --ghost buttons with exactly one aria-pressed. */
|
|
706
|
+
.fdy-btn--ghost[aria-pressed="true"],.fdy-btn--text[aria-pressed="true"]{background:var(--color-primary-soft);color:var(--color-primary-strong);border-color:var(--color-primary-border);box-shadow:none;}
|
|
707
|
+
.fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
|
|
676
708
|
|
|
677
709
|
/* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
|
|
678
710
|
.fdy-btn--icon{width:var(--control-h);padding:0;}
|
|
@@ -1518,7 +1550,13 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1518
1550
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
1519
1551
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
1520
1552
|
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
|
1521
|
-
|
|
1553
|
+
/* `aria-current="page"` is honoured alongside `aria-selected="true"` so the tab look can be used for
|
|
1554
|
+
ROUTE-driven sub-navigation (/settings/profile · /settings/billing), where the tabs are real links.
|
|
1555
|
+
A link cannot be `aria-selected` — that is invalid ARIA on an anchor; `aria-current` is the only
|
|
1556
|
+
correct marker, and it is what a router sets. Use `.fdy-tabs` + `freeday-tabs.js` (roving tabindex,
|
|
1557
|
+
a panel per tab) for in-page tabs; use these classes on plain links for routed ones, and
|
|
1558
|
+
`.fdy-nav--horizontal` for an application's PRIMARY navigation. */
|
|
1559
|
+
.fdy-tabs__tab[aria-selected="true"],.fdy-tabs__tab[aria-current="page"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
|
|
1522
1560
|
.fdy-tabs__tab:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
1523
1561
|
.fdy-tabs__panel{padding:var(--space-5) 0;}
|
|
1524
1562
|
.fdy-tabs__panel[hidden]{display:none;}
|
package/dist/freeday.css
CHANGED
|
@@ -103,6 +103,15 @@ a { color: var(--color-primary); }
|
|
|
103
103
|
.fdy-app__brand-title{display:block;font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-lg);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
104
104
|
.fdy-app__brand-subtitle{display:block;font-size:var(--text-xs);font-weight:var(--weight-regular);color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
105
105
|
.fdy-nav{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:2px;padding:var(--space-3) var(--space-3) var(--space-6);}
|
|
106
|
+
/* Horizontal variant — the SAME nav links laid out as a row, for a top-nav application (one that
|
|
107
|
+
* puts its primary navigation in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar). Deliberately
|
|
108
|
+
* a modifier and not a new block: the item, its states and `aria-current="page"` are unchanged. That
|
|
109
|
+
* is the point — a navigation link is a link marked `aria-current`, never `aria-selected` (invalid
|
|
110
|
+
* ARIA on an anchor) and never a tab role, so borrowing `.fdy-tabs` for routes half-adopts a contract
|
|
111
|
+
* (roving tabindex, a panel per tab) that route links do not honour. position:relative because the
|
|
112
|
+
* row scrolls; see base.css on why a scroller must be a containing block. */
|
|
113
|
+
.fdy-nav--horizontal{position:relative;flex:0 1 auto;flex-direction:row;align-items:center;gap:var(--space-1);padding:0;min-height:0;overflow-x:auto;}
|
|
114
|
+
.fdy-nav--horizontal .fdy-nav__item{white-space:nowrap;}
|
|
106
115
|
.fdy-nav__item{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);text-decoration:none;cursor:pointer;transition:background var(--dur-fast) var(--ease-standard),color var(--dur-fast) var(--ease-standard);}
|
|
107
116
|
.fdy-nav__item:hover{background:var(--color-surface-2);color:var(--color-text);}
|
|
108
117
|
/* Active indicator stays in the primary ramp so it never clashes with a re-themed accent. */
|
|
@@ -178,9 +187,18 @@ a { color: var(--color-primary); }
|
|
|
178
187
|
.fdy-appbar__brand svg{display:block;width:1.5rem;height:1.5rem;}
|
|
179
188
|
.fdy-appbar__spacer{flex:1;}
|
|
180
189
|
.fdy-appbar__actions{display:inline-flex;align-items:center;gap:var(--space-1);}
|
|
190
|
+
/* Primary navigation inside the bar: `<nav class="fdy-nav fdy-nav--horizontal">` with
|
|
191
|
+
`.fdy-nav__item` links (see app-shell.css). The bar ships no link class of its own — a nav link
|
|
192
|
+
is the same component whether the nav is a sidebar column or a top row. */
|
|
181
193
|
/* Controls on a coloured app bar go on-colour (icons stay legible, hover is a light wash) */
|
|
182
194
|
.fdy-appbar--primary .fdy-btn--ghost{background:transparent;border-color:transparent;color:var(--color-on-primary);box-shadow:none;}
|
|
183
195
|
.fdy-appbar--primary .fdy-btn--ghost:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);transform:none;}
|
|
196
|
+
/* Nav links on a coloured bar: full on-colour ink in every state (a dimmed variant would trade away
|
|
197
|
+
the contrast the bar's own token pair guarantees). The current page is carried by the background
|
|
198
|
+
wash plus the semibold weight the base rule already applies — and by `aria-current` itself. */
|
|
199
|
+
.fdy-appbar--primary .fdy-nav__item{color:var(--color-on-primary);}
|
|
200
|
+
.fdy-appbar--primary .fdy-nav__item:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);}
|
|
201
|
+
.fdy-appbar--primary .fdy-nav__item[aria-current="page"]{background:color-mix(in srgb,var(--color-on-primary) 24%,transparent);color:var(--color-on-primary);}
|
|
184
202
|
|
|
185
203
|
/* Freeday — Autocomplete (text input + filtered suggestion listbox, APG editable combobox).
|
|
186
204
|
* Enhanced by freeday-autocomplete.js. */
|
|
@@ -283,6 +301,14 @@ a { color: var(--color-primary); }
|
|
|
283
301
|
.fdy-btn:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-primary) 60%,transparent),inset 0 1px 0 rgba(255,255,255,.26);}
|
|
284
302
|
.fdy-btn:active{transform:translateY(1px);box-shadow:0 1px 4px -1px color-mix(in srgb,var(--color-primary) 46%,transparent);}
|
|
285
303
|
.fdy-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
|
|
304
|
+
/* Pressed / toggle. `aria-pressed` is already the right attribute for a toggle button, so this adds
|
|
305
|
+
no class and no new markup contract — and it is what makes .fdy-btn-group a complete segmented
|
|
306
|
+
control: the group joins the borders, this says which segment you are on. Each fill variant
|
|
307
|
+
defines its OWN pressed look below; a single shared rule cannot work, because a gradient is a
|
|
308
|
+
background-IMAGE and would blank the ghost variant's background-color. Solid: invert the gradient
|
|
309
|
+
and sink the shadow inward, so a held button reads as depressed, not merely primary-coloured. */
|
|
310
|
+
.fdy-btn[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 12%,var(--color-primary)),var(--color-primary));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
|
|
311
|
+
.fdy-btn[aria-pressed="true"]:hover{transform:none;box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
|
|
286
312
|
|
|
287
313
|
.fdy-btn--ghost{background:var(--color-surface);color:var(--color-primary-strong);border-color:var(--color-border-strong);box-shadow:var(--shadow-1);}
|
|
288
314
|
.fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
|
|
@@ -291,6 +317,7 @@ a { color: var(--color-primary); }
|
|
|
291
317
|
.fdy-btn--danger{background:linear-gradient(180deg,var(--color-danger-btn),color-mix(in srgb,#000 12%,var(--color-danger-btn)));color:var(--color-on-danger);box-shadow:0 2px 9px -1px color-mix(in srgb,var(--color-danger-btn) 46%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
|
|
292
318
|
.fdy-btn--danger:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-danger-btn) 54%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
|
|
293
319
|
.fdy-btn--danger:active{transform:translateY(1px);}
|
|
320
|
+
.fdy-btn--danger[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 14%,var(--color-danger-btn)),var(--color-danger-btn));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 24%,transparent);}
|
|
294
321
|
|
|
295
322
|
.fdy-btn--sm{height:calc(var(--control-h) - var(--space-2));padding:0 var(--space-3);font-size:var(--text-xs);border-radius:var(--radius-sm);}
|
|
296
323
|
.fdy-btn--lg{height:calc(var(--control-h) + var(--space-2));padding:0 var(--space-6);font-size:var(--text-base);border-radius:var(--radius-lg);}
|
|
@@ -303,6 +330,11 @@ a { color: var(--color-primary); }
|
|
|
303
330
|
.fdy-btn--text{background:none;color:var(--color-primary-strong);border-color:transparent;box-shadow:none;}
|
|
304
331
|
.fdy-btn--text:hover{transform:none;background:var(--color-primary-soft);box-shadow:none;}
|
|
305
332
|
.fdy-btn--text:active{transform:translateY(1px);box-shadow:none;}
|
|
333
|
+
/* Ghost/text pressed: the kit's selected-surface pair (soft fill + strong ink) — the same pairing
|
|
334
|
+
.fdy-nav__item uses for the current page, and one the AA contrast gate already covers. This is
|
|
335
|
+
the realistic segmented control: a row of --ghost buttons with exactly one aria-pressed. */
|
|
336
|
+
.fdy-btn--ghost[aria-pressed="true"],.fdy-btn--text[aria-pressed="true"]{background:var(--color-primary-soft);color:var(--color-primary-strong);border-color:var(--color-primary-border);box-shadow:none;}
|
|
337
|
+
.fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
|
|
306
338
|
|
|
307
339
|
/* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
|
|
308
340
|
.fdy-btn--icon{width:var(--control-h);padding:0;}
|
|
@@ -1148,7 +1180,13 @@ fieldset.fdy-field>legend{padding:0;float:none;}
|
|
|
1148
1180
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
1149
1181
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
1150
1182
|
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
|
1151
|
-
|
|
1183
|
+
/* `aria-current="page"` is honoured alongside `aria-selected="true"` so the tab look can be used for
|
|
1184
|
+
ROUTE-driven sub-navigation (/settings/profile · /settings/billing), where the tabs are real links.
|
|
1185
|
+
A link cannot be `aria-selected` — that is invalid ARIA on an anchor; `aria-current` is the only
|
|
1186
|
+
correct marker, and it is what a router sets. Use `.fdy-tabs` + `freeday-tabs.js` (roving tabindex,
|
|
1187
|
+
a panel per tab) for in-page tabs; use these classes on plain links for routed ones, and
|
|
1188
|
+
`.fdy-nav--horizontal` for an application's PRIMARY navigation. */
|
|
1189
|
+
.fdy-tabs__tab[aria-selected="true"],.fdy-tabs__tab[aria-current="page"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
|
|
1152
1190
|
.fdy-tabs__tab:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
1153
1191
|
.fdy-tabs__panel{padding:var(--space-5) 0;}
|
|
1154
1192
|
.fdy-tabs__panel[hidden]{display:none;}
|
package/docs/agent-onboarding.md
CHANGED
|
@@ -18,8 +18,38 @@ the **root of the consuming project**:
|
|
|
18
18
|
```markdown
|
|
19
19
|
## UI: Freeday design system (@cahyo-dimas/freeday)
|
|
20
20
|
|
|
21
|
-
All UI in this project is built from Freeday
|
|
22
|
-
|
|
21
|
+
All UI in this project is built from Freeday: a **token-driven CSS kit** (`fdy-*` classes on plain
|
|
22
|
+
markup) with **typed components for Vue, React and Blazor** layered on top. Most of the kit is
|
|
23
|
+
markup + classes; ten interactive components also ship a typed wrapper, and in those three stacks
|
|
24
|
+
the wrapper is the correct way to use them.
|
|
25
|
+
|
|
26
|
+
**0. First decide which entry point this project uses. This is not an optimisation — get it wrong
|
|
27
|
+
and the code looks correct and fails later.**
|
|
28
|
+
|
|
29
|
+
| This project's stack | Import the ten components from | Binding |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Vue 3 | `@cahyo-dimas/freeday/vue` | `v-model` |
|
|
32
|
+
| React 18/19 | `@cahyo-dimas/freeday/react` | `value` + `onChange` |
|
|
33
|
+
| Blazor (net8.0) | `@using Freeday.Blazor` (RCL) | `@bind-Value` |
|
|
34
|
+
| Static HTML, Svelte, server-rendered templates… | no wrapper — raw markup + the enhancer script | `fdy-*` DOM events |
|
|
35
|
+
|
|
36
|
+
The ten: **FdyCombo · FdyDatepicker · FdyDateRange · FdyAutocomplete · FdyCascade · FdyCfl ·
|
|
37
|
+
FdyChart · FdyTable · FdyModal · FdyDrawer**. In Vue/React/Blazor, **never hand-write the raw
|
|
38
|
+
markup + enhancer for these ten.** The raw path *appears* to work — the enhancer auto-initialises
|
|
39
|
+
once on `DOMContentLoaded` and the first render is correct — then fails quietly: DOM your framework
|
|
40
|
+
renders later is never hydrated, and the widget's state lives in the DOM instead of in your
|
|
41
|
+
framework's state.
|
|
42
|
+
|
|
43
|
+
Everything else is the same in every stack: plain `fdy-*` markup (button, card, badge, alert,
|
|
44
|
+
table markup, layout…). For the *other* interactive components (chips, stepper, input mask, file
|
|
45
|
+
upload, tree, tabs, menu, rating, slider, form validation, carousel, timepicker) there is no
|
|
46
|
+
wrapper — use the raw markup and hydrate it:
|
|
47
|
+
|
|
48
|
+
- **Vue / React** — `useFreeday(rootRef)` from the same import path, plus `import '@cahyo-dimas/freeday'`
|
|
49
|
+
once at app entry to register the enhancers. (The ten typed components do **not** need this: they
|
|
50
|
+
are native Vue/React implementations of the same markup, not wrappers over the enhancer.)
|
|
51
|
+
- **Blazor** — `FreedayBlazor.initAll` interop. (Here the typed components *are* thin wrappers over
|
|
52
|
+
the enhancers, so the enhancer script is always required.)
|
|
23
53
|
|
|
24
54
|
**Before writing or editing any markup/CSS, read these (they ship inside the package):**
|
|
25
55
|
- `node_modules/@cahyo-dimas/freeday/COMPONENTS.md` — every class that exists, with minimal markup
|
|
@@ -40,8 +70,9 @@ enhancers**, not a component framework — components are plain markup with `fdy
|
|
|
40
70
|
5. Assemble from the frame down: `.fdy-app` → `.fdy-page` → `.fdy-page-section` → components.
|
|
41
71
|
6. Form errors: `aria-invalid="true"` + `aria-describedby` → a `.fdy-help.fdy-help--error`.
|
|
42
72
|
Icon-only buttons need `aria-label`. Status is never colour-only.
|
|
43
|
-
7. Interactive components need their enhancer script loaded (see the table in COMPONENTS.md);
|
|
44
|
-
|
|
73
|
+
7. Interactive components need their enhancer script loaded (see the table in COMPONENTS.md); in an
|
|
74
|
+
SPA, re-hydrate dynamic DOM with `useFreeday` / `FreedayBlazor.initAll`. This applies to the
|
|
75
|
+
components **without** a typed wrapper — for the ten in step 0, use the wrapper instead.
|
|
45
76
|
8. Freeday owns components + tokens, **not layout**. Grids/stacks/one-off gaps come from our own
|
|
46
77
|
layout layer — build its theme on `var(--space-N)` so both systems stay in step.
|
|
47
78
|
```
|
package/docs/getting-started.md
CHANGED
|
@@ -17,10 +17,15 @@ Freeday = **CSS** (semantic tokens + `fdy-*` classes) + **zero-dependency JS enh
|
|
|
17
17
|
1. **Static vs interactive.** Static components (button, card, badge, plain input, layout) need
|
|
18
18
|
only the **`fdy-*` classes** — no JS. Interactive components (select/combo, cascade, date/time
|
|
19
19
|
picker, table, dropzone, form validation, input mask, chip) need the **JS enhancers**.
|
|
20
|
-
2. **The enhancer is the source of truth
|
|
21
|
-
the widget's DOM. You **listen for `fdy-*` events** (all bubbling
|
|
22
|
-
`event.detail`) → store them in your framework state. Event/API contract
|
|
23
|
-
[`integrations.md` §Event & API contract](integrations.md).
|
|
20
|
+
2. **The enhancer is the source of truth — *on the raw path*.** You don't re-implement components;
|
|
21
|
+
the enhancer owns the widget's DOM. You **listen for `fdy-*` events** (all bubbling
|
|
22
|
+
`CustomEvent`s, data in `event.detail`) → store them in your framework state. Event/API contract
|
|
23
|
+
table: [`integrations.md` §Event & API contract](integrations.md).
|
|
24
|
+
**On Vue, React or Blazor this is not the path to take for ten of the components** — `FdyCombo`,
|
|
25
|
+
`FdyDatepicker`, `FdyDateRange`, `FdyAutocomplete`, `FdyCascade`, `FdyCfl`, `FdyChart`,
|
|
26
|
+
`FdyTable`, `FdyModal`, `FdyDrawer` ship typed wrappers that own the state properly (Vue and
|
|
27
|
+
React re-implement the interaction natively; Blazor wraps the enhancer over interop). Use them;
|
|
28
|
+
the raw path is for the components without a wrapper, and for stacks without an adapter.
|
|
24
29
|
3. **Hydrate dynamic DOM.** Enhancers auto-init once on `DOMContentLoaded`. DOM an SPA renders
|
|
25
30
|
**after** that must be re-hydrated: `window.Freeday<X>.initAll(el)` (idempotent, safe to repeat).
|
|
26
31
|
Each framework's adapter wraps this — you don't call it manually.
|
|
@@ -179,7 +184,7 @@ live docs also have a copy button per component.
|
|
|
179
184
|
```bash
|
|
180
185
|
npm i @cahyo-dimas/freeday
|
|
181
186
|
```
|
|
182
|
-
Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.
|
|
187
|
+
Lands in `package.json` as `"@cahyo-dimas/freeday": "^1.22.0"` (public npm package). `dist/` is
|
|
183
188
|
committed and published → no build step; `npm ci` runs without auth.
|
|
184
189
|
|
|
185
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.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"scripts": {
|
|
72
72
|
"build": "node tokens/build.mjs",
|
|
73
73
|
"test": "node --test",
|
|
74
|
-
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs",
|
|
74
|
+
"test:browser": "node --test browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs",
|
|
75
75
|
"prepack": "node tokens/build.mjs",
|
|
76
76
|
"version": "node tokens/build.mjs && git add dist",
|
|
77
77
|
"typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit"
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
.fdy-app__brand-title{display:block;font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-lg);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
25
25
|
.fdy-app__brand-subtitle{display:block;font-size:var(--text-xs);font-weight:var(--weight-regular);color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
26
26
|
.fdy-nav{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:2px;padding:var(--space-3) var(--space-3) var(--space-6);}
|
|
27
|
+
/* Horizontal variant — the SAME nav links laid out as a row, for a top-nav application (one that
|
|
28
|
+
* puts its primary navigation in `.fdy-appbar` / `.fdy-app__topbar` and has no sidebar). Deliberately
|
|
29
|
+
* a modifier and not a new block: the item, its states and `aria-current="page"` are unchanged. That
|
|
30
|
+
* is the point — a navigation link is a link marked `aria-current`, never `aria-selected` (invalid
|
|
31
|
+
* ARIA on an anchor) and never a tab role, so borrowing `.fdy-tabs` for routes half-adopts a contract
|
|
32
|
+
* (roving tabindex, a panel per tab) that route links do not honour. position:relative because the
|
|
33
|
+
* row scrolls; see base.css on why a scroller must be a containing block. */
|
|
34
|
+
.fdy-nav--horizontal{position:relative;flex:0 1 auto;flex-direction:row;align-items:center;gap:var(--space-1);padding:0;min-height:0;overflow-x:auto;}
|
|
35
|
+
.fdy-nav--horizontal .fdy-nav__item{white-space:nowrap;}
|
|
27
36
|
.fdy-nav__item{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);text-decoration:none;cursor:pointer;transition:background var(--dur-fast) var(--ease-standard),color var(--dur-fast) var(--ease-standard);}
|
|
28
37
|
.fdy-nav__item:hover{background:var(--color-surface-2);color:var(--color-text);}
|
|
29
38
|
/* Active indicator stays in the primary ramp so it never clashes with a re-themed accent. */
|
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
.fdy-appbar__brand svg{display:block;width:1.5rem;height:1.5rem;}
|
|
11
11
|
.fdy-appbar__spacer{flex:1;}
|
|
12
12
|
.fdy-appbar__actions{display:inline-flex;align-items:center;gap:var(--space-1);}
|
|
13
|
+
/* Primary navigation inside the bar: `<nav class="fdy-nav fdy-nav--horizontal">` with
|
|
14
|
+
`.fdy-nav__item` links (see app-shell.css). The bar ships no link class of its own — a nav link
|
|
15
|
+
is the same component whether the nav is a sidebar column or a top row. */
|
|
13
16
|
/* Controls on a coloured app bar go on-colour (icons stay legible, hover is a light wash) */
|
|
14
17
|
.fdy-appbar--primary .fdy-btn--ghost{background:transparent;border-color:transparent;color:var(--color-on-primary);box-shadow:none;}
|
|
15
18
|
.fdy-appbar--primary .fdy-btn--ghost:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);transform:none;}
|
|
19
|
+
/* Nav links on a coloured bar: full on-colour ink in every state (a dimmed variant would trade away
|
|
20
|
+
the contrast the bar's own token pair guarantees). The current page is carried by the background
|
|
21
|
+
wash plus the semibold weight the base rule already applies — and by `aria-current` itself. */
|
|
22
|
+
.fdy-appbar--primary .fdy-nav__item{color:var(--color-on-primary);}
|
|
23
|
+
.fdy-appbar--primary .fdy-nav__item:hover{background:color-mix(in srgb,var(--color-on-primary) 16%,transparent);color:var(--color-on-primary);}
|
|
24
|
+
.fdy-appbar--primary .fdy-nav__item[aria-current="page"]{background:color-mix(in srgb,var(--color-on-primary) 24%,transparent);color:var(--color-on-primary);}
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
.fdy-btn:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-primary) 60%,transparent),inset 0 1px 0 rgba(255,255,255,.26);}
|
|
11
11
|
.fdy-btn:active{transform:translateY(1px);box-shadow:0 1px 4px -1px color-mix(in srgb,var(--color-primary) 46%,transparent);}
|
|
12
12
|
.fdy-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
|
|
13
|
+
/* Pressed / toggle. `aria-pressed` is already the right attribute for a toggle button, so this adds
|
|
14
|
+
no class and no new markup contract — and it is what makes .fdy-btn-group a complete segmented
|
|
15
|
+
control: the group joins the borders, this says which segment you are on. Each fill variant
|
|
16
|
+
defines its OWN pressed look below; a single shared rule cannot work, because a gradient is a
|
|
17
|
+
background-IMAGE and would blank the ghost variant's background-color. Solid: invert the gradient
|
|
18
|
+
and sink the shadow inward, so a held button reads as depressed, not merely primary-coloured. */
|
|
19
|
+
.fdy-btn[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 12%,var(--color-primary)),var(--color-primary));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
|
|
20
|
+
.fdy-btn[aria-pressed="true"]:hover{transform:none;box-shadow:inset 0 2px 6px color-mix(in srgb,#000 22%,transparent);}
|
|
13
21
|
|
|
14
22
|
.fdy-btn--ghost{background:var(--color-surface);color:var(--color-primary-strong);border-color:var(--color-border-strong);box-shadow:var(--shadow-1);}
|
|
15
23
|
.fdy-btn--ghost:hover{transform:none;background:var(--color-primary-soft);border-color:var(--color-primary-border);box-shadow:var(--shadow-1);}
|
|
@@ -18,6 +26,7 @@
|
|
|
18
26
|
.fdy-btn--danger{background:linear-gradient(180deg,var(--color-danger-btn),color-mix(in srgb,#000 12%,var(--color-danger-btn)));color:var(--color-on-danger);box-shadow:0 2px 9px -1px color-mix(in srgb,var(--color-danger-btn) 46%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
|
|
19
27
|
.fdy-btn--danger:hover{transform:translateY(-1px);box-shadow:0 6px 18px -2px color-mix(in srgb,var(--color-danger-btn) 54%,transparent),inset 0 1px 0 rgba(255,255,255,.22);}
|
|
20
28
|
.fdy-btn--danger:active{transform:translateY(1px);}
|
|
29
|
+
.fdy-btn--danger[aria-pressed="true"]{background:linear-gradient(180deg,color-mix(in srgb,#000 14%,var(--color-danger-btn)),var(--color-danger-btn));box-shadow:inset 0 2px 6px color-mix(in srgb,#000 24%,transparent);}
|
|
21
30
|
|
|
22
31
|
.fdy-btn--sm{height:calc(var(--control-h) - var(--space-2));padding:0 var(--space-3);font-size:var(--text-xs);border-radius:var(--radius-sm);}
|
|
23
32
|
.fdy-btn--lg{height:calc(var(--control-h) + var(--space-2));padding:0 var(--space-6);font-size:var(--text-base);border-radius:var(--radius-lg);}
|
|
@@ -30,6 +39,11 @@
|
|
|
30
39
|
.fdy-btn--text{background:none;color:var(--color-primary-strong);border-color:transparent;box-shadow:none;}
|
|
31
40
|
.fdy-btn--text:hover{transform:none;background:var(--color-primary-soft);box-shadow:none;}
|
|
32
41
|
.fdy-btn--text:active{transform:translateY(1px);box-shadow:none;}
|
|
42
|
+
/* Ghost/text pressed: the kit's selected-surface pair (soft fill + strong ink) — the same pairing
|
|
43
|
+
.fdy-nav__item uses for the current page, and one the AA contrast gate already covers. This is
|
|
44
|
+
the realistic segmented control: a row of --ghost buttons with exactly one aria-pressed. */
|
|
45
|
+
.fdy-btn--ghost[aria-pressed="true"],.fdy-btn--text[aria-pressed="true"]{background:var(--color-primary-soft);color:var(--color-primary-strong);border-color:var(--color-primary-border);box-shadow:none;}
|
|
46
|
+
.fdy-btn--ghost[aria-pressed="true"]:hover,.fdy-btn--text[aria-pressed="true"]:hover{background:var(--color-primary-soft);transform:none;box-shadow:none;}
|
|
33
47
|
|
|
34
48
|
/* Icon-only — square; combine with --ghost/--text/--sm/--lg. Requires aria-label. */
|
|
35
49
|
.fdy-btn--icon{width:var(--control-h);padding:0;}
|
package/src/components/tabs.css
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
6
6
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
7
7
|
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
|
8
|
-
|
|
8
|
+
/* `aria-current="page"` is honoured alongside `aria-selected="true"` so the tab look can be used for
|
|
9
|
+
ROUTE-driven sub-navigation (/settings/profile · /settings/billing), where the tabs are real links.
|
|
10
|
+
A link cannot be `aria-selected` — that is invalid ARIA on an anchor; `aria-current` is the only
|
|
11
|
+
correct marker, and it is what a router sets. Use `.fdy-tabs` + `freeday-tabs.js` (roving tabindex,
|
|
12
|
+
a panel per tab) for in-page tabs; use these classes on plain links for routed ones, and
|
|
13
|
+
`.fdy-nav--horizontal` for an application's PRIMARY navigation. */
|
|
14
|
+
.fdy-tabs__tab[aria-selected="true"],.fdy-tabs__tab[aria-current="page"]{color:var(--color-primary-strong);border-bottom-color:var(--color-primary);}
|
|
9
15
|
.fdy-tabs__tab:focus-visible{outline:none;border-radius:var(--radius-sm);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
10
16
|
.fdy-tabs__panel{padding:var(--space-5) 0;}
|
|
11
17
|
.fdy-tabs__panel[hidden]{display:none;}
|