@cahyo-dimas/freeday 1.18.0 → 1.20.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/COMPONENTS.md +748 -0
  3. package/README.id.md +9 -1
  4. package/README.md +16 -2
  5. package/USAGE.md +34 -11
  6. package/adapters/blazor/FdyTable.razor.cs +66 -7
  7. package/adapters/blazor/TableTypes.cs +6 -0
  8. package/adapters/react/components/FdyTable.tsx +32 -8
  9. package/adapters/vue/components/FdyTable.vue +30 -6
  10. package/dist/freeday.bundle.css +35 -1
  11. package/dist/freeday.css +30 -0
  12. package/dist/freeday.tokens.css +5 -1
  13. package/docs/agent-onboarding.md +151 -0
  14. package/docs/getting-started.md +454 -0
  15. package/docs/integrations.md +321 -0
  16. package/docs/reference-screen.html +461 -0
  17. package/package.json +9 -3
  18. package/src/components/list.css +29 -0
  19. package/tokens/breakpoints.d.ts +3 -0
  20. package/tokens/breakpoints.mjs +8 -1
  21. package/src/components/.gitkeep +0 -0
  22. package/src/freeday-autocomplete.js +0 -135
  23. package/src/freeday-breakpoint.js +0 -51
  24. package/src/freeday-carousel.js +0 -111
  25. package/src/freeday-cascade.js +0 -256
  26. package/src/freeday-cfl.js +0 -213
  27. package/src/freeday-chart.js +0 -429
  28. package/src/freeday-chip.js +0 -83
  29. package/src/freeday-datepicker.js +0 -321
  30. package/src/freeday-datetime.js +0 -83
  31. package/src/freeday-drawer.js +0 -43
  32. package/src/freeday-form.js +0 -181
  33. package/src/freeday-mask.js +0 -114
  34. package/src/freeday-menu.js +0 -93
  35. package/src/freeday-popover.js +0 -69
  36. package/src/freeday-rating.js +0 -50
  37. package/src/freeday-select.js +0 -218
  38. package/src/freeday-slider.js +0 -34
  39. package/src/freeday-stepper.js +0 -90
  40. package/src/freeday-table.js +0 -475
  41. package/src/freeday-tabs.js +0 -68
  42. package/src/freeday-timepicker.js +0 -180
  43. package/src/freeday-toast.js +0 -105
  44. package/src/freeday-tree.js +0 -94
  45. package/src/freeday-upload.js +0 -206
