@cahyo-dimas/freeday 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,46 @@
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.18.0] — 2026-08-11
7
+ ### Fixed
8
+ - **`FdyModal` / `FdyDrawer` (Vue) were non-dismissible when `dismissible` was omitted — an
9
+ accessibility defect.** Vue's boolean-cast delivers an omitted Boolean prop as `false`, not
10
+ `undefined`, so the defensive `props.dismissible !== false` evaluated to `false`: no Escape, no
11
+ backdrop close, and (modal) no close button rendered — the opposite of the documented
12
+ `dismissible: true` default. Both now use `withDefaults(…, { dismissible: true })`. React was
13
+ unaffected (it leaves omitted props `undefined`); `FdyCombo`/`FdyDatepicker` use `=== true`, which
14
+ is correct for an intended-`false` default.
15
+ - **`.fdy-card--interactive` hover transform now respects reduced motion** — the `-3px` lift is dropped
16
+ under `prefers-reduced-motion` (the shadow still signals the affordance).
17
+ ### Added
18
+ - **Page-composition primitives + type roles (`src/components/composition.css`).** `.fdy-page`,
19
+ `.fdy-page__header`, `.fdy-page-section`, `.fdy-toolbar`, `.fdy-stats`/`.fdy-stat` (a KPI tile that is
20
+ deliberately *not* a card, so a metric strip doesn't become an identical-card grid), and three title
21
+ roles `.fdy-title-page` / `-section` / `-card` (+ `.fdy-eyebrow`, `.fdy-text-muted`/`-caption`). They
22
+ encode *how a page is assembled* so independently-built screens cohere.
23
+ - **`USAGE.md` — the usage doctrine.** Which token/role/shadow to use when: type roles, spacing rhythm,
24
+ elevation, one-primary-per-screen, semantic-vs-categorical colour, density, and the shell-down
25
+ composition order. The line where a component library becomes a design system.
26
+ - **General categorical palette `--tone-1`…`--tone-8`** (a chart-neutral alias of the validated,
27
+ theme-aware `--chart-1`…`8`) + **`.fdy-chip--tone-N`** to match `.fdy-avatar--tone-N` — non-semantic
28
+ category colours (chips, tags, legends), WCAG AA in light & dark (gated).
29
+ - **`FdyTable` surfaces its processed rows** — a `process` event (Vue) / `onProcess` callback (React),
30
+ `{ rows, total }`, in **both** client and server modes — so the same filtered/sorted/paged set can
31
+ drive a responsive card list, a selection summary, or CSV export without re-deriving the pipeline.
32
+ - **`./table-model` export.** The pure `filterRows`/`sortRows`/`paginate`/`cellValue`/… functions
33
+ (`adapters/core/table-model.js`) are now reachable as `@cahyo-dimas/freeday/table-model`, so a consumer
34
+ can pre-compute exactly what `FdyTable` does instead of re-deriving it and risking drift.
35
+ ### Changed
36
+ - **`data-density="compact"` now steps the mid-range spacing scale** (`--space-3`…`--space-6`) as well as
37
+ `--control-h`, so Freeday components — and a utility theme built on `var(--space-N)` — actually densify;
38
+ previously it changed only control height.
39
+ - **Docs:** getting-started leads with the `.fdy-app` shell + a loud "load the fonts yourself" step (the
40
+ package names Sora / IBM Plex Sans / JetBrains Mono but bundles none); README links `USAGE.md`.
41
+
42
+ Additive except the two bug fixes (which restore documented behaviour). Gate: `node --test` 22/22 ·
43
+ `typecheck:react` 0 · `test:browser` 6/6 · new CSS browser-verified in real Chrome. From real-app
44
+ consumption feedback (three instalments).
45
+
6
46
  ## [1.17.0] — 2026-08-11
7
47
  ### Added
8
48
  - **Avatar identity tones (`--tone-1`…`--tone-8`).** Decorative tints (from the categorical chart
package/README.id.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **Lebih banyak _free day_ buat dev — UI kit-nya sudah siap pakai.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.17.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.17.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.18.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.18.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`.
@@ -49,8 +49,18 @@ ter-publish → install tanpa build step; minify diserahkan ke bundler konsumen.
49
49
  Kelas komponen berprefix `fdy-` (mis. `fdy-btn`, `fdy-card`, `fdy-badge`). Pakai langsung di
50
50
  markup framework apa pun — Vue, React, Blazor, atau HTML polos.
51
51
 
52
+ > **⚠️ Muat font-nya sendiri — paket ini tidak.** Token tipe menamai **Sora** / **IBM Plex Sans** /
53
+ > **JetBrains Mono** tapi Freeday tak membundel file font. Muat (mis. `@import '@fontsource/sora/700.css'`
54
+ > …) atau override `--font-display`/`--font-body`/`--font-mono` — kalau tidak, kit jatuh ke fallback
55
+ > sistem dan terlihat "belum jadi". Detail: [`docs/getting-started.md`](docs/getting-started.md).
56
+
57
+ > **Token/role mana dipakai kapan → [`USAGE.md`](USAGE.md).** Freeday menjaga konsistensi *nilai*;
58
+ > `USAGE.md` = doktrin yang menjaga konsistensi *keputusan* — role tipe (`.fdy-title-page/-section/-card`),
59
+ > ritme spasi, elevasi, satu-primary-per-layar, palet kategorikal `--tone-1…8`, dan primitif komposisi
60
+ > halaman (`.fdy-page`, `.fdy-page-section`, `.fdy-stats`). Tiap app mulai di `.fdy-app`.
61
+
52
62
  > **Scope: komponen + token, bukan layout.** Freeday punya komponen & design token; helper layout
53
- > cuma `.fdy-hidden` / `.fdy-visually-hidden`. Layout (stack/grid/gap) dari layer-mu sendiri —
63
+ > cuma `.fdy-hidden` / `.fdy-visually-hidden` plus primitif komposisi di atas. Layout grid dari layer-mu sendiri —
54
64
  > pasangkan dengan utility framework (Tailwind, UnoCSS…) mode **utilities-only, preflight OFF**
55
65
  > (`base.css` Freeday = reset-nya). `base.css` itu reset *ringan* (tak me-reset margin `ul`/`ol`/`p` —
56
66
  > pakai `.fdy-list-reset` atau komponen list Freeday), dan skala spacing/radius/durasi adalah custom
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs — the UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v1.17.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.17.0)
8
+ [![Release](https://img.shields.io/badge/release-v1.18.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v1.18.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`.
@@ -49,13 +49,23 @@ bundler. Because it's on **public npm**, `npm ci` runs in CI without auth or an
49
49
  Component classes are prefixed `fdy-` (e.g. `fdy-btn`, `fdy-card`, `fdy-badge`). Use them directly
50
50
  in any framework's markup — Vue, React, Blazor, or plain HTML.
51
51
 
52
+ > **⚠️ Load the fonts — the package doesn't.** The type tokens name **Sora** / **IBM Plex Sans** /
53
+ > **JetBrains Mono** but Freeday bundles no font files. Load them (e.g. `@import '@fontsource/sora/700.css'`
54
+ > …) or override `--font-display`/`--font-body`/`--font-mono` — otherwise the kit renders in the system
55
+ > fallback and looks unfinished. See [`docs/getting-started.md` §Core concepts](docs/getting-started.md).
56
+
57
+ > **Which token/role to use when → [`USAGE.md`](USAGE.md).** Freeday enforces consistent *values*;
58
+ > `USAGE.md` is the doctrine that also makes *decisions* consistent — type roles (`.fdy-title-page/-section/-card`),
59
+ > spacing rhythm, elevation, one-primary-per-screen, the `--tone-1…8` categorical palette, and the page-
60
+ > composition primitives (`.fdy-page`, `.fdy-page-section`, `.fdy-stats`). Every app starts in `.fdy-app`.
61
+
52
62
  > **Scope: components + tokens, not layout.** Freeday owns components and design tokens; the only
53
- > layout helpers are `.fdy-hidden` / `.fdy-visually-hidden`. Bring your own layout layer — pair it
54
- > with a utility framework (Tailwind, UnoCSS…) run **utilities-only with preflight OFF** (Freeday's
55
- > `base.css` is the reset). `base.css` is a *light* reset (it doesn't strip `ul`/`ol`/`p` margins —
56
- > use `.fdy-list-reset` or a Freeday list component), and the spacing/radius/duration scales are
57
- > public custom properties (`--space-0`…`--space-24`, …) you can build your utility theme on. Full
58
- > notes: [`docs/getting-started.md` §Core concepts](docs/getting-started.md).
63
+ > layout helpers are `.fdy-hidden` / `.fdy-visually-hidden` plus the page-composition primitives above.
64
+ > Bring your own grid layer — pair it with a utility framework (Tailwind, UnoCSS…) run **utilities-only
65
+ > with preflight OFF** (Freeday's `base.css` is the reset). `base.css` is a *light* reset (it doesn't
66
+ > strip `ul`/`ol`/`p` margins — use `.fdy-list-reset` or a Freeday list component), and the
67
+ > spacing/radius/duration scales are public custom properties (`--space-0`…`--space-24`, …) you can
68
+ > build your utility theme on. Full notes: [`docs/getting-started.md` §Core concepts](docs/getting-started.md).
59
69
 
60
70
  > `.fdy-btn` is **already** the primary button — there's no separate `.fdy-btn--primary` modifier.
61
71
  > Modifiers for other variants: `--ghost`, `--danger`, `--text`, `--sm`, `--lg`, `--icon`
package/USAGE.md ADDED
@@ -0,0 +1,104 @@
1
+ # Freeday — Usage doctrine
2
+
3
+ Freeday ships tokens and components. This file ships the **decisions** — which token to use when,
4
+ how a page is assembled, what earns emphasis. A component library enforces consistent *values*; a
5
+ design system also enforces consistent *decisions*. Skim this once before building a screen; it is
6
+ what makes screens built by different people (or different sessions) look like one product.
7
+
8
+ The rules are opinionated on purpose. When one conflicts with a real need, break it deliberately —
9
+ but start here, not from a blank page.
10
+
11
+ ---
12
+
13
+ ## 1. Type roles — three title levels, not eleven
14
+
15
+ Do **not** reuse `.fdy-card__title` for a page title. Collapsing page/section/card into one style is
16
+ the single biggest cause of "flat grey mush". Pick the role, not the size:
17
+
18
+ | Role | Class | When |
19
+ |---|---|---|
20
+ | Eyebrow | `.fdy-eyebrow` | A small uppercase label above a page/section title. Optional. |
21
+ | Page title | `.fdy-title-page` | One per screen. The `<h1>`. |
22
+ | Section title | `.fdy-title-section` | A region within the page (`<h2>`). |
23
+ | Card / row title | `.fdy-title-card` (or `.fdy-card__title`) | A title inside a card or list row (`<h3>`). |
24
+ | Body | *(default)* | Running text. |
25
+ | Muted / caption | `.fdy-text-muted`, `.fdy-text-caption` | Secondary text, timestamps, help. |
26
+
27
+ Everything else is body. If you reach for a fourth title size, you probably need a section, not a
28
+ font size.
29
+
30
+ ## 2. Spacing rhythm — three gaps, always from the scale
31
+
32
+ Never a loose value; always `var(--space-N)` (a 4px scale). Three rhythms carry most layouts, and the
33
+ composition primitives apply them for you:
34
+
35
+ - **Between page sections:** `--space-8` — `.fdy-page` sets this gap between its children.
36
+ - **Within a group** (heading ↔ its body, cards in a list): `--space-4` — `.fdy-page-section` sets it.
37
+ - **Inside a control/card** (label ↔ input, icon ↔ text): `--space-2` / `--space-3`.
38
+
39
+ Reserve `--space-1` for hairline pairs and `--space-10`+ for deliberate breathing room (a hero, an
40
+ empty state). Don't scatter `--space-6` everywhere — a page with one gap value has no rhythm.
41
+
42
+ ## 3. Elevation — most surfaces are flat
43
+
44
+ Shadow is a signal, not decoration. Spend it sparingly:
45
+
46
+ - **Flat** (no shadow, border only, or nothing): the default. Page sections, list rows, the app shell.
47
+ - **`--shadow-1`:** a card that is a distinct object you could pick up — a workspace tile, a panel.
48
+ - **`--shadow-2`/`3`:** something that *floats over* the page — a popover, menu, or dropdown.
49
+ - **`--shadow-4`:** a modal / drawer overlay only.
50
+
51
+ If every box on the screen has the same card shadow, none of them read as special — that's the
52
+ "identical card grid" failure. Prefer flat sections with **one** raised element that matters.
53
+
54
+ ## 4. Emphasis — exactly one primary per screen
55
+
56
+ `.fdy-btn` is *already* the primary action (there is no `--primary` modifier). Use it **once** per
57
+ screen — the one thing you want the user to do. Everything else is `--ghost` or `--text`. Two primary
58
+ buttons on a screen means neither is. The same rule governs colour fills and `--shadow`: one focal
59
+ point, everything around it quiet.
60
+
61
+ ## 5. Colour — semantic is reserved; categorical is `--tone`
62
+
63
+ - **Accent** (`--color-primary`, and `--color-accent` sparingly): interactive + brand. This is your
64
+ one accent hue.
65
+ - **Semantic is reserved** and is *not* your accent: `--color-success` = good only, `--color-warning`
66
+ = caution only, `--color-danger` = destructive/error only. Never decorative. Encode state in a pill
67
+ or chip, not just colour.
68
+ - **Categorical** (`--tone-1` … `--tone-8`, the general alias of the validated chart palette): N
69
+ visually-distinct **non-semantic** colours — avatar tones, category chips, tags, legend swatches.
70
+ Use the modifiers `.fdy-avatar--tone-N` / `.fdy-chip--tone-N` (both stay WCAG AA in light & dark),
71
+ and hash a stable index off the full string so the same category always gets the same colour.
72
+ - **Surfaces:** most backgrounds are `--color-surface`; `--color-surface-2`/`-3` for a recessed area;
73
+ `--color-primary-soft` only when you want a tinted callout, not as a default panel colour.
74
+
75
+ ## 6. Density — `compact` for data-dense screens
76
+
77
+ `data-density="compact"` on `<html>` tightens control height **and** the mid-range spacing scale
78
+ (`--space-3`…`--space-6` step down a notch), so cards, toolbars and tables get denser. Use it on
79
+ table-heavy back-office screens; leave `comfortable` (the default) for forms and marketing-adjacent
80
+ pages. Set it once at the app root, not per component.
81
+
82
+ ## 7. Assemble the page from the frame down
83
+
84
+ 1. **Shell:** every application starts inside **`.fdy-app`** (`__topbar`, `__sidebar`, `__main`,
85
+ `__content`, `__navtoggle`, `__backdrop`). Don't hand-roll a shell from flexbox — the toggle and
86
+ backdrop plumbing are already there. See `docs/getting-started.md`.
87
+ 2. **Page:** wrap the screen body in **`.fdy-page`** (vertical section rhythm), opening with a
88
+ **`.fdy-page__header`** (eyebrow + `.fdy-title-page` + `.fdy-page__desc` on the left, the one
89
+ primary action on the right).
90
+ 3. **Sections:** each region is a **`.fdy-page-section`** (a `.fdy-title-section` + optional
91
+ `.fdy-toolbar`, then its body).
92
+ 4. **KPIs:** a **`.fdy-stats`** grid of **`.fdy-stat`** tiles — deliberately *not* cards, so a metric
93
+ strip doesn't become an identical-card grid. Wrap in `.fdy-stats--boxed` for one shared strip.
94
+ 5. **Content:** components (`.fdy-card`, `.fdy-datatable`, `.fdy-chart`, …) go inside sections.
95
+
96
+ Freeday deliberately owns **components + tokens, not layout**. Everything above is layout in the kit's
97
+ own language; for the rest (grids, one-off spacing), pair a utility framework run **utilities-only,
98
+ preflight-off** — and build its theme on `var(--space-N)` so the two systems agree. See
99
+ `docs/getting-started.md` §Core concepts.
100
+
101
+ ---
102
+
103
+ *Layout classes here live in `src/components/composition.css`. If a screen needs a primitive that
104
+ isn't here, it probably belongs here — open an issue rather than re-inventing it per screen.*
@@ -57,6 +57,10 @@ export interface FdyTableProps<Row extends object> {
57
57
  rowClass?: (row: Row) => string | undefined;
58
58
  /** A row was activated (click, or Enter/Space while the row itself is focused). */
59
59
  onRowActivate?: (row: Row) => void;
60
+ /** Called with the processed page of rows (after filter/sort/paginate) plus the total row count —
61
+ * in BOTH modes, whenever they change. Lets a consumer render the SAME processed set elsewhere
62
+ * (a `< md` card list, a "selected" summary, export-to-CSV) without re-deriving the pipeline. */
63
+ onProcess?: (result: { rows: Row[]; total: number }) => void;
60
64
  /** Controlled: row keys whose detail is shown as a full-width row beneath them. */
61
65
  expandedKeys?: ReadonlyArray<string | number>;
62
66
  /** Renders the expandable detail row for an expanded row (React equivalent of Vue's `row-detail` slot). */
@@ -110,6 +114,13 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
110
114
  if (!serverPaged && internalPageIndex > totalPages - 1) setInternalPageIndex(Math.max(0, totalPages - 1));
111
115
  }, [serverPaged, internalPageIndex, totalPages]);
112
116
 
117
+ // Surface the processed page + total to the parent (both modes), so the same result can drive a
118
+ // responsive card list / summary / export without re-implementing filter/sort/paginate.
119
+ const onProcess = props.onProcess;
120
+ useEffect((): void => {
121
+ onProcess?.({ rows: displayRows, total: totalCount });
122
+ }, [onProcess, displayRows, totalCount]);
123
+
113
124
  function ariaSortOf(col: FdyTableColumn<Row>): 'ascending' | 'descending' | undefined {
114
125
  if (effectiveSort === null || effectiveSort.key !== col.key) return undefined;
115
126
  return effectiveSort.dir === 'asc' ? 'ascending' : 'descending';
@@ -8,12 +8,15 @@ import { computed, onMounted, useId, watch, type ComputedRef, type Ref, ref } fr
8
8
  // focus trap, focus restore, top-layer stacking and inert background; `dismissible` (default true)
9
9
  // gates Esc + backdrop dismissal.
10
10
 
11
- const props = defineProps<{
11
+ // dismissible MUST go through withDefaults: Vue's boolean-cast gives an omitted Boolean prop
12
+ // `false`, not `undefined`, so a bare `props.dismissible !== false` would make an un-annotated
13
+ // drawer non-dismissible (no Esc, no backdrop, no close button) — the opposite of the default.
14
+ const props = withDefaults(defineProps<{
12
15
  open: boolean;
13
16
  title: string;
14
17
  side?: 'left' | 'right';
15
18
  dismissible?: boolean;
16
- }>();
19
+ }>(), { dismissible: true });
17
20
 
18
21
  const emit = defineEmits<{
19
22
  close: [];
@@ -10,12 +10,15 @@ import { computed, onMounted, useId, watch, type ComputedRef, type Ref, ref } fr
10
10
  // Native <dialog> already provides the focus trap, focus restore, top-layer stacking and inert
11
11
  // background — the wrapper only avoids breaking them. `dismissible` (default true) gates Esc + backdrop.
12
12
 
13
- const props = defineProps<{
13
+ // dismissible MUST go through withDefaults: Vue's boolean-cast gives an omitted Boolean prop
14
+ // `false`, not `undefined`, so a bare `props.dismissible !== false` would make an un-annotated
15
+ // modal non-dismissible (no Esc, no backdrop, no close button) — the opposite of the default.
16
+ const props = withDefaults(defineProps<{
14
17
  open: boolean;
15
18
  title: string;
16
19
  size?: 'sm' | 'md' | 'lg' | 'wide';
17
20
  dismissible?: boolean;
18
- }>();
21
+ }>(), { dismissible: true });
19
22
 
20
23
  const emit = defineEmits<{
21
24
  close: [];
@@ -58,6 +58,10 @@ const emit = defineEmits<{
58
58
  'update:page': [page: FdyPageState];
59
59
  /** A row was activated (click, or Enter/Space while the row itself is focused). */
60
60
  'row-activate': [row: Row];
61
+ /** The processed page of rows (after filter/sort/paginate) plus the total row count — fires in
62
+ * BOTH modes whenever they change. Lets a consumer render the SAME processed set elsewhere
63
+ * (a `< md` card list, a "selected" summary, export-to-CSV) without re-deriving the pipeline. */
64
+ 'process': [result: { rows: Row[]; total: number }];
61
65
  }>();
62
66
 
63
67
  const internalSort: Ref<FdySortState | null> = ref(null);
@@ -125,6 +129,14 @@ watch(totalPages, (tp: number): void => {
125
129
  if (!serverPaged.value && internalPageIndex.value > tp - 1) internalPageIndex.value = Math.max(0, tp - 1);
126
130
  });
127
131
 
132
+ // Surface the processed page + total to the parent (both modes), so the same result can drive a
133
+ // responsive card list / summary / export without re-implementing filter/sort/paginate.
134
+ watch(
135
+ [displayRows, totalCount],
136
+ (): void => emit('process', { rows: displayRows.value, total: totalCount.value }),
137
+ { immediate: true },
138
+ );
139
+
128
140
  function ariaSortOf(col: FdyTableColumn<Row>): 'ascending' | 'descending' | undefined {
129
141
  const s: FdySortState | null = effectiveSort.value;
130
142
  if (s === null || s.key !== col.key) return undefined;
@@ -166,6 +166,14 @@
166
166
  --chart-8: #e34948;
167
167
  --chart-grid: var(--slate-200);
168
168
  --chart-tick: var(--slate-400);
169
+ --tone-1: var(--chart-1);
170
+ --tone-2: var(--chart-2);
171
+ --tone-3: var(--chart-3);
172
+ --tone-4: var(--chart-4);
173
+ --tone-5: var(--chart-5);
174
+ --tone-6: var(--chart-6);
175
+ --tone-7: var(--chart-7);
176
+ --tone-8: var(--chart-8);
169
177
  --focus-ring: var(--azure-600);
170
178
  --focus-ring-width: 2px;
171
179
  }
@@ -340,6 +348,10 @@
340
348
  --focus-ring: var(--azure-600);
341
349
  }
342
350
  :root[data-density="compact"] {
351
+ --space-3: 0.625rem;
352
+ --space-4: 0.75rem;
353
+ --space-5: 1rem;
354
+ --space-6: 1.25rem;
343
355
  --control-h: 2rem;
344
356
  }
345
357
 
@@ -534,14 +546,14 @@ a { color: var(--color-primary); }
534
546
  * test/contrast.test.mjs). Use to disambiguate same-initial avatars (e.g. many "PT …" names):
535
547
  * hash a stable 1..8 index off the FULL name in your app, then apply .fdy-avatar--tone-<n>. */
536
548
  .fdy-avatar--tone-1,.fdy-avatar--tone-2,.fdy-avatar--tone-3,.fdy-avatar--tone-4,.fdy-avatar--tone-5,.fdy-avatar--tone-6,.fdy-avatar--tone-7,.fdy-avatar--tone-8{background:color-mix(in srgb,var(--_fdy-avatar-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-avatar-tone) 50%,var(--color-text));}
537
- .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--chart-1);}
538
- .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--chart-2);}
539
- .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--chart-3);}
540
- .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--chart-4);}
541
- .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--chart-5);}
542
- .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--chart-6);}
543
- .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--chart-7);}
544
- .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--chart-8);}
549
+ .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--tone-1);}
550
+ .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--tone-2);}
551
+ .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--tone-3);}
552
+ .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--tone-4);}
553
+ .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--tone-5);}
554
+ .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--tone-6);}
555
+ .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--tone-7);}
556
+ .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--tone-8);}
545
557
 
