@openreachtech/furo-vue 1.5.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.
Files changed (89) hide show
  1. package/README.ja.md +2 -0
  2. package/README.md +2 -0
  3. package/docs/design-tokens.md +419 -0
  4. package/docs/furo-accordion.md +109 -0
  5. package/docs/furo-alert-dialog.md +107 -0
  6. package/docs/furo-attachment.md +133 -0
  7. package/docs/furo-autocomplete-field.md +719 -0
  8. package/docs/furo-avatar-group.md +87 -0
  9. package/docs/furo-avatar.md +101 -0
  10. package/docs/furo-badge.md +126 -0
  11. package/docs/furo-breadcrumb.md +89 -0
  12. package/docs/furo-button.md +263 -0
  13. package/docs/furo-checkbox.md +384 -0
  14. package/docs/furo-collapsible.md +104 -0
  15. package/docs/furo-control-block.md +102 -0
  16. package/docs/furo-date-picker.md +270 -0
  17. package/docs/furo-date-time-picker.md +110 -0
  18. package/docs/furo-dialog.md +88 -0
  19. package/docs/furo-drawer.md +122 -0
  20. package/docs/furo-dropdown-menu.md +298 -0
  21. package/docs/furo-editable-field.md +391 -0
  22. package/docs/furo-editor.md +304 -0
  23. package/docs/furo-email-field.md +93 -0
  24. package/docs/furo-empty-state.md +97 -0
  25. package/docs/furo-error-state.md +97 -0
  26. package/docs/furo-file-field.md +163 -0
  27. package/docs/furo-icon.md +288 -0
  28. package/docs/furo-number-field.md +397 -0
  29. package/docs/furo-pagination.md +101 -0
  30. package/docs/furo-password-field.md +75 -0
  31. package/docs/furo-popover.md +114 -0
  32. package/docs/furo-radio.md +125 -0
  33. package/docs/furo-scroll-area.md +112 -0
  34. package/docs/furo-search-field.md +94 -0
  35. package/docs/furo-section-header.md +135 -0
  36. package/docs/furo-select.md +443 -0
  37. package/docs/furo-separator.md +87 -0
  38. package/docs/furo-sidebar.md +178 -0
  39. package/docs/furo-skeleton.md +92 -0
  40. package/docs/furo-splitter.md +103 -0
  41. package/docs/furo-stepper.md +127 -0
  42. package/docs/furo-switch.md +107 -0
  43. package/docs/furo-table.md +233 -0
  44. package/docs/furo-tabs.md +173 -0
  45. package/docs/furo-text-field.md +249 -0
  46. package/docs/furo-textarea.md +214 -0
  47. package/docs/furo-time-field.md +276 -0
  48. package/docs/furo-toast.md +217 -0
  49. package/docs/furo-toggle-group.md +123 -0
  50. package/docs/furo-toggle.md +104 -0
  51. package/docs/furo-tool-bar.md +88 -0
  52. package/docs/furo-tooltip.md +107 -0
  53. package/lib/assets/css/furo/0030.variables-semantic-color.css +15 -1
  54. package/lib/components/atoms/FuroButton/FuroButtonContext.js +8 -8
  55. package/lib/components/atoms/FuroCheckbox/FuroCheckboxContext.js +1 -1
  56. package/lib/components/atoms/FuroIcon/FuroIcon.vue +1 -1
  57. package/lib/components/atoms/FuroSwitch/FuroSwitchContext.js +1 -1
  58. package/lib/components/atoms/FuroToggle/FuroToggleContext.js +1 -1
  59. package/lib/components/molecules/FuroAccordion/FuroAccordionContext.js +1 -1
  60. package/lib/components/molecules/FuroAttachment/FuroAttachment.vue +1 -1
  61. package/lib/components/molecules/FuroBreadcrumb/FuroBreadcrumb.vue +1 -1
  62. package/lib/components/molecules/FuroCollapsible/FuroCollapsibleContext.js +1 -1
  63. package/lib/components/molecules/FuroControlBlock/FuroControlBlock.vue +2 -2
  64. package/lib/components/molecules/FuroDropdownMenu/FuroDropdownMenu.vue +1 -1
  65. package/lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuContext.js +1 -2
  66. package/lib/components/molecules/FuroErrorState/FuroErrorState.vue +1 -1
  67. package/lib/components/molecules/FuroPopover/FuroPopoverContext.js +1 -1
  68. package/lib/components/molecules/FuroSearchField/FuroSearchField.vue +1 -1
  69. package/lib/components/molecules/FuroSelect/FuroSelect.vue +8 -6
  70. package/lib/components/molecules/FuroSelect/FuroSelectContext.js +35 -0
  71. package/lib/components/molecules/FuroSelect/FuroSelectOptionText.vue +40 -0
  72. package/lib/components/molecules/FuroSelect/FuroSelectOptionTextContext.js +275 -0
  73. package/lib/components/molecules/FuroSplitter/FuroSplitterContext.js +1 -1
  74. package/lib/components/molecules/FuroToggleGroup/FuroToggleGroupContext.js +1 -1
  75. package/lib/components/molecules/FuroTooltip/FuroTooltipContext.js +1 -1
  76. package/lib/components/organisms/FuroSidebar/FuroSidebar.vue +3 -3
  77. package/lib/components/organisms/FuroTable/FuroTable.vue +1 -1
  78. package/lib/components/organisms/FuroTable/FuroTableContext.js +3 -3
  79. package/lib/components/organisms/FuroToast/FuroToastContext.js +6 -2
  80. package/lib/components/organisms/FuroToast/ToastQueue.js +4 -4
  81. package/nuxt/index.js +9 -9
  82. package/package.json +23 -22
  83. package/public/furo-vue/components.json +1 -1
  84. package/types/generated/components/atoms/FuroButton/FuroButtonContext.d.ts +4 -4
  85. package/types/generated/components/molecules/FuroSelect/FuroSelect.vue.d.ts +23 -0
  86. package/types/generated/components/molecules/FuroSelect/FuroSelectContext.d.ts +14 -0
  87. package/types/generated/components/molecules/FuroSelect/FuroSelectOptionText.vue.d.ts +50 -0
  88. package/types/generated/components/molecules/FuroSelect/FuroSelectOptionTextContext.d.ts +141 -0
  89. package/types/generated/components/organisms/FuroTable/FuroTableContext.d.ts +2 -2