package/COMPONENTS.md ADDED
@@ -0,0 +1,748 @@
1
+ # Freeday — Component reference
2
+
3
+ The complete public class surface, in one flat file. Written to be read start-to-finish by a
4
+ person **or an AI coding agent** that has to produce correct Freeday markup without opening the
5
+ live docs.
6
+
7
+ - **Which class exists** and what its modifiers are → here.
8
+ - **Which class to reach for, and when** → [`USAGE.md`](USAGE.md) (the doctrine: type roles,
9
+ spacing rhythm, elevation, one primary per screen).
10
+ - **How to install / import per stack** → [`docs/getting-started.md`](docs/getting-started.md).
11
+ - **A whole screen assembled** → [`docs/reference-screen.html`](docs/reference-screen.html).
12
+
13
+ ## Hard rules (these are not style preferences)
14
+
15
+ 1. **Prefix is `fdy-`, pattern is compact BEM**: block `fdy-card`, element `fdy-card__title`,
16
+ modifier `fdy-card--elevated`. A modifier is **always** written alongside its block class:
17
+ `class="fdy-btn fdy-btn--ghost"`, never `fdy-btn--ghost` alone.
18
+ 2. **Never invent a class.** If it is not in this file, it does not exist. Freeday has no
19
+ `fdy-grid`, no `fdy-flex`, no `fdy-mt-4`, no `fdy-primary`.
20
+ 3. **Never write a raw hex or px value** in app CSS. Use the tokens — `var(--color-primary)`,
21
+ `var(--space-4)`, `var(--radius-md)`, `var(--shadow-1)`, `var(--dur-2)`. Spacing is a 4px scale
22
+ (`--space-0`…`--space-24`); a loose value breaks density switching.
23
+ 4. **Freeday owns components + tokens, not layout.** The only layout it ships is the shell
24
+ (`.fdy-app`), the composition primitives below, and `.fdy-hidden`/`.fdy-visually-hidden`.
25
+ Grids, stacks and one-off gaps come from your own layout layer (a utility framework run
26
+ **utilities-only, preflight off**, with its theme defined in terms of `var(--space-N)`).
27
+ 5. **`aria-invalid="true"` is the canonical error state** on form controls — it drives the visual
28
+ state *and* the screen-reader state. Pair it with `aria-describedby` → the `id` of a
29
+ `.fdy-help.fdy-help--error`. The `--error` class modifiers exist only for the case where the
30
+ invalid state does not belong to the control itself.
31
+ 6. **Interactive components need their enhancer script.** Static ones (button, card, badge, table,
32
+ alert, breadcrumb, timeline, accordion, tree-without-cascade…) are CSS-only.
33
+
34
+ ## Enhancers — hook, script, global, events
35
+
36
+ Every enhancer is zero-dependency, auto-initialises once on `DOMContentLoaded`, and is idempotent:
37
+ re-hydrate SPA-rendered DOM with `window.Freeday<X>.initAll(root)`. Events are bubbling
38
+ `CustomEvent`s; the payload is in `event.detail`.
39
+
40
+ | Markup hook | Script | Global | Emits |
41
+ |---|---|---|---|
42
+ | `data-fdy-combo` | `freeday-select.js` | `FreedayCombo` | `fdy-change` |
43
+ | `data-fdy-autocomplete` | `freeday-autocomplete.js` | `FreedayAutocomplete` | `fdy-autocomplete-select` |
44
+ | `data-fdy-cascade` | `freeday-cascade.js` | `FreedayCascade` | `fdy-cascade-change` |
45
+ | `data-fdy-cfl` | `freeday-cfl.js` | `FreedayCfl` | `fdy-cfl-select` |
46
+ | `data-fdy-datepicker`, `data-fdy-daterange` | `freeday-datepicker.js` | `FreedayDatepicker` | `fdy-datepicker-change` |
47
+ | `data-fdy-timepicker` | `freeday-timepicker.js` | `FreedayTimepicker` | `fdy-time-select` |
48
+ | `data-fdy-datetimepicker` | `freeday-datetime.js` | `FreedayDatetime` | `fdy-datetime-change` |
49
+ | `data-fdy-table` | `freeday-table.js` | `FreedayTable` | `fdy-table-change` |
50
+ | `data-fdy-dropzone` | `freeday-upload.js` | `FreedayUpload` | `fdy-upload-add`, `fdy-upload-remove` |
51
+ | `data-fdy-validate` | `freeday-form.js` | `FreedayForm` | `fdy-form-valid`, `fdy-form-invalid` |
52
+ | `data-fdy-mask`, `data-fdy-password` | `freeday-mask.js` | `FreedayMask` | `fdy-mask` |
53
+ | `data-fdy-chips` | `freeday-chip.js` | `FreedayChip` | `fdy-chip-change`, `fdy-chip-remove` |
54
+ | `data-fdy-stepper` | `freeday-stepper.js` | `FreedayStepper` | `fdy-step-change`, `fdy-step-finish` |
55
+ | `data-fdy-carousel` | `freeday-carousel.js` | `FreedayCarousel` | `fdy-carousel-change` |
56
+ | `data-fdy-menu` | `freeday-menu.js` | `FreedayMenu` | — |
57
+ | `data-fdy-tabs` | `freeday-tabs.js` | `FreedayTabs` | — |
58
+ | `data-fdy-tree` | `freeday-tree.js` | `FreedayTree` | — |
59
+ | `data-fdy-rating` | `freeday-rating.js` | `FreedayRating` | — (native `change`) |
60
+ | `data-fdy-slider` | `freeday-slider.js` | `FreedaySlider` | — (native `input`) |
61
+ | `data-fdy-drawer="id"` | `freeday-drawer.js` | `FreedayDrawer` | — |
62
+ | `data-fdy-chart="type"` | `freeday-chart.js` | `FreedayChart` | — (`.update(el)` to repaint) |
63
+ | — (imperative) | `freeday-toast.js` | `Freeday.toast()` / `Freeday.dismiss()` | — |
64
+ | — (stamps `<html data-breakpoint>`) | `freeday-breakpoint.js` | `FreedayBreakpoint` | `fdy-breakpoint-change` |
65
+ | — (internal positioning) | `freeday-popover.js` | `FreedayPopover` | — |
66
+
67
+ **Shared conventions across the select-type widgets** (combo, cascade, datepicker, daterange,
68
+ timepicker, autocomplete, cfl): `data-value` seeds the value, `data-placeholder` the empty text,
69
+ `data-label` the accessible name, `data-fdy-disabled` / `data-fdy-readonly` / `data-fdy-invalid`
70
+ the states, `data-fdy-no-icon` drops the trailing icon, and a `<template data-fdy-icon>` child
71
+ replaces it. `readonly` is **not** `disabled`: full contrast, still focusable and copyable, opening
72
+ is a no-op.
73
+
74
+ **State classes** the enhancers set (read them, don't hand-author them): `is-active`,
75
+ `is-complete`, `is-open`, `is-selected`, `is-highlighted`, `is-filled`, `is-today`, `is-dragover`,
76
+ `is-leaving`, `is-error`, `is-disabled`, `is-hidden`, `is-visible`, `is-outside`.
77
+
78
+ ---
79
+
80
+ # Shell & composition
81
+
82
+ ## App shell — `.fdy-app`
83
+ The frame every application lives in. Do not hand-roll one from flexbox: the responsive sidebar,
84
+ off-canvas drawer and backdrop are built in.
85
+
86
+ - Parts: `__sidebar` `__brand` (`__brand-mark` `__brand-text` `__brand-title` `__brand-subtitle`)
87
+ `__content` `__topbar` `__navtoggle` `__title` `__main` `__backdrop`
88
+ - Modifiers: `--nav-open` (mobile drawer open, ≤720px) · `--nav-collapsed` (collapse to zero width,
89
+ ≥721px) · `--static` (embed the shell in a page instead of filling the viewport)
90
+ - Also: `.fdy-skip` — the skip-to-content link, first child of the shell.
91
+ - JS: none. Toggle `--nav-collapsed` (≥721px) or `--nav-open` (≤720px) on the root from
92
+ `__navtoggle`'s click; clear `--nav-open` when `__backdrop` is clicked.
93
+
94
+ **The nesting is fixed, not free-form:** `.fdy-app` is a flex **row** of `[__sidebar | __content]`,
95
+ and `__content` is the column holding `__topbar` + `__main` — it exists to give the sticky topbar a
96
+ tall containing block. The brand goes in the **sidebar** (sized to match the topbar height), and
97
+ `__main` already carries the page padding, so don't wrap your screen in another padded box.
98
+
99
+ ```html
100
+ <div class="fdy-app">
101
+ <a class="fdy-skip" href="#main">Skip to content</a>
102
+
103
+ <aside class="fdy-app__sidebar">
104
+ <a class="fdy-app__brand" href="/">
105
+ <span class="fdy-app__brand-mark"><!--logo--></span>
106
+ <span class="fdy-app__brand-text"><span class="fdy-app__brand-title">Acme</span></span>
107
+ </a>
108
+ <nav class="fdy-nav"><!--nav items--></nav>
109
+ </aside>
110
+
111
+ <div class="fdy-app__content">
112
+ <header class="fdy-app__topbar">
113
+ <button class="fdy-app__navtoggle" type="button" aria-label="Toggle navigation"><!--svg--></button>
114
+ <h1 class="fdy-app__title">Invoices</h1><!-- auto-spacer: pushes what follows right -->
115
+ <!-- topbar actions -->
116
+ </header>
117
+ <main class="fdy-app__main" id="main"><!-- .fdy-page goes here --></main>
118
+ </div>
119
+
120
+ <div class="fdy-app__backdrop"></div>
121
+ </div>
122
+ ```
123
+
124
+ ## Page composition — `.fdy-page`
125
+ Vertical section rhythm (`--space-8` between children). Assemble every screen from these; they
126
+ carry the doctrine in markup.
127
+
128
+ - `.fdy-page` — the screen body
129
+ - `.fdy-page__header` — `.fdy-page__heading` (eyebrow + page title + `.fdy-page__desc`) on the
130
+ left, `.fdy-page__actions` (**one** primary button) on the right
131
+ - `.fdy-page-section` — a region: `.fdy-page-section__head` + body, `--space-4` rhythm
132
+ - `.fdy-toolbar` — a horizontal control row; `__spacer` (flex filler), `__search` (capped width).
133
+ It is `align-items:center`, so it is for **bare** controls. A `.fdy-field` with a *visible* label
134
+ sits half a label-height low in it — labelled fields belong in `.fdy-filterbar`
135
+ (`align-items:flex-end`). In a toolbar, hide the label with `.fdy-visually-hidden`.
136
+ - `.fdy-stats` — a KPI grid of `.fdy-stat` (`__label` `__value` `__meta`). Deliberately **not**
137
+ cards. `.fdy-stats--boxed` for one shared surface with dividers.
138
+
139
+ ```html
140
+ <div class="fdy-page">
141
+ <header class="fdy-page__header">
142
+ <div class="fdy-page__heading">
143
+ <p class="fdy-eyebrow">Sales</p>
144
+ <h1 class="fdy-title-page">Invoices</h1>
145
+ <p class="fdy-page__desc">Every invoice issued this period.</p>
146
+ </div>
147
+ <div class="fdy-page__actions"><button class="fdy-btn">New invoice</button></div>
148
+ </header>
149
+
150
+ <section class="fdy-page-section">
151
+ <div class="fdy-stats">
152
+ <div class="fdy-stat">
153
+ <span class="fdy-stat__label">Outstanding</span>
154
+ <span class="fdy-stat__value">1,240<small> M</small></span>
155
+ <span class="fdy-stat__meta">12 invoices</span>
156
+ </div>
157
+ </div>
158
+ </section>
159
+
160
+ <section class="fdy-page-section">
161
+ <div class="fdy-page-section__head">
162
+ <h2 class="fdy-title-section">All invoices</h2>
163
+ <div class="fdy-toolbar"><!-- filters, actions --></div>
164
+ </div>
165
+ <!-- component: table, cards, chart… -->
166
+ </section>
167
+ </div>
168
+ ```
169
+
170
+ ## Type roles & text
171
+ One role per level of hierarchy — never re-use a card title for a page title.
172
+
173
+ | Class | Element | Use |
174
+ |---|---|---|
175
+ | `.fdy-eyebrow` | `<p>` | Small uppercase label above a title. Optional. |
176
+ | `.fdy-title-page` | `<h1>` | One per screen. |
177
+ | `.fdy-title-section` | `<h2>` | A region inside the page. |
178
+ | `.fdy-title-card` | `<h3>` | A title inside a card or row (`.fdy-card__title` is equivalent). |
179
+ | `.fdy-text-muted` · `.fdy-text-subtle` | any | Secondary / tertiary text colour. |
180
+ | `.fdy-text-caption` | `<p>` | Small muted text: timestamps, help. |
181
+ | `.fdy-mono` | any | Tabular/monospace data (codes, amounts, ids). |
182
+
183
+ ## Utilities
184
+ `.fdy-hidden` · `.fdy-visually-hidden` (screen-reader-only) · `.fdy-list-reset` (strip native
185
+ bullets/indent from a semantic list — needed when your utility framework's preflight is off) ·
186
+ `.fdy-divider` (+`--vertical`) · `.fdy-kbd`.
187
+
188
+ Responsive display: `.fdy-hide-below-sm|md|lg` · `.fdy-hide-above-sm|md|lg`.
189
+ Breakpoints (min-width): `sm` 600 · `md` 960 · `lg` 1280 · `xl` 1920 — importable as
190
+ `import { breakpoints } from '@cahyo-dimas/freeday/breakpoints'`.
191
+
192
+ **`breakpoints.nav` (721) is separate and is the one the shell uses.** `.fdy-app` switches the
193
+ sidebar from off-canvas drawer to static column at 721px, *not* at `md`. Any `matchMedia` guard or
194
+ utility variant that has to agree with the shell must use `nav`; using `md` leaves 721–959px broken
195
+ (sidebar already static while your script still treats it as an overlay).
196
+
197
+ Density: `data-density="compact"` works on `<html>` **or any wrapper** — the selector is a bare
198
+ `[data-density="compact"]` over inheriting custom properties, so one screen can be dense while the
199
+ rest of the app is not.
200
+
201
+ ---
202
+
203
+ # Actions
204
+
205
+ ## Button — `.fdy-btn`
206
+ **The base class is already the primary action** — there is no `--primary`. Use it once per screen.
207
+
208
+ - Modifiers: `--ghost` `--danger` `--text` `--icon` (square, icon-only) `--sm` `--lg`
209
+ - Parts: `.fdy-btn__icon` (wraps the `<svg>`) · `.fdy-btn-group` (segmented row, `role="group"` +
210
+ `aria-label`)
211
+ - A11y: `--icon` **requires** `aria-label`. Use `<button type="button">` unless it submits.
212
+
213
+ ```html
214
+ <button class="fdy-btn"><span class="fdy-btn__icon"><!--svg--></span>Save</button>
215
+ <button class="fdy-btn fdy-btn--ghost">Cancel</button>
216
+ <button class="fdy-btn fdy-btn--ghost fdy-btn--icon" type="button" aria-label="Settings">
217
+ <span class="fdy-btn__icon"><!--svg--></span>
218
+ </button>
219
+ ```
220
+
221
+ ## FAB — `.fdy-fab`
222
+ Floating circular action. Modifiers: `--sm` `--accent` `--danger` `--extended` (pill with a label).
223
+ Always `aria-label` unless `--extended` carries visible text.
224
+
225
+ ## Menu & split button — `.fdy-menu`
226
+ WAI-ARIA APG menu-button. Keyboard ↑/↓/Home/End/Esc handled by the enhancer.
227
+
228
+ - Wrapper: `.fdy-menu-wrap` (or `.fdy-btn-split` for a split button, with
229
+ `.fdy-btn-split__toggle` as the second button) + `data-fdy-menu`
230
+ - List: `.fdy-menu` (`--end` aligns right), items `.fdy-menu__item` (`--danger`), separator
231
+ `<hr class="fdy-menu__sep">`
232
+ - A11y: trigger gets `aria-haspopup="menu"` + `aria-expanded="false"`; `<ul role="menu" hidden>`;
233
+ each `<li role="none">` wraps a `<button role="menuitem">`.
234
+
235
+ ```html
236
+ <div class="fdy-menu-wrap" data-fdy-menu>
237
+ <button class="fdy-btn fdy-btn--ghost" aria-haspopup="menu" aria-expanded="false">Actions</button>
238
+ <ul class="fdy-menu" role="menu" hidden>
239
+ <li role="none"><button class="fdy-menu__item" role="menuitem">Edit</button></li>
240
+ <li role="none"><hr class="fdy-menu__sep"></li>
241
+ <li role="none"><button class="fdy-menu__item fdy-menu__item--danger" role="menuitem">Delete</button></li>
242
+ </ul>
243
+ </div>
244
+ ```
245
+
246
+ ---
247
+
248
+ # Forms
249
+
250
+ ## Field, label, input — `.fdy-field`
251
+ The field wrapper owns the vertical rhythm; `<label class="fdy-field">` when it wraps a single
252
+ native control, otherwise a `<div>` + explicitly associated label.
253
+
254
+ - `.fdy-field` (+ `--full` inside `.fdy-form-grid`; widths `--w-sm` `--w-lg` `--w-xl` `--w-2xl`
255
+ `--w-grow` inside `.fdy-filterbar`)
256
+ - `.fdy-label` · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`)
257
+ - `[readonly]` is styled on input/textarea: full contrast, focusable, copyable.
258
+
259
+ ```html
260
+ <label class="fdy-field">
261
+ <span class="fdy-label">Customer</span>
262
+ <input class="fdy-input" aria-describedby="cust-help">
263
+ <span class="fdy-help" id="cust-help">As printed on the document.</span>
264
+ </label>
265
+
266
+ <label class="fdy-field">
267
+ <span class="fdy-label">Email</span>
268
+ <input class="fdy-input" aria-invalid="true" aria-describedby="email-err">
269
+ <span class="fdy-help fdy-help--error" id="email-err">Not a valid email address.</span>
270
+ </label>
271
+ ```
272
+
273
+ ## Input group — `.fdy-input-group`
274
+ Prefix/suffix addons around an input: text (`Rp`, `%`), a decorative icon
275
+ (`__addon--icon`), or an action button (`__btn`).
276
+
277
+ - `.fdy-input-group` · `__addon` (+`--icon`) · `__btn`
278
+
279
+ ```html
280
+ <div class="fdy-input-group">
281
+ <span class="fdy-input-group__addon">Rp</span>
282
+ <input class="fdy-input" inputmode="numeric" aria-label="Amount">
283
+ </div>
284
+ ```
285
+
286
+ ## Checkbox · radio · switch
287
+ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrapping `<label>`;
288
+ `.fdy-checkbox` on a bare `<input type="checkbox">` used inside tables/trees.
289
+
290
+ ```html
291
+ <label class="fdy-check"><input type="checkbox" checked> Email me a copy</label>
292
+ <label class="fdy-switch"><input type="checkbox"> Enable notifications</label>
293
+ ```
294
+
295
+ ## Select / combobox — `.fdy-combo`
296
+ Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
297
+
298
+ - `.fdy-combo` (+`--error`, `--no-icon`) · `__button` `__value` (+`--placeholder`) `__listbox`
299
+ `__option` `__check` `__icon` (custom trailing icon, with `--no-icon`)
300
+
301
+ ```html
302
+ <div class="fdy-field">
303
+ <span class="fdy-label" id="st-l">Status</span>
304
+ <div class="fdy-combo" data-fdy-combo data-value="pending">
305
+ <button type="button" class="fdy-combo__button" role="combobox" aria-haspopup="listbox"
306
+ aria-expanded="false" aria-labelledby="st-l st-v">
307
+ <span class="fdy-combo__value" id="st-v">Pending</span>
308
+ </button>
309
+ <ul class="fdy-combo__listbox" role="listbox" aria-labelledby="st-l" hidden>
310
+ <li class="fdy-combo__option" role="option" data-value="paid" aria-selected="false"><span class="fdy-combo__check"></span>Paid</li>
311
+ <li class="fdy-combo__option" role="option" data-value="pending" aria-selected="true"><span class="fdy-combo__check">✓</span>Pending</li>
312
+ </ul>
313
+ </div>
314
+ </div>
315
+ ```
316
+
317
+ ## Autocomplete — `.fdy-autocomplete`
318
+ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
319
+
320
+ - `.fdy-autocomplete` · `__listbox` `__option` `__empty`
321
+ - The `<input>` carries `role="combobox"`, `aria-expanded`, `aria-autocomplete="list"`,
322
+ `aria-controls` → the listbox `id`, `autocomplete="off"`.
323
+
324
+ ## Cascade select — `.fdy-cascade`
325
+ Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
326
+ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
327
+
328
+ - `.fdy-cascade` (+`--error`) · `__trigger` `__value` (+`--placeholder`) `__panel` `__head`
329
+ `__back` `__crumb` `__list` `__opt` `__opt-label` `__opt-arrow`
330
+ - Author only the wrapper + nested list; the enhancer builds the trigger and panel.
331
+
332
+ ```html
333
+ <div data-fdy-cascade data-label="Category" data-placeholder="Pick a category">
334
+ <ul>
335
+ <li data-value="electronics">Electronics
336
+ <ul><li data-value="phones">Phones</li><li data-value="laptops">Laptops</li></ul>
337
+ </li>
338
+ <li data-value="other">Other</li>
339
+ </ul>
340
+ </div>
341
+ ```
342
+
343
+ ## Choose-from-list (CFL) — `data-fdy-cfl`
344
+ A read-only field backed by master data: the button opens a searchable dialog, the picked row fills
345
+ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
346
+
347
+ - Field: `.fdy-input-group` + `data-fdy-cfl="<dialog id>"`, `data-fdy-cfl-display="code"`,
348
+ `data-fdy-cfl-summary="{n} selected"` (multi)
349
+ - Dialog: `<dialog class="fdy-modal fdy-modal--cfl">` + `data-fdy-cfl-multiple` for multi-select
350
+ - Inside: `.fdy-cfl__search` `__results` `__row` `__check` `__empty` `__count` `__actions`
351
+ - Hooks: `data-fdy-cfl-search` (the search input), `data-fdy-cfl-empty`, `data-fdy-cfl-count`,
352
+ `data-fdy-cfl-confirm` (multi only), `data-fdy-cfl-trigger`, `data-fdy-cfl-open`
353
+ - Rows are server state — in a real app drive them from a controlled fetch, not a global store.
354
+
355
+ ```html
356
+ <div class="fdy-field">
357
+ <span class="fdy-label" id="c-l">Customer code</span>
358
+ <div class="fdy-input-group" data-fdy-cfl="dlg-cust" data-fdy-cfl-display="code">
359
+ <input class="fdy-input" aria-labelledby="c-l" placeholder="Pick a customer…" readonly>
360
+ <button class="fdy-input-group__btn" type="button" aria-haspopup="dialog" aria-label="Pick a customer"><!--svg--></button>
361
+ </div>
362
+ </div>
363
+
364
+ <dialog class="fdy-modal fdy-modal--cfl" id="dlg-cust" aria-labelledby="dlg-cust-t">
365
+ <div class="fdy-modal__header">
366
+ <h3 class="fdy-modal__title" id="dlg-cust-t">Pick a customer</h3>
367
+ <button class="fdy-modal__close" aria-label="Close" data-close>&times;</button>
368
+ </div>
369
+ <div class="fdy-modal__body">
370
+ <div class="fdy-cfl__search">
371
+ <div class="fdy-input-group">
372
+ <span class="fdy-input-group__addon fdy-input-group__addon--icon"><!--svg--></span>
373
+ <input class="fdy-input" type="search" aria-label="Search customers" data-fdy-cfl-search>
374
+ </div>
375
+ </div>
376
+ <div class="fdy-cfl__results">
377
+ <table class="fdy-table">
378
+ <thead><tr><th scope="col">Code</th><th scope="col">Name</th></tr></thead>
379
+ <tbody>
380
+ <tr class="fdy-cfl__row" data-code="C-1001" data-name="Acme Ltd"><td>C-1001</td><td>Acme Ltd</td></tr>
381
+ </tbody>
382
+ </table>
383
+ <p class="fdy-cfl__empty" data-fdy-cfl-empty hidden>No match.</p>
384
+ </div>
385
+ </div>
386
+ <div class="fdy-modal__footer">
387
+ <span class="fdy-cfl__count">Click a row to pick</span>
388
+ <div class="fdy-cfl__actions"><button class="fdy-btn fdy-btn--ghost" type="button" data-close>Close</button></div>
389
+ </div>
390
+ </dialog>
391
+ ```
392
+
393
+ ## Date picker — `data-fdy-datepicker`
394
+ Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author an empty `<div>`**
395
+ — the enhancer builds everything.
396
+
397
+ - `.fdy-datepicker` (+`--error`) · `__trigger` `__value` (+`--placeholder`) `__icon` `__clear`
398
+ `__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` `__dow` `__day`
399
+ - Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
400
+ get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start.
401
+ - Attributes: `data-value="YYYY-MM-DD"`, `data-placeholder`, `data-label`, `data-fdy-no-icon`
402
+
403
+ ```html
404
+ <div class="fdy-field">
405
+ <span class="fdy-label">Invoice date</span>
406
+ <div data-fdy-datepicker data-value="2026-07-21" data-label="Invoice date"></div>
407
+ </div>
408
+
409
+ <div class="fdy-daterange" data-fdy-daterange role="group" aria-label="Reporting period">
410
+ <div data-fdy-datepicker data-role="from" data-placeholder="From" data-label="Start date"></div>
411
+ <span class="fdy-daterange__sep" aria-hidden="true">–</span>
412
+ <div data-fdy-datepicker data-role="to" data-placeholder="To" data-label="End date"></div>
413
+ </div>
414
+ ```
415
+
416
+ ## Time picker — `data-fdy-timepicker`
417
+ 24-hour `HH:MM` listbox. `data-step` (minutes, default 30), `data-min`, `data-max`. Needs
418
+ `freeday-timepicker.js`. Classes: `.fdy-timepicker` (+`--error`) · `__trigger` `__value`
419
+ (+`--placeholder`) `__icon` `__panel` `__opt`.
420
+
421
+ ## Datetime picker — `data-fdy-datetimepicker`
422
+ A **composition** of the two above, not a monolith: nest one datepicker and one timepicker inside
423
+ the wrapper. `freeday-datetime.js` joins them and emits one `fdy-datetime-change` with
424
+ `YYYY-MM-DDTHH:MM`. Class `.fdy-datetimepicker`.
425
+
426
+ ## File upload — `.fdy-dropzone`
427
+ Click/Enter opens the file dialog; drop works too. Needs `freeday-upload.js`.
428
+
429
+ - `.fdy-dropzone` (+`--row`) · `__icon` `__text` `__title` `__hint`; contains a hidden
430
+ `<input type="file">`
431
+ - List: `.fdy-filelist` (+`--grid`) of `.fdy-file` (+`--success`, `--error`) · `__icon` `__meta`
432
+ `__name` `__sub` `__progress` `__remove`
433
+ - Attributes: `data-max-size` (bytes), `data-filelist="#id"`
434
+ - A11y: the dropzone is `role="button" tabindex="0"` + `aria-label`.
435
+ - Wire `fdy-upload-add` to your real upload; render explicit progress/success/error state.
436
+
437
+ ## Form validation — `data-fdy-validate`
438
+ Native Constraint Validation wired to accessible inline errors: `aria-invalid` +
439
+ `aria-describedby`, focus to the first invalid field on submit, live re-validation on
440
+ blur/input. Needs `freeday-form.js`.
441
+
442
+ - Per-control messages: `data-fdy-msg-required`, `-type`, `-minlength`, `-min`, `-mismatch`, …
443
+ - Cross-field: `data-fdy-match="#otherFieldId"`
444
+ - Emits `fdy-form-valid` / `fdy-form-invalid`.
445
+
446
+ ## Password & mask
447
+ `data-fdy-password` on `<input type="password">` adds a reveal toggle (reusing the input-group
448
+ button chrome). `data-fdy-mask="####-####"` formats while typing: `#` digit, `A` letter, `*`
449
+ alphanumeric, anything else is a literal. Needs `freeday-mask.js`.
450
+
451
+ ## Form grid — `.fdy-form-grid`
452
+ Responsive two-column document header. Children are `.fdy-field`; `.fdy-field--full` spans both.
453
+
454
+ ## Filter bar — `.fdy-filterbar`
455
+ A consistent filter row of `.fdy-field`s with a width rhythm (`--w-sm` · default · `--w-lg` ·
456
+ `--w-xl` · `--w-2xl`), one `--w-grow` field (usually search) absorbing the slack, and
457
+ `.fdy-filterbar__actions` pinned at the end. `--actions-inline` keeps the actions on the control
458
+ line. Zero JS. Composes with `.fdy-table-toolbar` on a shared baseline.
459
+
460
+ ## Slider — `.fdy-slider`
461
+ Native `<input type="range">` themed via `accent-color`; `freeday-slider.js` only mirrors the value
462
+ into an `<output>`. Modifiers `--accent` `--danger`. Wrapper `.fdy-slider-field` + `__head`,
463
+ value `.fdy-slider__value` (`data-fdy-prefix` / `data-fdy-suffix`), scale `.fdy-slider__scale`.
464
+
465
+ ## Rating — `.fdy-rating`
466
+ A native radio group (arrow keys, form-associated). Filled vs empty differ by **fill, not colour
467
+ alone**. Modifiers `--sm` `--readonly` `--disabled` `--error`; star `.fdy-rating__star`. Read-only
468
+ variant is `role="img"` + `aria-label`; each interactive star needs a `.fdy-visually-hidden` label.
469
+
470
+ ## Stepper / wizard — `.fdy-stepper`
471
+ Linear multi-step flow: marker → check, one panel at a time, back/next. Needs
472
+ `freeday-stepper.js`.
473
+
474
+ - `.fdy-stepper` (`<ol>`) of `.fdy-step` (`is-active` / `is-complete` set by JS) ·
475
+ `.fdy-step__btn` `__marker` `__label`
476
+ - `.fdy-step-panels` wrapping one `.fdy-step-panel` per step (`hidden` on the inactive ones)
477
+ - `.fdy-step-nav` with `data-fdy-step-prev` / `data-fdy-step-next` buttons
478
+
479
+ ---
480
+
481
+ # Data
482
+
483
+ ## Table — `.fdy-table`
484
+ Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-table-scroll`
485
+ (horizontal scroll). Always a `<caption>` (use `.fdy-visually-hidden` if it shouldn't show) and
486
+ `<th scope="col">`. Numeric cells get `.fdy-table__num` (right-aligned, tabular).
487
+
488
+ ```html
489
+ <div class="fdy-table-wrap">
490
+ <table class="fdy-table">
491
+ <caption>Recent invoices</caption>
492
+ <thead><tr><th scope="col">Invoice</th><th scope="col" class="fdy-table__num">Amount</th></tr></thead>
493
+ <tbody>
494
+ <tr><td>INV-1042</td><td class="fdy-table__num">1,240,000</td></tr>
495
+ </tbody>
496
+ </table>
497
+ </div>
498
+ ```
499
+
500
+ ## Data table — `.fdy-datatable`
501
+ The interactive table: global search, sort, per-column filters, row selection + bulk bar,
502
+ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` + `data-fdy-table`
503
+ (`data-page-size="N"`).
504
+
505
+ - Toolbar: `.fdy-table-toolbar` · `__search` `__spacer` `__count` (hooks `data-fdy-table-search`,
506
+ `data-fdy-table-count`)
507
+ - Bulk bar: `.fdy-table-bulkbar` · `__count` `__spacer` `__actions` (hooks `data-fdy-table-bulk`,
508
+ `data-fdy-table-bulk-count`, `data-fdy-table-bulk-clear`)
509
+ - Header cells: `<button class="fdy-table__sortbtn" data-fdy-sort>` (`data-fdy-sort="number"` for
510
+ numeric); `data-fdy-filter="text|enum|number"` on the `<th>` adds the funnel
511
+ (`.fdy-table__filterbtn` / `__filterwrap` → panel `.fdy-filter` · `__title` `__list` `__check`
512
+ `__range` `__foot`)
513
+ - Selection: `.fdy-table__selcol` cells with `data-fdy-select-all` / `data-fdy-row-select`
514
+ - Rows: `.fdy-table__row--activatable` (clickable rows), `.fdy-table__detailrow` (expandable
515
+ detail), `.fdy-table__state` (in-table empty/loading row)
516
+ - Footer: `.fdy-table-footer` · `__info` (`data-fdy-table-info`) + `<nav class="fdy-pagination"
517
+ data-fdy-table-pagination>`
518
+ - Sort values: put the raw value in `data-sort-value` when the cell text is formatted.
519
+ - **Language caveat:** the vanilla enhancer writes the footer and bulk-count strings in Indonesian
520
+ (`Menampilkan 1–5 dari 7`, `N dipilih`) and has no override hook. The Vue/React/Blazor `FdyTable`
521
+ components are English. For an English static page, render those two nodes yourself from the
522
+ `fdy-table-change` event instead of using `data-fdy-table-info` / `data-fdy-table-bulk-count`.
523
+
524
+ ## Pagination — `.fdy-pagination`
525
+ The block class on the `<nav>` is a **structural hook only** — it carries no rule of its own; the
526
+ `__list` / `__link` / `__ellipsis` elements do all the styling, and the data table targets
527
+ `data-fdy-table-pagination`. Keep it on the wrapper anyway, for consistency with the rest of the kit.
528
+
529
+ `<nav class="fdy-pagination" aria-label="Pagination">` → `<ul class="fdy-pagination__list">`;
530
+ each item is a `.fdy-pagination__link` (`<button>` when navigable, `<span aria-current="page">`
531
+ for the current page, `<span aria-disabled="true">` for a dead arrow) and
532
+ `.fdy-pagination__ellipsis` for the gap.
533
+
534
+ ## States — `.fdy-state`
535
+ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__icon`
536
+ (`aria-hidden="true"`) `__title` `__text` `__actions`. Put it inside the card or table area it
537
+ replaces.
538
+
539
+ ## Charts — `data-fdy-chart`
540
+ Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`; call
541
+ `FreedayChart.update(el)` after changing data (or use `FdyChart` in Vue/React/Blazor).
542
+
543
+ - Types: `sparkline` `bar` `line` `area` `donut`
544
+ - Roots: `.fdy-sparkline` · `.fdy-bars` · `.fdy-donut` · `.fdy-chart-xy` (built for line/area/bar)
545
+ - Data: `data-values="1,2,3"` (single series) or `data-series='[{"label":"A","values":[…]}]'`
546
+ (multi), `data-labels="Jan,Feb"`, `data-fdy-stacked`, `data-fdy-center` (donut)
547
+ - Colour: `data-fdy-color="primary"` or `data-fdy-colors="success,warning,danger"` — semantic token
548
+ names **or** `chart-1`…`chart-8` slots to pin a category's colour. Multi-series defaults to the
549
+ validated categorical palette `--chart-1`…`--chart-8`.
550
+ - Format: `data-fdy-format="number|percent|currency"`; legend `data-fdy-legend` (`none` to drop);
551
+ axes `data-fdy-axes`
552
+ - A11y: every chart is `role="img"` + `aria-label`, with a `<table>` fallback inside the element.
553
+ - Internal parts (set by JS, useful for overrides): `.fdy-chart__legend` (+`--row`) `__swatch`
554
+ `__tip`; `.fdy-bars__col` `__bar` `__track` `__label` `__val`; `.fdy-chart-xy__plot` `__grid`
555
+ `__tick` `__xlabel` `__line` `__area` `__bar` `__dot` `__band`; `.fdy-donut__ring` `__seg`
556
+ `__center` `__hit`; `.fdy-sparkline__line` `__area` `__dot`.
557
+
558
+ ## Tree view — `.fdy-tree`
559
+ Native `<details>` hierarchy — zero JS for expand/collapse. `.fdy-tree__branch` on `<details>`,
560
+ `.fdy-tree__leaf` on a leaf `<li>`, plus `__chevron` and `__icon` svgs inside `<summary>`.
561
+ `.fdy-tree--checkbox` + `data-fdy-tree` (needs `freeday-tree.js`) adds cascading selection:
562
+ `.fdy-tree__check` on each `<input class="fdy-checkbox">`; checking a branch checks its children,
563
+ a partial set makes the branch indeterminate. Checking never toggles expansion.
564
+
565
+ ## List (flat rows) — `.fdy-list`
566
+ The **flat** row container: one bordered surface, hairline dividers, **no shadow**. This is what a
567
+ responsive `.fdy-datatable` should become below `md` — not a stack of `.fdy-card`s, which carries
568
+ `--shadow-lift` and turns ten rows into ten floating objects.
569
+
570
+ - `.fdy-list` on a `<ul>`/`<ol>` (list-style is reset for you) or a `<div>`
571
+ - `.fdy-list__row` — one row; `--interactive` for hover feedback, `--button` when the row **is** the
572
+ control (render it as a real `<button>`/`<a>`; the UA box is reset without losing the list surface)
573
+ - Row internals: `.fdy-list__main` (truncating stack) → `.fdy-list__title` + `.fdy-list__meta`, and
574
+ `.fdy-list__aside` pinned right
575
+ - Not `.fdy-list-reset` — that utility only strips UA bullets/indent from a semantic list.
576
+
577
+ ```html
578
+ <ul class="fdy-list">
579
+ <li>
580
+ <button type="button" class="fdy-list__row fdy-list__row--button">
581
+ <span class="fdy-list__main">
582
+ <span class="fdy-list__title">Northwind Trading</span>
583
+ <span class="fdy-list__meta">INV-2042 · due 12 Jul <span class="fdy-badge fdy-badge--warning">Overdue</span></span>
584
+ </span>
585
+ <span class="fdy-list__aside fdy-mono">290,000</span>
586
+ </button>
587
+ </li>
588
+ </ul>
589
+ ```
590
+
591
+ ## Description list — `.fdy-dl`
592
+ Key–value detail view: `<dl class="fdy-dl">` with `<dt>`/`<dd>` pairs. `--rows` for a row-per-pair
593
+ layout.
594
+
595
+ ---
596
+
597
+ # Feedback
598
+
599
+ ## Alert — `.fdy-alert`
600
+ Inline message. Modifiers `--info` `--success` `--warning` `--danger`. Parts `__icon` `__body`
601
+ `__title` `__text` `__close`. The danger variant takes `role="alert"`.
602
+
603
+ ```html
604
+ <div class="fdy-alert fdy-alert--danger" role="alert">
605
+ <div class="fdy-alert__body">
606
+ <span class="fdy-alert__title">Failed</span>
607
+ <span class="fdy-alert__text">Connection to the server was lost.</span>
608
+ </div>
609
+ <button class="fdy-alert__close" type="button" aria-label="Dismiss">&times;</button>
610
+ </div>
611
+ ```
612
+
613
+ ## Toast — `Freeday.toast()`
614
+ Transient notification in a live region. Imperative only:
615
+
616
+ ```js
617
+ const node = Freeday.toast({ variant: 'success', title: 'Saved', message: 'INV-1042 saved.' })
618
+ Freeday.toast({ variant: 'danger', title: 'Failed', message: '…', key: 'net-fail' }) // same key replaces
619
+ Freeday.dismiss('net-fail') // or Freeday.dismiss(node)
620
+ ```
621
+ Every field is optional; it returns the toast element. Classes (rendered for you):
622
+ `.fdy-toast-region`, `.fdy-toast` (+`--info` `--success` `--warning` `--danger`) · `__accent`
623
+ `__body` `__title` `__text` `__close`.
624
+
625
+ ## Tooltip — `.fdy-tooltip`
626
+ Hover/focus only, never the sole carrier of information. Wrap in `.fdy-tooltip-wrap`; the trigger
627
+ gets `aria-describedby` → the `.fdy-tooltip[role="tooltip"]` `id`.
628
+
629
+ ## Progress & spinner
630
+ `.fdy-progress` + `.fdy-progress__bar` (set `style="width:N%"`); `--indeterminate` for unknown
631
+ duration. `role="progressbar"` with `aria-valuenow/min/max` (omit `valuenow` when indeterminate).
632
+ `.fdy-spinner` (+`--sm` `--lg`) with `role="status"` + `aria-label`.
633
+
634
+ ## Skeleton — `.fdy-skeleton`
635
+ Size-matched placeholders so nothing shifts when data lands: `--title` `--text` `--circle`
636
+ `--avatar` (+`--avatar-sm` `--avatar-lg`, exactly `.fdy-avatar`'s box).
637
+
638
+ ---
639
+
640
+ # Navigation
641
+
642
+ ## Nav (sidebar menu) — `.fdy-nav`
643
+ Vertical navigation, used by the app shell. Items are `<a class="fdy-nav__item">` with
644
+ `__icon` / `__label` / `__badge`; the current one gets `aria-current="page"`.
645
+ `--flat` drops the surface. Nested groups are native `<details>`:
646
+
647
+ - `.fdy-nav__tree` + `<summary class="fdy-nav__item">` + `.fdy-nav__caret` → children in
648
+ `.fdy-nav__sub`
649
+ - `.fdy-nav__group` + `<summary class="fdy-nav__grouplabel">` → a collapsible section
650
+
651
+ ## App bar — `.fdy-appbar`
652
+ Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--sticky`
653
+ `--elevated` `--dense` `--primary` (on-colour controls). Parts `__brand` `__spacer` `__actions`.
654
+
655
+ ## Breadcrumb — `.fdy-breadcrumb`
656
+ `<nav class="fdy-breadcrumb" aria-label="Breadcrumb">` → `<ol class="fdy-breadcrumb__list">` with
657
+ `.fdy-breadcrumb__item` (last one `aria-current="page"`, no link), `.fdy-breadcrumb__link`, and
658
+ `.fdy-breadcrumb__sep` marked `aria-hidden="true"`.
659
+
660
+ ## Tabs — `.fdy-tabs`
661
+ APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
662
+
663
+ ```html
664
+ <div class="fdy-tabs" data-fdy-tabs>
665
+ <div class="fdy-tabs__list" role="tablist" aria-label="Invoice detail">
666
+ <button class="fdy-tabs__tab" role="tab" id="t-1" aria-controls="p-1" aria-selected="true">Summary</button>
667
+ <button class="fdy-tabs__tab" role="tab" id="t-2" aria-controls="p-2" aria-selected="false" tabindex="-1">Items</button>
668
+ </div>
669
+ <div class="fdy-tabs__panel" role="tabpanel" id="p-1" aria-labelledby="t-1" tabindex="0">…</div>
670
+ <div class="fdy-tabs__panel" role="tabpanel" id="p-2" aria-labelledby="t-2" tabindex="0" hidden>…</div>
671
+ </div>
672
+ ```
673
+
674
+ ---
675
+
676
+ # Overlays
677
+
678
+ ## Modal — `.fdy-modal`
679
+ Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
680
+ `--wide`; `--cfl` for the choose-from-list dialog. Parts `__header` `__title` `__body` `__footer`
681
+ `__close`. The body scrolls; the footer never clips. `aria-labelledby` → the title's `id`;
682
+ `data-close` on any button that should close it.
683
+
684
+ ## Drawer — `.fdy-drawer`
685
+ Temporary side panel on native `<dialog>` — left by default, `--right` to flip. Parts `__header`
686
+ `__title` `__body` `__footer` `__close`. Open it from any
687
+ `<button data-fdy-drawer="<dialog id>">`. Needs `freeday-drawer.js`.
688
+
689
+ ## Accordion — `.fdy-accordion`
690
+ Native `<details>`, zero JS: `.fdy-accordion__item` on each `<details>`, content in
691
+ `.fdy-accordion__panel`, the trigger is the `<summary>`.
692
+
693
+ ## Carousel — `.fdy-carousel`
694
+ Scroll-snap slides + arrows + dots; optional `data-fdy-autoplay` (pauses on hover/focus). Needs
695
+ `freeday-carousel.js`. Parts `__viewport` `__slide` `__arrow` (+`--prev` `--next`) `__dot` `__dots`.
696
+ Root is `role="region" aria-roledescription="carousel"` + `aria-label` + `tabindex="0"`; each slide
697
+ is `role="group" aria-roledescription="slide"`.
698
+
699
+ ---
700
+
701
+ # Display
702
+
703
+ ## Card — `.fdy-card`
704
+ Parts `__body` `__title` `__desc` `__footer`. Modifiers:
705
+
706
+ - `--elevated` — raise it (use sparingly; most surfaces are flat)
707
+ - `--interactive` — cursor + hover-lift for a card that *has* a click handler
708
+ - `--button` — for a card that **is** the control: render it as `<button>` and add this to reset
709
+ the UA button box **without** losing the card surface/border
710
+ - `.fdy-card--button` never replaces keyboard semantics — a clickable card must be a real
711
+ `<button>` or `<a>`.
712
+
713
+ ## Badge — `.fdy-badge`
714
+ Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only —
715
+ the text carries the meaning.
716
+
717
+ **Notification badge is the overlay variant**, not a separate component: wrap anything in
718
+ `.fdy-badge-wrap` and add `.fdy-badge-ov` (`--primary` `--danger` `--accent` `--info` `--success`
719
+ `--warning`, `--dot` for a bare dot, `--top-left` / `--bottom-right` / `--bottom-left` to move it).
720
+ Put the count in the wrapper's `aria-label` and mark the overlay `aria-hidden="true"`.
721
+
722
+ ## Avatar — `.fdy-avatar`
723
+ Initials or image. Sizes `--sm` / default / `--lg`; `.fdy-avatar-group` stacks them (label the
724
+ group). `--tone-1`…`--tone-8` are decorative tints (WCAG AA in both themes) to distinguish
725
+ same-initial avatars — hash the index off the **full** name, not the initials.
726
+
727
+ ## Chip — `.fdy-chip`
728
+ Three roles inside a `.fdy-chips` row:
729
+
730
+ - **static / removable** — `<span class="fdy-chip">`, `--primary` for an active filter, with
731
+ `.fdy-chip__remove` (`aria-label` says what it removes)
732
+ - **choice** — `<button class="fdy-chip fdy-chip--choice" aria-pressed>` toggle
733
+ - **filter** — `--filter`, same but with a `.fdy-chip__check` mark
734
+
735
+ Interactive chips are managed by `freeday-chip.js`: wrap the row in `data-fdy-chips` (+
736
+ `data-single` for pick-one, `data-label` for the group name), give each `data-value`.
737
+ `--tone-1`…`--tone-8` for non-semantic category colours.
738
+
739
+ ## Timeline — `.fdy-timeline`
740
+ Vertical event feed, pure CSS. `<ol class="fdy-timeline">` of `.fdy-timeline__item` (`--muted`
741
+ `--success` `--warning` `--danger`) with `__marker` `__time` `__title` `__text`. Status is carried
742
+ by the accompanying text, not the marker colour alone.
743
+
744
+ ---
745
+
746
+ *Anything not listed here is not part of the public surface. If a screen needs a primitive that
747
+ isn't in this file, it probably belongs in the kit — open an issue instead of re-inventing it
748
+ locally.*