546
558
  /* Freeday — Badge */
547
559
  .fdy-badge{display:inline-flex;align-items:center;gap:var(--space-1);font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);line-height:1;white-space:nowrap;padding:.35em .7em;border-radius:var(--radius-full);color:var(--color-primary-strong);background:var(--color-primary-soft);border:var(--bw) solid color-mix(in srgb,var(--color-primary) 22%,transparent);box-shadow:0 1px 1.5px rgba(16,14,30,.05);}
@@ -661,6 +673,9 @@ a { color: var(--color-primary); }
661
673
  .fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
662
674
  .fdy-card--interactive{cursor:pointer;}
663
675
  .fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
676
+ /* The global reduced-motion reset kills the ANIMATION; drop the -3px shift too so the card
677
+ * doesn't jump. The shadow still communicates the affordance. */
678
+ @media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover{transform:none;}}
664
679
  /* When the whole card IS the control — a real <button> so the row is keyboard-reachable and
665
680
  * announced — reset the UA button box. Note: it does NOT reset background/border, because
666
681
  * .fdy-card already sets its own surface + border (resetting them here would strip the card). */
@@ -816,6 +831,18 @@ a { color: var(--color-primary); }
816
831
  /* Freeday — Chip (tag / removable filter) */
817
832
  .fdy-chip{display:inline-flex;align-items:center;gap:var(--space-2);height:1.75rem;padding:0 var(--space-3);border-radius:var(--radius-full);font-size:var(--text-xs);font-weight:var(--weight-medium);color:var(--color-text);background:var(--color-surface-3);border:var(--bw) solid var(--color-border);}