@@ -0,0 +1,87 @@
1
+ # `<FuroAvatarGroup>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ `<FuroAvatarGroup>` is a **shipped** avatar-group molecule in the Furo-Vue component system. It shows a set of people as overlapping avatars and collapses past a configurable count, so a row of assignees stays a fixed width instead of overflowing.
6
+
7
+ It is purely presentational: it carries no value and emits nothing.
8
+
9
+ **Public SFC prop:** `parcel` only. HTML attributes pass through to the root.
10
+
11
+ ## Summary
12
+
13
+ | Field | Value |
14
+ |---|---|
15
+ | **Layer** | Molecule |
16
+ | **Path** | `lib/components/molecules/FuroAvatarGroup/` |
17
+ | **Base primitive** | None; a row of `FuroAvatar` |
18
+ | **Value** | None |
19
+ | **Emits** | None |
20
+ | **Composes** | `FuroAvatar` |
21
+
22
+ ## `parcel` keys
23
+
24
+ | Key | Type | Default | Meaning |
25
+ |---|---|---|---|
26
+ | `avatars` | `Array<FuroAvatarGroupEntry>` | `[]` | The people, in the order they render. |
27
+ | `maxVisibleAvatars` | `number` | `5` | How many render before the rest become a count. Values below one are raised to one. |
28
+ | `spacing` | `'wide' \| 'narrow'` | `'wide'` | How far the avatars overlap. |
29
+ | `size` | `string` | `'1.5rem'` | Diameter of one avatar. |
30
+
31
+ ### `FuroAvatarGroupEntry`
32
+
33
+ The subset of `FuroAvatar`'s parcel that identifies a person:
34
+
35
+ | Key | Type | Meaning |
36
+ |---|---|---|
37
+ | `imageSource` | `string` | Photograph. |
38
+ | `imageAlternativeText` | `string` | Alternative text for it. |
39
+ | `fallbackText` | `string` | Shown while the image is absent or loading, for example initials. |
40
+
41
+ The group pins every avatar to a circle, because a row of overlapping squares reads as a stack of cards rather than as a set of people.
42
+
43
+ ## Collapsing
44
+
45
+ A group longer than `maxVisibleAvatars` renders that many avatars and then one count standing for the rest:
46
+
47
+ | People | Renders |
48
+ |---|---|
49
+ | 5 or fewer | one avatar each |
50
+ | 8, showing 5 | five avatars and `+3` |
51
+
52
+ Values below one are raised to one: a group showing no avatar and only a count is a number, not a group.
53
+
54
+ The overflow slot receives both the hidden entries and their count, so a consumer can name the hidden people in a tooltip without recomputing which ones they are.
55
+
56
+ ## Slots
57
+
58
+ | Slot | Scope | Purpose |
59
+ |---|---|---|
60
+ | `overflow` | `{ avatars, count }` | Replaces the count, receiving the hidden entries and how many there are. Default: `+N`. |
61
+
62
+ ## Styling
63
+
64
+ | Part | Class | Notes |
65
+ |---|---|---|
66
+ | Root | `.unit-avatar-group` | Also carries `spacing-wide` or `spacing-narrow` |
67
+ | One person | `.member` | A `FuroAvatar` |
68
+ | The count | `.overflow` | Only rendered by the default slot content |
69
+
70
+ Each avatar and the count carry a ring in `--color-border`, so an overlap still reads as two people rather than one clipped shape. `--furo-avatar-group-size` sets the diameter and is fed from `parcel.size`.
71
+
72
+ ## Read from the design
73
+
74
+ The design draws the group at one avatar step, its smallest, and gives two spacings. The overlap was derived from the widths it draws rather than read from a variable, because the design carries no spacing token for it:
75
+
76
+ | Spacing | Widths drawn for 2, 3, 4, 5 and 5+ people | Step per person | Overlap |
77
+ |---|---|---|---|
78
+ | `wide` | 40, 56, 72, 88, 104 | 16px | 8px |
79
+ | `narrow` | 36, 48, 60, 72, 84 | 12px | 12px |
80
+
81
+ Each row is `24 + (slots - 1) × step` wide, and the collapsed form adds one slot for the count rather than replacing an avatar with it, which is where the default fold at five comes from.
82
+
83
+ The design's own names for the two spacings are `stack_wide` and `stack_narrow`. They are shortened here because the component is already a stack.
84
+
85
+ ### One gap in the existing avatar, recorded rather than fixed
86
+
87
+ The design's avatar has three size steps, 40px, 32px and 24px, and it renders an icon, text or an image. `FuroAvatar` has no size channel: it declares `--furo-avatar-size` at 2.5rem, which is the design's largest step, and a consumer overrides the custom property. That is why this group sets the property rather than passing a size down. Giving `FuroAvatar` a size channel belongs to the fidelity pass on the existing components, not here.
@@ -0,0 +1,101 @@
1
+ # FuroAvatar
2
+
3
+ Image avatar atom with a graceful text fallback. Built on a headless avatar primitive that swaps to the fallback when the image is absent, loading, or fails to load. It carries no value and emits nothing.
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Atom |
10
+ | **Path** | `lib/components/atoms/FuroAvatar/` |
11
+ | **Base primitive** | Headless avatar (`Root` + `Image` + `Fallback`) |
12
+ | **Composes** | Nothing — a single styled avatar |
13
+ | **Emits** | None |
14
+ | **Shape** | `circle` (default) or `square` |
15
+
16
+ ---
17
+
18
+ ## Public API
19
+
20
+ ### Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `parcel` | `FuroAvatarParcel \| null` | `null` | Reactive data object holding the image source, fallback, and shape. |
25
+
26
+ ### Parcel keys
27
+
28
+ | Key | Type | Default | Description |
29
+ |---|---|---|---|
30
+ | `imageSource` | `string` | unset | Image URL. When unset, only the fallback renders. |
31
+ | `imageAlternativeText` | `string` | `''` | Alternative text applied to the image. |
32
+ | `fallbackText` | `string` | `''` | Text shown while the image is absent or loading (e.g. initials). |
33
+ | `fallbackDelay` | `number` | unset | Milliseconds to wait before showing the fallback, avoiding a flash on fast loads. |
34
+ | `shape` | `'circle' \| 'square'` | `'circle'` | Avatar shape; circle is fully rounded, square uses the medium radius. |
35
+
36
+ ### Slots
37
+
38
+ | Slot | Scoped props | Description |
39
+ |---|---|---|
40
+ | `fallback` | — | Custom fallback content; overrides `fallbackText` (e.g. an icon). |
41
+
42
+ ---
43
+
44
+ ## Usage
45
+
46
+ ```vue
47
+ <FuroAvatar
48
+ :parcel="{
49
+ imageSource: '/jane.png',
50
+ imageAlternativeText: 'Jane Doe',
51
+ fallbackText: 'JD',
52
+ }"
53
+ />
54
+ ```
55
+
56
+ Fallback only (no image source):
57
+
58
+ ```vue
59
+ <FuroAvatar :parcel="{ fallbackText: 'AB' }" />
60
+ ```
61
+
62
+ Square shape and a custom size:
63
+
64
+ ```vue
65
+ <FuroAvatar
66
+ :parcel="{ imageSource: '/team.png', shape: 'square' }"
67
+ style="--furo-avatar-size: 4rem"
68
+ />
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Sizing
74
+
75
+ The avatar fills a box driven by the `--furo-avatar-size` custom property (default `2.5rem`). Override it on the element (or an ancestor) to scale the avatar; no size prop is needed.
76
+
77
+ ```vue
78
+ <FuroAvatar :parcel="{ fallbackText: 'XL' }" style="--furo-avatar-size: 5rem" />
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Accessibility
84
+
85
+ - Provide `imageAlternativeText` so the image has an accessible name.
86
+ - The fallback text is uppercased visually; pass readable initials.
87
+ - Colors reference semantic tokens only (`--color-muted` background, `--color-muted-foreground` text); dimensions use `rem`.
88
+
89
+ ---
90
+
91
+ ## Types
92
+
93
+ ```ts
94
+ type FuroAvatarParcel = {
95
+ imageSource?: string
96
+ imageAlternativeText?: string
97
+ fallbackText?: string
98
+ fallbackDelay?: number
99
+ shape?: 'circle' | 'square'
100
+ }
101
+ ```
@@ -0,0 +1,126 @@
1
+ # `<FuroBadge>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ `<FuroBadge>` is a **shipped** badge atom in the Furo-Vue component system. It labels a record's state so that the same state reads the same way on every screen.
6
+
7
+ It is purely presentational: it carries no value, emits nothing, and takes no primitive from a headless library. Everything it renders comes from `parcel`.
8
+
9
+ **Public SFC prop:** `parcel` only. HTML attributes pass through to the root.
10
+
11
+ ## Why the name differs from the design's
12
+
13
+ The design calls this a chip. That word is already claimed in this project's glossary for the token rendered inside a multi-select, and one word carrying two meanings inside the document that defines the shared language is worse than a translation step. So the component is named for what it is.
14
+
15
+ ## Why one component and not five
16
+
17
+ The design draws four separate components, one per shape, and treats its status component as a fifth. All five are the same object seen from different sides: a small label whose colour carries meaning. They are one component here, with a `shape` channel and a `tone` channel, rather than five components with overlapping surfaces.
18
+
19
+ ## Summary
20
+
21
+ | Field | Value |
22
+ |---|---|
23
+ | **Layer** | Atom |
24
+ | **Path** | `lib/components/atoms/FuroBadge/` |
25
+ | **Base primitive** | None; a plain root element |
26
+ | **Value** | None |
27
+ | **Emits** | None |
28
+
29
+ ## `parcel` keys
30
+
31
+ | Key | Type | Default | Meaning |
32
+ |---|---|---|---|
33
+ | `label` | `string` | `''` | Text shown inside the badge. Ignored by the `dot` shape. |
34
+ | `count` | `number` | `null` | Number shown by the `count` shape. Read instead of `label` there, so a consumer passing a number does not stringify it first. A `count` shape with no count renders `0`. |
35
+ | `shape` | `FuroBadgeShape` | `'text'` | `text`, `count`, `state` or `dot`. |
36
+ | `tone` | `FuroBadgeTone` | `'accent'` | `accent`, `neutral`, `success`, `warning` or `error`. |
37
+ | `emphasis` | `FuroBadgeEmphasis` | `'solid'` | `solid`, `subtle` or `outline`. Meaningful for the `text` and `count` shapes only. |
38
+ | `size` | `FuroBadgeSize` | `'medium'` | `large`, `medium` or `small`. |
39
+
40
+ ### Shapes
41
+
42
+ | Shape | Renders | Geometry |
43
+ |---|---|---|
44
+ | `text` | A label on a surface | 28px, 24px or 20px tall; 6px corner, 4px at the small step |
45
+ | `count` | A number on a surface | Fully rounded. A single digit is a circle; two or more digits widen into a pill |
46
+ | `state` | A dot beside a coloured label | No surface, no border |
47
+ | `dot` | A 6px dot | No surface, no border, no text |
48
+
49
+ ### Tones
50
+
51
+ `accent` is the design's own default and reads the `--color-info` family. The other four are the status roles the token layer already declares, which is what the design's state component draws under its own colour names:
52
+
53
+ | Tone | Reads | The design calls it |
54
+ |---|---|---|
55
+ | `accent` | `--color-info` | default |
56
+ | `neutral` | `--color-foreground-secondary` | neutral |
57
+ | `success` | `--color-success` | green |
58
+ | `warning` | `--color-warning` | orange |
59
+ | `error` | `--color-error` | red |
60
+
61
+ The design's own state labels are `due soon`, `dued` and `completed`. Those are domain vocabulary, so they stay in the consumer application; this component exposes the roles, not the workflow.
62
+
63
+ ### Emphasis
64
+
65
+ `emphasis` selects which of the tone's three values the surface, the border and the text read:
66
+
67
+ | Emphasis | Surface | Border | Text |
68
+ |---|---|---|---|
69
+ | `solid` | the tone's solid surface | none | that surface's `-foreground` |
70
+ | `subtle` | the tone's subtle surface | none | that surface's `-foreground` |
71
+ | `outline` | the tone's subtle surface | the tone's border | that surface's `-foreground` |
72
+
73
+ Only `--color-info` carries a distinct `-lighter` border step, because the design draws an outlined badge in the accent family and the neutral family and in no other. The three status families use their base colour for the border.
74
+
75
+ The `solid` emphasis reads a pair of its own rather than the tone's accent colour, so a fill always carries a label chosen against it:
76
+
77
+ | Tone | Solid surface | Solid text | Light | Dark |
78
+ |---|---|---|---|---|
79
+ | `accent` | `--color-info` | `--color-info-foreground` | 5.13:1 | 7.43:1 |
80
+ | `neutral` | `--color-primary` | `--color-primary-foreground` | 9.31:1 | 14.69:1 |
81
+ | `success` | `--color-success` | `--color-success-foreground` | 7.44:1 | 9.82:1 |
82
+ | `warning` | `--color-warning` | `--color-warning-foreground` | 6.74:1 | 8.34:1 |
83
+ | `error` | `--color-error` | `--color-error-foreground` | 5.14:1 | 7.01:1 |
84
+
85
+ Two things follow. The status label is near-black in both themes, because a status fill stays mid-tone in both and the near-white it used to read landed at 2.42:1 in light. And the `neutral` tone fills from `--color-primary` rather than from `--color-foreground-secondary`, which is the colour it still reads as text under `subtle` and `outline`: a text role is never a fill, so the solid cell needed a surface. See [ADR 0039](./adr/0039-an-intent-surface-ships-the-label-it-can-carry.md).
86
+
87
+ The `subtle` and `outline` emphases read a pair of their own on the same terms, because a tint cannot carry the family's mid-tone accent as text:
88
+
89
+ | Tone | Tinted surface | Tinted text | Light | Dark |
90
+ |---|---|---|---|---|
91
+ | `accent` | `--color-info-subtle` | `--color-info-subtle-foreground` | 6.16:1 | 8.15:1 |
92
+ | `neutral` | `--color-muted-subtle` | `--color-foreground-secondary` | 5.04:1 | 11.85:1 |
93
+ | `success` | `--color-success-subtle` | `--color-success-subtle-foreground` | 5.21:1 | 9.94:1 |
94
+ | `warning` | `--color-warning-subtle` | `--color-warning-subtle-foreground` | 6.38:1 | 6.92:1 |
95
+ | `error` | `--color-error-subtle` | `--color-error-subtle-foreground` | 5.72:1 | 8.27:1 |
96
+
97
+ Here the label does flip between the themes, where the solid label above does not: a tint travels from `emerald-050` to `emerald-950`, so its label travels with it. The `neutral` tone reads a foreground role instead, because a neutral family owns no accent step to darken. Painting the accent on its own tint is what put a `subtle` success badge at 2.41:1 in light, which left the two quiet emphases with no legible tone but the neutral one. See [ADR 0040](./adr/0040-a-tinted-surface-ships-its-own-label.md).
98
+
99
+ ## Slots
100
+
101
+ | Slot | Scope | Purpose |
102
+ |---|---|---|
103
+ | default | `{ text }` | Replaces the rendered text, so an application can put an icon, a truncation or tabular figures beside the state. Not rendered for the `dot` shape. |
104
+
105
+ ## Styling
106
+
107
+ | Part | Class | Notes |
108
+ |---|---|---|
109
+ | Root | `.unit-badge` | Also carries `shape-*`, `tone-*`, `emphasis-*`, `size-*`, and `single-digit` for a one-digit count |
110
+ | The mark | `.dot` | Rendered for the `state` and `dot` shapes |
111
+ | The text | `.label` | Only rendered by the default slot content |
112
+
113
+ Override `--color-info`, `--color-success`, `--color-warning` or `--color-error` to re-theme a tone; the badge reads them rather than holding its own colours. Each family's `-foreground`, `-subtle` and `-subtle-foreground` sit beside it in `0030`, so re-point those too when a fill or a tint needs a different label.
114
+
115
+ ## Read from the design, and the contradictions found
116
+
117
+ Every value here was read from the CRM Kit design system rather than inferred. Three things in it disagree with themselves. Each is followed exactly rather than normalised, matching how this project handled the warning family's subtle step, and each is recorded here so the designer can correct the source.
118
+
119
+ 1. **The neutral family's subtle badge carries regular weight** while every other combination carries medium. A badge whose weight changes with its colour reads as a mistake, but it is what the design draws.
120
+ 2. **The count shape's small step declares a 20px line height inside a 16px box**, which cannot fit. The box height wins here, so the badge renders at the size drawn.
121
+ 3. **The count shape's two steps disagree on weight**: its medium step is regular and its small step is medium.
122
+
123
+ Two further notes, not contradictions:
124
+
125
+ - **The count shape has two size steps, not three.** A badge asked for the `large` step renders the medium geometry.
126
+ - **The design draws no solid neutral badge.** The combination is implemented for a complete matrix rather than left to fall back silently, because a `tone` × `emphasis` surface where one cell behaves differently is a trap.
@@ -0,0 +1,89 @@
1
+ # `<FuroBreadcrumb>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ `<FuroBreadcrumb>` is a **shipped** breadcrumb molecule in the Furo-Vue component system. It shows where a record sits in a hierarchy, so a user deep in a tree can see the path and retrace it.
6
+
7
+ It is purely presentational: it carries no value, emits nothing, and takes no primitive from a headless library. The trail comes from `parcel.items`, and navigation is whatever the consumer puts on each entry.
8
+
9
+ **Public SFC prop:** `parcel` only. HTML attributes pass through to the `nav` root.
10
+
11
+ ## Summary
12
+
13
+ | Field | Value |
14
+ |---|---|
15
+ | **Layer** | Molecule |
16
+ | **Path** | `lib/components/molecules/FuroBreadcrumb/` |
17
+ | **Base primitive** | None; a `nav` wrapping an ordered list |
18
+ | **Value** | None |
19
+ | **Emits** | None |
20
+ | **Composes** | `FuroIcon` for the separator and the overflow marker |
21
+
22
+ ## The last entry is the current one
23
+
24
+ Position decides which entry is current, not a flag. A flag would let a caller mark two entries current, or none, and neither is a trail. So the rule is positional and there is nothing to keep in sync.
25
+
26
+ The current entry renders as text even when it carries a destination, because it is where the user already is. It is announced with `aria-current="page"`, which is what a screen reader reports instead of the visual weight change.
27
+
28
+ ## `parcel` keys
29
+
30
+ | Key | Type | Default | Meaning |
31
+ |---|---|---|---|
32
+ | `items` | `Array<FuroBreadcrumbItem>` | `[]` | The trail, root first. The last entry is the current one. |
33
+ | `maxVisibleItems` | `number` | `4` | How many entries show before the trail collapses. Values below two are raised to two. |
34
+
35
+ ### `FuroBreadcrumbItem`
36
+
37
+ | Key | Type | Default | Meaning |
38
+ |---|---|---|---|
39
+ | `label` | `string` | — | Text shown for the entry. |
40
+ | `href` | `string` | — | Destination. An entry with no destination renders as text. |
41
+ | `disabled` | `boolean` | `false` | Renders as text and is announced with `aria-disabled`. |
42
+
43
+ ## Collapsing a deep trail
44
+
45
+ A trail longer than `maxVisibleItems` keeps the **root** and the **tail**, and folds everything between into one overflow marker:
46
+
47
+ | Depth | Renders |
48
+ |---|---|
49
+ | 4 or fewer | every entry |
50
+ | 6, folding at 4 | `Level 1` · marker · `Level 4` `Level 5` `Level 6` |
51
+ | 5, folding at 4 | `Level 1` · marker · `Level 3` `Level 4` `Level 5` |
52
+
53
+ The root is kept because it is the one entry that says where the tree begins. The tail is kept because it says where the user is and how they got to the last turn.
54
+
55
+ `maxVisibleItems` counts the entries shown, not counting the marker. Values below two are raised to two: a fold that shows neither the root nor the current entry has nothing left to say.
56
+
57
+ The overflow slot receives the entries the marker stands for, so a consumer can hang a menu off the marker without recomputing which entries went missing. The component does not open a menu itself: that would mean a molecule reaching for an organism, and the layer order forbids it.
58
+
59
+ ## Slots
60
+
61
+ | Slot | Scope | Purpose |
62
+ |---|---|---|
63
+ | `crumb` | `{ crumb }` | Replaces one entry, for a router link or an icon. Default: the entry label. |
64
+ | `overflow` | `{ items }` | Replaces the overflow marker, receiving the entries it stands for. Default: an ellipsis icon. |
65
+ | `separator` | — | Replaces the mark between entries. Default: a right caret icon. |
66
+
67
+ ## Styling
68
+
69
+ | Part | Class | Notes |
70
+ |---|---|---|
71
+ | Root | `.unit-breadcrumb` | A `nav` |
72
+ | The list | `.trail` | An `ol` with its list styling removed |
73
+ | One position | `.entry` | An `li` holding the entry and its separator |
74
+ | One entry | `.crumb` | Gains `.current` and `.disabled`. Renders as `a` or `span` |
75
+ | The separator | `.separator` | `aria-hidden`, so it is not read out between entries |
76
+ | The marker | `.overflow` | Only rendered by the default slot content |
77
+
78
+ ## Read from the design
79
+
80
+ | Part | Design value | Token |
81
+ |---|---|---|
82
+ | An entry | `#646c7d`, regular 13.5px/20 | `--color-foreground-secondary`, `--font-body-medium` |
83
+ | An entry, hovered | `#424752` | `--color-foreground` |
84
+ | The current entry | `#424752`, medium 13.5px/20 | `--color-foreground`, `--font-label-small` |
85
+ | The separator and the marker | `#8b93a2` | `--color-foreground-glyph` |
86
+
87
+ The current entry is the only place the trail changes weight. The design draws trails at two, three and four levels plus one collapsed form beyond that, which is where the default fold at four comes from.
88
+
89
+ The design also draws entries carrying an icon, a dropdown, or both. Those are reachable through the `crumb` slot rather than as parcel keys, so the component does not grow a key per decoration.
@@ -0,0 +1,263 @@
1
+ # `<FuroButton>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ `<FuroButton>` is a **shipped** action primitive atom in the Furo-Vue component system. Its **public contract is Furo-defined**: application developers pass `parcel`, listen to Furo emits, and use documented slots — without importing the underlying primitive library.
6
+
7
+ Implementation uses a headless action/polymorphic primitive inside `lib/components/atoms/FuroButton/*` only. That dependency is **not** part of the application API.
8
+
9
+ Contract style aligns with other atoms:
10
+
11
+ - behavior via `:parcel` (`FuroButtonParcel`)
12
+ - HTML attributes via fallthrough (`id`, `aria-*`, `type` override via parcel, …)
13
+ - Furo emit `click` with `ButtonEmitPayload`
14
+
15
+ **Public SFC prop:** `parcel` only.
16
+
17
+ **No `v-model`:** `FuroButton` is an action trigger, not a form value control.
18
+
19
+ **Source files:**
20
+
21
+ - `lib/components/atoms/FuroButton/FuroButton.vue`
22
+ - `lib/components/atoms/FuroButton/FuroButtonContext.js`
23
+ - `lib/components/atoms/FuroButton/ButtonEmitPayload.js`
24
+
25
+ **Live samples:** `pages/samples/components/playground/FuroButtonPlayground/index.vue`
26
+
27
+ ---
28
+
29
+ ## Public API at a glance
30
+
31
+ | Area | Status | Notes |
32
+ | ---- | ------ | ----- |
33
+ | **SFC surface** | | |
34
+ | Public prop `parcel` only (+ attribute fallthrough) | **Implemented** | See [`FuroButtonParcel`](#furobuttonparcel) |
35
+ | **Visual** | | |
36
+ | Variants `default`, `secondary`, `destructive`, `outline`, `ghost`, `link` | **Implemented** | Via `parcel.variant` |
37
+ | Sizes `default`, `sm`, `lg`, `icon` | **Implemented** | Via `parcel.size` |
38
+ | `disabled`, `loading` | **Implemented** | Blocks click emit; `aria-disabled` / `aria-busy` |
39
+ | `asChild` | **Implemented** | Headless composition for menu/dialog triggers |
40
+ | **Emits** | | |
41
+ | `click` | **Implemented** | `ButtonEmitPayload`; suppressed when disabled/loading |
42
+ | **Slots** | | |
43
+ | `default` | **Implemented** | Button label / content |
44
+ | `loading` | **Implemented** | Custom spinner (default: `ph:circle-notch` icon) |
45
+
46
+ ---
47
+
48
+ ## Abstraction boundary
49
+
50
+ | Layer | Responsibility |
51
+ | ----- | -------------- |
52
+ | Application | `parcel`, listeners, slots |
53
+ | Atom (`FuroButton`) | Maps parcel → the underlying primitive; DOM, emits, default loading indicator |
54
+ | Underlying primitive (maintainers) | Internal only; not part of the application API |
55
+
56
+ **Rules:**
57
+
58
+ 1. Documented `parcel` keys are the **Furo contract**. Furo-owned keys are stripped in `extractRootAttributes()` before optional passthrough to the underlying primitive.
59
+ 2. Do **not** import the underlying primitive in application code.
60
+ 3. Use `parcel.asChild` when composing with `FuroDropdownMenu` or other underlying-primitive triggers — not ad-hoc wrapper divs.
61
+
62
+ ---
63
+
64
+ ## Props
65
+
66
+ ### Rule: `parcel` vs attributes
67
+
68
+ | Mechanism | When to use | Application sees |
69
+ | --------- | ----------- | ---------------- |
70
+ | `:parcel` | Furo button behavior | `FuroButtonParcel` (+ merged defaults) |
71
+ | Attributes on `<FuroButton>` | e.g. `id`, `aria-label` | Vue fallthrough to the underlying primitive (merged with `extractRootAttributes()`) |
72
+
73
+ > **`parcel` must be JSON-serializable** — no functions inside `parcel`.
74
+
75
+ ```html
76
+ <FuroButton
77
+ id="create-record"
78
+ :parcel="{
79
+ variant: 'default',
80
+ size: 'default',
81
+ }"
82
+ @click="context.onCreateRecord({ payload: $event })"
83
+ >
84
+ Create record
85
+ </FuroButton>
86
+ ```
87
+
88
+ ### Component props (public)
89
+
90
+ | Prop | Type | Default | Description |
91
+ | ---- | ---- | ------- | ----------- |
92
+ | `parcel` | `FuroButtonParcel` or `null` | `null` | Furo behavior (see table below) |
93
+
94
+ ### `FuroButtonParcel`
95
+
96
+ Keys read in `FuroButtonContext` (defaults from `parcel` getter):
97
+
98
+ | Key | Type | Default (getter) | Description |
99
+ | --- | ---- | ---------------- | ----------- |
100
+ | `variant` | `FuroButtonVariant` | `'default'` | Visual style preset |
101
+ | `size` | `FuroButtonSize` | `'default'` | Size preset |
102
+ | `disabled` | `boolean` | `false` | Disables interaction (also respects fallthrough `disabled`) |
103
+ | `loading` | `boolean` | `false` | Shows loading slot; blocks click emit |
104
+ | `asChild` | `boolean` | `false` | Merge props/behavior onto single child (headless composition) |
105
+ | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button `type` when not `asChild` |
106
+
107
+ ### `FuroButtonVariant`
108
+
109
+ `'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'`
110
+
111
+ ### `FuroButtonSize`
112
+
113
+ `'default' | 'sm' | 'lg' | 'icon'`
114
+
115
+ Furo-owned keys above are **not** forwarded via `extractRootAttributes()`.
116
+
117
+ ---
118
+
119
+ ## Emits
120
+
121
+ | Event | Payload | When | Implemented |
122
+ | ----- | ------- | ---- | ----------- |
123
+ | `click` | `ButtonEmitPayload` | User activates button | Yes — not fired when `disabled` or `loading` |
124
+
125
+ ### Parent handler style
126
+
127
+ ```js
128
+ onCreateRecord ({
129
+ payload,
130
+ }) {
131
+ if (payload.isDefaultPrevented()) {
132
+ return
133
+ }
134
+
135
+ this.createRecord()
136
+ }
137
+ ```
138
+
139
+ ```html
140
+ @click="context.onCreateRecord({ payload: $event })"
141
+ ```
142
+
143
+ ---
144
+
145
+ ## `ButtonEmitPayload` API
146
+
147
+ `ButtonEmitPayload` extends [`BaseEmitPayload`](../lib/emit-payload/BaseEmitPayload.js). Built from native `MouseEvent` / `PointerEvent` on click.
148
+
149
+ | Member | Type | Description |
150
+ | ------ | ---- | ----------- |
151
+ | `controlElement` | `HTMLElement \| null` | Button root (`currentTarget` preferred over `target`) |
152
+ | `isDefaultPrevented()` | `boolean` | Whether the native event called `preventDefault()` |
153
+
154
+ ---
155
+
156
+ ## Slots
157
+
158
+ | Slot | Scoped props | Default |
159
+ | ---- | ------------ | ------- |
160
+ | `default` | — | Button label / icon content |
161
+ | `loading` | — | `ph:circle-notch` spinner in loading overlay |
162
+
163
+ TypeScript / JSDoc: `FuroButtonSlots` in `FuroButtonContext.js` (`default`, `loading` — no scoped props).
164
+
165
+ When `loading` is `true`, the `default` slot content stays in the layout (`visibility: hidden`) so the button keeps its size. The `loading` slot renders in an absolutely positioned overlay on top of the label — no layout shift for the button or adjacent UI.
166
+
167
+ The default `ph:circle-notch` spinner turns once every `--transition-timer-spin` (`0.75s`), a duration of its own so retuning `--transition-timer` for hover and focus transitions never changes the spin. Under `prefers-reduced-motion: reduce` the spinner stays still. Set `--transition-timer-spin` on `:root` to change the pace.
168
+
169
+ When `parcel.asChild` is `true`, composition forwards to a single child element; the overlay pattern is not applied (use `asChild: false` when you need loading overlay).
170
+
171
+ ---
172
+
173
+ ## `asChild` composition
174
+
175
+ Use when another headless primitive or custom element must receive button styling and behavior:
176
+
177
+ ```html
178
+ <FuroButton :parcel="{ asChild: true, variant: 'outline' }">
179
+ <a href="/docs">
180
+ Documentation
181
+ </a>
182
+ </FuroButton>
183
+ ```
184
+
185
+ `FuroDropdownMenu` (build #2) will use `FuroButton` as the default trigger with `parcel.asChild: true`.
186
+
187
+ ---
188
+
189
+ ## Accessibility requirements
190
+
191
+ - Provide visible label text or `aria-label` on the button.
192
+ - Keyboard: Enter and Space activate (native button / the underlying primitive with `as="button"`).
193
+ - `aria-disabled="true"` when `parcel.disabled` or fallthrough `disabled`.
194
+ - `aria-busy="true"` when `parcel.loading`.
195
+ - Focus ring via `:focus-visible` using `var(--color-ring)`.
196
+ - Icon-only buttons: use `parcel.size: 'icon'` and `aria-label`.
197
+
198
+ ---
199
+
200
+ ## Styling convention
201
+
202
+ Styles in SFC `@layer furo`:
203
+
204
+ - Root: `.unit-button.{variant}` + optional `.size-sm`, `.size-lg`, `.size-icon`
205
+ - States: `.disabled`, `.loading`, `:hover`, `:focus-visible`, `:active`
206
+ - Tokens: `--color-primary`, `--color-secondary`, `--color-destructive`, `--color-border`, `--color-accent`, `--color-ring`, `--size-input-height`, `--size-border-radius-medium`
207
+
208
+ **Note:** `FuroButton` uses `unit-button`, not `unit-form-control` (form atoms only).
209
+
210
+ ---
211
+
212
+ ## Examples
213
+
214
+ ### Variants
215
+
216
+ ```html
217
+ <FuroButton :parcel="{ variant: 'default' }">
218
+ Primary
219
+ </FuroButton>
220
+
221
+ <FuroButton :parcel="{ variant: 'destructive' }">
222
+ Delete
223
+ </FuroButton>
224
+ ```
225
+
226
+ ### Loading
227
+
228
+ ```html
229
+ <FuroButton
230
+ :parcel="{
231
+ loading: context.statusReactive.isSubmitting,
232
+ disabled: context.statusReactive.isSubmitting,
233
+ }"
234
+ @click="context.submitForm({ payload: $event })"
235
+ >
236
+ Save
237
+ </FuroButton>
238
+ ```
239
+
240
+ ### Icon button
241
+
242
+ ```html
243
+ <FuroButton
244
+ :parcel="{
245
+ variant: 'outline',
246
+ size: 'icon',
247
+ }"
248
+ aria-label="Add record"
249
+ >
250
+ <Icon name="ph:plus" />
251
+ </FuroButton>
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Implementation status
257
+
258
+ | Deliverable | Status |
259
+ | ----------- | ------ |
260
+ | `FuroButton.vue` + context + payload | Shipped |
261
+ | Public prop: `parcel` only | Shipped |
262
+ | Playground `FuroButtonPlayground` | Shipped |
263
+ | Unit tests `FuroButtonContext.js`, `ButtonEmitPayload.js` (jsdom) | Shipped |