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