818
833
  .fdy-chip--primary{color:var(--color-primary-strong);background:var(--color-primary-soft);border-color:color-mix(in srgb,var(--color-primary) 22%,transparent);}
834
+ /* Categorical tone chips — non-semantic category tags / legend swatches, from the general
835
+ * --tone-1..8 palette. Same validated AA formula as .fdy-avatar--tone-* (text >=4.5:1 both themes,
836
+ * gated by test/contrast.test.mjs). */
837
+ .fdy-chip--tone-1,.fdy-chip--tone-2,.fdy-chip--tone-3,.fdy-chip--tone-4,.fdy-chip--tone-5,.fdy-chip--tone-6,.fdy-chip--tone-7,.fdy-chip--tone-8{background:color-mix(in srgb,var(--_fdy-chip-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-chip-tone) 50%,var(--color-text));border-color:color-mix(in srgb,var(--_fdy-chip-tone) 30%,var(--color-border));}
838
+ .fdy-chip--tone-1{--_fdy-chip-tone:var(--tone-1);}
839
+ .fdy-chip--tone-2{--_fdy-chip-tone:var(--tone-2);}
840
+ .fdy-chip--tone-3{--_fdy-chip-tone:var(--tone-3);}
841
+ .fdy-chip--tone-4{--_fdy-chip-tone:var(--tone-4);}
842
+ .fdy-chip--tone-5{--_fdy-chip-tone:var(--tone-5);}
843
+ .fdy-chip--tone-6{--_fdy-chip-tone:var(--tone-6);}
844
+ .fdy-chip--tone-7{--_fdy-chip-tone:var(--tone-7);}
845
+ .fdy-chip--tone-8{--_fdy-chip-tone:var(--tone-8);}
819
846
  .fdy-chip__remove{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;margin-right:calc(var(--space-1)*-1);border:0;padding:0;border-radius:var(--radius-full);background:transparent;color:currentColor;opacity:.6;cursor:pointer;font-size:var(--text-sm);line-height:1;}
