@cahyo-dimas/freeday 1.54.0 → 2.1.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 +713 -545
- package/COMPONENTS.md +417 -139
- package/README.id.md +46 -45
- package/README.md +46 -43
- package/USAGE.md +34 -34
- package/adapters/blazor/FdyAppShell.razor +2 -2
- package/adapters/blazor/FdyAppShell.razor.cs +6 -6
- package/adapters/blazor/FdyCfl.razor.cs +1 -1
- package/adapters/blazor/FdyChart.razor +1 -1
- package/adapters/blazor/FdyChart.razor.cs +3 -3
- package/adapters/blazor/FdyChartSeries.cs +1 -1
- package/adapters/blazor/FdyCombo.razor.cs +2 -2
- package/adapters/blazor/FdyModal.razor +1 -1
- package/adapters/blazor/FdyTable.razor.cs +7 -7
- package/adapters/blazor/FdyTableFilter.razor +1 -1
- package/adapters/blazor/FdyTableFooter.razor +1 -1
- package/adapters/blazor/FdyTableFooter.razor.cs +4 -4
- package/adapters/blazor/Freeday.Blazor.csproj +1 -1
- package/adapters/blazor/FreedayComponentBase.cs +1 -1
- package/adapters/blazor/TableModel.cs +3 -3
- package/adapters/blazor/TableTypes.cs +3 -3
- package/adapters/blazor/freeday-blazor.js +5 -5
- package/adapters/core/app-shell.d.ts +1 -1
- package/adapters/core/app-shell.js +6 -6
- package/adapters/core/cfl-value.d.ts +11 -0
- package/adapters/core/cfl-value.js +31 -0
- package/adapters/core/table-model.d.ts +2 -2
- package/adapters/core/table-model.js +8 -8
- package/adapters/react/components/FdyAppShell.tsx +5 -5
- package/adapters/react/components/FdyAutocomplete.tsx +1 -1
- package/adapters/react/components/FdyCascade.tsx +1 -1
- package/adapters/react/components/FdyCfl.tsx +11 -11
- package/adapters/react/components/FdyDateRange.tsx +2 -2
- package/adapters/react/components/FdyDatepicker.tsx +5 -5
- package/adapters/react/components/FdyDrawer.tsx +1 -1
- package/adapters/react/components/FdyModal.tsx +2 -2
- package/adapters/react/components/FdyTable.tsx +7 -7
- package/adapters/react/components/FdyTableFilter.tsx +1 -1
- package/adapters/react/components/FdyTableFooter.tsx +5 -5
- package/adapters/react/index.d.ts +4 -1
- package/adapters/react/index.js +1 -0
- package/adapters/react/useFreeday.js +3 -3
- package/adapters/vue/components/FdyAppShell.vue +5 -5
- package/adapters/vue/components/FdyAutocomplete.vue +1 -1
- package/adapters/vue/components/FdyCascade.vue +1 -1
- package/adapters/vue/components/FdyCfl.vue +8 -8
- package/adapters/vue/components/FdyChart.vue +2 -2
- package/adapters/vue/components/FdyCombo.vue +1 -1
- package/adapters/vue/components/FdyDateRange.vue +2 -2
- package/adapters/vue/components/FdyDatepicker.vue +5 -5
- package/adapters/vue/components/FdyDrawer.vue +3 -3
- package/adapters/vue/components/FdyModal.vue +3 -3
- package/adapters/vue/components/FdyTable.vue +7 -7
- package/adapters/vue/components/FdyTableFilter.vue +1 -1
- package/adapters/vue/components/FdyTableFooter.vue +5 -5
- package/adapters/vue/index.d.ts +5 -2
- package/adapters/vue/index.js +1 -0
- package/adapters/vue/useFreeday.js +3 -3
- package/dist/asset.d.ts +3 -3
- package/dist/freeday-app-shell.js +9 -9
- package/dist/freeday-autocomplete.js +2 -2
- package/dist/freeday-breakpoint.js +1 -1
- package/dist/freeday-carousel.js +8 -7
- package/dist/freeday-cascade.js +10 -9
- package/dist/freeday-cfl.js +12 -11
- package/dist/freeday-chart.js +11 -11
- package/dist/freeday-chip.js +3 -3
- package/dist/freeday-datepicker.js +11 -7
- package/dist/freeday-datetime.js +5 -5
- package/dist/freeday-drawer.js +1 -1
- package/dist/freeday-form.js +17 -16
- package/dist/freeday-mask.js +9 -8
- package/dist/freeday-menu.js +1 -1
- package/dist/freeday-number.js +3 -3
- package/dist/freeday-popover.js +3 -3
- package/dist/freeday-rating.js +2 -2
- package/dist/freeday-select.js +5 -5
- package/dist/freeday-slider.js +2 -2
- package/dist/freeday-stepper.js +10 -9
- package/dist/freeday-table.js +18 -17
- package/dist/freeday-tabs.js +1 -1
- package/dist/freeday-timepicker.js +2 -2
- package/dist/freeday-toast.js +11 -10
- package/dist/freeday-tree.js +3 -3
- package/dist/freeday-upload.js +28 -27
- package/dist/freeday.bundle.css +176 -176
- package/dist/freeday.css +170 -170
- package/dist/freeday.d.ts +2 -2
- package/dist/freeday.js +189 -176
- package/dist/freeday.tokens.css +5 -5
- package/docs/agent-onboarding.md +49 -46
- package/docs/getting-started.md +49 -40
- package/docs/integrations.md +18 -18
- package/docs/reference-screen.html +14 -14
- package/package.json +4 -3
- package/src/base.css +6 -6
- package/src/components/accordion.css +4 -4
- package/src/components/alert.css +1 -1
- package/src/components/app-shell.css +10 -10
- package/src/components/appbar.css +3 -3
- package/src/components/autocomplete.css +1 -1
- package/src/components/avatar.css +3 -3
- package/src/components/badge.css +5 -5
- package/src/components/breadcrumb.css +1 -1
- package/src/components/breakpoints.css +4 -4
- package/src/components/button.css +13 -13
- package/src/components/card.css +8 -8
- package/src/components/carousel.css +2 -2
- package/src/components/cascade.css +2 -2
- package/src/components/cfl.css +4 -4
- package/src/components/chart.css +3 -3
- package/src/components/chip.css +2 -2
- package/src/components/combo.css +3 -3
- package/src/components/composition.css +7 -7
- package/src/components/datepicker.css +6 -6
- package/src/components/datetimepicker.css +1 -1
- package/src/components/description-list.css +1 -1
- package/src/components/divider.css +1 -1
- package/src/components/drawer.css +1 -1
- package/src/components/file-upload.css +5 -5
- package/src/components/filterbar.css +7 -7
- package/src/components/form-grid.css +2 -2
- package/src/components/input-group.css +6 -6
- package/src/components/input.css +6 -6
- package/src/components/kbd.css +1 -1
- package/src/components/list.css +6 -6
- package/src/components/menu.css +3 -3
- package/src/components/modal.css +1 -1
- package/src/components/pagination.css +1 -1
- package/src/components/progress.css +1 -1
- package/src/components/rating.css +2 -2
- package/src/components/selection.css +4 -4
- package/src/components/skeleton.css +1 -1
- package/src/components/slider.css +1 -1
- package/src/components/spinner.css +1 -1
- package/src/components/states.css +1 -1
- package/src/components/stepper.css +2 -2
- package/src/components/table.css +15 -15
- package/src/components/tabs.css +3 -3
- package/src/components/timeline.css +1 -1
- package/src/components/timepicker.css +1 -1
- package/src/components/toast.css +3 -3
- package/src/components/tooltip.css +1 -1
- package/src/components/tree.css +2 -2
- package/tokens/breakpoints.d.ts +2 -2
- package/tokens/breakpoints.mjs +3 -3
- package/tokens/tokens.json +1 -1
package/COMPONENTS.md
CHANGED
|
@@ -15,30 +15,31 @@ live docs.
|
|
|
15
15
|
1. **Prefix is `fdy-`, pattern is compact BEM**: block `fdy-card`, element `fdy-card__title`,
|
|
16
16
|
modifier `fdy-card--elevated`. A modifier is **always** written alongside its block class:
|
|
17
17
|
`class="fdy-btn fdy-btn--ghost"`, never `fdy-btn--ghost` alone.
|
|
18
|
-
**One documented exception:** `.fdy-input-group__addon--icon` is **standalone
|
|
18
|
+
**One documented exception:** `.fdy-input-group__addon--icon` is **standalone**. It is a
|
|
19
19
|
borderless leading glyph, and adding the base `.fdy-input-group__addon` would give it the grey
|
|
20
20
|
fill and divider of a `Rp` / `%` prefix, which is not what a search icon is. Written
|
|
21
21
|
`class="fdy-input-group__addon--icon"`, alone. It is the only one; everywhere else the rule holds.
|
|
22
22
|
2. **Never invent a class.** If it is not in this file, it does not exist. Freeday has no
|
|
23
23
|
`fdy-grid`, no `fdy-flex`, no `fdy-mt-4`, no `fdy-primary`.
|
|
24
|
-
3. **Never write a raw hex or px value** in app CSS. Use the tokens
|
|
24
|
+
3. **Never write a raw hex or px value** in app CSS. Use the tokens: `var(--color-primary)`,
|
|
25
25
|
`var(--space-4)`, `var(--radius-md)`, `var(--shadow-1)`, `var(--dur-2)`. Spacing is a 4px scale
|
|
26
26
|
(`--space-0`…`--space-24`); a loose value breaks density switching.
|
|
27
27
|
4. **Freeday owns components + tokens, not layout.** The only layout it ships is the shell
|
|
28
28
|
(`.fdy-app`), the composition primitives below, and `.fdy-hidden`/`.fdy-visually-hidden`.
|
|
29
29
|
Grids, stacks and one-off gaps come from your own layout layer (a utility framework run
|
|
30
30
|
**utilities-only, preflight off**, with its theme defined in terms of `var(--space-N)`).
|
|
31
|
-
5. **`aria-invalid="true"` is the canonical error state** on form controls
|
|
31
|
+
5. **`aria-invalid="true"` is the canonical error state** on form controls. It drives the visual
|
|
32
32
|
state *and* the screen-reader state. Pair it with `aria-describedby` → the `id` of a
|
|
33
33
|
`.fdy-help.fdy-help--error`. The `--error` class modifiers exist only for the case where the
|
|
34
34
|
invalid state does not belong to the control itself.
|
|
35
35
|
6. **Interactive components need their enhancer script.** Static ones (button, card, badge, table,
|
|
36
36
|
alert, breadcrumb, timeline, accordion, tree-without-cascade…) are CSS-only.
|
|
37
|
-
7. **On Vue, React or Blazor,
|
|
37
|
+
7. **On Vue, React or Blazor, eleven components have a typed wrapper. Use it.** `FdyCombo` ·
|
|
38
38
|
`FdyDatepicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` ·
|
|
39
|
-
`FdyTable` · `FdyModal` · `FdyDrawer`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
|
|
39
|
+
`FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
|
|
40
40
|
`Freeday.Blazor` RCL. Each is flagged at its own section below. Hand-writing their raw markup in
|
|
41
|
-
those stacks *looks* right
|
|
41
|
+
those stacks *looks* right at first, because the enhancer initialises once and the first render is
|
|
42
|
+
correct. Then
|
|
42
43
|
breaks quietly: later-rendered DOM is never hydrated and the widget's state sits in the DOM
|
|
43
44
|
instead of your framework's. Everything else is the same markup everywhere; hydrate the other
|
|
44
45
|
interactive components with `useFreeday` (Vue/React) or `FreedayBlazor.initAll` (Blazor).
|
|
@@ -47,11 +48,11 @@ live docs.
|
|
|
47
48
|
|
|
48
49
|
`.fdy-visually-hidden` is `position:absolute`, and `clip` hides **painting**, not **layout**. An
|
|
49
50
|
absolutely positioned box resolves against its nearest *positioned* ancestor, and `overflow` clips
|
|
50
|
-
only what is contained that way
|
|
51
|
+
only what is contained that way, so in an unpositioned scroller, a hidden label parks at its static
|
|
51
52
|
position (possibly thousands of px to the right) and drags the **whole document** sideways. It is
|
|
52
53
|
invisible in the DOM and immune to `overflow-x: hidden` on every wrapper.
|
|
53
54
|
|
|
54
|
-
Every kit container that clips or scrolls therefore declares `position: relative
|
|
55
|
+
Every kit container that clips or scrolls therefore declares `position: relative`:
|
|
55
56
|
`.fdy-table-scroll`, `.fdy-table-wrap`, `.fdy-list`, `.fdy-card`, `.fdy-tabs__list`,
|
|
56
57
|
`.fdy-carousel__viewport`, `.fdy-accordion` (the rest are already inside a positioned ancestor).
|
|
57
58
|
Two consequences for you:
|
|
@@ -66,7 +67,7 @@ Two consequences for you:
|
|
|
66
67
|
|
|
67
68
|
Every enhancer is zero-dependency, auto-initialises once on `DOMContentLoaded`, and is idempotent:
|
|
68
69
|
re-hydrate SPA-rendered DOM with `window.Freeday<X>.initAll(root)`. **`root` may be the widget
|
|
69
|
-
itself or an ancestor of it
|
|
70
|
+
itself or an ancestor of it**. Both work, so a framework ref placed directly on the enhanced
|
|
70
71
|
element is fine. Events are bubbling
|
|
71
72
|
`CustomEvent`s; the payload is in `event.detail`.
|
|
72
73
|
|
|
@@ -114,7 +115,26 @@ is a no-op.
|
|
|
114
115
|
# Shell & composition
|
|
115
116
|
|
|
116
117
|
## App shell — `.fdy-app`
|
|
117
|
-
> **Typed wrapper: `<FdyAppShell
|
|
118
|
+
> **Typed wrapper: `<FdyAppShell>`**. Vue (`v-model:navOpen`) · React (`navOpen`/`onNavOpenChange`) · Blazor (`@bind-NavOpen`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
119
|
+
|
|
120
|
+
### Props — `<FdyAppShell>`
|
|
121
|
+
|
|
122
|
+
Vue and React names, `?` marking optional. Vue takes the regions as **slots** of the same names,
|
|
123
|
+
which is why React's list is longer while offering the same thing.
|
|
124
|
+
|
|
125
|
+
| Prop | Type | What it does |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `navOpen?` | `boolean` | Whether the nav is visible. Optional because the default is the **viewport's**, and a caller cannot state that in one initial value. Vue binds `v-model:navOpen`. |
|
|
128
|
+
| `onNavOpenChange?` | `(open: boolean) => void` | React. Visibility changed: the toggle, Escape, the backdrop, or a followed nav item. Vue emits `update:navOpen`. |
|
|
129
|
+
| `title?` | `string` (Vue) · `ReactNode` | Topbar title. |
|
|
130
|
+
| `toggleLabel?` | `string` | Accessible name for the nav toggle button. |
|
|
131
|
+
| `toggleIcon?` | `ReactNode` | React. Replaces the default hamburger; Vue uses the slot of that name. |
|
|
132
|
+
| `skip?` | `ReactNode` | React. Skip link, rendered first in the tab order. |
|
|
133
|
+
| `brand?` · `nav?` · `topbar?` | `ReactNode` | React. Sidebar brand, sidebar nav, topbar contents. |
|
|
134
|
+
| `children?` | `ReactNode` | React. The page itself; Vue's default slot. |
|
|
135
|
+
|
|
136
|
+
Blazor takes the regions as `RenderFragment`s — `BrandContent`, `NavContent`, `TopbarContent`,
|
|
137
|
+
`SkipContent`, `TitleContent`, `ChildContent` — and binds with `@bind-NavOpen`.
|
|
118
138
|
|
|
119
139
|
The frame every application lives in. Do not hand-roll one from flexbox: the responsive sidebar,
|
|
120
140
|
off-canvas drawer and backdrop are built in.
|
|
@@ -123,14 +143,14 @@ off-canvas drawer and backdrop are built in.
|
|
|
123
143
|
`__content` `__topbar` `__navtoggle` `__title` `__main` `__backdrop`
|
|
124
144
|
- Modifiers: `--nav-open` (mobile drawer open, ≤720px) · `--nav-collapsed` (collapse to zero width,
|
|
125
145
|
≥721px) · `--static` (embed the shell in a page instead of filling the viewport)
|
|
126
|
-
- Also: `.fdy-skip
|
|
127
|
-
- **Behaviour: `freeday-app-shell.js`.** Opt in with `data-fdy-app` on the root
|
|
146
|
+
- Also: `.fdy-skip`, the skip-to-content link, first child of the shell.
|
|
147
|
+
- **Behaviour: `freeday-app-shell.js`.** Opt in with `data-fdy-app` on the root. The markup below
|
|
128
148
|
is unchanged. It owns the toggle in both modes plus everything an overlay needs that hand-rolling
|
|
129
149
|
reliably forgets: Escape, backdrop click, closing when a `.fdy-nav__item` is followed, focus moved
|
|
130
150
|
into the panel on open and returned to `__navtoggle` on close, `inert` on `__content` while the
|
|
131
151
|
overlay is up, and a Tab trap inside the panel. `aria-expanded` on `__navtoggle` answers "is the
|
|
132
152
|
nav showing?" in both modes, so you never read the state classes yourself.
|
|
133
|
-
- **A hidden nav is not a tabbable nav.** `__sidebar` is `inert` whenever the nav is not visible
|
|
153
|
+
- **A hidden nav is not a tabbable nav.** `__sidebar` is `inert` whenever the nav is not visible:
|
|
134
154
|
collapsed at ≥721px (`width:0`) or off-canvas at ≤720px (`translateX(-100%)`). Both hide it from
|
|
135
155
|
the eye; neither hides it from the keyboard, so without this a nav nobody can see still swallows
|
|
136
156
|
Tab on the way to the page.
|
|
@@ -140,21 +160,21 @@ off-canvas drawer and backdrop are built in.
|
|
|
140
160
|
- **`navOpen` is one idea, in both modes.** The typed wrappers expose "is the nav visible?" and the
|
|
141
161
|
kit maps it: above the breakpoint a hidden nav is `--nav-collapsed`, below it a visible nav is
|
|
142
162
|
`--nav-open`. Leave the prop unbound (`undefined` in Vue/React, `null` in Blazor) and the shell
|
|
143
|
-
starts from the viewport
|
|
163
|
+
starts from the viewport: a column on a wide screen, hidden on a narrow one. That default is why
|
|
144
164
|
the prop is optional: a caller cannot express it as a single initial value before it knows the
|
|
145
165
|
viewport.
|
|
146
166
|
- **Binding your own state to the vanilla shell:** it emits a bubbling `fdy-app-nav` CustomEvent
|
|
147
|
-
(`detail {visible}`) on every real change
|
|
167
|
+
(`detail {visible}`) on every real change, including the ones a viewport change causes, since a
|
|
148
168
|
bound value that stayed `true` while the nav went off-canvas would be describing a panel nobody
|
|
149
169
|
can see. Drive it with `FreedayAppShell.setVisible(root, visible)` rather than the class names;
|
|
150
170
|
setting what is already set announces nothing, so a bound host cannot loop on its own echo.
|
|
151
171
|
`FreedayAppShell.isVisible(root)` reads it back.
|
|
152
172
|
- Skip the enhancer and nothing breaks: the classes still mean what they always meant and the
|
|
153
|
-
behaviour is yours to write. `FreedayAppShell.init(root)` for markup mounted later
|
|
173
|
+
behaviour is yours to write. `FreedayAppShell.init(root)` for markup mounted later. Hand it the
|
|
154
174
|
shell root itself, which is what the Blazor bridge does after its first render.
|
|
155
175
|
|
|
156
176
|
**The nesting is fixed, not free-form:** `.fdy-app` is a flex **row** of `[__sidebar | __content]`,
|
|
157
|
-
and `__content` is the column holding `__topbar` + `__main
|
|
177
|
+
and `__content` is the column holding `__topbar` + `__main`, which exists to give the sticky topbar a
|
|
158
178
|
tall containing block. The brand goes in the **sidebar** (sized to match the topbar height), and
|
|
159
179
|
`__main` already carries the page padding, so don't wrap your screen in another padded box.
|
|
160
180
|
|
|
@@ -187,15 +207,15 @@ tall containing block. The brand goes in the **sidebar** (sized to match the top
|
|
|
187
207
|
Vertical section rhythm (`--space-8` between children). Assemble every screen from these; they
|
|
188
208
|
carry the doctrine in markup.
|
|
189
209
|
|
|
190
|
-
- `.fdy-page`
|
|
191
|
-
- `.fdy-page__header`
|
|
210
|
+
- `.fdy-page` is the screen body
|
|
211
|
+
- `.fdy-page__header` holds `.fdy-page__heading` (eyebrow + page title + `.fdy-page__desc`) on the
|
|
192
212
|
left, `.fdy-page__actions` (**one** primary button) on the right
|
|
193
|
-
- `.fdy-page-section`
|
|
194
|
-
- `.fdy-toolbar`
|
|
213
|
+
- `.fdy-page-section` is a region: `.fdy-page-section__head` + body, `--space-4` rhythm
|
|
214
|
+
- `.fdy-toolbar` is a horizontal control row; `__spacer` (flex filler), `__search` (capped width).
|
|
195
215
|
It is `align-items:center`, so it is for **bare** controls. A `.fdy-field` with a *visible* label
|
|
196
|
-
sits half a label-height low in it
|
|
216
|
+
sits half a label-height low in it, so labelled fields belong in `.fdy-filterbar`
|
|
197
217
|
(`align-items:flex-end`). In a toolbar, hide the label with `.fdy-visually-hidden`.
|
|
198
|
-
- `.fdy-stats`
|
|
218
|
+
- `.fdy-stats` is a KPI grid of `.fdy-stat` (`__label` `__value` `__meta`). Deliberately **not**
|
|
199
219
|
cards. `.fdy-stats--boxed` for one shared surface with dividers.
|
|
200
220
|
|
|
201
221
|
```html
|
|
@@ -230,7 +250,7 @@ carry the doctrine in markup.
|
|
|
230
250
|
```
|
|
231
251
|
|
|
232
252
|
## Type roles & text
|
|
233
|
-
One role per level of hierarchy
|
|
253
|
+
One role per level of hierarchy. Never re-use a card title for a page title.
|
|
234
254
|
|
|
235
255
|
| Class | Element | Use |
|
|
236
256
|
|---|---|---|
|
|
@@ -246,11 +266,11 @@ One role per level of hierarchy — never re-use a card title for a page title.
|
|
|
246
266
|
|
|
247
267
|
## Utilities
|
|
248
268
|
`.fdy-hidden` · `.fdy-visually-hidden` (screen-reader-only) · `.fdy-list-reset` (strip native
|
|
249
|
-
bullets/indent from a semantic list
|
|
269
|
+
bullets/indent from a semantic list, needed when your utility framework's preflight is off) ·
|
|
250
270
|
`.fdy-divider` (+`--vertical`) · `.fdy-kbd`.
|
|
251
271
|
|
|
252
272
|
Responsive display: `.fdy-hide-below-sm|md|lg` · `.fdy-hide-above-sm|md|lg`.
|
|
253
|
-
Breakpoints (min-width): `sm` 600 · `md` 960 · `lg` 1280 · `xl` 1920
|
|
273
|
+
Breakpoints (min-width): `sm` 600 · `md` 960 · `lg` 1280 · `xl` 1920, importable as
|
|
254
274
|
`import { breakpoints } from '@cahyo-dimas/freeday/breakpoints'`.
|
|
255
275
|
|
|
256
276
|
**`breakpoints.nav` (721) is separate and is the one the shell uses.** `.fdy-app` switches the
|
|
@@ -258,7 +278,7 @@ sidebar from off-canvas drawer to static column at 721px, *not* at `md`. Any `ma
|
|
|
258
278
|
utility variant that has to agree with the shell must use `nav`; using `md` leaves 721–959px broken
|
|
259
279
|
(sidebar already static while your script still treats it as an overlay).
|
|
260
280
|
|
|
261
|
-
Density: `data-density="compact"` works on `<html>` **or any wrapper
|
|
281
|
+
Density: `data-density="compact"` works on `<html>` **or any wrapper**, because the selector is a bare
|
|
262
282
|
`[data-density="compact"]` over inheriting custom properties, so one screen can be dense while the
|
|
263
283
|
rest of the app is not. `data-density="comfortable"` is a real rule too, so the reverse also works:
|
|
264
284
|
a compact root with one region opted back out (shared chrome that must match a sibling product).
|
|
@@ -268,13 +288,13 @@ a compact root with one region opted back out (shared chrome that must match a s
|
|
|
268
288
|
# Actions
|
|
269
289
|
|
|
270
290
|
## Button — `.fdy-btn`
|
|
271
|
-
**The base class is already the primary action
|
|
291
|
+
**The base class is already the primary action.** There is no `--primary`. Use it once per screen.
|
|
272
292
|
|
|
273
293
|
- Modifiers: `--ghost` `--danger` `--text` `--icon` (square, icon-only) `--sm` `--lg`
|
|
274
294
|
- Parts: `.fdy-btn__icon` (wraps the `<svg>`) · `.fdy-btn-group` (segmented row, `role="group"` +
|
|
275
295
|
`aria-label`)
|
|
276
296
|
- **Toggle / segmented:** `aria-pressed="true"` gives a real pressed state (soft primary fill on
|
|
277
|
-
`--ghost`/`--text`, an inset-sunk gradient on the solid button). No extra class
|
|
297
|
+
`--ghost`/`--text`, an inset-sunk gradient on the solid button). No extra class: `aria-pressed`
|
|
278
298
|
is already the right attribute, and it is what turns `.fdy-btn-group` from a joined row into a
|
|
279
299
|
complete segmented control.
|
|
280
300
|
- A11y: `--icon` **requires** `aria-label`. Use `<button type="button">` unless it submits.
|
|
@@ -288,7 +308,7 @@ a compact root with one region opted back out (shared chrome that must match a s
|
|
|
288
308
|
```
|
|
289
309
|
|
|
290
310
|
**Quiet destructive:** combine `--danger` with `--ghost` or `--text`. The ground stays quiet and only
|
|
291
|
-
the ink turns red
|
|
311
|
+
the ink turns red. `.fdy-menu__item--danger` has always worked this way. Use it whenever Delete
|
|
292
312
|
shares a screen with the one primary action; a solid Delete beside Save is a second primary in all
|
|
293
313
|
but name.
|
|
294
314
|
|
|
@@ -330,7 +350,7 @@ native control, otherwise a `<div>` + explicitly associated label.
|
|
|
330
350
|
- `.fdy-label` (+`--required`) · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`)
|
|
331
351
|
|
|
332
352
|
**Grouped controls are a `<fieldset>`, not a new block.** Put `.fdy-field` on the fieldset and
|
|
333
|
-
`.fdy-label` on the legend
|
|
353
|
+
`.fdy-label` on the legend. The kit already resets the UA border/padding and supplies the spacing:
|
|
334
354
|
|
|
335
355
|
```html
|
|
336
356
|
<fieldset class="fdy-field">
|
|
@@ -342,7 +362,7 @@ native control, otherwise a `<div>` + explicitly associated label.
|
|
|
342
362
|
|
|
343
363
|
**`.fdy-label--required` marks the label, not the accessibility tree.** The control already carries
|
|
344
364
|
`required`; the asterisk is painted through `::after` with CSS alt text, so a screen reader never
|
|
345
|
-
reads "star" after the label
|
|
365
|
+
reads "star" after the label, which a `<span>` you have to remember to mark `aria-hidden`
|
|
346
366
|
cannot guarantee.
|
|
347
367
|
- `[readonly]` is styled on input/textarea: full contrast, focusable, copyable.
|
|
348
368
|
|
|
@@ -360,8 +380,8 @@ cannot guarantee.
|
|
|
360
380
|
</label>
|
|
361
381
|
```
|
|
362
382
|
|
|
363
|
-
**Which `type` does `.fdy-input` cover?** Every text-like type
|
|
364
|
-
`url` `search` `number`
|
|
383
|
+
**Which `type` does `.fdy-input` cover?** Every text-like type (`text` `email` `password` `tel`
|
|
384
|
+
`url` `search` `number`) themes identically. Two carry a native widget the kit does *not* override:
|
|
365
385
|
|
|
366
386
|
| Type | What the UA still draws | What to do |
|
|
367
387
|
|---|---|---|
|
|
@@ -386,7 +406,7 @@ Prefix/suffix addons around an input: text (`Rp`, `%`), a decorative icon
|
|
|
386
406
|
|
|
387
407
|
## Number field — `[data-fdy-number]`
|
|
388
408
|
A number input with its increment/decrement affordance back, after `.fdy-input` removed the
|
|
389
|
-
browser's own. **Not a new block
|
|
409
|
+
browser's own. **Not a new block.** It is an `.fdy-input-group` with two `__btn`s, so it inherits
|
|
390
410
|
the shared border, focus ring and error promotion. Needs `freeday-number.js`.
|
|
391
411
|
|
|
392
412
|
```html
|
|
@@ -400,13 +420,13 @@ the shared border, focus ring and error promotion. Needs `freeday-number.js`.
|
|
|
400
420
|
```
|
|
401
421
|
|
|
402
422
|
- **No custom event.** Stepping fires native bubbling `input` + `change` on the input, so `v-model`,
|
|
403
|
-
`onChange` and `@bind` work with no adapter
|
|
423
|
+
`onChange` and `@bind` work with no adapter, because the input stays the source of truth.
|
|
404
424
|
- `min` / `max` / `step` live on the **input**; the buttons never do the arithmetic themselves
|
|
405
425
|
(`stepUp()`/`stepDown()` clamp for free) and go `disabled` at a bound, on a `disabled`/`readonly`
|
|
406
426
|
field, and when `step="any"` (a stepper cannot express "no defined increment").
|
|
407
427
|
- The buttons are **not tab stops** (`tabindex="-1"`): the input is already focusable and ↑/↓
|
|
408
428
|
already step it, so extra stops would cost every keyboard user and buy nothing. Keep the
|
|
409
|
-
`aria-label
|
|
429
|
+
`aria-label`, so pointer and browse-mode users still get a named control.
|
|
410
430
|
- `type="button"` is required. Inside a `<form>`, a bare `<button>` submits it.
|
|
411
431
|
|
|
412
432
|
## Checkbox · radio · switch
|
|
@@ -419,7 +439,25 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
|
|
|
419
439
|
```
|
|
420
440
|
|
|
421
441
|
## Select / combobox — `.fdy-combo`
|
|
422
|
-
> **Typed wrapper: `<FdyCombo
|
|
442
|
+
> **Typed wrapper: `<FdyCombo>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
443
|
+
|
|
444
|
+
### Props — `<FdyCombo>`
|
|
445
|
+
|
|
446
|
+
| Prop | Type | What it does |
|
|
447
|
+
|---|---|---|
|
|
448
|
+
| `modelValue` · `value` | `T extends string` | The selected value: `v-model` in Vue, `value` in React. |
|
|
449
|
+
| `onChange` | `(value: T) => void` | React. A selection was committed. Vue emits `update:modelValue` and `change`. |
|
|
450
|
+
| `options` | `ReadonlyArray<{ value: T; label: string }>` | The list. React exports the row type as `FdyComboOption<T>`. |
|
|
451
|
+
| `id?` | `string` | Explicit id for the combobox button; otherwise one is generated. |
|
|
452
|
+
| `ariaLabelledby?` | `string` | Id of the element that labels the combobox. |
|
|
453
|
+
| `placeholder?` | `string` | Shown when no option matches the current value. |
|
|
454
|
+
| `disabled?` | `boolean` | Greyed and out of the tab order, the native `disabled` semantics. |
|
|
455
|
+
| `readonly?` | `boolean` | Locked/view mode: keeps focus and tab order and shows its value, but cannot be opened or changed. Unlike `disabled` it is not greyed. |
|
|
456
|
+
| `invalid?` | `boolean` | Sets `aria-invalid`; pair it with `describedby` pointing at the error text. |
|
|
457
|
+
| `describedby?` | `string` | Id of the help or error text (`aria-describedby`). |
|
|
458
|
+
|
|
459
|
+
Blazor: the same names in `PascalCase`, bound with `@bind-Value` (`Value` / `ValueChanged`),
|
|
460
|
+
with one absence — there is no `Describedby`, so a Blazor combo cannot point at its own error text.
|
|
423
461
|
|
|
424
462
|
Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
|
|
425
463
|
|
|
@@ -444,12 +482,28 @@ Fully styleable dropdown, APG combobox+listbox. Needs `freeday-select.js`.
|
|
|
444
482
|
|
|
445
483
|
**The selected tick is CSS.** `.fdy-combo__check` is an empty box in the markup; the glyph is painted
|
|
446
484
|
by the stylesheet on `[aria-selected="true"]` with alt text, so it never enters the accessibility
|
|
447
|
-
tree. That keeps an option's accessible name **identical whether or not it is selected
|
|
485
|
+
tree. That keeps an option's accessible name **identical whether or not it is selected**. The state
|
|
448
486
|
is carried by `aria-selected` alone, announced once, and `getByRole('option', { name: 'August' })`
|
|
449
487
|
keeps matching after selection. Do not put a glyph in that span.
|
|
450
488
|
|
|
451
489
|
## Autocomplete — `.fdy-autocomplete`
|
|
452
|
-
> **Typed wrapper: `<FdyAutocomplete
|
|
490
|
+
> **Typed wrapper: `<FdyAutocomplete>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
491
|
+
|
|
492
|
+
### Props — `<FdyAutocomplete>`
|
|
493
|
+
|
|
494
|
+
| Prop | Type | What it does |
|
|
495
|
+
|---|---|---|
|
|
496
|
+
| `modelValue` · `value` | `string` | The typed text. Free text: the list suggests, it does not constrain. |
|
|
497
|
+
| `onChange` · `onSelect` | `(value: string) => void` | React. `onChange` on every keystroke, `onSelect` only when a suggestion is taken. Vue emits `update:modelValue` and `select`. |
|
|
498
|
+
| `options` | `ReadonlyArray<string>` | The suggestions to filter. |
|
|
499
|
+
| `emptyText?` | `string` | Shown when nothing matches. |
|
|
500
|
+
| `placeholder?` | `string` | |
|
|
501
|
+
| `id?` · `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Input id; its accessible name as text or as a reference; the help/error text it points at. |
|
|
502
|
+
| `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps focus and tab order and shows its value, but the input is not editable and the list will not open. Unlike `disabled` it is not greyed. |
|
|
503
|
+
|
|
504
|
+
Blazor takes six of these — `Value` / `ValueChanged`, `Options`, `Placeholder`, `AriaLabel`,
|
|
505
|
+
`EmptyText` — and no state flags or ids. Its wrapper renders a fixed seed element and does not
|
|
506
|
+
splat unmatched attributes, so those are not reachable from a Blazor page at all.
|
|
453
507
|
|
|
454
508
|
Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
|
|
455
509
|
|
|
@@ -458,7 +512,23 @@ Editable combobox that filters as you type. Needs `freeday-autocomplete.js`.
|
|
|
458
512
|
`aria-controls` → the listbox `id`, `autocomplete="off"`.
|
|
459
513
|
|
|
460
514
|
## Cascade select — `.fdy-cascade`
|
|
461
|
-
> **Typed wrapper: `<FdyCascade
|
|
515
|
+
> **Typed wrapper: `<FdyCascade>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
516
|
+
|
|
517
|
+
### Props — `<FdyCascade>`
|
|
518
|
+
|
|
519
|
+
| Prop | Type | What it does |
|
|
520
|
+
|---|---|---|
|
|
521
|
+
| `modelValue` · `value` | `string` | The selected **leaf** value; `''` means nothing selected. |
|
|
522
|
+
| `onChange` | `(value: string, labels: string[]) => void` | React. Vue emits `update:modelValue` and `change`. `labels` is the whole path, root → leaf. |
|
|
523
|
+
| `options` | `ReadonlyArray<CascadeNode>` | The tree. `CascadeNode` = `{ label, value, children? }`; a node **with** children is a branch, one without is selectable. |
|
|
524
|
+
| `separator?` | `string` | What joins the path in the display. Default `" / "`, matching the enhancer. |
|
|
525
|
+
| `backLabel?` | `string` | Accessible name for the up-one-level button. Default `Back one level`. |
|
|
526
|
+
| `label?` | `string` | Accessible name for the trigger and its listbox. |
|
|
527
|
+
| `placeholder?` | `string` | |
|
|
528
|
+
| `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
|
|
529
|
+
| `disabled?` · `readonly?` · `invalid?` | `boolean` | As on `<FdyCombo>`. |
|
|
530
|
+
|
|
531
|
+
Blazor calls the tree `Nodes`, adds `SubmenuLabel`, and takes neither the state flags nor the ids.
|
|
462
532
|
|
|
463
533
|
Hierarchical drill-down. The data model is a **nested `<ul>`** inside the wrapper: an `<li>` with a
|
|
464
534
|
child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
|
|
@@ -479,7 +549,38 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
|
|
|
479
549
|
```
|
|
480
550
|
|
|
481
551
|
## Choose-from-list (CFL) — `data-fdy-cfl`
|
|
482
|
-
> **Typed wrapper: `<FdyCfl
|
|
552
|
+
> **Typed wrapper: `<FdyCfl>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
553
|
+
|
|
554
|
+
### Props — `<FdyCfl>`
|
|
555
|
+
|
|
556
|
+
Four of these are required and carry the whole component: `fetchPage`, `columns`, `display`,
|
|
557
|
+
`rowKey`. The rest are copy and state.
|
|
558
|
+
|
|
559
|
+
| Prop | Type | What it does |
|
|
560
|
+
|---|---|---|
|
|
561
|
+
| `modelValue` · `value` | `Row \| Row[] \| null` | The picked row. An array is reachable only under `multiple`, `null` only under `clearable`; narrow it with `singleRow()` (see the bullet below the raw-path hooks). |
|
|
562
|
+
| `onChange` | `(value: Row \| Row[] \| null) => void` | React. Vue emits `update:modelValue` and `change`. |
|
|
563
|
+
| `fetchPage` | `(query: string, page: number) => Promise<CflPage<Row>>` | The only data source — `{ rows, hasMore }`. Called on open, on search, and on Load more. Server state, never a global store. |
|
|
564
|
+
| `columns` | `ReadonlyArray<CflColumn<Row>>` | `{ key, label }` per dialog column. |
|
|
565
|
+
| `display` | `(row: Row) => string` | What the closed field shows once a row is picked. |
|
|
566
|
+
| `rowKey` | `(row: Row) => string` | Row identity: ticks in multi mode, and seeding them when the dialog re-opens. |
|
|
567
|
+
| `pageSize?` | `number` | Advisory only. Your `fetchPage` owns paging; this documents the intent. |
|
|
568
|
+
| `title?` | `string` | The dialog's heading. Default `Choose data`. |
|
|
569
|
+
| `searchPlaceholder?` | `string` | Placeholder **and** accessible name of the search box. Default `Search…`. |
|
|
570
|
+
| `loadingText?` · `emptyText?` · `retryText?` · `moreText?` | `string` | Dialog states. Defaults `Loading…`, `No results.`, `Try again`, `Load more`. |
|
|
571
|
+
| `closeLabel?` · `openLabel?` | `string` | Accessible names for the dialog's × and the field's open button. Defaults `Close`, `Open search`. |
|
|
572
|
+
| `multiple?` | `boolean` | Tick rows and commit them together on Confirm instead of committing the clicked row, and widen the value to an array. |
|
|
573
|
+
| `selectedText?` · `confirmText?` · `hintText?` | `string` | Footer copy. Defaults `{n} selected` (with `{n}` substituted), `Confirm`, `Click a row to choose it`. |
|
|
574
|
+
| `placeholder?` | `string` | Field placeholder while nothing is picked. |
|
|
575
|
+
| `clearable?` | `boolean` | A clear button, so an **optional** foreign key can be unset. Without it the value type admits a `null` the component can never produce. |
|
|
576
|
+
| `clearLabel?` | `string` | Accessible name for that button. Default `Clear selection`. |
|
|
577
|
+
| `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps the picked value visible, focusable and copyable, but the dialog cannot be opened. |
|
|
578
|
+
| `id?` · `ariaLabelledby?` · `describedby?` | `string` | Field id; the element that labels it; the help/error text it points at. |
|
|
579
|
+
|
|
580
|
+
Blazor carries the same surface in `PascalCase` with three differences: multi-select is a second
|
|
581
|
+
pair (`Values` / `ValuesChanged`) rather than a widened single binding, the fetch delegate is
|
|
582
|
+
called `LoadPage`, and it has two parameters the JS adapters do not — `ErrorText` and
|
|
583
|
+
`SearchDebounceMs`.
|
|
483
584
|
|
|
484
585
|
A read-only field backed by master data: the button opens a searchable dialog, the picked row fills
|
|
485
586
|
the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
|
|
@@ -490,13 +591,20 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
|
|
|
490
591
|
- Inside: `.fdy-cfl__search` `__results` `__row` `__check` `__empty` `__count` `__actions`
|
|
491
592
|
- Hooks: `data-fdy-cfl-search` (the search input), `data-fdy-cfl-empty`, `data-fdy-cfl-count`,
|
|
492
593
|
`data-fdy-cfl-confirm` (multi only), `data-fdy-cfl-trigger`, `data-fdy-cfl-open`
|
|
493
|
-
- Rows are server state
|
|
594
|
+
- Rows are server state. In a real app, drive them from a controlled fetch rather than a global store.
|
|
494
595
|
- **Multi-select in the typed wrappers** (Vue · React · Blazor): `multiple` ticks rows and commits
|
|
495
596
|
them together on **Confirm** instead of committing the row that was clicked. Vue and React widen
|
|
496
597
|
their model to an array (`Row[] | null`); Blazor takes a second pair, `Values` /
|
|
497
598
|
`ValuesChanged`, because a nullable union is not a C# shape. Closing without Confirm leaves the
|
|
498
599
|
bound value untouched, and re-opening seeds the ticks from it. The field states `{n} selected`
|
|
499
600
|
rather than one row's `display()`. Strings: `selectedText`, `confirmText`, `hintText`.
|
|
601
|
+
- **A single-select in Vue or React needs one narrowing, and the kit ships it.** The emitted value
|
|
602
|
+
is typed `Row | Row[] | null` whatever the props are, so a single-select handler cannot take it
|
|
603
|
+
as `Row`: the array is reachable only under `multiple`, the null only under `clearable`, and the
|
|
604
|
+
type cannot say which. Import the guard instead of writing one per app:
|
|
605
|
+
`import { singleRow } from '@cahyo-dimas/freeday/vue'` (or `/react`). It returns `Row | null`,
|
|
606
|
+
and throws if a `multiple` field's array ever reaches it, rather than silently keeping the first
|
|
607
|
+
row. Blazor is unaffected: multi-select there is a separate `Values` / `ValuesChanged` pair.
|
|
500
608
|
|
|
501
609
|
```html
|
|
502
610
|
<div class="fdy-field">
|
|
@@ -537,32 +645,72 @@ the field. The value is always **chosen, never typed**. Needs `freeday-cfl.js`.
|
|
|
537
645
|
```
|
|
538
646
|
|
|
539
647
|
**`clearable` makes the value removable.** Without it a choose-from-list can be set but never unset,
|
|
540
|
-
which breaks every *optional* foreign key
|
|
648
|
+
which breaks every *optional* foreign key. The value type already allows null; only the component
|
|
541
649
|
could not produce it. With it, a clear button appears beside the trigger whenever a row is picked
|
|
542
650
|
and the field is editable; it emits `null` (Vue `update:modelValue`/`change`, React `onChange`,
|
|
543
651
|
Blazor `ValueChanged`) and returns focus to the trigger. It is a second `.fdy-input-group__btn`, not
|
|
544
652
|
a new class.
|
|
545
653
|
|
|
546
654
|
## Date picker — `data-fdy-datepicker`
|
|
547
|
-
> **Typed wrapper: `<FdyDatepicker
|
|
655
|
+
> **Typed wrapper: `<FdyDatepicker>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
656
|
+
|
|
657
|
+
### Props — `<FdyDatepicker>`
|
|
658
|
+
|
|
659
|
+
| Prop | Type | What it does |
|
|
660
|
+
|---|---|---|
|
|
661
|
+
| `modelValue` · `value` | `string \| null` | ISO `YYYY-MM-DD`, or `''`/`null` for empty. |
|
|
662
|
+
| `onChange` | `(value: string) => void` | React. Vue emits `update:modelValue` and `change`. Clearing sends `''`. |
|
|
663
|
+
| `min?` · `max?` | `string` | ISO bounds; days outside them cannot be picked. |
|
|
664
|
+
| `locale?` | `string` | BCP-47 tag deciding month and weekday names. Defaults to the page's `<html lang>`, then `en`. |
|
|
665
|
+
| `placeholder?` | `string` | |
|
|
666
|
+
| `id?` · `ariaLabelledby?` · `describedby?` | `string` | Trigger id; the element that labels it; the help/error text it points at. |
|
|
667
|
+
| `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps focus and tab order and shows its date, but it cannot be opened, cleared or changed. |
|
|
668
|
+
| `clearable?` | `boolean` | Show a × in the trigger once a date is set, so an optional date can be unset. Sends `''`. Off by default. |
|
|
669
|
+
| `clearLabel?` | `string` | Accessible name for that ×. Default `Clear date`. |
|
|
670
|
+
| `prevMonthLabel?` · `nextMonthLabel?` | `string` | The month arrows. Defaults `Previous month` / `Next month`. |
|
|
671
|
+
| `chooseMonthLabel?` · `chooseYearLabel?` | `string` | The title buttons that drill into the month and year grids. Defaults `Choose month` / `Choose year`. |
|
|
672
|
+
| `prevYearLabel?` · `nextYearLabel?` | `string` | Year arrows, shown in the month grid. Defaults `Previous year` / `Next year`. |
|
|
673
|
+
| `prevYearsLabel?` · `nextYearsLabel?` | `string` | Page arrows, shown in the year grid. Defaults `Previous years` / `Next years`. |
|
|
674
|
+
|
|
675
|
+
The ten label props exist because month and weekday names follow `locale` while the buttons around
|
|
676
|
+
them do not: without these, a Spanish calendar would be navigated by English arrows.
|
|
677
|
+
|
|
678
|
+
Blazor's picker is much thinner — `Value` / `ValueChanged`, `Label`, `Placeholder`, `Min`, `Max`
|
|
679
|
+
— and its seed element takes no unmatched attributes, so state flags, ids, `clearable` and every
|
|
680
|
+
label above are unreachable there.
|
|
681
|
+
|
|
682
|
+
### Props — `<FdyDateRange>`
|
|
683
|
+
|
|
684
|
+
| Prop | Type | What it does |
|
|
685
|
+
|---|---|---|
|
|
686
|
+
| `modelValue` · `value` | `DateRangeValue` = `{ start, end }`, each `string \| null` | Both ends at once, so the pair can never be half-applied. |
|
|
687
|
+
| `onChange` | `(value: DateRangeValue) => void` | React. Vue emits `update:modelValue` and `change`. |
|
|
688
|
+
| `min?` · `max?` | `string` | ISO bounds for both ends. The end can never precede the start. |
|
|
689
|
+
| `locale?` | `string` | As on the single picker. |
|
|
690
|
+
| `startPlaceholder?` · `endPlaceholder?` | `string` | One per field. |
|
|
691
|
+
| `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Names the group (`role="group"`), or points at the text that does. |
|
|
692
|
+
| `disabled?` · `readonly?` · `invalid?` | `boolean` | Applied to both pickers together. |
|
|
693
|
+
|
|
694
|
+
Blazor splits the value into two scalars, `From` / `FromChanged` and `To` / `ToChanged`, because a
|
|
695
|
+
C# binding cannot express one object bound in two places, and it adds `Separator`.
|
|
548
696
|
|
|
549
697
|
Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author an empty `<div>`**
|
|
550
|
-
|
|
698
|
+
The enhancer builds everything.
|
|
551
699
|
|
|
552
700
|
- `.fdy-datepicker` (+`--error`) · `__trigger` `__value` (+`--placeholder`) `__icon` `__clear`
|
|
553
701
|
`__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` (+`--months` `--years`)
|
|
554
702
|
`__dow` `__day` `__month` `__year`
|
|
555
703
|
- Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
|
|
556
704
|
get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start.
|
|
557
|
-
**Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor)
|
|
705
|
+
**Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor). Use it in those stacks.
|
|
558
706
|
**Getting to a distant date.** The title is a **button**, and it drills up one level each press:
|
|
559
707
|
day grid → 12-cell month grid (`.fdy-cal__grid--months` of `.fdy-cal__month`) → 12-cell year grid
|
|
560
|
-
(`.fdy-cal__grid--years` of `.fdy-cal__year`). The arrows follow the level shown
|
|
708
|
+
(`.fdy-cal__grid--years` of `.fdy-cal__year`). The arrows follow the level shown: months, then
|
|
561
709
|
years, then pages of twelve years. Picking a year drops to that year's months, picking a month
|
|
562
710
|
drops to its days: every level above the day grid is **navigation**, so nothing is committed until a
|
|
563
711
|
day is selected. From the year grid the title steps back down to months.
|
|
564
712
|
|
|
565
|
-
Year pages are **aligned**, not centred on the year in view
|
|
713
|
+
Year pages are **aligned**, not centred on the year in view (2016–2027, then 2028–2039), so the
|
|
566
714
|
pages tile and a given year always sits in the same place. From August 2026 to March 2022 that is 7
|
|
567
715
|
clicks; walking month by month is 53. To 1998 it is 8; walking is 336.
|
|
568
716
|
|
|
@@ -605,15 +753,15 @@ Click/Enter opens the file dialog; drop works too. Needs `freeday-upload.js`.
|
|
|
605
753
|
- List: `.fdy-filelist` (+`--grid`) of `.fdy-file` (+`--success`, `--error`) · `__icon` `__meta`
|
|
606
754
|
`__name` `__sub` `__progress` `__remove`
|
|
607
755
|
- Attributes: `data-max-size` (bytes), `data-filelist="#id"`, `data-fdy-upload-simulate` (demo only
|
|
608
|
-
|
|
756
|
+
the kit fakes a transfer to `done()`; never set it in an app)
|
|
609
757
|
- A11y: the dropzone is `role="button" tabindex="0"` + `aria-label`.
|
|
610
758
|
|
|
611
|
-
**Both events fire on the dropzone
|
|
759
|
+
**Both events fire on the dropzone**: `fdy-upload-add` *and* `fdy-upload-remove`. One listener, one
|
|
612
760
|
element. (The file list may be anywhere in the document; nothing is dispatched on the row, because a
|
|
613
761
|
row in a sibling list would never bubble through the zone.)
|
|
614
762
|
|
|
615
763
|
**The row is yours to drive.** `fdy-upload-add` carries `detail.row`, a small state machine over the
|
|
616
|
-
rendered `.fdy-file`. A dropped file **rests
|
|
764
|
+
rendered `.fdy-file`. A dropped file **rests**, showing its size and nothing else, until you say a
|
|
617
765
|
transfer started; the kit never claims one it is not performing.
|
|
618
766
|
|
|
619
767
|
| `detail.row` | State it renders |
|
|
@@ -627,15 +775,15 @@ transfer started; the kit never claims one it is not performing.
|
|
|
627
775
|
| `.ready()` | back to **rest** (e.g. after a failed attempt the user will retry) |
|
|
628
776
|
| `.el` | the row element |
|
|
629
777
|
|
|
630
|
-
**If your request outlives the transfer, drive `waiting()`.** Server-side work after the last byte
|
|
631
|
-
|
|
778
|
+
**If your request outlives the transfer, drive `waiting()`.** Server-side work after the last byte (OCR,
|
|
779
|
+
extraction, virus scanning, transcoding) is not uploading, and `setProgress(100)` left standing
|
|
632
780
|
is read as a hang. `waiting()` is the state for it: the label says what the server is doing, and the
|
|
633
781
|
bar stops claiming a percentage it no longer has.
|
|
634
782
|
|
|
635
783
|
```js
|
|
636
784
|
zone.addEventListener('fdy-upload-add', (e) => {
|
|
637
785
|
if (e.detail.rejected) return; // the kit already rendered the reason
|
|
638
|
-
const { file, row } = e.detail; // row is at rest
|
|
786
|
+
const { file, row } = e.detail; // row is at rest; nothing is in flight yet
|
|
639
787
|
submitBtn.onclick = async () => {
|
|
640
788
|
row.uploading();
|
|
641
789
|
await send(file, (pct) => row.setProgress(pct));
|
|
@@ -648,7 +796,7 @@ zone.addEventListener('fdy-upload-remove', (e) => forget(e.detail.file)); // s
|
|
|
648
796
|
```
|
|
649
797
|
|
|
650
798
|
**Bring your own row:** omit the file list entirely (no `data-filelist`, and no `.fdy-filelist`
|
|
651
|
-
sibling) and the enhancer renders nothing while still dispatching `fdy-upload-add
|
|
799
|
+
sibling) and the enhancer renders nothing while still dispatching `fdy-upload-add`, where `detail.row`
|
|
652
800
|
still works, its element simply isn't attached. Note the fallback when `data-filelist` is absent is
|
|
653
801
|
`parentNode.querySelector('.fdy-filelist')`, so a bare dropzone will adopt a list that happens to
|
|
654
802
|
share its parent; give the dropzone its own container if you mean "no list".
|
|
@@ -675,7 +823,7 @@ A consistent filter row of `.fdy-field`s with a width rhythm (`--w-sm` · defaul
|
|
|
675
823
|
`--w-xl` · `--w-2xl`), one `--w-grow` field (usually search) absorbing the slack, and
|
|
676
824
|
`.fdy-filterbar__actions` pinned at the end. A control with **no label of its own** (a
|
|
677
825
|
`.fdy-check`, `.fdy-switch`, `.fdy-radio`, or a lone `.fdy-btn`) is given `--control-h` so it
|
|
678
|
-
centres on the input line instead of hanging below it
|
|
826
|
+
centres on the input line instead of hanging below it. The bar aligns `flex-end` to line up
|
|
679
827
|
labelled fields, which would otherwise drop a 20px checkbox below a 32px input's centre.
|
|
680
828
|
`--actions-inline` keeps the actions on the control
|
|
681
829
|
line. Zero JS. Composes with `.fdy-table-toolbar` on a shared baseline.
|
|
@@ -705,7 +853,7 @@ Linear multi-step flow: marker → check, one panel at a time, back/next. Needs
|
|
|
705
853
|
|
|
706
854
|
**`__btn` is optional.** For a **read-only indicator** put `__marker` + `__label` straight in the
|
|
707
855
|
`<li>`; for a **navigable** stepper wrap them in `<button class="fdy-step__btn">` (add `disabled`
|
|
708
|
-
for a step that is not reachable yet). Both render identically
|
|
856
|
+
for a step that is not reachable yet). Both render identically; the marker carries its own lift
|
|
709
857
|
over the connector line, so omitting `__btn` cannot make the connector draw through the numbers.
|
|
710
858
|
|
|
711
859
|
```html
|
|
@@ -732,23 +880,23 @@ Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-tab
|
|
|
732
880
|
</div>
|
|
733
881
|
```
|
|
734
882
|
|
|
735
|
-
**Frozen axes
|
|
883
|
+
**Frozen axes, `.fdy-table--sticky`.** For a grid read against two axes at once (a rate
|
|
736
884
|
matrix, a timetable) or a wide one whose identity columns must stay visible. `.fdy-table--sticky`
|
|
737
885
|
is the enabler: it separates the borders, because a collapsed border belongs to the table rather
|
|
738
886
|
than the cell and scrolls out from under whatever you froze. Then ask for the axes you have:
|
|
739
887
|
|
|
740
|
-
- **Header row
|
|
888
|
+
- **Header row:** add `.fdy-table--sticky-head`, and put `.fdy-table-scroll--frozen` on the
|
|
741
889
|
wrapper (it scrolls both ways and is the scrollport the freeze resolves against; size it with
|
|
742
890
|
`--fdy-table-frozen-h`, default `30rem`). It is a separate modifier because a table that only
|
|
743
891
|
freezes columns scrolls with the **page**, and `top:0` against the page sticks the header under
|
|
744
|
-
the viewport edge
|
|
745
|
-
- **Columns
|
|
892
|
+
the viewport edge, over the app's own top bar.
|
|
893
|
+
- **Columns:** put `.fdy-table__freeze` on **every cell of the column, header included**, and give
|
|
746
894
|
each frozen column its own `--fdy-freeze-left`: `0` for the first, then the summed widths of the
|
|
747
895
|
frozen columns to its left. Mark the last one `.fdy-table__freeze--edge` so the reader can see
|
|
748
896
|
where the frozen block ends. One frozen column needs no variable at all.
|
|
749
897
|
|
|
750
898
|
The offsets are the caller's job because they depend on rendered column widths, which CSS cannot
|
|
751
|
-
sum
|
|
899
|
+
sum, so measure them once after render (and on resize) and set the variable.
|
|
752
900
|
|
|
753
901
|
```html
|
|
754
902
|
<div class="fdy-table-scroll fdy-table-scroll--frozen" style="--fdy-table-frozen-h:26rem">
|
|
@@ -769,7 +917,76 @@ sum — measure them once after render (and on resize) and set the variable.
|
|
|
769
917
|
```
|
|
770
918
|
|
|
771
919
|
## Data table — `.fdy-datatable`
|
|
772
|
-
> **Typed wrapper: `<FdyTable
|
|
920
|
+
> **Typed wrapper: `<FdyTable>`**. controlled: `columns` + `rows`, with sort/filter/page events (`update:pageIndex` · `onPageIndexChange` · `PageIndexChanged`) and `process` for driving a card list off the same processed set. The markup below is the raw enhancer path.
|
|
921
|
+
|
|
922
|
+
### Columns — the typed `FdyTableColumn<T>`
|
|
923
|
+
|
|
924
|
+
On Vue, React and Blazor, `columns` **is** the table: it is what those stacks write instead of the
|
|
925
|
+
markup hooks further down. Ten fields, `key` and `label` required. The authored doc comments live on
|
|
926
|
+
the type itself (`adapters/core/table-model.d.ts`, re-declared for Blazor in
|
|
927
|
+
`adapters/blazor/TableTypes.cs`); the list below is the discoverable copy, and `npm test` fails if
|
|
928
|
+
the two ever disagree.
|
|
929
|
+
|
|
930
|
+
| Field | Type | What it does |
|
|
931
|
+
|---|---|---|
|
|
932
|
+
| `key` | `string` | Row property key; the sort/filter identity and, unless `value` is set, the cell accessor. |
|
|
933
|
+
| `label` | `string` | Header label. |
|
|
934
|
+
| `labelHidden` | `boolean` | Render the label for assistive tech only; the header cell looks empty. |
|
|
935
|
+
| `sortable` | `boolean` | Show a sort toggle in the header. |
|
|
936
|
+
| `filter` | `'text' \| 'enum' \| 'number' \| 'date'` | Offer a column filter of this type (the funnel popover). |
|
|
937
|
+
| `align` | `'left' \| 'right' \| 'center'` | Cell alignment. Default `left`. |
|
|
938
|
+
| `mono` | `boolean` | Render cells in the monospace data font (`.fdy-mono`). |
|
|
939
|
+
| `sortType` | `'text' \| 'number' \| 'date'` | Override the comparator; defaults to one derived from `filter`, else `text`. |
|
|
940
|
+
| `value` | `(row: T) => unknown` | Custom accessor; defaults to `row[key]`. Feeds sort, filter and the default cell text. |
|
|
941
|
+
| `options` | `readonly string[]` | Explicit enum-filter options; they default to the distinct values in the rows on screen, so pass them in server-paged mode, where those are one page. |
|
|
942
|
+
|
|
943
|
+
Blazor spells the same fields `PascalCase` (`LabelHidden`, `SortType`), makes `Key`/`Label`
|
|
944
|
+
`required`, and adds one field with no TS twin: `Cell`, a `RenderFragment<TRow>` where Vue and React
|
|
945
|
+
use a slot.
|
|
946
|
+
|
|
947
|
+
A column of row controls is worth stating outright, because the obvious move is the wrong one:
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
const columns: FdyTableColumn<User>[] = [
|
|
951
|
+
{ key: 'email', label: 'Email', sortable: true, filter: 'text' },
|
|
952
|
+
{ key: 'total', label: 'Total', align: 'right', mono: true, sortType: 'number' },
|
|
953
|
+
// Not `label: ''`. A <th> with no text announces as nothing, and the label also names this
|
|
954
|
+
// column's sort button and filter popover, so it has to stay meaningful.
|
|
955
|
+
{ key: 'actions', label: 'Actions', labelHidden: true },
|
|
956
|
+
];
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
### Props — `<FdyTable>`
|
|
960
|
+
|
|
961
|
+
Controlled by default: hand it `columns` + `rows` and it filters, sorts and paginates internally;
|
|
962
|
+
provide the state prop for a concern and you own that concern instead. React names the callbacks
|
|
963
|
+
`onXChange`, Vue emits `update:x`, Blazor exposes `XChanged`.
|
|
964
|
+
|
|
965
|
+
| Prop | Type | What it does |
|
|
966
|
+
|---|---|---|
|
|
967
|
+
| `columns` | `ReadonlyArray<FdyTableColumn<Row>>` | The column contract documented above. |
|
|
968
|
+
| `rows` | `ReadonlyArray<Row>` | The current page's rows in server mode, the whole set in client mode. |
|
|
969
|
+
| `rowKey` | `(row: Row) => string \| number` | Row identity: keys, expansion, activation. |
|
|
970
|
+
| `sort?` · `onSortChange?` | `FdySortState \| null` · `(sort) => void` | Pass `sort`, **even as `null`**, to own sorting; omit it for the internal sort. |
|
|
971
|
+
| `filters?` · `onFiltersChange?` | `FdyFilterMap` · `(filters) => void` | The same, for filtering. |
|
|
972
|
+
| `page?` · `onPageChange?` | `FdyPageState` · `(page) => void` | Its **presence switches the table into server mode**: you own paging and get `{ index, size, total }` back. |
|
|
973
|
+
| `pageSize?` | `number` | Client mode: rows per page. Absent or 0 renders every row with no pager. |
|
|
974
|
+
| `pageIndex?` · `onPageIndexChange?` | `number` · `(index) => void` | Client mode: own the 0-based index while the table keeps filtering, sorting and paginating — this is what lets one external pager drive both a table and a card list. |
|
|
975
|
+
| `pageSizes?` · `onPageSizeChange?` | `readonly number[]` · `(size) => void` | Offer a rows-per-page control. Server mode reports the pick through the page event (read its `size`); client mode applies it itself and reports it too, so it works with nothing wired. |
|
|
976
|
+
| `pager?` | `boolean` | Default true. Turn it off to withhold the table's own footer when the screen renders one. |
|
|
977
|
+
| `loading?` | `boolean` | Show the in-table loading state. |
|
|
978
|
+
| `emptyText?` · `empty?` | `string` · `ReactNode` | The empty state as text, or as markup (React `empty`, Vue's `empty` slot). |
|
|
979
|
+
| `ariaLabel?` | `string` | Names the table for assistive tech. |
|
|
980
|
+
| `rowActivatable?` · `onRowActivate?` | `boolean` · `(row) => void` | Rows become focusable and activate on click, Enter or Space. |
|
|
981
|
+
| `rowClass?` | `(row: Row) => string \| undefined` | Per-row class hook, e.g. marking the selected row. |
|
|
982
|
+
| `expandedKeys?` · `renderRowDetail?` | `ReadonlyArray<string \| number>` · `(row) => ReactNode` | Controlled expansion: these keys get a full-width detail row. Vue uses the `row-detail` slot. |
|
|
983
|
+
| `renderCell?` | `(column, row, value) => ReactNode` | React. Custom cell rendering; Vue uses the `cell` slot. |
|
|
984
|
+
| `toolbar?` | `ReactNode` | React. Content above the table; Vue uses the `toolbar` slot. |
|
|
985
|
+
| `onProcess?` | `(result: { rows, total }) => void` | React. The processed page after filter/sort/paginate, in **both** modes. Vue emits `process`, Blazor calls it `Process`. Render the same set elsewhere — a card list, a summary, a CSV export — without re-deriving the pipeline. |
|
|
986
|
+
|
|
987
|
+
Blazor matches this surface (`LoadingText` and `EmptyContent` in place of `emptyText`/`empty`,
|
|
988
|
+
`Toolbar` and `RowDetail` as `RenderFragment`s) and is the one adapter that also exposes
|
|
989
|
+
`FiltersChanged`.
|
|
773
990
|
|
|
774
991
|
The interactive table: global search, sort, per-column filters, row selection + bulk bar,
|
|
775
992
|
pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` + `data-fdy-table`
|
|
@@ -788,20 +1005,24 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
|
|
|
788
1005
|
detail), `.fdy-table__state` (in-table empty/loading row)
|
|
789
1006
|
- Footer: `.fdy-table-footer` · `__info` (`data-fdy-table-info`) + optional `__size` (a label and a
|
|
790
1007
|
rows-per-page control) + `<nav class="fdy-pagination" data-fdy-table-pagination>`. The typed
|
|
791
|
-
wrappers render `FdyCombo` there
|
|
1008
|
+
wrappers render `FdyCombo` there, **never a native `<select>`**, whose open list is an OS menu no
|
|
792
1009
|
stylesheet reaches. In the raw path the control is app-authored markup (its options *are* the
|
|
793
1010
|
offer) carrying `data-fdy-table-page-size`; the enhancer only wires it, and listens for both
|
|
794
1011
|
`change` and the `fdy-change` a `.fdy-combo` emits, so either kind works.
|
|
795
1012
|
- Sort values: put the raw value in `data-sort-value` when the cell text is formatted.
|
|
796
|
-
- **Language
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1013
|
+
- **Language: English throughout, on every path.** The vanilla enhancers wrote Indonesian until
|
|
1014
|
+
2.0.0 while the typed wrappers wrote English, which meant an app mixing the two paths read as
|
|
1015
|
+
two products. A Blazor app mixes them by construction, since `FreedayBlazor.initAll` runs the
|
|
1016
|
+
enhancers. The defaults are now English everywhere, and the suite enforces it rather than
|
|
1017
|
+
stating it.
|
|
1018
|
+
**Dates are the exception, and deliberately so:** the datepicker formats through `Intl` using the
|
|
1019
|
+
page's `<html lang>`, falling back to `en`. An Indonesian app writes `lang="id"` and gets
|
|
1020
|
+
Indonesian month and weekday names with no configuration at all, which is a better hatch than anything
|
|
1021
|
+
the kit could invent, because it is the platform's.
|
|
1022
|
+
- **Overriding an enhancer's strings.** This is also the migration path for an Indonesian app
|
|
1023
|
+
upgrading past 2.0.0. Each enhancer keeps its strings in one `TEXT` table and
|
|
803
1024
|
reads them through `textOf()`, so any of them can be replaced per element with
|
|
804
|
-
**`data-fdy-text-<key>`**, the key kebab-cased
|
|
1025
|
+
**`data-fdy-text-<key>`**, the key kebab-cased. No forking, and no rendering the nodes yourself.
|
|
805
1026
|
(HTML lowercases attribute names, so a camelCase key like `filterText` is written
|
|
806
1027
|
`data-fdy-text-filter-text`; the run-together spelling still resolves.):
|
|
807
1028
|
|
|
@@ -820,44 +1041,52 @@ pagination. Needs `freeday-table.js`. Wrap the whole thing in `.fdy-datatable` +
|
|
|
820
1041
|
arrives overridable or not at all.
|
|
821
1042
|
|
|
822
1043
|
**Testing note:** a column's filter button and the dialog it opens deliberately share one
|
|
823
|
-
accessible name (`Filter <column>`)
|
|
1044
|
+
accessible name (`Filter <column>`), since a dialog named after its trigger is the normal pattern. In a
|
|
824
1045
|
Playwright/Testing-Library suite that means `getByLabel('Filter Name')` resolves to two elements;
|
|
825
1046
|
reach for `getByRole('button', { name: 'Filter Name' })` instead.
|
|
826
1047
|
|
|
827
1048
|
**Rows per page.** Pass `pageSizes` (Vue/React `:page-sizes="[10, 20, 50]"`, Blazor
|
|
828
1049
|
`PageSizes="…"`) and the footer grows a rows-per-page control between the range and the pager.
|
|
829
|
-
Omit it for none
|
|
1050
|
+
Omit it for none, which is the unchanged default. Picking a size keeps the reader on the row they were looking
|
|
830
1051
|
at rather than dropping them on page 1.
|
|
831
1052
|
|
|
832
|
-
- **server mode
|
|
1053
|
+
- **server mode:** reported through `update:page` / `onPageChange` / `PageChanged`, the same event
|
|
833
1054
|
as a page click, carrying the new `size`. Read `size` to tell the two apart.
|
|
834
|
-
- **client mode
|
|
1055
|
+
- **client mode:** the table applies it itself, so the control works with nothing wired, and also
|
|
835
1056
|
emits `update:pageSize` / `onPageSizeChange` / `PageSizeChanged` for a caller that wants to
|
|
836
1057
|
persist the choice. Changing the `pageSize` prop wins back.
|
|
837
1058
|
|
|
838
|
-
A footer with a size control stays visible on a single page
|
|
1059
|
+
A footer with a size control stays visible on a single page. Otherwise picking "100" on a
|
|
839
1060
|
ninety-row list would remove the only way back to twenty.
|
|
840
1061
|
|
|
841
|
-
**Typed wrapper: `<FdyTableFooter
|
|
1062
|
+
**Typed wrapper: `<FdyTableFooter>`.** The footer alone (`page` in, `update:page` /
|
|
842
1063
|
`onPageChange` / `PageChanged` out), for the one shape that cannot use the table's own: a
|
|
843
1064
|
**responsive** list, where a `.fdy-datatable` at `lg` and a `.fdy-list` below it are two renderings
|
|
844
1065
|
of one page of rows. A footer inside the table is inside the half a phone hides, so those screens
|
|
845
1066
|
render it once, outside both, with `pager={false}` on the table.
|
|
846
1067
|
|
|
1068
|
+
### Props — `<FdyTableFooter>`
|
|
1069
|
+
|
|
1070
|
+
| Prop | Type | What it does |
|
|
1071
|
+
|---|---|---|
|
|
1072
|
+
| `page` | `FdyPageState` = `{ index, size, total }` | The page being shown. `size` drives both the range text and the rows-per-page control's value. |
|
|
1073
|
+
| `onPageChange?` | `(page: FdyPageState) => void` | React. A page click or a size change. Vue emits `update:page`; Blazor `PageChanged`. |
|
|
1074
|
+
| `pageSizes?` | `readonly number[]` | Offer a rows-per-page control. Omit it and the footer is range + pager only. Picking a size reports the new `size` together with the index that still holds the row the reader was looking at. |
|
|
1075
|
+
|
|
847
1076
|
**Who draws the pager.** The table renders its own footer (range + pager) whenever there is more
|
|
848
1077
|
than one page. Two ways to take it over:
|
|
849
1078
|
|
|
850
|
-
- **client mode
|
|
851
|
-
- **either mode
|
|
1079
|
+
- **client mode:** pass `pageIndex` (with `pageSize`, without `page`) and drive it yourself;
|
|
1080
|
+
- **either mode:** pass `pager={false}` (Vue `:pager="false"`, Blazor `Pager="false"`) and render
|
|
852
1081
|
your own control. In server mode this is the only way: the app already owns the page there, and
|
|
853
|
-
was still being handed a second control. The typical shape is a responsive list
|
|
854
|
-
`.fdy-list` below it
|
|
1082
|
+
was still being handed a second control. The typical shape is a responsive list (a table at `lg`,
|
|
1083
|
+
`.fdy-list` below it) where one pager has to serve both views so they cannot disagree.
|
|
855
1084
|
|
|
856
1085
|
Do not hide the footer with CSS. `display:none` works, but it reaches into a component's internals
|
|
857
1086
|
and breaks the moment the class changes.
|
|
858
1087
|
|
|
859
1088
|
## Pagination — `.fdy-pagination`
|
|
860
|
-
The block class on the `<nav>` is a **structural hook only
|
|
1089
|
+
The block class on the `<nav>` is a **structural hook only**. It carries no rule of its own; the
|
|
861
1090
|
`__list` / `__link` / `__ellipsis` elements do all the styling, and the data table targets
|
|
862
1091
|
`data-fdy-table-pagination`. Keep it on the wrapper anyway, for consistency with the rest of the kit.
|
|
863
1092
|
|
|
@@ -872,7 +1101,26 @@ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__ic
|
|
|
872
1101
|
replaces.
|
|
873
1102
|
|
|
874
1103
|
## Charts — `data-fdy-chart`
|
|
875
|
-
> **Typed wrapper: `<FdyChart
|
|
1104
|
+
> **Typed wrapper: `<FdyChart>`**. data props in all three (`type` + `values` / `series`); it repaints on data change, so `FreedayChart.update(el)` is only for the raw path below.
|
|
1105
|
+
|
|
1106
|
+
### Props — `<FdyChart>`
|
|
1107
|
+
|
|
1108
|
+
| Prop | Type | What it does |
|
|
1109
|
+
|---|---|---|
|
|
1110
|
+
| `type` | `'line' \| 'area' \| 'bar' \| 'sparkline' \| 'donut'` | Which chart to draw. |
|
|
1111
|
+
| `values?` | `ReadonlyArray<number>` | One series. |
|
|
1112
|
+
| `series?` | `ReadonlyArray<FdyChartSeries>` | Several: each `{ label, values }`. Use one or the other, not both. |
|
|
1113
|
+
| `labels?` | `ReadonlyArray<string>` | Category labels: the x axis, or the donut's slices. |
|
|
1114
|
+
| `format?` | `'number' \| 'percent' \| 'currency'` | How values are written in labels and the donut centre. |
|
|
1115
|
+
| `stacked?` | `boolean` | Bar charts: stack the series instead of grouping them side by side. |
|
|
1116
|
+
| `legend?` | `'auto' \| 'always' \| 'none'` | `auto` shows one when there is more than one series. |
|
|
1117
|
+
| `colors?` | `ReadonlyArray<string>` | Per-series override. Each entry is a semantic token name (`primary`, `accent`, `success`, `warning`, `danger`, `info`) or a categorical slot `chart-1`…`chart-8`. Omit for the default fixed-order palette. |
|
|
1118
|
+
| `color?` | `string` | The same, for a single-series chart (sparkline, simple bar or line). |
|
|
1119
|
+
| `center?` | `string \| number` | Donut only: what to print in the middle. |
|
|
1120
|
+
| `aria-label?` | `string` | React. The chart's accessible name, and in practice the **whole** text alternative: a named chart's painted subtree is `aria-hidden`, see the a11y note in this section. Vue and Blazor set it as a plain attribute (Blazor's parameter is `AriaLabel`). |
|
|
1121
|
+
| `children?` | `ReactNode` | React. Content inside the element before the chart paints. The renderer replaces it, so it is a pre-paint placeholder, not a text alternative. |
|
|
1122
|
+
|
|
1123
|
+
Blazor adds `ChildContent` for the same placeholder role.
|
|
876
1124
|
|
|
877
1125
|
Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`; call
|
|
878
1126
|
`FreedayChart.update(el)` after changing data (or use `FdyChart` in Vue/React/Blazor).
|
|
@@ -881,27 +1129,27 @@ Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`
|
|
|
881
1129
|
- Roots: `.fdy-sparkline` · `.fdy-bars` · `.fdy-donut` · `.fdy-chart-xy` (built for line/area/bar)
|
|
882
1130
|
- Data: `data-values="1,2,3"` (single series) or `data-series='[{"label":"A","values":[…]}]'`
|
|
883
1131
|
(multi), `data-labels="Jan,Feb"`, `data-fdy-stacked`, `data-fdy-center` (donut)
|
|
884
|
-
- Colour: `data-fdy-color="primary"` or `data-fdy-colors="success,warning,danger"`
|
|
1132
|
+
- Colour: `data-fdy-color="primary"` or `data-fdy-colors="success,warning,danger"` for semantic token
|
|
885
1133
|
names **or** `chart-1`…`chart-8` slots to pin a category's colour. Multi-series defaults to the
|
|
886
1134
|
validated categorical palette `--chart-1`…`--chart-8`.
|
|
887
1135
|
- Format: `data-fdy-format="number|percent|currency"`; legend `data-fdy-legend` (`none` to drop);
|
|
888
1136
|
axes `data-fdy-axes`
|
|
889
|
-
- Sizing is already set by the kit
|
|
1137
|
+
- Sizing is already set by the kit. Override **these**, never a `height` on the chart root (a
|
|
890
1138
|
root height fights `aspect-ratio` instead of setting it): `.fdy-chart-xy__plot` is `width:100%`
|
|
891
1139
|
+ `aspect-ratio:16/9` + `min-height:8rem`; `.fdy-bars` is a fixed `height:9rem`; `.fdy-sparkline`
|
|
892
1140
|
is an inline-block `8rem × 2.25rem`.
|
|
893
1141
|
- Axis type: `--fdy-chart-tick-size` (default `var(--text-xs)`) sets the y-tick and x-label size on
|
|
894
|
-
`.fdy-chart-xy`. It is a real CSS size
|
|
895
|
-
one user unit is one pixel
|
|
1142
|
+
`.fdy-chart-xy`. It is a real CSS size: a cartesian chart's `viewBox` is measured to its plot, so
|
|
1143
|
+
one user unit is one pixel, and the renderer reads it back to size the y-gutter and to decide how
|
|
896
1144
|
many x-labels fit. Set the token rather than `font-size` on `__tick`/`__xlabel`, or the gutter and
|
|
897
1145
|
the autoskip will still be computed from the old size.
|
|
898
1146
|
- A11y: every chart is `role="img"` + an **author-supplied** `aria-label`, and that label is the
|
|
899
|
-
*entire* text alternative
|
|
900
|
-
marks everything it draws `aria-hidden="true"`
|
|
901
|
-
SVG
|
|
1147
|
+
*entire* text alternative, so write one that carries the numbers rather than just the shape. The renderer
|
|
1148
|
+
marks everything it draws `aria-hidden="true"` (legend, bar values and labels, donut centre, the
|
|
1149
|
+
SVG), so nothing inside the chart competes with your label or repeats it. `role="img"` alone would
|
|
902
1150
|
**not** have done that: it is Children Presentational per ARIA, but browsers keep the subtree in
|
|
903
1151
|
the accessibility tree anyway, so the kit hides it rather than relying on the spec. That hiding
|
|
904
|
-
needs a name to work from
|
|
1152
|
+
needs a name to work from: a chart with **no** `aria-label`/`aria-labelledby` keeps its contents
|
|
905
1153
|
exposed, because an image with neither a name nor any text is worse than one that leaks its
|
|
906
1154
|
legend. Any `<table>`/text placed inside the element is **pre-render only** and is replaced on
|
|
907
1155
|
render. Good: `aria-label="Monthly posting trend: Posted 62% → 88%, Draft 24% → 16%"`.
|
|
@@ -911,7 +1159,7 @@ Pure SVG/CSS, no dependency, re-colours with the theme. Needs `freeday-chart.js`
|
|
|
911
1159
|
`__center` `__hit`; `.fdy-sparkline__line` `__area` `__dot`.
|
|
912
1160
|
|
|
913
1161
|
## Tree view — `.fdy-tree`
|
|
914
|
-
Native `<details>` hierarchy
|
|
1162
|
+
Native `<details>` hierarchy, with zero JS for expand/collapse. `.fdy-tree__branch` on `<details>`,
|
|
915
1163
|
`.fdy-tree__leaf` on a leaf `<li>`, plus `__chevron` and `__icon` svgs inside `<summary>`.
|
|
916
1164
|
`.fdy-tree--checkbox` + `data-fdy-tree` (needs `freeday-tree.js`) adds cascading selection:
|
|
917
1165
|
`.fdy-tree__check` on each `<input class="fdy-checkbox">`; checking a branch checks its children,
|
|
@@ -919,19 +1167,19 @@ a partial set makes the branch indeterminate. Checking never toggles expansion.
|
|
|
919
1167
|
|
|
920
1168
|
## List (flat rows) — `.fdy-list`
|
|
921
1169
|
The **flat** row container: one bordered surface, hairline dividers, **no shadow**. This is what a
|
|
922
|
-
responsive `.fdy-datatable` should become below `md
|
|
1170
|
+
responsive `.fdy-datatable` should become below `md`, rather than a stack of `.fdy-card`s, which carries
|
|
923
1171
|
`--shadow-lift` and turns ten rows into ten floating objects.
|
|
924
1172
|
|
|
925
1173
|
- `.fdy-list` on a `<ul>`/`<ol>` (list-style is reset for you) or a `<div>`
|
|
926
|
-
- `.fdy-list__row`
|
|
1174
|
+
- `.fdy-list__row` is one row; `--interactive` for hover feedback, `--button` when the row **is** the
|
|
927
1175
|
control (render it as a real `<button>`/`<a>`; the UA box is reset without losing the list surface)
|
|
928
1176
|
- Row internals: `.fdy-list__main` (truncating stack) → `.fdy-list__title` + `.fdy-list__meta`, and
|
|
929
1177
|
`.fdy-list__aside` pinned right
|
|
930
|
-
- **Disabled
|
|
1178
|
+
- **Disabled:** `disabled` on the `<button>` (or `aria-disabled="true"` when the row is an `<a>`/
|
|
931
1179
|
`<div>`) dims the row and withdraws the hover tint and the pointer cursor, same as every other
|
|
932
1180
|
control in the kit. Do not hand-roll it: an undimmed row that still lights up under the pointer
|
|
933
1181
|
reads as clickable while it is refusing input.
|
|
934
|
-
- Not `.fdy-list-reset
|
|
1182
|
+
- Not `.fdy-list-reset`, which only strips UA bullets/indent from a semantic list.
|
|
935
1183
|
|
|
936
1184
|
```html
|
|
937
1185
|
<ul class="fdy-list">
|
|
@@ -982,11 +1230,11 @@ Every field is optional; it returns the toast element. Classes (rendered for you
|
|
|
982
1230
|
`__body` `__title` `__text` `__close`.
|
|
983
1231
|
|
|
984
1232
|
**Above modals, and not by z-index.** The region is a `popover` and re-enters the **top layer** as
|
|
985
|
-
each toast lands, because `.fdy-modal` is a native `<dialog
|
|
1233
|
+
each toast lands, because `.fdy-modal` is a native `<dialog>`. See the note under Modal below. If
|
|
986
1234
|
a toast of yours is hidden behind something, a bigger `z-index` is not the fix; check whether that
|
|
987
1235
|
something is in the top layer too. Where the Popover API is unavailable (before Chrome 114 /
|
|
988
1236
|
Safari 17 / Firefox 125) the region falls back to a fixed block at `z-index: 200`, which cannot
|
|
989
|
-
clear an open modal
|
|
1237
|
+
clear an open modal, the message is still announced to a screen reader.
|
|
990
1238
|
|
|
991
1239
|
## Tooltip — `.fdy-tooltip`
|
|
992
1240
|
Hover/focus only, never the sole carrier of information. Wrap in `.fdy-tooltip-wrap`; the trigger
|
|
@@ -1006,7 +1254,7 @@ Size-matched placeholders so nothing shifts when data lands: `--title` `--text`
|
|
|
1006
1254
|
# Navigation
|
|
1007
1255
|
|
|
1008
1256
|
## Nav (menu) — `.fdy-nav`
|
|
1009
|
-
Navigation links
|
|
1257
|
+
Navigation links, **vertical by default** (the app shell sidebar), horizontal with `--horizontal`. Items are `<a class="fdy-nav__item">` with
|
|
1010
1258
|
`__icon` / `__label` / `__badge`; the current one gets `aria-current="page"`.
|
|
1011
1259
|
`--flat` drops the surface. Nested groups are native `<details>`:
|
|
1012
1260
|
|
|
@@ -1030,17 +1278,17 @@ Navigation links — **vertical by default** (the app shell sidebar), horizontal
|
|
|
1030
1278
|
</header>
|
|
1031
1279
|
```
|
|
1032
1280
|
|
|
1033
|
-
> **Which one for navigation?** A nav link is a link marked `aria-current="page"
|
|
1281
|
+
> **Which one for navigation?** A nav link is a link marked `aria-current="page"`, never
|
|
1034
1282
|
> `aria-selected` (invalid ARIA on an anchor) and never `role="tab"`. Use `.fdy-nav--horizontal`
|
|
1035
1283
|
> for an application's **primary** navigation. For **routed sub-navigation** that should look like
|
|
1036
1284
|
> tabs (`/settings/profile` · `/settings/billing`), put `.fdy-tabs__list` / `.fdy-tabs__tab` on
|
|
1037
|
-
> plain `<a>`s
|
|
1285
|
+
> plain `<a>`s, since those classes honour `aria-current="page"` too, and do **not** add the tab roles
|
|
1038
1286
|
> or `freeday-tabs.js`, which promise a keyboard contract routes do not have.
|
|
1039
1287
|
|
|
1040
1288
|
## App bar — `.fdy-appbar`
|
|
1041
1289
|
Standalone top bar (distinct from the shell's `.fdy-app__topbar`). Modifiers `--sticky`
|
|
1042
1290
|
`--elevated` `--dense` `--primary` (on-colour controls). Parts `__brand` `__spacer` `__actions`.
|
|
1043
|
-
It ships **no link class of its own
|
|
1291
|
+
It ships **no link class of its own**. Put `<nav class="fdy-nav fdy-nav--horizontal">` in it
|
|
1044
1292
|
for primary navigation (see Nav above).
|
|
1045
1293
|
|
|
1046
1294
|
## Breadcrumb — `.fdy-breadcrumb`
|
|
@@ -1051,7 +1299,7 @@ for primary navigation (see Nav above).
|
|
|
1051
1299
|
## Tabs — `.fdy-tabs`
|
|
1052
1300
|
APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
|
|
1053
1301
|
`.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
|
|
1054
|
-
so the same look serves routed sub-navigation built from plain links
|
|
1302
|
+
so the same look serves routed sub-navigation built from plain links. See the note under Nav.
|
|
1055
1303
|
|
|
1056
1304
|
```html
|
|
1057
1305
|
<div class="fdy-tabs" data-fdy-tabs>
|
|
@@ -1069,13 +1317,29 @@ so the same look serves routed sub-navigation built from plain links — see the
|
|
|
1069
1317
|
# Overlays
|
|
1070
1318
|
|
|
1071
1319
|
## Modal — `.fdy-modal`
|
|
1072
|
-
> **Typed wrapper: `<FdyModal
|
|
1320
|
+
> **Typed wrapper: `<FdyModal>`**. Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
1321
|
+
|
|
1322
|
+
### Props — `<FdyModal>`
|
|
1323
|
+
|
|
1324
|
+
| Prop | Type | What it does |
|
|
1325
|
+
|---|---|---|
|
|
1326
|
+
| `open` | `boolean` | Controlled: your state decides. The wrapper cancels the native `cancel` event so the DOM can never disagree with it. |
|
|
1327
|
+
| `title` | `string` (Vue) · `ReactNode` | Names the dialog through `aria-labelledby`. Vue also offers a `title` slot when the heading needs markup. |
|
|
1328
|
+
| `onClose` | `() => void` | React. Vue emits `close`. Fires from Escape, a backdrop click and the ×, all three of which exist only under `dismissible`. |
|
|
1329
|
+
| `size?` | `'sm' \| 'md' \| 'lg' \| 'wide'` | Dialog width. |
|
|
1330
|
+
| `dismissible?` | `boolean` | Whether Escape, the backdrop and the × can close it. Off means the reader has to take a footer action, which is the point of a blocking confirm. |
|
|
1331
|
+
| `footer?` | `ReactNode` | React. Vue: the `footer` slot. |
|
|
1332
|
+
| `children?` | `ReactNode` | React. The body. Vue: the default slot. |
|
|
1333
|
+
|
|
1334
|
+
Blazor binds `@bind-Open` and takes `TitleContent` / `ChildContent` / `FooterContent`, plus two
|
|
1335
|
+
things Vue and React do not have: `OnClose` and **`CloseLabel`**. The × in Vue and React is labelled
|
|
1336
|
+
`Close` and cannot be renamed.
|
|
1073
1337
|
|
|
1074
1338
|
Native `<dialog>`: focus trap, Esc and backdrop come from the browser. Sizes `--sm` `--md` `--lg`
|
|
1075
1339
|
`--wide`; `--cfl` for the choose-from-list dialog.
|
|
1076
1340
|
|
|
1077
1341
|
**It is in the TOP LAYER.** `showModal()` paints the dialog above every stacking context on the
|
|
1078
|
-
page, so no `z-index` anywhere can put anything over it
|
|
1342
|
+
page, so no `z-index` anywhere can put anything over it. Only another top-layer element (a
|
|
1079
1343
|
`popover`, or another modal) can. It also makes the rest of the document inert, which means
|
|
1080
1344
|
`elementFromPoint` outside the dialog returns the dialog: do not use a hit test to check what is
|
|
1081
1345
|
painted on top of a modal, because it will tell you the dialog is, even when it visibly is not. Parts `__header` `__title` `__body` `__footer`
|
|
@@ -1083,34 +1347,50 @@ painted on top of a modal, because it will tell you the dialog is, even when it
|
|
|
1083
1347
|
`data-close` on any button that should close it.
|
|
1084
1348
|
|
|
1085
1349
|
**Stacking two overlays is supported.** Modal and drawer are both `<dialog>` opened with
|
|
1086
|
-
`showModal()`, so both enter the top layer and paint in the order they were opened
|
|
1350
|
+
`showModal()`, so both enter the top layer and paint in the order they were opened. A confirm
|
|
1087
1351
|
modal opened from inside an open drawer paints **above** it, and neither needs a `z-index` for
|
|
1088
1352
|
that. Escape closes the topmost first, so cancelling returns to the still-open drawer; a second
|
|
1089
1353
|
Escape then closes the drawer. Closing them out of order is fine too: closing the drawer while the
|
|
1090
1354
|
modal is up leaves the modal open, painted and dismissible.
|
|
1091
1355
|
|
|
1092
1356
|
One caveat, and it is the browser's rather than the kit's: **the second overlay's Escape can only
|
|
1093
|
-
be refused if both were opened by a real gesture.** A `showModal()` that runs from script
|
|
1094
|
-
or after an `await` that outlived the click
|
|
1357
|
+
be refused if both were opened by a real gesture.** A `showModal()` that runs from script (a timer,
|
|
1358
|
+
or after an `await` that outlived the click) still closes only the topmost, but its `cancel` event
|
|
1095
1359
|
arrives **non-cancelable**, so `preventDefault()` on it is ignored and an "unsaved changes, stay
|
|
1096
1360
|
open" guard silently stops working. Routing Escape through app state instead, which is what the
|
|
1097
1361
|
typed wrappers do, is unaffected: the dialog closes and your state follows it.
|
|
1098
1362
|
|
|
1099
1363
|
*Correction to 1.51.1, which said such a dialog has its close watcher grouped with the one below so
|
|
1100
1364
|
that a single Escape closes both.* That happens only on a page that has received **no user input at
|
|
1101
|
-
all
|
|
1365
|
+
all**: a test harness, never an app. Six seconds after one click the overlays are still
|
|
1102
1366
|
independent. Open overlays with real clicks in a test, or a stacking assertion fails for a reason
|
|
1103
1367
|
that is not the kit. Both rules measured on Chromium 133 and 151, and pinned as a matrix in
|
|
1104
1368
|
`browser/overlay-stack.mjs`.
|
|
1105
1369
|
|
|
1106
1370
|
## Drawer — `.fdy-drawer`
|
|
1107
|
-
> **Typed wrapper: `<FdyDrawer
|
|
1371
|
+
> **Typed wrapper: `<FdyDrawer>`**. Vue (`:open` + `@close`) · React (`open` + `onClose`) · Blazor (`@bind-Open`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
1372
|
+
|
|
1373
|
+
### Props — `<FdyDrawer>`
|
|
1374
|
+
|
|
1375
|
+
The modal's props with `side` in place of `size`; the behaviour notes there apply here too.
|
|
1376
|
+
|
|
1377
|
+
| Prop | Type | What it does |
|
|
1378
|
+
|---|---|---|
|
|
1379
|
+
| `open` | `boolean` | Controlled, as with the modal. |
|
|
1380
|
+
| `title` | `string` (Vue) · `ReactNode` | Names the drawer (`aria-labelledby`). |
|
|
1381
|
+
| `onClose` | `() => void` | React. Vue emits `close`. |
|
|
1382
|
+
| `side?` | `'left' \| 'right'` | Which edge it slides from. |
|
|
1383
|
+
| `dismissible?` | `boolean` | Escape, backdrop and × on or off. |
|
|
1384
|
+
| `footer?` | `ReactNode` | React. Vue: the `footer` slot. |
|
|
1385
|
+
| `children?` | `ReactNode` | React. The body. Vue: the default slot. |
|
|
1386
|
+
|
|
1387
|
+
Blazor: `@bind-Open`, `TitleContent` / `ChildContent` / `FooterContent`, `OnClose`, `CloseLabel`.
|
|
1108
1388
|
|
|
1109
|
-
Temporary side panel on native `<dialog
|
|
1389
|
+
Temporary side panel on native `<dialog>`, left by default, `--right` to flip. Parts `__header`
|
|
1110
1390
|
`__title` `__body` `__footer` `__close`. Open it from any
|
|
1111
1391
|
`<button data-fdy-drawer="<dialog id>">`. Needs `freeday-drawer.js`.
|
|
1112
1392
|
|
|
1113
|
-
Opened with `showModal()`, so it is in the **top layer** on the same terms as the modal
|
|
1393
|
+
Opened with `showModal()`, so it is in the **top layer** on the same terms as the modal, including
|
|
1114
1394
|
opening a modal over an open drawer, which is supported and described under
|
|
1115
1395
|
[Modal → *Stacking two overlays*](#modal--fdy-modal). It slides in on `transform` and never animates
|
|
1116
1396
|
opacity: wait on its position, not on `opacity`, if you need to know when it has arrived.
|
|
@@ -1132,25 +1412,25 @@ is `role="group" aria-roledescription="slide"`.
|
|
|
1132
1412
|
## Card — `.fdy-card`
|
|
1133
1413
|
Parts `__body` `__title` `__desc` `__footer`. Modifiers:
|
|
1134
1414
|
|
|
1135
|
-
- `--elevated`
|
|
1136
|
-
- `--interactive`
|
|
1137
|
-
only:** it is the one affordance in the kit whose correctness lives outside it
|
|
1415
|
+
- `--elevated` raises it (use sparingly; most surfaces are flat)
|
|
1416
|
+
- `--interactive` gives cursor + hover-lift to a card that *has* a click handler. **Presentational
|
|
1417
|
+
only:** it is the one affordance in the kit whose correctness lives outside it, because the CSS cannot
|
|
1138
1418
|
know whether a handler exists, so the modifier without a control (a card that looks clickable and
|
|
1139
1419
|
swallows every click) and a control without the modifier both fail silently. Pair it with a real
|
|
1140
|
-
control
|
|
1141
|
-
- `--button`
|
|
1420
|
+
control, normally the stretched target below.
|
|
1421
|
+
- `--button` is for a card that **is** the control: render it as `<button>` and add this to reset
|
|
1142
1422
|
the UA button box **without** losing the card surface/border
|
|
1143
|
-
- `.fdy-card--button` never replaces keyboard semantics
|
|
1423
|
+
- `.fdy-card--button` never replaces keyboard semantics. A clickable card must be a real
|
|
1144
1424
|
`<button>` or `<a>`.
|
|
1145
|
-
- **Disabled
|
|
1425
|
+
- **Disabled:** `disabled` (or `aria-disabled="true"`) dims the card and withdraws the pointer
|
|
1146
1426
|
cursor and the `--interactive` hover-lift.
|
|
1147
1427
|
- The card is `position:relative`, so a badge or ribbon you absolutely position inside it anchors
|
|
1148
|
-
to the card. That is also what keeps hidden labels inside it from escaping
|
|
1428
|
+
to the card. That is also what keeps hidden labels inside it from escaping. See *Containment*.
|
|
1149
1429
|
|
|
1150
1430
|
### One card, one primary action, one escape hatch
|
|
1151
1431
|
|
|
1152
1432
|
A card with **two** actions cannot be `--button`: interactive content nested in a `<button>` is
|
|
1153
|
-
invalid HTML. Give the primary control `.fdy-btn--stretch
|
|
1433
|
+
invalid HTML. Give the primary control `.fdy-btn--stretch`: its hit area covers the whole card,
|
|
1154
1434
|
while it stays a real `<button>`/`<a>` with real keyboard semantics.
|
|
1155
1435
|
|
|
1156
1436
|
```html
|
|
@@ -1167,34 +1447,32 @@ while it stays a real `<button>`/`<a>` with real keyboard semantics.
|
|
|
1167
1447
|
```
|
|
1168
1448
|
|
|
1169
1449
|
- **The escape hatch needs nothing.** Every focusable element in a card that holds a stretched
|
|
1170
|
-
target is raised above the overlay automatically
|
|
1450
|
+
target is raised above the overlay automatically. Forgetting a `z-index` here fails silently, and
|
|
1171
1451
|
a silent failure is what this pattern exists to remove. The `position` that raising needs is only a
|
|
1172
|
-
**default** (zero-specificity), so a control you pin yourself
|
|
1452
|
+
**default** (zero-specificity), so a control you pin yourself (a corner dismiss, a favourite star)
|
|
1173
1453
|
keeps its own `position:absolute` and is raised anyway.
|
|
1174
1454
|
- **Name the stretched button for what the whole card does** ("Open workspace Alpha", not "Open").
|
|
1175
1455
|
Its label is the accessible name of the entire hit area.
|
|
1176
|
-
- **Do not add `.fdy-btn--stretch` to more than one control per card
|
|
1456
|
+
- **Do not add `.fdy-btn--stretch` to more than one control per card.** The last one wins the
|
|
1177
1457
|
overlap, and which one that is depends on source order.
|
|
1178
1458
|
- Text under the overlay is no longer selectable; that is the cost of the pattern, not a bug.
|
|
1179
1459
|
- **Cards and list rows.** The overlay anchors to the nearest *positioned* ancestor, and both hosts
|
|
1180
1460
|
supply one: `.fdy-card` always, `.fdy-list__row` when it contains a stretched target. Everything
|
|
1181
|
-
above applies to a row unchanged
|
|
1461
|
+
above applies to a row unchanged: same markup, same automatic raise. In any *other* container,
|
|
1182
1462
|
make the element `position:relative` yourself, or the overlay resolves against whatever is
|
|
1183
1463
|
positioned further up (in a list that used to be `.fdy-list` itself, so every row's target covered
|
|
1184
|
-
the whole list and the last one in the DOM won
|
|
1464
|
+
the whole list and the last one in the DOM won, so a click on row one opened row two).
|
|
1185
1465
|
A row whose *whole* surface is one control is still better as `.fdy-list__row--button`.
|
|
1186
1466
|
- `.fdy-btn` nudges itself on `:hover`/`:active`, and a transformed element becomes the containing
|
|
1187
|
-
block for its own absolutely positioned descendants
|
|
1467
|
+
block for its own absolutely positioned descendants, so a hand-rolled `::after{inset:0}` overlay
|
|
1188
1468
|
**re-anchors to the button mid-gesture** and the click never lands. `--stretch` neutralises those
|
|
1189
1469
|
transforms and moves the press feedback to the card. This is why the pattern is shipped rather
|
|
1190
1470
|
than documented.
|
|
1191
1471
|
|
|
1192
1472
|
## Badge — `.fdy-badge`
|
|
1193
|
-
Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only
|
|
1194
|
-
the text carries the meaning.
|
|
1473
|
+
Inline status pill: `--success` `--warning` `--danger` `--info` `--outline`. Never colour-only: the text carries the meaning.
|
|
1195
1474
|
|
|
1196
|
-
`--tone-1`…`--tone-8` for a status vocabulary larger than the semantic palette. Semantics first
|
|
1197
|
-
a state that IS good, bad or waiting takes `--success` / `--danger` / `--warning`; these are for
|
|
1475
|
+
`--tone-1`…`--tone-8` for a status vocabulary larger than the semantic palette. Semantics first: a state that IS good, bad or waiting takes `--success` / `--danger` / `--warning`; these are for
|
|
1198
1476
|
the rest, so two states a workflow distinguishes never share a look. Map each status to a FIXED
|
|
1199
1477
|
tone, never to a hash of the string: a status is a closed set, and a hash reassigns colours the
|
|
1200
1478
|
moment the set grows.
|
|
@@ -1207,19 +1485,19 @@ Put the count in the wrapper's `aria-label` and mark the overlay `aria-hidden="t
|
|
|
1207
1485
|
## Avatar — `.fdy-avatar`
|
|
1208
1486
|
Initials or image. Sizes `--sm` / default / `--lg`; `.fdy-avatar-group` stacks them (label the
|
|
1209
1487
|
group). `--tone-1`…`--tone-8` are decorative tints (WCAG AA in both themes) to distinguish
|
|
1210
|
-
same-initial avatars
|
|
1488
|
+
same-initial avatars. Hash the index off the **full** name, not the initials.
|
|
1211
1489
|
|
|
1212
1490
|
Sizes: `--xs` 1.5rem · *(base)* 2.5rem · `--sm` 2rem · `--lg` 3.5rem. Use **`--xs` inside a control**
|
|
1213
|
-
|
|
1491
|
+
because `.fdy-btn--sm` is itself 2rem tall, so an `--sm` avatar fills a small button edge to edge and the
|
|
1214
1492
|
ghost border cuts across the circle.
|
|
1215
1493
|
|
|
1216
1494
|
## Chip — `.fdy-chip`
|
|
1217
1495
|
Three roles inside a `.fdy-chips` row:
|
|
1218
1496
|
|
|
1219
|
-
- **static / removable
|
|
1497
|
+
- **static / removable:** `<span class="fdy-chip">`, `--primary` for an active filter, with
|
|
1220
1498
|
`.fdy-chip__remove` (`aria-label` says what it removes)
|
|
1221
|
-
- **choice
|
|
1222
|
-
- **filter
|
|
1499
|
+
- **choice:** `<button class="fdy-chip fdy-chip--choice" aria-pressed>` toggle
|
|
1500
|
+
- **filter:** `--filter`, same but with a `.fdy-chip__check` mark
|
|
1223
1501
|
|
|
1224
1502
|
Interactive chips are managed by `freeday-chip.js`: wrap the row in `data-fdy-chips` (+
|
|
1225
1503
|
`data-single` for pick-one, `data-label` for the group name), give each `data-value`.
|
|
@@ -1233,5 +1511,5 @@ by the accompanying text, not the marker colour alone.
|
|
|
1233
1511
|
---
|
|
1234
1512
|
|
|
1235
1513
|
*Anything not listed here is not part of the public surface. If a screen needs a primitive that
|
|
1236
|
-
isn't in this file, it probably belongs in the kit
|
|
1514
|
+
isn't in this file, it probably belongs in the kit. Open an issue instead of re-inventing it
|
|
1237
1515
|
locally.*
|