@openreachtech/furo-vue 1.6.0 → 1.7.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/README.ja.md +2 -0
- package/README.md +2 -0
- package/docs/design-tokens.md +419 -0
- package/docs/furo-accordion.md +109 -0
- package/docs/furo-alert-dialog.md +107 -0
- package/docs/furo-attachment.md +133 -0
- package/docs/furo-autocomplete-field.md +719 -0
- package/docs/furo-avatar-group.md +87 -0
- package/docs/furo-avatar.md +101 -0
- package/docs/furo-badge.md +126 -0
- package/docs/furo-breadcrumb.md +89 -0
- package/docs/furo-button.md +263 -0
- package/docs/furo-checkbox.md +384 -0
- package/docs/furo-collapsible.md +104 -0
- package/docs/furo-control-block.md +102 -0
- package/docs/furo-date-picker.md +270 -0
- package/docs/furo-date-time-picker.md +110 -0
- package/docs/furo-dialog.md +88 -0
- package/docs/furo-drawer.md +122 -0
- package/docs/furo-dropdown-menu.md +298 -0
- package/docs/furo-editable-field.md +391 -0
- package/docs/furo-editor.md +304 -0
- package/docs/furo-email-field.md +93 -0
- package/docs/furo-empty-state.md +97 -0
- package/docs/furo-error-state.md +97 -0
- package/docs/furo-file-field.md +163 -0
- package/docs/furo-icon.md +288 -0
- package/docs/furo-number-field.md +397 -0
- package/docs/furo-pagination.md +101 -0
- package/docs/furo-password-field.md +75 -0
- package/docs/furo-popover.md +114 -0
- package/docs/furo-radio.md +125 -0
- package/docs/furo-scroll-area.md +112 -0
- package/docs/furo-search-field.md +94 -0
- package/docs/furo-section-header.md +135 -0
- package/docs/furo-select.md +443 -0
- package/docs/furo-separator.md +87 -0
- package/docs/furo-sidebar.md +178 -0
- package/docs/furo-skeleton.md +92 -0
- package/docs/furo-splitter.md +103 -0
- package/docs/furo-stepper.md +127 -0
- package/docs/furo-switch.md +107 -0
- package/docs/furo-table.md +233 -0
- package/docs/furo-tabs.md +173 -0
- package/docs/furo-text-field.md +249 -0
- package/docs/furo-textarea.md +214 -0
- package/docs/furo-time-field.md +276 -0
- package/docs/furo-toast.md +217 -0
- package/docs/furo-toggle-group.md +123 -0
- package/docs/furo-toggle.md +104 -0
- package/docs/furo-tool-bar.md +88 -0
- package/docs/furo-tooltip.md +107 -0
- package/lib/components/atoms/FuroButton/FuroButtonContext.js +8 -8
- package/lib/components/atoms/FuroCheckbox/FuroCheckboxContext.js +1 -1
- package/lib/components/atoms/FuroSwitch/FuroSwitchContext.js +1 -1
- package/lib/components/atoms/FuroToggle/FuroToggleContext.js +1 -1
- package/lib/components/molecules/FuroAccordion/FuroAccordionContext.js +1 -1
- package/lib/components/molecules/FuroCollapsible/FuroCollapsibleContext.js +1 -1
- package/lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuContext.js +1 -2
- package/lib/components/molecules/FuroPopover/FuroPopoverContext.js +1 -1
- package/lib/components/molecules/FuroSelect/FuroSelect.vue +8 -6
- package/lib/components/molecules/FuroSelect/FuroSelectContext.js +35 -0
- package/lib/components/molecules/FuroSelect/FuroSelectOptionText.vue +40 -0
- package/lib/components/molecules/FuroSelect/FuroSelectOptionTextContext.js +275 -0
- package/lib/components/molecules/FuroSplitter/FuroSplitterContext.js +1 -1
- package/lib/components/molecules/FuroToggleGroup/FuroToggleGroupContext.js +1 -1
- package/lib/components/molecules/FuroTooltip/FuroTooltipContext.js +1 -1
- package/lib/components/organisms/FuroTable/FuroTableContext.js +3 -3
- package/lib/components/organisms/FuroToast/FuroToastContext.js +6 -2
- package/lib/components/organisms/FuroToast/ToastQueue.js +4 -4
- package/nuxt/index.js +9 -9
- package/package.json +19 -17
- package/public/furo-vue/components.json +1 -1
- package/types/generated/components/atoms/FuroButton/FuroButtonContext.d.ts +4 -4
- package/types/generated/components/molecules/FuroSelect/FuroSelect.vue.d.ts +23 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectContext.d.ts +14 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectOptionText.vue.d.ts +50 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectOptionTextContext.d.ts +141 -0
- package/types/generated/components/organisms/FuroTable/FuroTableContext.d.ts +2 -2
package/README.ja.md
CHANGED
|
@@ -255,6 +255,8 @@ import {
|
|
|
255
255
|
- `node_modules/@openreachtech/furo-vue/public/furo-vue/components.json` — 完全な
|
|
256
256
|
マニフェスト。各コンポーネントの `import` パス、`props`、`parcel` のキー、`events`、
|
|
257
257
|
`slots`、`features` を含みます。
|
|
258
|
+
- `node_modules/@openreachtech/furo-vue/docs/` には、マニフェストの各 `docs` パスが指す
|
|
259
|
+
仕様書が、インストールしたバージョンのまま入っています。
|
|
258
260
|
|
|
259
261
|
### Claude Code にマニフェストを参照させる
|
|
260
262
|
|
package/README.md
CHANGED
|
@@ -254,6 +254,8 @@ live inside the package:
|
|
|
254
254
|
- `node_modules/@openreachtech/furo-vue/public/furo-vue/components.json` — the
|
|
255
255
|
full manifest: for each component its `import` path, `props`, `parcel` keys,
|
|
256
256
|
`events`, `slots`, and `features`.
|
|
257
|
+
- `node_modules/@openreachtech/furo-vue/docs/` holds the spec that each manifest
|
|
258
|
+
`docs` path names, at the installed version.
|
|
257
259
|
|
|
258
260
|
### Point Claude Code at the manifest
|
|
259
261
|
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# Furo Design Tokens
|
|
2
|
+
|
|
3
|
+
Furo-vue ships a layered CSS custom property system for consistent UI/UX across the component library.
|
|
4
|
+
|
|
5
|
+
> **Visual reskin (1.2.1):** the token system is the single source of truth for the reskin — Inter typography, a 14px base with a `calc()` type scale, a 10px-base radius scale, a foreground emphasis scale, four status roles (+`-subtle`), a two-tier border, and two elevation shadows. All of it is expressed as tokens, so a consumer reskins by overriding tokens (see [Overriding tokens](#overriding-tokens-in-a-consumer-app)) without touching component CSS.
|
|
6
|
+
|
|
7
|
+
## Mental model
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
palette (primitives)
|
|
11
|
+
→ semantic color
|
|
12
|
+
→ semantic dimension
|
|
13
|
+
→ component overrides
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Layer | File | Prefix | Use in components? |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| Palette | `lib/assets/css/furo/0010.variables-palette-color-scale.css` | `--palette-{hue}-{scale}` | No — reference semantic tokens only |
|
|
19
|
+
| Z-index | `lib/assets/css/furo/0020.variables-z-index.css` | `--value-z-index-layer-*` | Yes |
|
|
20
|
+
| Semantic color | `lib/assets/css/furo/0030.variables-semantic-color.css` | `--color-{role}` | Yes |
|
|
21
|
+
| Semantic dimension | `lib/assets/css/furo/0035.variables-semantic-dimension.css` | `--size-*`, `--font-size-*` | Yes |
|
|
22
|
+
| Component | `lib/assets/css/furo/0040.variables-component-furo.css` | `--furo-{component}-*` | Optional overrides |
|
|
23
|
+
|
|
24
|
+
## Consumption
|
|
25
|
+
|
|
26
|
+
### Nuxt (this repo)
|
|
27
|
+
|
|
28
|
+
`nuxt.config.js` loads the single library entry.
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
css: [
|
|
32
|
+
'~/app/assets/css/nuxt/0000.furo.css',
|
|
33
|
+
'~/lib/assets/css/furo.css',
|
|
34
|
+
// ...
|
|
35
|
+
],
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### External consumers
|
|
39
|
+
|
|
40
|
+
Import the public entry once:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
@openreachtech/furo-vue/lib/assets/css/furo.css
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Overriding tokens in a consumer app
|
|
47
|
+
|
|
48
|
+
A consumer app rebrands furo-vue by **redefining semantic tokens** (`--color-*`, `--size-*`), never the `--palette-*` primitives and never a component's CSS. Follow these four rules; the first two cover where an override sits and how it splits by theme.
|
|
49
|
+
|
|
50
|
+
### 1. Keep your overrides outside any cascade layer
|
|
51
|
+
|
|
52
|
+
Furo declares every token inside `@layer furo`, and a declaration outside any layer beats every layered one. An unlayered theme file therefore wins wherever it loads, before or after `furo.css`. In Nuxt:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
// nuxt.config.js
|
|
56
|
+
export default defineNuxtConfig({
|
|
57
|
+
css: [
|
|
58
|
+
'@openreachtech/furo-vue/lib/assets/css/furo.css',
|
|
59
|
+
'~/assets/css/furo-theme.css', // unlayered, so its position in this list does not matter
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If your app puts its overrides in a layer of its own, that layer must come after `furo`. Declare the order before any stylesheet loads, as in `@layer reset, base, furo, app;`, and write the overrides inside `@layer app`.
|
|
65
|
+
|
|
66
|
+
### 2. Override per theme under `[data-theme="..."]`
|
|
67
|
+
|
|
68
|
+
An unlayered `:root` override beats furo's light and dark blocks alike, so a token set on `:root` holds one value in both themes. That suits a token that should not change with the theme, such as `--size-input-height`. To give a color one value per theme, redefine it under the `[data-theme="..."]` selectors furo ships:
|
|
69
|
+
|
|
70
|
+
```css
|
|
71
|
+
/* assets/css/furo-theme.css */
|
|
72
|
+
|
|
73
|
+
[data-theme="light"] {
|
|
74
|
+
/* Neutral/white base is already the default; just re-point the brand roles */
|
|
75
|
+
--color-primary: #1d4ed8;
|
|
76
|
+
--color-primary-foreground: #ffffff;
|
|
77
|
+
--color-ring: #1d4ed8;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[data-theme="dark"] {
|
|
81
|
+
--color-primary: #60a5fa;
|
|
82
|
+
--color-primary-foreground: #0b1220;
|
|
83
|
+
--color-ring: #60a5fa;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If your app never toggles dark mode and only ever sets `<html data-theme="light">`, overriding `[data-theme="light"]` alone is enough.
|
|
88
|
+
|
|
89
|
+
### 3. Override the semantic role, not the palette
|
|
90
|
+
|
|
91
|
+
Point the semantic role at whatever color you want; do not edit `--palette-*` (they are internal primitives) and do not restyle a component's class:
|
|
92
|
+
|
|
93
|
+
```css
|
|
94
|
+
/* ✅ semantic role */
|
|
95
|
+
[data-theme="light"] { --color-destructive: #dc2626; }
|
|
96
|
+
|
|
97
|
+
/* ❌ palette primitive — internal, may change between versions */
|
|
98
|
+
[data-theme="light"] { --palette-red-500: #dc2626; }
|
|
99
|
+
|
|
100
|
+
/* ❌ component CSS — brittle, defeats the token system */
|
|
101
|
+
.unit-button.destructive { background-color: #dc2626; }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 4. Re-point, or shift the whole neutral base
|
|
105
|
+
|
|
106
|
+
- **Re-point brand roles only** (most common): override `--color-primary`, `--color-ring`, `--color-destructive`, etc., and keep furo's neutral surfaces. This is safe on its own: no shipped component paints a brand surface as text, so re-pointing one changes the surfaces it fills and the text those surfaces carry through the matching `-foreground`, and nothing else. Author rule 7 below states the guarantee and a test enforces it.
|
|
107
|
+
|
|
108
|
+
Re-pointing an intent role (`--color-destructive`, `--color-error`, `--color-info`, `--color-success`, `--color-warning`) is the same single-token override, and each one ships the `-foreground` companion its fill carries, so the label travels with the surface. Text in the danger, warning and informational families reads a `--color-destructive-text`, `--color-warning-text` or `--color-info-text` sibling instead, so re-point that role as well when the text should follow the fill. The test measures furo's own values, not yours: a fill darker than the one furo ships needs its companion re-pointed as well, and the two roles sit next to each other in `0030` so you can see what you are moving.
|
|
109
|
+
- **Shift the whole base** (e.g. a warmer or cooler neutral): override the surface roles together: `--color-background`, `--color-foreground`, `--color-card`, `--color-muted`, `--color-accent`, `--color-border`, `--color-input`, under each `[data-theme="..."]`. Keep light and dark in sync so both schemes stay legible.
|
|
110
|
+
|
|
111
|
+
The full list of overridable semantic roles is the [Semantic color tokens](#semantic-color-tokens) table below; dimension roles are in [Semantic dimension tokens](#semantic-dimension-tokens).
|
|
112
|
+
|
|
113
|
+
## Color scheme (OS + app toggle)
|
|
114
|
+
|
|
115
|
+
- Default: follow OS via `FuroColorSchemeDirector` (`system` preference).
|
|
116
|
+
- User override: `light` or `dark` persisted in `localStorage` (`furo-color-scheme-preference`).
|
|
117
|
+
- Resolved theme is applied as `data-theme="light|dark"` on `<html>` with matching `color-scheme` in CSS.
|
|
118
|
+
|
|
119
|
+
## Prefix rules
|
|
120
|
+
|
|
121
|
+
- **Colors** always use `--color-` for semantic roles.
|
|
122
|
+
- **Primitives** use `--palette-` with a 050–950 scale.
|
|
123
|
+
- **Non-color** tokens use `--size-*` or `--font-size-*` — never `--color-`.
|
|
124
|
+
- **Component overrides** use `--furo-{component}-{property}` with fallbacks in component CSS.
|
|
125
|
+
|
|
126
|
+
## Semantic color tokens
|
|
127
|
+
|
|
128
|
+
| Token | What it controls | Used by |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| `--color-background` / `--color-foreground` | Default app background and text | Page shell, sections, default text |
|
|
131
|
+
| `--color-foreground-title` | Title-weight text, one step darker than the default | Headings, section titles |
|
|
132
|
+
| `--color-foreground-inverse` | Text on a dark surface | Labels on brand surfaces, inverted chips |
|
|
133
|
+
| `--color-foreground-secondary` | De-emphasized text one step below `--color-foreground` | Secondary labels, meta text |
|
|
134
|
+
| `--color-foreground-tertiary` | Further de-emphasized text | Captions, timestamps |
|
|
135
|
+
| `--color-foreground-glyph` | The caption colour for an icon, a caret or a separator, held to 3:1 | Sidebar caret, search field icon, breadcrumb separator |
|
|
136
|
+
| `--color-foreground-light` | Low-emphasis text | Placeholders, hints |
|
|
137
|
+
| `--color-foreground-extra-light` | Lowest-emphasis text | Disabled labels, faint text |
|
|
138
|
+
| `--color-link` | Hyperlink text. `blue-600` in light, because `blue-500` is 3.68:1 on the page | Links, link buttons |
|
|
139
|
+
| `--color-card` / `--color-card-foreground` | Elevated surfaces and content | Cards, dashboard panels, settings panels |
|
|
140
|
+
| `--color-popover` / `--color-popover-foreground` | Floating surfaces and content | Popovers, dropdowns, context menus |
|
|
141
|
+
| `--color-primary` / `--color-primary-foreground` | High-emphasis actions and brand surfaces | Primary buttons, selected states, badges, checkbox checked |
|
|
142
|
+
| `--color-secondary` / `--color-secondary-foreground` | Lower-emphasis filled actions and supporting surfaces | Secondary buttons, chips, file selector button |
|
|
143
|
+
| `--color-muted` / `--color-muted-foreground` | Subtle surfaces and lower-emphasis content | Descriptions, placeholders, helper text, disabled tone |
|
|
144
|
+
| `--color-accent` / `--color-accent-foreground` | Interactive hover, focus, and active surfaces | Menu highlights, hovered rows, ghost control hover |
|
|
145
|
+
| `--color-mention` / `--color-mention-foreground` | The mention chip's surface and label | `.furo-mention`, inside a FuroEditor and in saved content rendered outside one |
|
|
146
|
+
| `--color-destructive` / `--color-destructive-foreground` | Destructive actions and error emphasis | Invalid states, destructive actions. The companion is `gray-950` in both themes, for the reason [ADR 0039](./adr/0039-an-intent-surface-ships-the-label-it-can-carry.md) gives |
|
|
147
|
+
| `--color-destructive-text` | Text drawn in the danger family, read where the fill would fall under 4.5:1 on the page. `rose-600` in light, `rose-500` in dark | Validation messages, required marks, error states, a destructive menu item, a table error row |
|
|
148
|
+
| `--color-warning-text` | Text drawn in the warning family. `orange-700` in light, `orange-400` in dark | The unresolved-icon glyph of FuroIcon |
|
|
149
|
+
| `--color-info-text` | Text drawn in the informational family. `blue-600` in light, `blue-400` in dark | The FuroAttachment spinner |
|
|
150
|
+
| `--color-border` | Default borders and separators | Cards, menus, tables, dividers |
|
|
151
|
+
| `--color-input` | Form control borders | Input, textarea, select, outline controls |
|
|
152
|
+
| `--color-ring` | Focus rings and outlines | Focusable controls |
|
|
153
|
+
|
|
154
|
+
Light and dark values are defined in `lib/assets/css/furo/0030.variables-semantic-color.css` using the **gray** palette scale, which matches the CRM Kit greyscale on all eleven steps.
|
|
155
|
+
|
|
156
|
+
The text tiers map onto the design's text roles: `--color-foreground` is the design's *body* colour rather than its *title* colour, because body is by far the design's most-used text colour and the token already means "default text".
|
|
157
|
+
|
|
158
|
+
| Design role | Token | Palette step |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| Text / Title | `--color-foreground-title` | `gray-900` |
|
|
161
|
+
| Text / Body | `--color-foreground` | `gray-800` |
|
|
162
|
+
| Text / Subtitle | `--color-foreground-secondary` | `gray-700` |
|
|
163
|
+
| Text / Caption | `--color-foreground-tertiary` | `gray-600` |
|
|
164
|
+
| (no design counterpart) | `--color-foreground-light` | `gray-400` |
|
|
165
|
+
| Text / Disabled | `--color-foreground-extra-light` | `gray-300` |
|
|
166
|
+
| Text / Negative | `--color-foreground-inverse` | `gray-050` |
|
|
167
|
+
|
|
168
|
+
> **Which variable set these come from.** The design carries two parallel sets. The one the Colors page binds is prefixed `Brand/` and it disagrees with the authoritative `Grayscale/` set by one step on exactly Subtitle and Caption. The tiebreak is what the design system's own Button binds, and it binds `Grayscale/`: `Grayscale/Text/Body` `#424752`, `Grayscale/Text/Subtitle` `#646c7d`, `Grayscale/Text/Caption` `#8b93a2`. The values above are the `Grayscale/` set. This also matters for legibility: the `Brand/` values put Subtitle at 3.09:1 and Caption at 1.97:1 against white, where the `Grayscale/` values give 5.27:1 and 3.09:1. Caption is the 3.09:1 value, under the 4.5:1 text floor, so no shipped component paints text with `--color-foreground-tertiary`. Glyphs that took the caption colour read `--color-foreground-glyph`, which carries the same step and is held to the 3:1 non-text floor. [ADR 0043](./adr/0043-text-on-the-page-clears-the-text-floor.md) gives the reason.
|
|
169
|
+
|
|
170
|
+
### Brand color tokens
|
|
171
|
+
|
|
172
|
+
The design carries two families: a blue action family and a dark neutral brand family. `--color-primary` keeps its existing dark neutral meaning, and this family absorbs the design's brand surfaces and borders. Consumers that want a blue action colour override `--color-primary`.
|
|
173
|
+
|
|
174
|
+
| Token | Light value | Palette step |
|
|
175
|
+
| --- | --- | --- |
|
|
176
|
+
| `--color-brand-surface-subtle` | `#f1f2f4` | `gray-100` |
|
|
177
|
+
| `--color-brand-surface-lighter` | `#c4c7cf` | `gray-400` |
|
|
178
|
+
| `--color-brand-surface` | `#22252a` | `gray-900` |
|
|
179
|
+
| `--color-brand-surface-darker` | `#101114` | `gray-950` |
|
|
180
|
+
| `--color-brand-border-subtle` | `#f1f2f4` | `gray-100` |
|
|
181
|
+
| `--color-brand-border-lighter` | `#d2d5db` | `gray-300` |
|
|
182
|
+
| `--color-brand-border` | `#8b93a2` | `gray-600` |
|
|
183
|
+
| `--color-brand-border-darker` | `#424752` | `gray-800` |
|
|
184
|
+
| `--color-brand-foreground` | `#424752` | `gray-800` |
|
|
185
|
+
| `--color-brand-foreground-darker` | `#22252a` | `gray-900` |
|
|
186
|
+
|
|
187
|
+
### Status color tokens
|
|
188
|
+
|
|
189
|
+
Four status roles, each with a `-subtle` background companion. Both light and dark values are defined in `0030.variables-semantic-color.css`.
|
|
190
|
+
|
|
191
|
+
| Token | What it controls | Light step |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `--color-success` / `--color-success-subtle` | Positive state emphasis and its tinted background | `emerald-500` / `emerald-050` |
|
|
194
|
+
| `--color-warning` / `--color-warning-subtle` | Cautionary state emphasis and its tinted background | `orange-500` / `orange-100` |
|
|
195
|
+
| `--color-info` / `--color-info-subtle` | Informational state emphasis and its tinted background | `blue-500` / `blue-050` |
|
|
196
|
+
| `--color-error` / `--color-error-subtle` | Error state emphasis and its tinted background | `rose-500` / `rose-050` |
|
|
197
|
+
|
|
198
|
+
Each family also ships the label its own fill can carry, so a consumer re-pointing the role moves both or neither:
|
|
199
|
+
|
|
200
|
+
| Token | What it controls | Light step | Dark step |
|
|
201
|
+
| --- | --- | --- | --- |
|
|
202
|
+
| `--color-success-foreground` | Text on a filled success surface | `gray-950` | `gray-950` |
|
|
203
|
+
| `--color-warning-foreground` | Text on a filled warning surface | `gray-950` | `gray-950` |
|
|
204
|
+
| `--color-info-foreground` | Text on a filled informational surface | `gray-950` | `gray-950` |
|
|
205
|
+
| `--color-error-foreground` | Text on a filled error surface | `gray-950` | `gray-950` |
|
|
206
|
+
|
|
207
|
+
> **The companion does not flip between themes.** Every one resolves to `gray-950` in light and in dark. A status surface stays mid-tone in both (`--color-success` moves `emerald-500` to `emerald-400`), so only a near-black label clears the 4.5:1 text floor on it. Reaching for `--color-foreground-inverse` instead is what put a solid success badge at 2.42:1 in light. See [ADR 0039](./adr/0039-an-intent-surface-ships-the-label-it-can-carry.md).
|
|
208
|
+
|
|
209
|
+
Each tint ships a label of its own as well, on the same terms:
|
|
210
|
+
|
|
211
|
+
| Token | What it controls | Light step | Dark step |
|
|
212
|
+
| --- | --- | --- | --- |
|
|
213
|
+
| `--color-success-subtle-foreground` | Text on a tinted success surface | `emerald-700` | `emerald-300` |
|
|
214
|
+
| `--color-warning-subtle-foreground` | Text on a tinted warning surface | `orange-800` | `orange-200` |
|
|
215
|
+
| `--color-info-subtle-foreground` | Text on a tinted informational surface | `blue-700` | `blue-300` |
|
|
216
|
+
| `--color-error-subtle-foreground` | Text on a tinted error surface | `rose-700` | `rose-300` |
|
|
217
|
+
|
|
218
|
+
> **A tint label does flip, and it is the family's own dark step.** Where a fill stays mid-tone in both themes, a tint travels from `emerald-050` to `emerald-950`, so its label travels with it: `700` in light mirrors to `300` in dark, the reflection `--color-brand-border-lighter` already makes. Painting the family's mid-tone accent on its own tint instead put a subtle success badge at 2.41:1 in light. Every pairing above clears 5.21:1 or better. See [ADR 0040](./adr/0040-a-tinted-surface-ships-its-own-label.md).
|
|
219
|
+
|
|
220
|
+
> **Warning's subtle step is deliberate.** Success, info and error draw their subtle surface from the `050` step; warning draws its from `100`. The design does this, and it is followed here rather than normalised. The inconsistency has been reported to the designer. Warning's tint label takes the `800` step for the same reason: on a tint one step darker, `orange-700` clears the floor by two hundredths and `orange-800` clears it by 1.88.
|
|
221
|
+
|
|
222
|
+
> **Dark values are derived, not designed.** The design defines a single light theme. The dark block preserves the relationship between palette steps that the light values establish. It has not been signed off by a designer.
|
|
223
|
+
|
|
224
|
+
> **Two-tier border:** the reskin distinguishes `--color-border` (default surface borders: cards, menus, tables, dividers) from `--color-input` (a stronger tone for form-control borders). There is no `--color-border-strong` token; reach for `--color-input` when a control needs the stronger edge.
|
|
225
|
+
|
|
226
|
+
### Selection color tokens
|
|
227
|
+
|
|
228
|
+
The blue family a selection control fills with when it is chosen. It exists because `--color-primary` is a dark neutral in this library, so before this family a checked checkbox came out near-black and every consumer had to override it to match the design.
|
|
229
|
+
|
|
230
|
+
| Token | What it controls | Light step | Dark step |
|
|
231
|
+
| --- | --- | --- | --- |
|
|
232
|
+
| `--color-selected` | The chosen fill | `blue-500` | `blue-400` |
|
|
233
|
+
| `--color-selected-darker` | The chosen fill while pressed | `blue-700` | `blue-300` |
|
|
234
|
+
| `--color-selected-lighter` | The chosen fill while hovered | `blue-300` | `blue-700` |
|
|
235
|
+
| `--color-selected-border-darker` | The edge of a chosen control | `blue-800` | `blue-200` |
|
|
236
|
+
| `--color-selected-foreground` | The mark sitting on a chosen fill | `gray-050` | `gray-900` |
|
|
237
|
+
|
|
238
|
+
Read by `FuroCheckbox`, `FuroRadio` and `FuroSwitch`, so the three selection controls re-theme together from one override.
|
|
239
|
+
|
|
240
|
+
> **`--color-selected-foreground` is not pure white.** It is `gray-050` `#f8fafc`, one palette step off the `#ffffff` the design draws for a switch knob and a check mark. No token carries pure white in a role that survives dark mode: `--color-background` flips to `gray-950` and would hide the mark, and `--color-primary-foreground` belongs to the dark neutral action family. A consumer that needs the exact design value overrides this one token.
|
|
241
|
+
|
|
242
|
+
> **There is no progress role.** `FuroAttachment`'s upload bar reads `--color-info`, which carries the same `blue-500`, because "informational" is the closest existing meaning and `--color-selected` would be wrong: an uploading file has not been chosen. If a `--color-progress` role is added later, that component is the one to re-point.
|
|
243
|
+
|
|
244
|
+
Two further roles exist and are read by shipped components:
|
|
245
|
+
|
|
246
|
+
| Token | What it controls | Light step | Dark step |
|
|
247
|
+
| --- | --- | --- | --- |
|
|
248
|
+
| `--color-info-lighter` | A softer step of the informational blue | `blue-400` | `blue-300` |
|
|
249
|
+
| `--color-muted-subtle` | The faintest neutral surface, below `--color-muted` | `gray-050` | `gray-900` |
|
|
250
|
+
|
|
251
|
+
## Semantic dimension tokens
|
|
252
|
+
|
|
253
|
+
| Token | Role |
|
|
254
|
+
| --- | --- |
|
|
255
|
+
| `--size-input-height-large` | Single-line control height, tallest step (`2.75rem` → 44px) |
|
|
256
|
+
| `--size-input-height-medium` | Single-line control height, middle step (`2.5rem` → 40px) |
|
|
257
|
+
| `--size-input-height-small` | Single-line control height, shortest step (`2rem` → 32px) |
|
|
258
|
+
| `--size-input-height` | Compatibility alias for `--size-input-height-small`, not a design default |
|
|
259
|
+
| `--size-option-height` | Minimum height of an option row in `FuroSelect` and a menu row in `FuroDropdownMenu` (`--size-input-height` less `0.5rem` → 24px) |
|
|
260
|
+
| `--size-close-button-small` | Built-in close button of `FuroToast` (`1.25rem` → 20px) |
|
|
261
|
+
| `--size-close-button-medium` | Built-in close button of `FuroDialog` and `FuroDrawer` (`1.75rem` → 28px) |
|
|
262
|
+
| `--size-sidebar-width` | Navigation rail width, expanded (`16rem` → 256px) |
|
|
263
|
+
| `--size-sidebar-width-collapsed` | Navigation rail width, collapsed (`4rem` → 64px) |
|
|
264
|
+
| `--size-nav-width` | Deprecated alias of `--size-sidebar-width`, removed in the next minor |
|
|
265
|
+
| `--size-focus-ring-spread` | Focus ring width (`0.125rem` → 2px), read by the focus ring of every furo component |
|
|
266
|
+
| `--size-thinnest` | Hairline border width (`1px`, the one exemption to the rem rule) |
|
|
267
|
+
| `--size-table-cell-inline` | `FuroTable` cell padding, inline axis (`1rem` → 16px). Compact density and the selection cell derive from it |
|
|
268
|
+
| `--size-table-cell-block` | `FuroTable` cell padding, block axis (`0`, row height comes from the row) |
|
|
269
|
+
| `--font-size` | Body text base (`0.875rem` → 14px) |
|
|
270
|
+
| `--transition-timer` | Default transition duration |
|
|
271
|
+
| `--transition-timer-spin` | Duration of one turn of the `FuroButton` loading spinner (`0.75s`) |
|
|
272
|
+
|
|
273
|
+
The height scale has **no default step**. The design names four steps and marks none of them the default, so `--size-input-height` carries the height shipped fields already rendered rather than a value read from the design. Re-pointing it moves every field that does not name a step. See [ADR 0042](./adr/0042-the-input-height-scale-has-no-default-step.md).
|
|
274
|
+
|
|
275
|
+
The collapsed rail width is the library's own, derived from the rail's padding and icon box because the design draws no collapsed variant. See [ADR 0041](./adr/0041-a-value-the-design-does-not-draw-is-the-librarys-to-choose.md).
|
|
276
|
+
|
|
277
|
+
Both scales below are defined in `0035.variables-semantic-dimension.css`.
|
|
278
|
+
|
|
279
|
+
### Radius scale
|
|
280
|
+
|
|
281
|
+
Every step is an absolute value. Radius used to derive from a single `0.625rem` base through `calc()` multiples, but no factor applied to that base reaches the design's steps while preserving the existing ones, so derivation is gone and each step is spelled out. `--size-border-radius` survives as the default step and points at `--size-border-radius-large`.
|
|
282
|
+
|
|
283
|
+
| Token | Value | px | Role |
|
|
284
|
+
| --- | --- | --- | --- |
|
|
285
|
+
| `--size-border-radius-3x-small` | `0.125rem` | 2 | Hairline rounding |
|
|
286
|
+
| `--size-border-radius-2x-small` | `0.25rem` | 4 | Smallest visible rounding |
|
|
287
|
+
| `--size-border-radius-small` | `0.375rem` | 6 | Small radius (checkbox, chips) |
|
|
288
|
+
| `--size-border-radius-medium` | `0.5rem` | 8 | Default control radius |
|
|
289
|
+
| `--size-border-radius-large` | `0.625rem` | 10 | Large radius |
|
|
290
|
+
| `--size-border-radius-x-large` | `0.75rem` | 12 | Cards and panels |
|
|
291
|
+
| `--size-border-radius-2x-large` | `1rem` | 16 | Large surfaces |
|
|
292
|
+
| `--size-border-radius-3x-large` | `1.25rem` | 20 | Larger surfaces |
|
|
293
|
+
| `--size-border-radius-4x-large` | `1.5rem` | 24 | Dialogs and sheets |
|
|
294
|
+
| `--size-border-radius-5x-large` | `2rem` | 32 | Feature surfaces |
|
|
295
|
+
| `--size-border-radius-6x-large` | `2.5rem` | 40 | Largest drawn step |
|
|
296
|
+
| `--size-border-radius-full` | `625rem` | n/a | Fully rounded, for pill controls |
|
|
297
|
+
| `--size-border-radius` | `0.625rem` | 10 | Default step, points at `-large` |
|
|
298
|
+
|
|
299
|
+
`--size-border-radius-full` is a token in its own right so a pill control needs no magic number. It carries `625rem` rather than the conventional `9999px` because the library forbids raw `px` outside `--size-thinnest`; any value past half the element's longer side rounds the ends completely.
|
|
300
|
+
|
|
301
|
+
Note for consumers upgrading from 1.2.x: overriding `--size-border-radius` no longer rescales the whole ladder. It used to be the `calc()` base every step multiplied, so one override moved all of them; it is now just the default step. Re-theme the radius by overriding the individual steps you want to move.
|
|
302
|
+
|
|
303
|
+
### Spacing scale
|
|
304
|
+
|
|
305
|
+
A 4px grid with semantic names. `snug` (10px) and `spacious` (40px) fill the two steps the design uses that the scale was missing.
|
|
306
|
+
|
|
307
|
+
| Token | Value | px | Role |
|
|
308
|
+
| --- | --- | --- | --- |
|
|
309
|
+
| `--size-space-none` | `0` | 0 | No spacing |
|
|
310
|
+
| `--size-space-3x-small` | `0.125rem` | 2 | Hairline spacing, for stacked text inside one block |
|
|
311
|
+
| `--size-space-tiny` | `0.25rem` | 4 | Minimal spacing |
|
|
312
|
+
| `--size-space-2x-small` | `0.375rem` | 6 | Between tiny and small |
|
|
313
|
+
| `--size-space-small` | `0.5rem` | 8 | Tight spacing |
|
|
314
|
+
| `--size-space-snug` | `0.625rem` | 10 | Between small and compact |
|
|
315
|
+
| `--size-space-compact` | `0.75rem` | 12 | Between small and medium |
|
|
316
|
+
| `--size-space-medium` | `1rem` | 16 | Default spacing |
|
|
317
|
+
| `--size-space-relaxed` | `1.25rem` | 20 | Between medium and large |
|
|
318
|
+
| `--size-space-large` | `1.5rem` | 24 | Section spacing |
|
|
319
|
+
| `--size-space-x-large` | `2rem` | 32 | Large section spacing |
|
|
320
|
+
| `--size-space-spacious` | `2.5rem` | 40 | Between x-large and 2x-large |
|
|
321
|
+
| `--size-space-2x-large` | `3rem` | 48 | Extra large spacing |
|
|
322
|
+
| `--size-space-3x-large` | `4rem` | 64 | Layout spacing |
|
|
323
|
+
| `--size-space-4x-large` | `5rem` | 80 | Wide layout spacing |
|
|
324
|
+
| `--size-space-5x-large` | `6rem` | 96 | Maximum layout spacing |
|
|
325
|
+
|
|
326
|
+
### Typography tokens
|
|
327
|
+
|
|
328
|
+
Two families. **Noto Sans JP** carries body, label and headline text; **Noto Serif JP** carries display and title text. The library only declares the families; the playground loads them through a Google Fonts `<link>` in `nuxt.config.js`, and consumers load their own faces and override `--font-family` / `--font-family-display` to swap them. All typography tokens live in `0035.variables-semantic-dimension.css`.
|
|
329
|
+
|
|
330
|
+
| Token | Role | Value |
|
|
331
|
+
| --- | --- | --- |
|
|
332
|
+
| `--font-family` | Body font stack | `'Noto Sans JP', system-ui, …, sans-serif` |
|
|
333
|
+
| `--font-family-display` | Display font stack | `'Noto Serif JP', georgia, …, serif` |
|
|
334
|
+
| `--font-size` | Body text base | `0.875rem` (14px) |
|
|
335
|
+
| `--font-size-small` … `--font-size-4x-large` | Type scale steps | `calc()` multiples of `--font-size` |
|
|
336
|
+
| `--font-weight-regular` | Body weight | `400` |
|
|
337
|
+
| `--font-weight-medium` | Medium emphasis | `500` |
|
|
338
|
+
| `--font-weight-semibold` | High emphasis | `600` |
|
|
339
|
+
| `--font-weight-bold` | Display and title weight | `700` |
|
|
340
|
+
| `--font-line-height-body` | Body line height | `1.4` |
|
|
341
|
+
|
|
342
|
+
#### Composite text styles
|
|
343
|
+
|
|
344
|
+
One token per text style the design defines, each carrying family, size, weight and line height in a single value, so a size can never be paired with the wrong weight. Consume with the `font` shorthand:
|
|
345
|
+
|
|
346
|
+
```css
|
|
347
|
+
.section-title {
|
|
348
|
+
font: var(--font-title-medium);
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
| Token | Family | Weight | Size | Line height |
|
|
353
|
+
| --- | --- | --- | --- | --- |
|
|
354
|
+
| `--font-display-large` | Serif | 700 | `4rem` (64px) | `5rem` (80px) |
|
|
355
|
+
| `--font-display-medium` | Serif | 700 | `3rem` (48px) | `4rem` (64px) |
|
|
356
|
+
| `--font-title-large` | Serif | 700 | `2.25rem` (36px) | `3rem` (48px) |
|
|
357
|
+
| `--font-title-medium` | Serif | 700 | `1.75rem` (28px) | `2.25rem` (36px) |
|
|
358
|
+
| `--font-headline-large` | Sans | 500 | `1.5rem` (24px) | `2rem` (32px) |
|
|
359
|
+
| `--font-headline-medium` | Sans | 500 | `1.25rem` (20px) | `1.75rem` (28px) |
|
|
360
|
+
| `--font-headline-small` | Sans | 500 | `1.125rem` (18px) | `1.75rem` (28px) |
|
|
361
|
+
| `--font-body-x-large` | Sans | 400 | `1.125rem` (18px) | `1.75rem` (28px) |
|
|
362
|
+
| `--font-body-large` | Sans | 400 | `0.9375rem` (15px) | `1.5rem` (24px) |
|
|
363
|
+
| `--font-body-medium` | Sans | 400 | `0.84375rem` (13.5px) | `1.25rem` (20px) |
|
|
364
|
+
| `--font-body-small` | Sans | 400 | `0.75rem` (12px) | `1.25rem` (20px) |
|
|
365
|
+
| `--font-label-large` | Sans | 500 | `1.125rem` (18px) | `1.75rem` (28px) |
|
|
366
|
+
| `--font-label-medium` | Sans | 500 | `0.9375rem` (15px) | `1.5rem` (24px) |
|
|
367
|
+
| `--font-label-small` | Sans | 500 | `0.84375rem` (13.5px) | `1.25rem` (20px) |
|
|
368
|
+
| `--font-label-tiny` | Sans | 500 | `0.75rem` (12px) | `1.25rem` (20px) |
|
|
369
|
+
|
|
370
|
+
> **The `font` shorthand resets other font properties.** `font-style`, `font-variant`, `font-stretch` and `font-size-adjust` all return to their initial values. A component that sets `font-style: italic` and then `font: var(--font-body-medium)` loses the italic, and declaration order decides the outcome. Set `font` first, then any modifier. See [ADR 0032](./adr/0032-composite-text-style-tokens-via-font-shorthand.md).
|
|
371
|
+
|
|
372
|
+
> **The sans ramp has no `1rem` step.** It runs `1.5rem` → `1.25rem` → `1.125rem` → `0.9375rem` → `0.84375rem` → `0.75rem` (24px → 20px → 18px → 15px → 13.5px → 12px in the design), and `1rem` is the browser root default rather than a size the design draws. A component that reaches for `font-size: 1rem` is following the browser, not the design; map it onto the nearest role the ramp does carry. The editor `h4` is the worked example: it renders Label/Medium at `0.9375rem`, one step below the Headline/Small its `h3` renders.
|
|
373
|
+
|
|
374
|
+
> **Two values carried through exactly.** `--font-body-medium` and `--font-label-small` are 13.5px in the design. They are carried through rather than rounded so the rendered result matches. `--font-display-large` also has `-0.25px` letter spacing in the design, which the `font` shorthand cannot express; apply `letter-spacing` separately if you need it.
|
|
375
|
+
|
|
376
|
+
### Shadow (elevation) tokens
|
|
377
|
+
|
|
378
|
+
Two elevation levels, both re-tuned for dark mode in `[data-theme="dark"]`.
|
|
379
|
+
|
|
380
|
+
| Token | Role | Used by |
|
|
381
|
+
| --- | --- | --- |
|
|
382
|
+
| `--shadow-light` | Low elevation | Resting cards, subtle raised surfaces |
|
|
383
|
+
| `--shadow-strong` | High elevation | Overlays — dialogs, popovers, dropdowns, toasts |
|
|
384
|
+
|
|
385
|
+
## Component tokens
|
|
386
|
+
|
|
387
|
+
| Token | Default | Used by |
|
|
388
|
+
| --- | --- | --- |
|
|
389
|
+
| `--furo-checkbox-size` | `1.125rem` | `FuroCheckbox` |
|
|
390
|
+
| `--furo-checkbox-icon-size` | `1rem` | `FuroCheckbox` |
|
|
391
|
+
| `--furo-dialog-overlay-background` | Overlay scrim | `FuroDialog` |
|
|
392
|
+
| `--furo-toast-{status}-bg` / `-border` / `-foreground` / `-accent` | Per-status toast surface, border, text, accent | `FuroToast` (`success` / `warning` / `info` / `error`) |
|
|
393
|
+
|
|
394
|
+
## Dark mode
|
|
395
|
+
|
|
396
|
+
Dark semantic tokens ship in `[data-theme="dark"]` in `0030.variables-semantic-color.css`. Use `FuroColorSchemeDirector` to cycle `system → light → dark` or call `saveColorSchemePreference({ preference })` directly.
|
|
397
|
+
|
|
398
|
+
## Playground
|
|
399
|
+
|
|
400
|
+
Open `/samples/components/playground` and select the **Design Tokens** tab to inspect resolved palette swatches, semantic colors, dimension samples, component previews, and the full reference table.
|
|
401
|
+
|
|
402
|
+
## Rules for library authors
|
|
403
|
+
|
|
404
|
+
1. Reference `--color-*` and `--size-*` in `@layer furo` component styles.
|
|
405
|
+
2. Do not reference `--palette-*` directly in components.
|
|
406
|
+
3. Do not hardcode hex/rgb in component styles.
|
|
407
|
+
4. Add new semantic tokens in `0030` or `0035` before using them in components.
|
|
408
|
+
5. Use `0040` only when a component family needs a dedicated override knob.
|
|
409
|
+
6. Use `--size-space-*` tokens for `gap` only. Use literal `rem` values for `padding`, `margin`, and other box-model spacing outside `gap`.
|
|
410
|
+
7. Never paint a surface role as text. A surface role names a place text sits on: `--color-primary`, `--color-secondary`, `--color-accent`, `--color-card`, `--color-popover`, `--color-muted`, `--color-selected`, `--color-mention`, `--color-background`, the `--color-brand-surface*` family, and every `-subtle` tint (`--color-error-subtle`, `--color-info-subtle`, `--color-success-subtle`, `--color-warning-subtle`, `--color-muted-subtle`), since nothing means "the colour of a pale green wash" and a tint is therefore only ever a place. Each carries the value a consumer picked to contrast with its own `-foreground` companion, so read as text it lands on whatever surface happens to be underneath, and a consumer re-pointing the role for its surface alone silently decides the contrast of text they never touched. Text reads one of three things instead: the `-foreground` companion of the surface it sits on, a foreground role (`--color-foreground*`, `--color-link`), or an intent role, which names a meaning rather than a place. An intent role that also fills a surface is read as text through a `-text` sibling, by rule 12. Where a surface needs a label of its own, ship the pair together in `0030` so a consumer moves both or neither, as `--color-mention` / `--color-mention-foreground` does. `DesignTokenAuditor#collectSurfaceTextViolations()` enforces this over every shipped stylesheet and component. Reasoning in [ADR 0038](./adr/0038-a-surface-role-is-never-painted-as-text.md).
|
|
411
|
+
8. Ship an intent surface with the label it can carry. The five intent families (`--color-destructive`, `--color-error`, `--color-info`, `--color-success`, `--color-warning`) each declare a `-foreground` companion in both blocks of `0030`, and text on one of those fills reads that companion rather than `--color-foreground-inverse`. The inverse role means the inverse of the page's text, which is right on a neutral family that flips between themes and wrong on a status family that stays mid-tone in both: reading it put a solid success badge at 2.42:1 in light. `DesignTokenAuditor#collectContrastViolations()` resolves every pairing the library declares through the palette and asserts 4.5:1 in both themes, or 3:1 where the pairing carries a graphic rather than text, and `collectSurfacesMissingCompanion()` reports an intent family shipped without a companion. A pairing that fails today is listed in `RECORDED_CONTRAST_EXCEPTIONS` with its issue, and `collectResolvedContrastExceptions()` makes a fix prune its own record. That list is currently empty: every pairing the library declares clears its floor in both themes. Reasoning in [ADR 0039](./adr/0039-an-intent-surface-ships-the-label-it-can-carry.md).
|
|
412
|
+
9. Set text through a composite role (`font: var(--font-…)`), never through a bare `font-size`. A size declared on its own takes whatever line height it inherits, which is the pairing the composite exists to fix. This rule binds the text a change writes or edits, and no script checks it: component rules written before the composite roles existed still set a size alone, mostly the size-modifier and narrow-viewport overrides of the form controls, and each moves onto a role as its component is next touched.
|
|
413
|
+
10. Ship a tinted surface with a label of its own. The four `-subtle` status surfaces each declare a `-subtle-foreground` companion, and unlike the fill companion of rule 8 it mirrors between the themes, because a tint travels from `emerald-050` to `emerald-950` while a fill stays mid-tone in both. Reading the family's own accent step on its tint is what put a subtle success badge at 2.41:1 in light. Reasoning in [ADR 0040](./adr/0040-a-tinted-surface-ships-its-own-label.md).
|
|
414
|
+
11. Declare a token inside the `@layer furo` block of its file. An app's unlayered override then beats it wherever the two load, which is what [Overriding tokens](#overriding-tokens-in-a-consumer-app) promises. `tests/__tests__/node/lib/assets/css/furo.js` fails on a stylesheet `furo.css` imports outside the layer.
|
|
415
|
+
12. Measure every role painted as text against the page. `DesignTokenAuditor#collectTextContrastViolations()` reads each `--color-*` role a shipped `color:` declaration paints, leaves out a label role (which rule 8 measures on its own fill), the glyph role `--color-foreground-glyph` (held to 3:1 by its own pairing), a `--furo-*` knob and a rule styling only an inactive control, and asserts 4.5:1 against `--color-background` in both themes. An intent role that fills a surface keeps its value and ships a `--color-<family>-text` sibling for text, because darkening the fill would push its `-foreground` label under the floor. The three families the library paints as text today (danger, warning, informational) ship one, and a component that starts painting `--color-success` or `--color-error` as text fails this check until a sibling is added. A role that fails is fixed, never listed in `RECORDED_CONTRAST_EXCEPTIONS`. Reasoning in [ADR 0043](./adr/0043-text-on-the-page-clears-the-text-floor.md).
|
|
416
|
+
|
|
417
|
+
## Decision record
|
|
418
|
+
|
|
419
|
+
See [docs/adr/0022-reskin-via-semantic-token-layer.md](./adr/0022-reskin-via-semantic-token-layer.md) and [docs/adr/0023-additive-semantic-tokens.md](./adr/0023-additive-semantic-tokens.md).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# FuroAccordion
|
|
2
|
+
|
|
3
|
+
Multi-section collapse: a stack of headers, each toggling its own content region. `single` mode keeps at most one section open; `multiple` mode allows several. Built on a headless accordion primitive that manages expansion state, ARIA wiring, roving focus, and the measured content height for smooth animation.
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
| Field | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| **Layer** | Molecule |
|
|
10
|
+
| **Path** | `lib/components/molecules/FuroAccordion/` |
|
|
11
|
+
| **Base primitive** | Headless accordion (`Root` + `Item` + `Header` + `Trigger` + `Content`) |
|
|
12
|
+
| **Composes** | A repeated header/content collapse region per option |
|
|
13
|
+
| **Model** | `value` (expanded section(s)) via `v-model:value` |
|
|
14
|
+
| **Emits** | `change-value`, `update:value` |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Public API
|
|
19
|
+
|
|
20
|
+
### Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `parcel` | `FuroAccordionParcel \| null` | `null` | Reactive data object holding the sections, selection mode, and expansion value. |
|
|
25
|
+
|
|
26
|
+
### Parcel keys
|
|
27
|
+
|
|
28
|
+
| Key | Type | Default | Description |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `options` | `Array<{ value, label?, disabled? }>` | `[]` | The sections. `value` is the unique key; `label` is the default header text; `disabled` disables a single section. |
|
|
31
|
+
| `value` | `string \| Array<string> \| null` | unset | Expanded section(s). A single value in `single` mode, an array in `multiple` mode. When present, the component is **controlled**; when absent, it manages its own state. |
|
|
32
|
+
| `selectionMode` | `'single' \| 'multiple'` | `'single'` | Whether one or several sections can be open. Maps to the primitive's type. |
|
|
33
|
+
| `collapsible` | `boolean` | `true` | `single` mode only: whether the open section can be closed by clicking its header. `multiple` mode is always collapsible. |
|
|
34
|
+
| `disabled` | `boolean` | `false` | Disables the whole accordion. |
|
|
35
|
+
| `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout / keyboard navigation axis. |
|
|
36
|
+
|
|
37
|
+
### Emits
|
|
38
|
+
|
|
39
|
+
| Event | Payload | Description |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `change-value` | `AccordionEmitPayload` | Fired when expansion changes. `payload.value` is the new value; `payload.isExpanded({ value })` is a convenience predicate. |
|
|
42
|
+
| `update:value` | `string \| Array<string> \| null` | `v-model:value` sync of the expanded section(s). |
|
|
43
|
+
|
|
44
|
+
### Slots
|
|
45
|
+
|
|
46
|
+
| Slot | Slot props | Description |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `header` | `{ option, expanded }` | Header content for a section. Defaults to `option.label` (or `option.value`). |
|
|
49
|
+
| `content` | `{ option, expanded }` | Body content revealed when the section is open. |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Behavior
|
|
54
|
+
|
|
55
|
+
| Concern | Behavior |
|
|
56
|
+
|---|---|
|
|
57
|
+
| **Selection mode** | `selectionMode` maps to the primitive's type. `single` holds one value; `multiple` holds an array. `isMultiple()` drives the empty default (`null` vs `[]`). |
|
|
58
|
+
| **Controlled vs uncontrolled** | `isControlled()` returns true when `parcel` owns a `value` key. A watcher syncs the controlled value into the internal ref; uncontrolled instances track their own state. |
|
|
59
|
+
| **Collapsible** | `isCollapsible()` is always true in `multiple` mode and reads `parcel.collapsible` in `single` mode. |
|
|
60
|
+
| **Per-section disable** | `isOptionDisabled()` is true when the accordion is disabled or the option is disabled. |
|
|
61
|
+
| **Animation** | Each content wrapper animates between `0` and the primitive's measured `--reka-accordion-content-height`. |
|
|
62
|
+
| **Passthrough** | Non-Furo parcel keys and extra attributes fall through to the root (`inheritAttrs: false` + `v-bind="$attrs"`). |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
```vue
|
|
69
|
+
<!-- Single (collapsible) -->
|
|
70
|
+
<FuroAccordion :parcel="{ options: sections }">
|
|
71
|
+
<template #content="{ option }">{{ option.body }}</template>
|
|
72
|
+
</FuroAccordion>
|
|
73
|
+
|
|
74
|
+
<!-- Multiple -->
|
|
75
|
+
<FuroAccordion :parcel="{ selectionMode: 'multiple', options: sections }">
|
|
76
|
+
<template #header="{ option }">{{ option.label }}</template>
|
|
77
|
+
<template #content="{ option }">{{ option.body }}</template>
|
|
78
|
+
</FuroAccordion>
|
|
79
|
+
|
|
80
|
+
<!-- Controlled -->
|
|
81
|
+
<FuroAccordion
|
|
82
|
+
:parcel="{ value: openSection, options: sections }"
|
|
83
|
+
@update:value="openSection = $event"
|
|
84
|
+
>
|
|
85
|
+
<template #content="{ option }">{{ option.body }}</template>
|
|
86
|
+
</FuroAccordion>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Composition notes
|
|
92
|
+
|
|
93
|
+
- Imports nothing from the app — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
|
|
94
|
+
- All logic lives in `FuroAccordionContext`: extract-only getters, the controlled/uncontrolled sync, and the `extractRootClassNames()` / `extractItemClassNames()` / `extractRootAttributes()` mappers. The component template only binds context methods.
|
|
95
|
+
- The public `value` / `selectionMode` keys are value-oriented; the primitive's own prop names appear only in the template bindings.
|
|
96
|
+
- CSS is in `@layer furo`, scoped, with `unit-accordion` root and `unit-accordion-item` per section. Dimensions use `rem`; `gap` uses `--size-space-*`; colors reference semantic tokens only.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Testing
|
|
101
|
+
|
|
102
|
+
- Context logic is fully covered by jsdom unit tests: `parcel` defaults, `isControlled()`, `isMultiple()`, `isCollapsible()`, `resolveSyncedValue()`, `isDisabled()`, `isOptionDisabled()`, `isExpanded()` (split into truthy / falsy describe blocks), `extractOptionLabel()`, `extractRootClassNames()`, `extractItemClassNames()`, `extractRootAttributes()` passthrough, the controlled/uncontrolled `setupComponent()` sync, and `onChangeValue()` emit behavior for single and multiple modes.
|
|
103
|
+
- No SFC mount test: behavior is declarative bindings over the tested context plus the headless primitive's own expansion handling, verified by manual QA in the playground.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Playground
|
|
108
|
+
|
|
109
|
+
`/samples/components/playground` → **Accordion** tab. Demonstrates single (collapsible), multiple, controlled, and per-section disabled accordions.
|