820
847
  .fdy-chip__remove:hover{opacity:1;background:color-mix(in srgb,currentColor 16%,transparent);}
821
848
  .fdy-chip__remove:focus-visible{outline:none;opacity:1;box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
@@ -865,6 +892,56 @@ a { color: var(--color-primary); }
865
892
  @media (prefers-reduced-motion:no-preference){.fdy-combo__listbox:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
866
893
  @keyframes fdy-combo-in{from{opacity:0;transform:translateY(-4px);}to{opacity:1;transform:none;}}
867
894
 
895
+ /* Freeday — Page composition & type roles.
896
+ *
897
+ * The kit stops at the component; these primitives encode *how a page is assembled* so screens
898
+ * built by different people cohere. They carry the usage doctrine (see USAGE.md) in markup: a
899
+ * named type role gets used, a doc gets skimmed. Everything here is token-driven layout — no new
900
+ * colours, no new shadows. Pair with .fdy-app (the shell) and the components inside.
901
+ */
902
+
903
+ /* ---- Type roles (three title levels + supporting text) -------------------------------------
904
+ * One role per level of hierarchy. Reach for these instead of re-using .fdy-card__title for a
905
+ * page title (the mistake that collapses three levels into one grey mush). */
906
+ .fdy-eyebrow{font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
907
+ .fdy-title-page{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:var(--leading-tight);color:var(--color-text);margin:0;text-wrap:balance;}
908
+ .fdy-title-section{font-family:var(--font-display);font-size:var(--text-xl);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);line-height:var(--leading-snug);color:var(--color-text);margin:0;text-wrap:balance;}
909
+ .fdy-title-card{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0;}
910
+ .fdy-text-muted{color:var(--color-text-muted);}
911
+ .fdy-text-subtle{color:var(--color-text-subtle);}
912
+ .fdy-text-caption{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;}
913
+
914
+ /* ---- Page: the vertical stack of sections, with the section rhythm between them ------------- */
915
+ .fdy-page{display:flex;flex-direction:column;gap:var(--space-8);}
916
+
917
+ /* ---- Page header: eyebrow + title + description on the left, primary action(s) on the right -- */
918
+ .fdy-page__header{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--space-4);flex-wrap:wrap;}
919
+ .fdy-page__heading{display:flex;flex-direction:column;gap:var(--space-2);min-width:0;}
920
+ .fdy-page__desc{font-size:var(--text-base);color:var(--color-text-muted);margin:0;max-width:62ch;line-height:var(--leading-normal);}
921
+ .fdy-page__actions{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}
922
+
923
+ /* ---- Page section: a heading (+ optional toolbar) and its body, with the intra-group rhythm --- */
924
+ .fdy-page-section{display:flex;flex-direction:column;gap:var(--space-4);}
925
+ .fdy-page-section__head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap;}
926
+
927
+ /* ---- Toolbar: a horizontal control row (search + actions), wraps cleanly ---------------------- */
928
+ .fdy-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;}
929
+ .fdy-toolbar__spacer{flex:1;}
930
+ .fdy-toolbar__search{max-width:18rem;flex:1 1 12rem;}
931
+
932
+ /* ---- Stat tile: a KPI metric that is deliberately NOT a card, so a KPI strip doesn't become an
933
+ * identical-white-card grid. Flat by default; wrap the .fdy-stats grid in one card if you want a
934
+ * single boxed strip, or use .fdy-stats--boxed for shared surface + dividers. */
935
+ .fdy-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:var(--space-5);}
936
+ .fdy-stat{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;}
937
+ .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
938
+ .fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
939
+ .fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
940
+ .fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
941
+ .fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
942
+ .fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
943
+ .fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
944
+
868
945
  /* Freeday — Date picker (input-styled trigger + calendar popover).
869
946
  * Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
870
947
  .fdy-datepicker{position:relative;display:inline-block;width:100%;max-width:14rem;}
package/dist/freeday.css CHANGED
@@ -189,14 +189,14 @@ a { color: var(--color-primary); }
189
189
  * test/contrast.test.mjs). Use to disambiguate same-initial avatars (e.g. many "PT …" names):
190
190
  * hash a stable 1..8 index off the FULL name in your app, then apply .fdy-avatar--tone-<n>. */
