@cahyo-dimas/freeday 1.20.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 +133 -0
- package/COMPONENTS.md +91 -2
- package/README.id.md +25 -1
- package/README.md +24 -1
- package/USAGE.md +42 -0
- package/dist/freeday.bundle.css +96 -10
- package/dist/freeday.css +85 -8
- package/dist/freeday.tokens.css +11 -2
- package/docs/agent-onboarding.md +39 -6
- package/docs/getting-started.md +15 -8
- package/package.json +2 -2
- package/src/base.css +15 -0
- package/src/components/accordion.css +5 -1
- 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/card.css +7 -1
- package/src/components/carousel.css +4 -1
- package/src/components/list.css +8 -1
- package/src/components/table.css +5 -2
- package/src/components/tabs.css +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,139 @@
|
|
|
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
|
+
|
|
69
|
+
## [1.21.0] — 2026-08-12
|
|
70
|
+
Fifth round of real-app consumption feedback, written while that app adopted 1.20.0. Two findings —
|
|
71
|
+
and a **withdrawal**: the reporter retracted 1.20's rejected §A themselves after isolating the real
|
|
72
|
+
mechanism, which turned out to be a genuine kit bug hiding behind it. Both halves of that rejection
|
|
73
|
+
reproduce here, so the rejection stands and the withdrawal is recorded rather than re-litigated.
|
|
74
|
+
### Fixed
|
|
75
|
+
- **Hidden labels could scroll the whole page.** `.fdy-visually-hidden` is `position:absolute`, and
|
|
76
|
+
`clip` hides *painting*, not *layout*. With no positioned ancestor its containing block is the
|
|
77
|
+
document, and `overflow` clips only what is contained inside the overflow box — so a hidden label in
|
|
78
|
+
a horizontally scrolling table (the kit's own recommended way to name an icon button) parks at its
|
|
79
|
+
static position and drags the document sideways. Measured in Chrome: **1351px** of phantom page
|
|
80
|
+
scroll from 11 spans in one table; `overflow-x:hidden` on the scroller, the shell, `body` and `html`
|
|
81
|
+
each changed it by **0px**; removing the spans took it to 0. Every clipping/scrolling container that
|
|
82
|
+
holds consumer markup now declares `position:relative` — `.fdy-table-scroll`, `.fdy-table-wrap`,
|
|
83
|
+
`.fdy-list`, `.fdy-card`, `.fdy-tabs__list`, `.fdy-carousel__viewport`, `.fdy-accordion` — each
|
|
84
|
+
measured to take its own case to 0 with the scroller still scrolling internally. The rest are
|
|
85
|
+
already inside something the kit positions (`<dialog>`, the sticky sidebar, a fixed popover) and are
|
|
86
|
+
listed with that reason in the new test.
|
|
87
|
+
**`.fdy-accordion` is the one to note:** it was contained only by its panel's reveal *animation*
|
|
88
|
+
(a transform makes a containing block), and that animation sits behind
|
|
89
|
+
`prefers-reduced-motion: no-preference` — so the bug was reachable **only** by readers who asked for
|
|
90
|
+
reduced motion. Measured with the animation off: 2906px → 0.
|
|
91
|
+
*Possible migration:* if you absolutely positioned something inside a card, list or table scroller
|
|
92
|
+
and relied on it escaping, it now anchors to that container. Anchoring is the reason for the change.
|
|
93
|
+
- **`--button` rows and cards ignored `:disabled`.** `.fdy-list__row--button` adopts the UA button box
|
|
94
|
+
but not its disabled state, so a row disabled mid-flight kept its hover tint and pointer cursor — a
|
|
95
|
+
control answering the pointer while refusing input. `:disabled` and `[aria-disabled="true"]` now dim
|
|
96
|
+
it and withdraw hover, for `.fdy-list__row--button`, `.fdy-list__row--interactive`,
|
|
97
|
+
`.fdy-card--button` and `.fdy-card--interactive`. The report suggested `cursor:default`; the kit uses
|
|
98
|
+
`opacity:.5` + `cursor:not-allowed` on every other disabled control, and consistency wins.
|
|
99
|
+
### Changed
|
|
100
|
+
- **`data-theme` is no longer root-scoped** — the same move density made in 1.20.0, and for the same
|
|
101
|
+
reason. The two *explicit* selectors are now bare `[data-theme="dark"]` / `[data-theme="light"]`, so
|
|
102
|
+
a `<section data-theme="dark">` inverts that region and **every component inside it follows** —
|
|
103
|
+
card surfaces, inputs, and text roles like `.fdy-title-page` that set `color: var(--color-text)`
|
|
104
|
+
explicitly and therefore never saw a consumer's hand-rolled override. A dark brand panel beside a
|
|
105
|
+
light sign-in form is an ordinary layout; it should not cost a re-colouring pass. Setting the
|
|
106
|
+
attribute on `<html>` is unchanged, and a `[data-theme="light"]` island nested inside a dark region
|
|
107
|
+
wins in turn.
|
|
108
|
+
**The system default stays root-scoped, deliberately** — the report asked for "the two generated
|
|
109
|
+
selectors", but there are three. Un-rooting `@media (prefers-color-scheme: dark) { :root:not(...) }`
|
|
110
|
+
would make it match every element that does not itself carry `data-theme="light"`, including the
|
|
111
|
+
*children* of a light island, dragging them back to dark. Measured: with the un-rooted variant that
|
|
112
|
+
island renders light ink on a light surface; root-scoped, it stays correct.
|
|
113
|
+
### Added — guards
|
|
114
|
+
- **`test/css.test.mjs`** — the containment invariant, CI-gated: a rule that declares `overflow` must
|
|
115
|
+
also be positioned, or be listed with the ancestor that already contains it. Single-line truncation
|
|
116
|
+
(`text-overflow:ellipsis` on a label) is excluded by shape, not by name, so new truncating labels
|
|
117
|
+
don't accumulate in an allowlist. A new clipping container fails the test until someone decides
|
|
118
|
+
which case it is.
|
|
119
|
+
- **`browser/layout.mjs`** — the same bug end-to-end in real Chrome (`npm run test:browser`): ten
|
|
120
|
+
hidden labels in a wide table, asserting the page cannot scroll horizontally *and* that the scroller
|
|
121
|
+
still scrolls. A static test cannot see this failure; only a layout engine can. Both guards
|
|
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.
|
|
128
|
+
- **`browser/theme.mjs`** — subtree theming end-to-end: a `.fdy-title-page` and a `.fdy-card` inside
|
|
129
|
+
`<section data-theme="dark">` take the dark tokens, a nested light island goes back, and
|
|
130
|
+
`data-theme` on `<html>` still themes everything. `test/build.test.mjs` guards the selector shape
|
|
131
|
+
(not root-scoped, media block still is, and the block order the cascade depends on); this guards the
|
|
132
|
+
behaviour that shape exists for. Mutation-checked: re-rooting the selectors fails it.
|
|
133
|
+
### Notes
|
|
134
|
+
- Not adopted from the report, deliberately: `.fdy-datatable`, `.fdy-modal__body`, `.fdy-drawer__body`
|
|
135
|
+
were listed as needing the same fix. Measured: they do not escape — `<dialog>` is `position:fixed`
|
|
136
|
+
and is already their containing block, and the datatable's own scrolling child now carries it. They
|
|
137
|
+
are in the test's allowlist with that reason instead of carrying a declaration that does nothing.
|
|
138
|
+
|
|
6
139
|
## [1.20.0] — 2026-08-12
|
|
7
140
|
Two bodies of work in one release (1.19.0 was prepared but never committed, tagged or published, so
|
|
8
141
|
it is folded in here rather than left as a phantom version):
|
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,33 @@ 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).
|
|
45
|
+
|
|
46
|
+
### Containment — why the kit's containers are `position:relative`
|
|
47
|
+
|
|
48
|
+
`.fdy-visually-hidden` is `position:absolute`, and `clip` hides **painting**, not **layout**. An
|
|
49
|
+
absolutely positioned box resolves against its nearest *positioned* ancestor, and `overflow` clips
|
|
50
|
+
only what is contained that way — so in an unpositioned scroller, a hidden label parks at its static
|
|
51
|
+
position (possibly thousands of px to the right) and drags the **whole document** sideways. It is
|
|
52
|
+
invisible in the DOM and immune to `overflow-x: hidden` on every wrapper.
|
|
53
|
+
|
|
54
|
+
Every kit container that clips or scrolls therefore declares `position: relative` —
|
|
55
|
+
`.fdy-table-scroll`, `.fdy-table-wrap`, `.fdy-list`, `.fdy-card`, `.fdy-tabs__list`,
|
|
56
|
+
`.fdy-carousel__viewport`, `.fdy-accordion` (the rest are already inside a positioned ancestor).
|
|
57
|
+
Two consequences for you:
|
|
58
|
+
|
|
59
|
+
- **Do the same in your own scrollers.** A container with `overflow` that holds arbitrary markup
|
|
60
|
+
needs `position: relative`, or the hidden labels *you* write will escape it.
|
|
61
|
+
- **Diagnose it correctly.** `document.documentElement.scrollWidth` sees the escaped box;
|
|
62
|
+
`document.body.scrollWidth` does not. The honest check is `window.scrollTo(9999, 0)` then reading
|
|
63
|
+
`window.scrollX`.
|
|
33
64
|
|
|
34
65
|
## Enhancers — hook, script, global, events
|
|
35
66
|
|
|
@@ -208,6 +239,10 @@ rest of the app is not.
|
|
|
208
239
|
- Modifiers: `--ghost` `--danger` `--text` `--icon` (square, icon-only) `--sm` `--lg`
|
|
209
240
|
- Parts: `.fdy-btn__icon` (wraps the `<svg>`) · `.fdy-btn-group` (segmented row, `role="group"` +
|
|
210
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.
|
|
211
246
|
- A11y: `--icon` **requires** `aria-label`. Use `<button type="button">` unless it submits.
|
|
212
247
|
|
|
213
248
|
```html
|
|
@@ -293,6 +328,8 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
|
|
|
293
328
|
```
|
|
294
329
|
|
|
295
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
|
+
|
|
296
333
|
Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
|
|
297
334
|
|
|
298
335
|
- `.fdy-combo` (+`--error`, `--no-icon`) · `__button` `__value` (+`--placeholder`) `__listbox`
|
|
@@ -315,6 +352,8 @@ Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
|
|
|
315
352
|
```
|
|
316
353
|
|
|
317
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
|
+
|
|
318
357
|
Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
|
|
319
358
|
|
|
320
359
|
- `.fdy-autocomplete` · `__listbox` `__option` `__empty`
|
|
@@ -322,6 +361,8 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
|
|
|
322
361
|
`aria-controls` → the listbox `id`, `autocomplete="off"`.
|
|
323
362
|
|
|
324
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
|
+
|
|
325
366
|
Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
|
|
326
367
|
child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
|
|
327
368
|
|
|
@@ -341,6 +382,8 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
|
|
|
341
382
|
```
|
|
342
383
|
|
|
343
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
|
+
|
|
344
387
|
A read-only field backed by master data: the button opens a searchable dialog, the picked row fills
|
|
345
388
|
the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
|
|
346
389
|
|
|
@@ -391,6 +434,8 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
|
|
|
391
434
|
```
|
|
392
435
|
|
|
393
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
|
+
|
|
394
439
|
Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author an empty `<div>`**
|
|
395
440
|
— the enhancer builds everything.
|
|
396
441
|
|
|
@@ -398,6 +443,7 @@ Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author
|
|
|
398
443
|
`__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` `__dow` `__day`
|
|
399
444
|
- Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
|
|
400
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.
|
|
401
447
|
- Attributes: `data-value="YYYY-MM-DD"`, `data-placeholder`, `data-label`, `data-fdy-no-icon`
|
|
402
448
|
|
|
403
449
|
```html
|
|
@@ -498,6 +544,8 @@ Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-tab
|
|
|
498
544
|
```
|
|
499
545
|
|
|
500
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
|
+
|
|
501
549
|
The interactive table: global search, sort, per-column filters, row selection + bulk bar,
|
|
502
550
|
pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` + `data-fdy-table`
|
|
503
551
|
(`data-page-size="N"`).
|
|
@@ -537,6 +585,8 @@ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__ic
|
|
|
537
585
|
replaces.
|
|
538
586
|
|
|
539
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
|
+
|
|
540
590
|
Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`; call
|
|
541
591
|
`FreedayChart.update(el)` after changing data (or use `FdyChart` in Vue/React/Blazor).
|
|
542
592
|
|
|
@@ -572,6 +622,10 @@ responsive `.fdy-datatable` should become below `md` — not a stack of `.fdy-ca
|
|
|
572
622
|
control (render it as a real `<button>`/`<a>`; the UA box is reset without losing the list surface)
|
|
573
623
|
- Row internals: `.fdy-list__main` (truncating stack) → `.fdy-list__title` + `.fdy-list__meta`, and
|
|
574
624
|
`.fdy-list__aside` pinned right
|
|
625
|
+
- **Disabled** — `disabled` on the `<button>` (or `aria-disabled="true"` when the row is an `<a>`/
|
|
626
|
+
`<div>`) dims the row and withdraws the hover tint and the pointer cursor, same as every other
|
|
627
|
+
control in the kit. Do not hand-roll it: an undimmed row that still lights up under the pointer
|
|
628
|
+
reads as clickable while it is refusing input.
|
|
575
629
|
- Not `.fdy-list-reset` — that utility only strips UA bullets/indent from a semantic list.
|
|
576
630
|
|
|
577
631
|
```html
|
|
@@ -639,18 +693,43 @@ Size-matched placeholders so nothing shifts when data lands: `--title` `--text`
|
|
|
639
693
|
|
|
640
694
|
# Navigation
|
|
641
695
|
|
|
642
|
-
## Nav (
|
|
643
|
-
|
|
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
|
|
644
698
|
`__icon` / `__label` / `__badge`; the current one gets `aria-current="page"`.
|
|
645
699
|
`--flat` drops the surface. Nested groups are native `<details>`:
|
|
646
700
|
|
|
647
701
|
- `.fdy-nav__tree` + `<summary class="fdy-nav__item">` + `.fdy-nav__caret` → children in
|
|
648
702
|
`.fdy-nav__sub`
|
|
649
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.
|
|
650
727
|
|
|
651
728
|
## App bar — `.fdy-appbar`
|
|
652
729
|
Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--sticky`
|
|
653
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).
|
|
654
733
|
|
|
655
734
|
## Breadcrumb — `.fdy-breadcrumb`
|
|
656
735
|
`<nav class="fdy-breadcrumb" aria-label="Breadcrumb">` → `<ol class="fdy-breadcrumb__list">` with
|
|
@@ -659,6 +738,8 @@ Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--
|
|
|
659
738
|
|
|
660
739
|
## Tabs — `.fdy-tabs`
|
|
661
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.
|
|
662
743
|
|
|
663
744
|
```html
|
|
664
745
|
<div class="fdy-tabs" data-fdy-tabs>
|
|
@@ -676,12 +757,16 @@ APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
|
|
|
676
757
|
# Overlays
|
|
677
758
|
|
|
678
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
|
+
|
|
679
762
|
Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
|
|
680
763
|
`--wide`; `--cfl` for the choose-from-list dialog. Parts `__header` `__title` `__body` `__footer`
|
|
681
764
|
`__close`. The body scrolls; the footer never clips. `aria-labelledby` → the title's `id`;
|
|
682
765
|
`data-close` on any button that should close it.
|
|
683
766
|
|
|
684
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
|
+
|
|
685
770
|
Temporary side panel on native `<dialog>` — left by default, `--right` to flip. Parts `__header`
|
|
686
771
|
`__title` `__body` `__footer` `__close`. Open it from any
|
|
687
772
|
`<button data-fdy-drawer="<dialog id>">`. Needs `freeday-drawer.js`.
|
|
@@ -709,6 +794,10 @@ Parts `__body` `__title` `__desc` `__footer`. Modifiers:
|
|
|
709
794
|
the UA button box **without** losing the card surface/border
|
|
710
795
|
- `.fdy-card--button` never replaces keyboard semantics — a clickable card must be a real
|
|
711
796
|
`<button>` or `<a>`.
|
|
797
|
+
- **Disabled** — `disabled` (or `aria-disabled="true"`) dims the card and withdraws the pointer
|
|
798
|
+
cursor and the `--interactive` hover-lift.
|
|
799
|
+
- The card is `position:relative`, so a badge or ribbon you absolutely position inside it anchors
|
|
800
|
+
to the card. That is also what keeps hidden labels inside it from escaping — see *Containment*.
|
|
712
801
|
|
|
713
802
|
## Badge — `.fdy-badge`
|
|
714
803
|
Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only —
|
package/README.id.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.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
|
|
@@ -121,6 +143,8 @@ container-nya, bukan scroll sendiri.
|
|
|
121
143
|
|
|
122
144
|
### 3. Theming — 3 sumbu lewat `data-*` di root
|
|
123
145
|
- `data-theme="light|dark"` — redefinisi token semantic (bind ke state tema app-mu).
|
|
146
|
+
Berlaku di **ancestor mana pun**, tak harus root: `<section data-theme="dark">` membalik region itu
|
|
147
|
+
dan semua komponen di dalamnya ikut (lihat [`USAGE.md`](USAGE.md) §5b).
|
|
124
148
|
- `data-density="comfortable|compact"` — tinggi kontrol (`--control-h`) untuk layar padat data.
|
|
125
149
|
Ini auto-apply hanya ke kontrol bawaan Freeday (button, input, combo, dst); komponen
|
|
126
150
|
custom/hand-built harus baca `--control-h` sendiri (mis. `height: var(--control-h)`) supaya ikut
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **More free days for devs — the UI kit is ready to use.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.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
|
|
@@ -121,6 +142,8 @@ of scrolling.
|
|
|
121
142
|
|
|
122
143
|
### 3. Theming — 3 axes via `data-*` on the root
|
|
123
144
|
- `data-theme="light|dark"` — redefines the semantic tokens (bind it to your app's theme state).
|
|
145
|
+
Works on **any ancestor**, not just the root: `<section data-theme="dark">` inverts that region
|
|
146
|
+
and every component inside it follows (see [`USAGE.md`](USAGE.md) §5b).
|
|
124
147
|
- `data-density="comfortable|compact"` — control height (`--control-h`) for data-dense screens.
|
|
125
148
|
This auto-applies only to Freeday's built-in controls (button, input, combo, etc.);
|
|
126
149
|
custom/hand-built components must read `--control-h` themselves (e.g. `height: var(--control-h)`)
|
package/USAGE.md
CHANGED
|
@@ -82,6 +82,27 @@ point, everything around it quiet.
|
|
|
82
82
|
- **Surfaces:** most backgrounds are `--color-surface`; `--color-surface-2`/`-3` for a recessed area;
|
|
83
83
|
`--color-primary-soft` only when you want a tinted callout, not as a default panel colour.
|
|
84
84
|
|
|
85
|
+
## 5b. Theme — global by default, per-subtree when a region is inverted
|
|
86
|
+
|
|
87
|
+
`data-theme="light|dark"` redefines the semantic tokens. Set it on `<html>` and it themes the app;
|
|
88
|
+
that is the normal case and nothing about it has changed.
|
|
89
|
+
|
|
90
|
+
**It is also per-subtree.** The two explicit selectors are bare `[data-theme="dark"]` /
|
|
91
|
+
`[data-theme="light"]`, and semantic tokens are inheriting custom properties — so a
|
|
92
|
+
`<section data-theme="dark">` inverts that region and **every Freeday component inside it follows**:
|
|
93
|
+
card surfaces, inputs, buttons, and text roles like `.fdy-title-page` that set
|
|
94
|
+
`color: var(--color-text)` explicitly. A dark brand panel beside a light sign-in form needs no
|
|
95
|
+
per-element re-colouring, and a `[data-theme="light"]` island nested back inside a dark region wins
|
|
96
|
+
in turn.
|
|
97
|
+
|
|
98
|
+
Do **not** invert a region by hand with `--color-inverse-*` plus `color-mix`. That pair is right for
|
|
99
|
+
a one-off band of your own markup, but it stops scaling the moment the region contains a real
|
|
100
|
+
component — anything that sets its own colour from a token never sees your override, and you end up
|
|
101
|
+
restating colours per element.
|
|
102
|
+
|
|
103
|
+
The **system** default (`@media (prefers-color-scheme: dark)`) stays root-scoped on purpose: that
|
|
104
|
+
rule is about the document, and un-rooting it would re-darken the children of a light island.
|
|
105
|
+
|
|
85
106
|
## 6. Density — `compact` for data-dense screens
|
|
86
107
|
|
|
87
108
|
`data-density="compact"` tightens control height **and** the mid-range spacing scale
|
|
@@ -94,6 +115,27 @@ container, a single `<section>`. An app whose two list screens are dense and who
|
|
|
94
115
|
are not should scope it per screen rather than densifying everything. Set it at one level per screen,
|
|
95
116
|
never per component.
|
|
96
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
|
+
|
|
97
139
|
## 7. Assemble the page from the frame down
|
|
98
140
|
|
|
99
141
|
1. **Shell:** every application starts inside **`.fdy-app`** — a flex row of `__sidebar` (with
|