191
191
  .fdy-avatar--tone-1,.fdy-avatar--tone-2,.fdy-avatar--tone-3,.fdy-avatar--tone-4,.fdy-avatar--tone-5,.fdy-avatar--tone-6,.fdy-avatar--tone-7,.fdy-avatar--tone-8{background:color-mix(in srgb,var(--_fdy-avatar-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-avatar-tone) 50%,var(--color-text));}
192
- .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--chart-1);}
193
- .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--chart-2);}
194
- .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--chart-3);}
195
- .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--chart-4);}
196
- .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--chart-5);}
197
- .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--chart-6);}
198
- .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--chart-7);}
199
- .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--chart-8);}
192
+ .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--tone-1);}
193
+ .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--tone-2);}
194
+ .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--tone-3);}
195
+ .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--tone-4);}
196
+ .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--tone-5);}
197
+ .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--tone-6);}
198
+ .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--tone-7);}
199
+ .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--tone-8);}
200
200
 
201
201
  /* Freeday — Badge */
202
202
  .fdy-badge{display:inline-flex;align-items:center;gap:var(--space-1);font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);line-height:1;white-space:nowrap;padding:.35em .7em;border-radius:var(--radius-full);color:var(--color-primary-strong);background:var(--color-primary-soft);border:var(--bw) solid color-mix(in srgb,var(--color-primary) 22%,transparent);box-shadow:0 1px 1.5px rgba(16,14,30,.05);}
@@ -316,6 +316,9 @@ a { color: var(--color-primary); }
316
316
  .fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
317
317
  .fdy-card--interactive{cursor:pointer;}
318
318
  .fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
319
+ /* The global reduced-motion reset kills the ANIMATION; drop the -3px shift too so the card
320
+ * doesn't jump. The shadow still communicates the affordance. */
321
+ @media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover{transform:none;}}
319
322
  /* When the whole card IS the control — a real <button> so the row is keyboard-reachable and
320
323
  * announced — reset the UA button box. Note: it does NOT reset background/border, because
321
324
  * .fdy-card already sets its own surface + border (resetting them here would strip the card). */
@@ -471,6 +474,18 @@ a { color: var(--color-primary); }
471
474
  /* Freeday — Chip (tag / removable filter) */
472
475
  .fdy-chip{display:inline-flex;align-items:center;gap:var(--space-2);height:1.75rem;padding:0 var(--space-3);border-radius:var(--radius-full);font-size:var(--text-xs);font-weight:var(--weight-medium);color:var(--color-text);background:var(--color-surface-3);border:var(--bw) solid var(--color-border);}
473
476
  .fdy-chip--primary{color:var(--color-primary-strong);background:var(--color-primary-soft);border-color:color-mix(in srgb,var(--color-primary) 22%,transparent);}
477
+ /* Categorical tone chips — non-semantic category tags / legend swatches, from the general
478
+ * --tone-1..8 palette. Same validated AA formula as .fdy-avatar--tone-* (text >=4.5:1 both themes,
479
+ * gated by test/contrast.test.mjs). */
480
+ .fdy-chip--tone-1,.fdy-chip--tone-2,.fdy-chip--tone-3,.fdy-chip--tone-4,.fdy-chip--tone-5,.fdy-chip--tone-6,.fdy-chip--tone-7,.fdy-chip--tone-8{background:color-mix(in srgb,var(--_fdy-chip-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-chip-tone) 50%,var(--color-text));border-color:color-mix(in srgb,var(--_fdy-chip-tone) 30%,var(--color-border));}
481
+ .fdy-chip--tone-1{--_fdy-chip-tone:var(--tone-1);}
482
+ .fdy-chip--tone-2{--_fdy-chip-tone:var(--tone-2);}
483
+ .fdy-chip--tone-3{--_fdy-chip-tone:var(--tone-3);}
484
+ .fdy-chip--tone-4{--_fdy-chip-tone:var(--tone-4);}
485
+ .fdy-chip--tone-5{--_fdy-chip-tone:var(--tone-5);}
486
+ .fdy-chip--tone-6{--_fdy-chip-tone:var(--tone-6);}
487
+ .fdy-chip--tone-7{--_fdy-chip-tone:var(--tone-7);}
488
+ .fdy-chip--tone-8{--_fdy-chip-tone:var(--tone-8);}
474
489
  .fdy-chip__remove{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;margin-right:calc(var(--space-1)*-1);border:0;padding:0;border-radius:var(--radius-full);background:transparent;color:currentColor;opacity:.6;cursor:pointer;font-size:var(--text-sm);line-height:1;}
475
490
  .fdy-chip__remove:hover{opacity:1;background:color-mix(in srgb,currentColor 16%,transparent);}
476
491
  .fdy-chip__remove:focus-visible{outline:none;opacity:1;box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
@@ -520,6 +535,56 @@ a { color: var(--color-primary); }
520
535
  @media (prefers-reduced-motion:no-preference){.fdy-combo__listbox:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
521
536
  @keyframes fdy-combo-in{from{opacity:0;transform:translateY(-4px);}to{opacity:1;transform:none;}}
522
537
 
538
+ /* Freeday — Page composition & type roles.
539
+ *
540
+ * The kit stops at the component; these primitives encode *how a page is assembled* so screens
541
+ * built by different people cohere. They carry the usage doctrine (see USAGE.md) in markup: a
542
+ * named type role gets used, a doc gets skimmed. Everything here is token-driven layout — no new
543
+ * colours, no new shadows. Pair with .fdy-app (the shell) and the components inside.
544
+ */
545
+
546
+ /* ---- Type roles (three title levels + supporting text) -------------------------------------
547
+ * One role per level of hierarchy. Reach for these instead of re-using .fdy-card__title for a
548
+ * page title (the mistake that collapses three levels into one grey mush). */
549
+ .fdy-eyebrow{font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
550
+ .fdy-title-page{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:var(--leading-tight);color:var(--color-text);margin:0;text-wrap:balance;}
551
+ .fdy-title-section{font-family:var(--font-display);font-size:var(--text-xl);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);line-height:var(--leading-snug);color:var(--color-text);margin:0;text-wrap:balance;}
552
+ .fdy-title-card{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0;}
553
+ .fdy-text-muted{color:var(--color-text-muted);}
554
+ .fdy-text-subtle{color:var(--color-text-subtle);}
555
+ .fdy-text-caption{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;}
556
+
557
+ /* ---- Page: the vertical stack of sections, with the section rhythm between them ------------- */
558
+ .fdy-page{display:flex;flex-direction:column;gap:var(--space-8);}
559
+
560
+ /* ---- Page header: eyebrow + title + description on the left, primary action(s) on the right -- */
561
+ .fdy-page__header{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--space-4);flex-wrap:wrap;}
562
+ .fdy-page__heading{display:flex;flex-direction:column;gap:var(--space-2);min-width:0;}
563
+ .fdy-page__desc{font-size:var(--text-base);color:var(--color-text-muted);margin:0;max-width:62ch;line-height:var(--leading-normal);}
564
+ .fdy-page__actions{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}
565
+
566
+ /* ---- Page section: a heading (+ optional toolbar) and its body, with the intra-group rhythm --- */
567
+ .fdy-page-section{display:flex;flex-direction:column;gap:var(--space-4);}
568
+ .fdy-page-section__head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap;}
569
+
570
+ /* ---- Toolbar: a horizontal control row (search + actions), wraps cleanly ---------------------- */
571
+ .fdy-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;}
572
+ .fdy-toolbar__spacer{flex:1;}
573
+ .fdy-toolbar__search{max-width:18rem;flex:1 1 12rem;}
574
+
575
+ /* ---- Stat tile: a KPI metric that is deliberately NOT a card, so a KPI strip doesn't become an
576
+ * identical-white-card grid. Flat by default; wrap the .fdy-stats grid in one card if you want a
577
+ * single boxed strip, or use .fdy-stats--boxed for shared surface + dividers. */
578
+ .fdy-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:var(--space-5);}
579
+ .fdy-stat{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;}
580
+ .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
581
+ .fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
582
+ .fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
583
+ .fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
584
+ .fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
585
+ .fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
586
+ .fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
587
+
523
588
  /* Freeday — Date picker (input-styled trigger + calendar popover).
524
589
  * Built by freeday-datepicker.js from an empty [data-fdy-datepicker] wrapper. */
525
590
  .fdy-datepicker{position:relative;display:inline-block;width:100%;max-width:14rem;}
@@ -165,6 +165,14 @@
165
165
  --chart-8: #e34948;
166
166
  --chart-grid: var(--slate-200);
167
167
  --chart-tick: var(--slate-400);
168
+ --tone-1: var(--chart-1);
169
+ --tone-2: var(--chart-2);
170
+ --tone-3: var(--chart-3);
171
+ --tone-4: var(--chart-4);
172
+ --tone-5: var(--chart-5);
173
+ --tone-6: var(--chart-6);
174
+ --tone-7: var(--chart-7);
175
+ --tone-8: var(--chart-8);
168
176
  --focus-ring: var(--azure-600);
169
177
  --focus-ring-width: 2px;
170
178
  }
@@ -339,5 +347,9 @@
339
347
  --focus-ring: var(--azure-600);
340
348
  }
341
349
  :root[data-density="compact"] {
350
+ --space-3: 0.625rem;
351
+ --space-4: 0.75rem;
352
+ --space-5: 1rem;
353
+ --space-6: 1.25rem;
342
354
  --control-h: 2rem;
343
355
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,6 +28,7 @@
28
28
  "tokens/breakpoints.d.ts",
29
29
  "README.md",
30
30
  "README.id.md",
31
+ "USAGE.md",
31
32
  "CHANGELOG.md"
32
33
  ],
33
34
  "exports": {
@@ -36,6 +37,7 @@
36
37
  "./css/components": { "types": "./dist/asset.d.ts", "default": "./dist/freeday.css" },
37
38
  "./tokens": { "types": "./dist/asset.d.ts", "default": "./dist/freeday.tokens.css" },
38
39
  "./breakpoints": { "types": "./tokens/breakpoints.d.ts", "default": "./tokens/breakpoints.mjs" },
40
+ "./table-model": { "types": "./adapters/core/table-model.d.ts", "default": "./adapters/core/table-model.js" },
39
41
  "./enhancers/*": { "types": "./dist/asset.d.ts", "default": "./dist/freeday-*.js" },
40
42
  "./vue": {
41
43
  "types": "./adapters/vue/index.d.ts",
@@ -11,11 +11,11 @@
11
11
  * test/contrast.test.mjs). Use to disambiguate same-initial avatars (e.g. many "PT …" names):
12
12
  * hash a stable 1..8 index off the FULL name in your app, then apply .fdy-avatar--tone-<n>. */
13
13
  .fdy-avatar--tone-1,.fdy-avatar--tone-2,.fdy-avatar--tone-3,.fdy-avatar--tone-4,.fdy-avatar--tone-5,.fdy-avatar--tone-6,.fdy-avatar--tone-7,.fdy-avatar--tone-8{background:color-mix(in srgb,var(--_fdy-avatar-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-avatar-tone) 50%,var(--color-text));}
14
- .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--chart-1);}
15
- .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--chart-2);}
16
- .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--chart-3);}
17
- .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--chart-4);}
18
- .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--chart-5);}
19
- .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--chart-6);}
20
- .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--chart-7);}
21
- .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--chart-8);}
14
+ .fdy-avatar--tone-1{--_fdy-avatar-tone:var(--tone-1);}
15
+ .fdy-avatar--tone-2{--_fdy-avatar-tone:var(--tone-2);}
16
+ .fdy-avatar--tone-3{--_fdy-avatar-tone:var(--tone-3);}
17
+ .fdy-avatar--tone-4{--_fdy-avatar-tone:var(--tone-4);}
18
+ .fdy-avatar--tone-5{--_fdy-avatar-tone:var(--tone-5);}
19
+ .fdy-avatar--tone-6{--_fdy-avatar-tone:var(--tone-6);}
20
+ .fdy-avatar--tone-7{--_fdy-avatar-tone:var(--tone-7);}
21
+ .fdy-avatar--tone-8{--_fdy-avatar-tone:var(--tone-8);}
@@ -3,6 +3,9 @@
3
3
  .fdy-card--elevated{box-shadow:var(--shadow-lift-hover);}
4
4
  .fdy-card--interactive{cursor:pointer;}
5
5
  .fdy-card--interactive:hover{box-shadow:var(--shadow-lift-hover);transform:translateY(-3px);}
6
+ /* The global reduced-motion reset kills the ANIMATION; drop the -3px shift too so the card
7
+ * doesn't jump. The shadow still communicates the affordance. */
8
+ @media (prefers-reduced-motion:reduce){.fdy-card--interactive:hover{transform:none;}}
6
9
  /* When the whole card IS the control — a real <button> so the row is keyboard-reachable and
7
10
  * announced — reset the UA button box. Note: it does NOT reset background/border, because
8
11
  * .fdy-card already sets its own surface + border (resetting them here would strip the card). */
@@ -1,6 +1,18 @@
1
1
  /* Freeday — Chip (tag / removable filter) */
2
2
  .fdy-chip{display:inline-flex;align-items:center;gap:var(--space-2);height:1.75rem;padding:0 var(--space-3);border-radius:var(--radius-full);font-size:var(--text-xs);font-weight:var(--weight-medium);color:var(--color-text);background:var(--color-surface-3);border:var(--bw) solid var(--color-border);}
3
3
  .fdy-chip--primary{color:var(--color-primary-strong);background:var(--color-primary-soft);border-color:color-mix(in srgb,var(--color-primary) 22%,transparent);}
4
+ /* Categorical tone chips — non-semantic category tags / legend swatches, from the general
5
+ * --tone-1..8 palette. Same validated AA formula as .fdy-avatar--tone-* (text >=4.5:1 both themes,
6
+ * gated by test/contrast.test.mjs). */
7
+ .fdy-chip--tone-1,.fdy-chip--tone-2,.fdy-chip--tone-3,.fdy-chip--tone-4,.fdy-chip--tone-5,.fdy-chip--tone-6,.fdy-chip--tone-7,.fdy-chip--tone-8{background:color-mix(in srgb,var(--_fdy-chip-tone) 18%,var(--color-surface));color:color-mix(in srgb,var(--_fdy-chip-tone) 50%,var(--color-text));border-color:color-mix(in srgb,var(--_fdy-chip-tone) 30%,var(--color-border));}
8
+ .fdy-chip--tone-1{--_fdy-chip-tone:var(--tone-1);}
9
+ .fdy-chip--tone-2{--_fdy-chip-tone:var(--tone-2);}
10
+ .fdy-chip--tone-3{--_fdy-chip-tone:var(--tone-3);}
11
+ .fdy-chip--tone-4{--_fdy-chip-tone:var(--tone-4);}
12
+ .fdy-chip--tone-5{--_fdy-chip-tone:var(--tone-5);}
13
+ .fdy-chip--tone-6{--_fdy-chip-tone:var(--tone-6);}
14
+ .fdy-chip--tone-7{--_fdy-chip-tone:var(--tone-7);}
15
+ .fdy-chip--tone-8{--_fdy-chip-tone:var(--tone-8);}
4
16
  .fdy-chip__remove{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;margin-right:calc(var(--space-1)*-1);border:0;padding:0;border-radius:var(--radius-full);background:transparent;color:currentColor;opacity:.6;cursor:pointer;font-size:var(--text-sm);line-height:1;}
5
17
  .fdy-chip__remove:hover{opacity:1;background:color-mix(in srgb,currentColor 16%,transparent);}
6
18
  .fdy-chip__remove:focus-visible{outline:none;opacity:1;box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 45%,transparent);}
@@ -0,0 +1,49 @@
1
+ /* Freeday — Page composition & type roles.
2
+ *
3
+ * The kit stops at the component; these primitives encode *how a page is assembled* so screens
4
+ * built by different people cohere. They carry the usage doctrine (see USAGE.md) in markup: a
5
+ * named type role gets used, a doc gets skimmed. Everything here is token-driven layout — no new
6
+ * colours, no new shadows. Pair with .fdy-app (the shell) and the components inside.
7
+ */
8
+
9
+ /* ---- Type roles (three title levels + supporting text) -------------------------------------
10
+ * One role per level of hierarchy. Reach for these instead of re-using .fdy-card__title for a
11
+ * page title (the mistake that collapses three levels into one grey mush). */
12
+ .fdy-eyebrow{font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
13
+ .fdy-title-page{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:var(--leading-tight);color:var(--color-text);margin:0;text-wrap:balance;}
14
+ .fdy-title-section{font-family:var(--font-display);font-size:var(--text-xl);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);line-height:var(--leading-snug);color:var(--color-text);margin:0;text-wrap:balance;}
15
+ .fdy-title-card{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-tight);color:var(--color-text);margin:0;}
16
+ .fdy-text-muted{color:var(--color-text-muted);}
17
+ .fdy-text-subtle{color:var(--color-text-subtle);}
18
+ .fdy-text-caption{font-size:var(--text-sm);color:var(--color-text-muted);margin:0;}
19
+
20
+ /* ---- Page: the vertical stack of sections, with the section rhythm between them ------------- */
21
+ .fdy-page{display:flex;flex-direction:column;gap:var(--space-8);}
22
+
23
+ /* ---- Page header: eyebrow + title + description on the left, primary action(s) on the right -- */
24
+ .fdy-page__header{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--space-4);flex-wrap:wrap;}
25
+ .fdy-page__heading{display:flex;flex-direction:column;gap:var(--space-2);min-width:0;}
26
+ .fdy-page__desc{font-size:var(--text-base);color:var(--color-text-muted);margin:0;max-width:62ch;line-height:var(--leading-normal);}
27
+ .fdy-page__actions{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}
28
+
29
+ /* ---- Page section: a heading (+ optional toolbar) and its body, with the intra-group rhythm --- */
30
+ .fdy-page-section{display:flex;flex-direction:column;gap:var(--space-4);}
31
+ .fdy-page-section__head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap;}
32
+
33
+ /* ---- Toolbar: a horizontal control row (search + actions), wraps cleanly ---------------------- */
34
+ .fdy-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;}
35
+ .fdy-toolbar__spacer{flex:1;}
36
+ .fdy-toolbar__search{max-width:18rem;flex:1 1 12rem;}
37
+
38
+ /* ---- Stat tile: a KPI metric that is deliberately NOT a card, so a KPI strip doesn't become an
39
+ * identical-white-card grid. Flat by default; wrap the .fdy-stats grid in one card if you want a
40
+ * single boxed strip, or use .fdy-stats--boxed for shared surface + dividers. */
41
+ .fdy-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:var(--space-5);}
42
+ .fdy-stat{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;}
43
+ .fdy-stat__label{font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--color-text-subtle);}
44
+ .fdy-stat__value{font-family:var(--font-display);font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tighter);line-height:1;color:var(--color-text);font-variant-numeric:tabular-nums;}
45
+ .fdy-stat__value small{font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text-muted);}
46
+ .fdy-stat__meta{font-size:var(--text-sm);color:var(--color-text-muted);display:flex;align-items:center;gap:var(--space-2);}
47
+ .fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
48
+ .fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
49
+ .fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
@@ -42,8 +42,8 @@
42
42
  "weight": { "regular": { "$value": "400" }, "medium": { "$value": "500" }, "semibold": { "$value": "600" }, "bold": { "$value": "700" } },
43
43
 
44
44
  "space": {
45
- "0": { "$value": "0" }, "1": { "$value": "0.25rem" }, "2": { "$value": "0.5rem" }, "3": { "$value": "0.75rem" },
46
- "4": { "$value": "1rem" }, "5": { "$value": "1.25rem" }, "6": { "$value": "1.5rem" }, "8": { "$value": "2rem" },
45
+ "0": { "$value": "0" }, "1": { "$value": "0.25rem" }, "2": { "$value": "0.5rem" }, "3": { "$value": "0.75rem", "$compact": "0.625rem" },
46
+ "4": { "$value": "1rem", "$compact": "0.75rem" }, "5": { "$value": "1.25rem", "$compact": "1rem" }, "6": { "$value": "1.5rem", "$compact": "1.25rem" }, "8": { "$value": "2rem" },
47
47
  "10": { "$value": "2.5rem" }, "12": { "$value": "3rem" }, "16": { "$value": "4rem" }, "20": { "$value": "5rem" }, "24": { "$value": "6rem" }
48
48
  },
49
49
  "radius": { "xs": { "$value": "3px" }, "sm": { "$value": "4px" }, "md": { "$value": "6px" }, "lg": { "$value": "10px" }, "xl": { "$value": "14px" }, "full": { "$value": "999px" } },
@@ -118,6 +118,18 @@
118
118
  "tick": { "$value": "{slate.400}", "$dark": "{slate.600}" }
119
119
  },
120
120
 
121
+ "_comment_tone": "General categorical palette — the same 8 validated hues as chart, aliased under a chart-neutral name for avatars, chips, tags, legend swatches (theme-follows via chart's own $dark).",
122
+ "tone": {
123
+ "1": { "$value": "{chart.1}" },
124
+ "2": { "$value": "{chart.2}" },
125
+ "3": { "$value": "{chart.3}" },
126
+ "4": { "$value": "{chart.4}" },
127
+ "5": { "$value": "{chart.5}" },
128
+ "6": { "$value": "{chart.6}" },
129
+ "7": { "$value": "{chart.7}" },
130
+ "8": { "$value": "{chart.8}" }
131
+ },
132
+
121
133
  "focus-ring": { "$value": "{azure.600}", "$dark": "{azure.400}" },
122
134
  "focus-ring-width": { "$value": "2px" }
123
135
  }