@openreachtech/furo-vue 1.6.0 → 2.0.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 (119) 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/components/atoms/FuroButton/FuroButtonContext.js +8 -8
  54. package/lib/components/atoms/FuroCheckbox/FuroCheckboxContext.js +1 -1
  55. package/lib/components/atoms/FuroSwitch/FuroSwitchContext.js +1 -1
  56. package/lib/components/atoms/FuroToggle/FuroToggleContext.js +1 -1
  57. package/lib/components/molecules/FuroAccordion/FuroAccordionContext.js +1 -1
  58. package/lib/components/molecules/FuroCollapsible/FuroCollapsibleContext.js +1 -1
  59. package/lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuContext.js +1 -2
  60. package/lib/components/molecules/FuroPopover/FuroPopoverContext.js +1 -1
  61. package/lib/components/molecules/FuroSelect/FuroSelect.vue +8 -6
  62. package/lib/components/molecules/FuroSelect/FuroSelectContext.js +35 -0
  63. package/lib/components/molecules/FuroSelect/FuroSelectOptionText.vue +40 -0
  64. package/lib/components/molecules/FuroSelect/FuroSelectOptionTextContext.js +275 -0
  65. package/lib/components/molecules/FuroSplitter/FuroSplitterContext.js +1 -1
  66. package/lib/components/molecules/FuroToggleGroup/FuroToggleGroupContext.js +1 -1
  67. package/lib/components/molecules/FuroTooltip/FuroTooltipContext.js +1 -1
  68. package/lib/components/organisms/FuroTable/FuroTableContext.js +3 -3
  69. package/lib/components/organisms/FuroToast/FuroToastContext.js +6 -2
  70. package/lib/components/organisms/FuroToast/ToastQueue.js +4 -4
  71. package/nuxt/index.js +9 -9
  72. package/package.json +24 -22
  73. package/public/furo-vue/components.json +1 -1
  74. package/types/generated/components/atoms/FuroAvatar/FuroAvatar.vue.d.ts +6 -15
  75. package/types/generated/components/atoms/FuroBadge/FuroBadge.vue.d.ts +8 -43
  76. package/types/generated/components/atoms/FuroButton/FuroButton.vue.d.ts +10 -45
  77. package/types/generated/components/atoms/FuroButton/FuroButtonContext.d.ts +4 -4
  78. package/types/generated/components/atoms/FuroCheckbox/FuroCheckbox.vue.d.ts +8 -20
  79. package/types/generated/components/atoms/FuroNumberField/FuroNumberField.vue.d.ts +12 -12
  80. package/types/generated/components/atoms/FuroRadio/FuroRadio.vue.d.ts +8 -24
  81. package/types/generated/components/atoms/FuroSwitch/FuroSwitch.vue.d.ts +0 -13
  82. package/types/generated/components/atoms/FuroToggle/FuroToggle.vue.d.ts +8 -19
  83. package/types/generated/components/molecules/FuroAccordion/FuroAccordion.vue.d.ts +14 -30
  84. package/types/generated/components/molecules/FuroAttachment/FuroAttachment.vue.d.ts +8 -44
  85. package/types/generated/components/molecules/FuroAutocompleteField/FuroAutocompleteField.vue.d.ts +74 -105
  86. package/types/generated/components/molecules/FuroAvatarGroup/FuroAvatarGroup.vue.d.ts +9 -18
  87. package/types/generated/components/molecules/FuroBreadcrumb/FuroBreadcrumb.vue.d.ts +22 -48
  88. package/types/generated/components/molecules/FuroCollapsible/FuroCollapsible.vue.d.ts +12 -23
  89. package/types/generated/components/molecules/FuroControlBlock/FuroControlBlock.vue.d.ts +6 -4
  90. package/types/generated/components/molecules/FuroDatePicker/FuroDatePicker.vue.d.ts +14 -16
  91. package/types/generated/components/molecules/FuroDropdownMenu/FuroDropdownMenu.vue.d.ts +13 -13
  92. package/types/generated/components/molecules/FuroDropdownMenu/FuroDropdownMenuOptionRows.vue.d.ts +34 -47
  93. package/types/generated/components/molecules/FuroEditableField/FuroEditableField.vue.d.ts +93 -115
  94. package/types/generated/components/molecules/FuroEmptyState/FuroEmptyState.vue.d.ts +8 -7
  95. package/types/generated/components/molecules/FuroErrorState/FuroErrorState.vue.d.ts +8 -7
  96. package/types/generated/components/molecules/FuroPagination/FuroPagination.vue.d.ts +14 -15
  97. package/types/generated/components/molecules/FuroPopover/FuroPopover.vue.d.ts +8 -7
  98. package/types/generated/components/molecules/FuroScrollArea/FuroScrollArea.vue.d.ts +6 -4
  99. package/types/generated/components/molecules/FuroSearchField/FuroSearchField.vue.d.ts +8 -22
  100. package/types/generated/components/molecules/FuroSectionHeader/FuroSectionHeader.vue.d.ts +10 -25
  101. package/types/generated/components/molecules/FuroSelect/FuroSelect.vue.d.ts +97 -108
  102. package/types/generated/components/molecules/FuroSelect/FuroSelectContext.d.ts +14 -0
  103. package/types/generated/components/molecules/FuroSelect/FuroSelectOptionText.vue.d.ts +52 -0
  104. package/types/generated/components/molecules/FuroSelect/FuroSelectOptionTextContext.d.ts +141 -0
  105. package/types/generated/components/molecules/FuroSplitter/FuroSplitter.vue.d.ts +14 -29
  106. package/types/generated/components/molecules/FuroTimeField/FuroTimeField.vue.d.ts +8 -7
  107. package/types/generated/components/molecules/FuroToggleGroup/FuroToggleGroup.vue.d.ts +9 -24
  108. package/types/generated/components/molecules/FuroToolBar/FuroToolBar.vue.d.ts +6 -9
  109. package/types/generated/components/molecules/FuroTooltip/FuroTooltip.vue.d.ts +8 -7
  110. package/types/generated/components/organisms/FuroAlertDialog/FuroAlertDialog.vue.d.ts +17 -19
  111. package/types/generated/components/organisms/FuroDialog/FuroDialog.vue.d.ts +20 -24
  112. package/types/generated/components/organisms/FuroDrawer/FuroDrawer.vue.d.ts +20 -24
  113. package/types/generated/components/organisms/FuroEditor/FuroEditor.vue.d.ts +9 -42
  114. package/types/generated/components/organisms/FuroSidebar/FuroSidebar.vue.d.ts +23 -62
  115. package/types/generated/components/organisms/FuroStepper/FuroStepper.vue.d.ts +19 -19
  116. package/types/generated/components/organisms/FuroTable/FuroTable.vue.d.ts +54 -71
  117. package/types/generated/components/organisms/FuroTable/FuroTableContext.d.ts +2 -2
  118. package/types/generated/components/organisms/FuroTabs/FuroTabs.vue.d.ts +17 -17
  119. package/types/generated/components/organisms/FuroToast/FuroToast.vue.d.ts +25 -31
@@ -0,0 +1,217 @@
1
+ # FuroToast
2
+
3
+ Organism — a controlled notification stack built on a headless toast primitive
4
+ (`Provider` + `Viewport` + per-toast `Root` + `Title` + `Description` + `Action`
5
+ + `Close`). The **parent owns the queue** via `parcel.toasts`; FuroToast renders
6
+ it and emits `dismiss` / `action` by key so the parent updates its own array.
7
+ No internal queue, no imperative API, registry-free. Domain-agnostic: no
8
+ GraphQL, router, or Nuxt imports.
9
+
10
+ Location: `lib/components/organisms/FuroToast/`
11
+
12
+ ## Props
13
+
14
+ | Prop | Type | Default | Notes |
15
+ |------|------|---------|-------|
16
+ | `parcel` | `ToastParcel \| null` | `null` | Reactive behavior object (below) |
17
+
18
+ ### `parcel`
19
+
20
+ | Key | Type | Default | Notes |
21
+ |-----|------|---------|-------|
22
+ | `toasts` | `Array<ToastEntry>` | `[]` | The queue, owned by the parent. Render order follows the array; remove an entry on `dismiss`. |
23
+ | `position` | `'top-left' \| 'top-center' \| 'top-right' \| 'bottom-left' \| 'bottom-center' \| 'bottom-right'` | `'bottom-right'` | Viewport placement. |
24
+ | `durationInMilliseconds` | `number` | `4000` | Default auto-dismiss in ms. Per-toast `durationInMilliseconds` overrides it. |
25
+ | `swipeDirection` | `'right' \| 'left' \| 'up' \| 'down'` | derived from `position` | Swipe-to-dismiss direction. When unset, a left toast swipes left, a right toast swipes right, and a centered toast swipes toward its vertical edge. |
26
+ | `label` | `string` | `'Notifications'` | Accessible name of the toast region, in the app's own language. It also prefixes the announcement of each toast. When unset, the region keeps its default name, `Notifications (F8)`. |
27
+ | `hotkey` | `Array<string> \| null` | `null` | Keyboard shortcut to focus the viewport (e.g. `['F8']`). |
28
+ | `maxVisible` | `number` | `3` | Caps how many toasts render at once (the most recent). The rest stay queued in your array until a visible one is dismissed. |
29
+
30
+ ### `ToastEntry`
31
+
32
+ | Key | Type | Default | Notes |
33
+ |-----|------|---------|-------|
34
+ | `key` | `string \| number` | — | **Required, stable.** Identifies the toast in `dismiss` / `action` payloads. |
35
+ | `title` | `string \| null` | `null` | Heading text (or use the `title` slot). |
36
+ | `description` | `string \| null` | `null` | Body text (or use the `description` slot). |
37
+ | `type` | `'info' \| 'success' \| 'warning' \| 'error' \| 'loading'` | `'info'` | Shown as a small Phosphor (`ph:`) icon. `info` is neutral grey; `success`/`warning`/`error` tint only the icon. `loading` shows a spinner and is held open (no auto-dismiss) — see the promise pattern below. |
38
+ | `durationInMilliseconds` | `number \| null` | `null` | Per-toast auto-dismiss override; `null` uses `parcel.durationInMilliseconds`. Ignored while `loading`. |
39
+ | `closeText` | `string` | `'Close'` | Accessible label for the close button. |
40
+ | `actionText` | `string \| null` | `null` | When set, renders an action button. |
41
+ | `actionPending` | `boolean` | `false` | While `true`, the action button shows a spinner and is disabled — set it during an async action. |
42
+
43
+ ## Events
44
+
45
+ | Event | Payload | When |
46
+ |-------|---------|------|
47
+ | `dismiss` | `{ key }` | A toast closed (timeout, swipe, or close button). Remove it from your array. |
48
+ | `action` | `{ key }` | The action button was clicked. The toast also dismisses afterward. |
49
+
50
+ ## Slots
51
+
52
+ | Slot | Scope | Purpose |
53
+ |------|-------|---------|
54
+ | `title` | `{ toast }` | Overrides a toast's title region (falls back to `toast.title`). |
55
+ | `description` | `{ toast }` | Overrides a toast's description region (falls back to `toast.description`). |
56
+
57
+ ## Usage
58
+
59
+ ```vue
60
+ <script setup>
61
+ import { ref } from 'vue'
62
+
63
+ const toasts = ref([])
64
+ let nextKey = 1
65
+
66
+ function notify (entry) {
67
+ toasts.value = [...toasts.value, { key: nextKey++, ...entry }]
68
+ }
69
+
70
+ function onDismiss ({ key }) {
71
+ toasts.value = toasts.value.filter(toast => toast.key !== key)
72
+ }
73
+ </script>
74
+
75
+ <template>
76
+ <button @click="notify({ type: 'success', title: 'Saved' })">Save</button>
77
+
78
+ <FuroToast
79
+ :parcel="{ toasts, position: 'bottom-right' }"
80
+ @dismiss="onDismiss"
81
+ @action="({ key }) => retry(key)"
82
+ />
83
+ </template>
84
+ ```
85
+
86
+ ### Promise toast (loading → success/error)
87
+
88
+ Add a `loading` toast, await your work, then patch the same entry by key:
89
+
90
+ ```js
91
+ function notifyWithPromise () {
92
+ const key = nextKey++
93
+ toasts.value = [...toasts.value, { key, type: 'loading', title: 'Uploading…' }]
94
+
95
+ upload()
96
+ .then(() => patch(key, { type: 'success', title: 'Upload complete' }))
97
+ .catch(() => patch(key, { type: 'error', title: 'Upload failed', actionText: 'Retry' }))
98
+ }
99
+ ```
100
+
101
+ ### Async action (e.g. Retry)
102
+
103
+ The action button does **not** auto-dismiss the toast — clicking it only emits
104
+ `action`. You decide what happens: keep the toast as a `loading` promise toast
105
+ while the work runs, then **close it** or **show a success message** (your
106
+ choice). Two ways to show progress:
107
+
108
+ ```js
109
+ // A) Turn the toast into a loading promise toast, then close OR show success.
110
+ async function onAction ({ key }) {
111
+ patch(key, { type: 'loading', title: 'Retrying…', actionText: null })
112
+ await retry()
113
+
114
+ // close immediately…
115
+ onDismiss({ key })
116
+ // …or show success (auto-dismisses after its duration):
117
+ // patch(key, { type: 'success', title: 'Done' })
118
+ }
119
+
120
+ // B) Keep the action button but show a spinner on it while pending.
121
+ async function onAction ({ key }) {
122
+ patch(key, { actionPending: true })
123
+ await retry()
124
+ onDismiss({ key })
125
+ }
126
+ ```
127
+
128
+ ## Imperative helper
129
+
130
+ For global usage, mount `<FuroToaster />` once (e.g. in your app
131
+ layout), then trigger toasts from anywhere. This is an **opt-in layer** on top
132
+ of the controlled `FuroToast` — the organism itself stays controlled and
133
+ registry-free; a `ToastQueue` singleton bridges function handlers to the parcel.
134
+
135
+ The supported import is the package root:
136
+
137
+ ```js
138
+ import { toast } from '@openreachtech/furo-vue'
139
+ ```
140
+
141
+ The `@openreachtech/furo-vue/toast` subpath and the
142
+ `@openreachtech/furo-vue/lib/components/organisms/FuroToast/toast.js` deep path
143
+ reach the same helper. All three share one queue even when a bundler
144
+ pre-bundles the package into a second copy of the module, because the queue is
145
+ pinned to the realm under `Symbol.for('@openreachtech/furo-vue/toastQueue')`.
146
+
147
+ ```vue
148
+ <!-- app layout, once -->
149
+ <script setup>
150
+ import { FuroToaster } from '@openreachtech/furo-vue'
151
+ </script>
152
+ <template>
153
+ <FuroToaster />
154
+ </template>
155
+ ```
156
+
157
+ ```js
158
+ import { toast } from '@openreachtech/furo-vue'
159
+
160
+ const key = toast.show({
161
+ type: 'error',
162
+ title: 'Upload failed',
163
+ description: 'Could not upload the file.',
164
+ actions: [
165
+ { label: 'Retry', onClick: () => retry() },
166
+ { label: 'Dismiss', onClick: () => {} },
167
+ ],
168
+ })
169
+
170
+ toast.update(key, { type: 'success', title: 'Done' }) // loading → success
171
+ toast.hide(key)
172
+ toast.clear()
173
+ toast.configure({ position: 'top-right', richColors: true })
174
+ ```
175
+
176
+ | Method | Signature | Purpose |
177
+ |--------|-----------|---------|
178
+ | `toast.show` | `(options) => key` | Show a toast; returns its key. `options` mirrors a `ToastEntry` plus `actions: [{ label, onClick }]`. |
179
+ | `toast.hide` | `(key) => void` | Dismiss a toast by key. |
180
+ | `toast.update` | `(key, patch) => void` | Merge fields into a toast (promise pattern). |
181
+ | `toast.clear` | `() => void` | Dismiss all toasts. |
182
+ | `toast.configure` | `(config) => void` | Set viewport defaults (`position`, `durationInMilliseconds`, `maxVisible`, `richColors`, …). |
183
+
184
+ Unlike the controlled API, `actions[].onClick` are **functions held in the
185
+ queue** (not serialized). `<FuroToast>` stays JSON-serializable; `FuroToaster`
186
+ routes its `action` event back to the matching handler.
187
+
188
+ **SSR note:** the `toast` singleton is realm-global, so on a server it is
189
+ shared across requests. Trigger `toast.*` on the client only (toasts are a
190
+ client interaction).
191
+
192
+ ## Notes
193
+
194
+ - **Collapsed stack.** Toasts stack on top of each other: the
195
+ newest sits in front and pushes older ones down/back (offset + 5%-per-level
196
+ scale, all sharing the front toast's height). Hovering or focusing the stack
197
+ expands it into a full list; leaving collapses it again. Heights are measured
198
+ per toast, so variable-height toasts stack and expand correctly.
199
+ - **Direction-aware motion.** A toast enters by sliding in from its position's
200
+ edge, and on close / auto-dismiss it slides back out the same way (the reverse
201
+ of its entry). Swipe-to-dismiss follows the position: drag toward the left
202
+ edge for left positions, the right edge for right positions.
203
+ - `dismiss` is emitted ~250ms *after* the timer/close fires, so the exit
204
+ animation can play before you remove the entry. Remove it in your handler as
205
+ usual — the delay is internal.
206
+ - The parent is the single source of truth for the queue — FuroToast holds no
207
+ internal toast state. Always remove an entry in your `dismiss` handler, or the
208
+ toast reopens.
209
+ - Keys must be stable and unique; they are the only identifier in the emit payloads.
210
+ - The design uses a neutral surface, soft shadow, a single
211
+ small stroke icon per type, and a close button that fades in on hover.
212
+ - Type accents map to `--furo-toast-{info|success|warning|error}-accent`
213
+ component tokens (the icon color only); override them to retheme.
214
+ - The built-in close button takes its size from `--size-close-button-small`.
215
+ - Toasts are portaled by the underlying primitive; the viewport carries
216
+ `unit-toast-viewport {position}` and each toast `unit-toast {type}` inside
217
+ `@layer furo`.
@@ -0,0 +1,123 @@
1
+ # FuroToggleGroup
2
+
3
+ A set of related toggles where the group manages selection — text alignment (single choice), text styles like bold/italic/underline (multiple choices), or a table↔sequential view switch. Built on a headless toggle-group primitive that owns roving focus and selection state. The selection is exposed through the value channel (`v-model:value`), the same surface as the other Furo form controls.
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Molecule |
10
+ | **Path** | `lib/components/molecules/FuroToggleGroup/` |
11
+ | **Base primitive** | Headless toggle-group (`Root` + `Item`, roving focus, `data-state="on" / "off"`) |
12
+ | **Value** | `string` (single) / `string[]` (multiple) via `v-model:value` / `parcel.value` |
13
+ | **Emits** | `change-value`, `update:value` |
14
+ | **Composes** | Toggle items styled to match `FuroToggle`; building block for `FuroToolBar` |
15
+
16
+ ---
17
+
18
+ ## Public API
19
+
20
+ ### Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `parcel` | `FuroToggleGroupParcel \| null` | `null` | Reactive data object holding the selection, options, selection mode, variant, size, orientation, and disabled flag. |
25
+
26
+ ### Parcel keys
27
+
28
+ | Key | Type | Default | Description |
29
+ |---|---|---|---|
30
+ | `value` | `string \| string[]` | — | Controlled selection. Present → controlled; absent → uncontrolled. A single value in `single` mode, an array in `multiple` mode. |
31
+ | `selectionMode` | `'single' \| 'multiple'` | `'single'` | Whether one or many options may be pressed at once. |
32
+ | `options` | `Array<{ value, label?, disabled? }>` | `[]` | The toggle items. `label` falls back to `value` when omitted. |
33
+ | `variant` | `'default' \| 'outline'` | `'default'` | Item styling — `outline` adds a border. |
34
+ | `size` | `'default' \| 'sm' \| 'lg'` | `'default'` | Item height and padding. |
35
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Layout direction. |
36
+ | `disabled` | `boolean` | `false` | Disables the whole group; a per-option `disabled` flag disables a single item. |
37
+
38
+ ### Emits
39
+
40
+ | Event | Payload | When |
41
+ |---|---|---|
42
+ | `change-value` | `ToggleGroupEmitPayload` | On every selection change. Wraps a `CustomEvent` exposing `value` and the control element; `isSelected({ value })` reports per-option state. |
43
+ | `update:value` | `string \| string[]` | v-model sync of the selection. |
44
+
45
+ ### Slots
46
+
47
+ | Slot | Props | Description |
48
+ |---|---|---|
49
+ | `option` | `{ option, pressed }` | Custom item content (e.g. icons). Defaults to the option's label. `pressed` reflects whether the option is selected. |
50
+
51
+ ---
52
+
53
+ ## Behavior
54
+
55
+ | Concern | Behavior |
56
+ |---|---|
57
+ | **Controlled vs uncontrolled** | When `parcel.value` is present the internal model mirrors it via a watcher, and a click only emits the new value, so the group keeps showing `parcel.value` until the parent writes the new one back. Otherwise the primitive self-manages from an empty start. |
58
+ | **Domain-agnostic keys** | Public keys are `value` and `selectionMode`; the mapping to the primitive's `modelValue` / `type` happens inside the component. |
59
+ | **Selection mode** | `single` keeps the value a string; `multiple` keeps it an array. The empty value is `null` (single) or `[]` (multiple). |
60
+ | **Disabled** | `parcel.disabled` (or a fallthrough `disabled` attr) disables the group; `option.disabled` disables one item. |
61
+ | **Pressed styling** | The primitive's `data-state="on"` drives the pressed appearance — no JS branching in the template. |
62
+ | **Passthrough** | Non-Furo attributes (`aria-label`, …) fall through to the root (`inheritAttrs: false` + `v-bind="$attrs"`). |
63
+
64
+ ---
65
+
66
+ ## Usage
67
+
68
+ ```vue
69
+ <!-- Single selection -->
70
+ <FuroToggleGroup
71
+ v-model:value="alignment"
72
+ aria-label="Text alignment"
73
+ :parcel="{
74
+ selectionMode: 'single',
75
+ variant: 'outline',
76
+ options: [
77
+ { value: 'left', label: 'Left' },
78
+ { value: 'center', label: 'Center' },
79
+ { value: 'right', label: 'Right' },
80
+ ],
81
+ }"
82
+ />
83
+
84
+ <!-- Multiple selection with custom icon content -->
85
+ <FuroToggleGroup
86
+ v-model:value="textStyles"
87
+ aria-label="Text styles"
88
+ :parcel="{
89
+ selectionMode: 'multiple',
90
+ options: [
91
+ { value: 'bold' },
92
+ { value: 'italic' },
93
+ { value: 'underline' },
94
+ ],
95
+ }"
96
+ >
97
+ <template #option="{ option }">
98
+ <Icon :name="`ph:text-${option.value}`" />
99
+ </template>
100
+ </FuroToggleGroup>
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Composition notes
106
+
107
+ - Imports nothing from the app — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
108
+ - Uses the headless `Item` (not nested `FuroToggle`) so roving focus and single/multi selection come from the primitive; items only borrow `FuroToggle`'s visual tokens.
109
+ - All logic lives in `FuroToggleGroupContext`: extract-only getters (`parcel`, `options`), boolean methods (`isControlled()`, `isMultiple()`, `isDisabled()`, `isSelected()`, `isOptionDisabled()`), and the change handler that emits both channels.
110
+ - CSS is wrapped in `@layer furo`; dimensions use `rem`, `gap` uses `--size-space-*`, and colors reference semantic tokens (`--color-accent`, `--color-border`, `--color-ring`) only.
111
+
112
+ ---
113
+
114
+ ## Testing
115
+
116
+ - `FuroToggleGroupContext` is fully covered by jsdom unit tests: parcel defaults, the boolean methods split into truthy / falsy describe blocks, `resolveSyncedValue()`, label / class / attribute extraction, controlled-value sync in `setupComponent()`, and the dual emit in `onChangeValue()` for both single and multiple modes.
117
+ - No SFC mount test: rendering is declarative bindings over the tested context plus the headless primitive's own semantics, verified by manual QA in the playground.
118
+
119
+ ---
120
+
121
+ ## Playground
122
+
123
+ `/samples/components/playground` → **Toggle Group** tab. Demonstrates single selection, multiple selection (`v-model:value`), vertical orientation, and disabled states.
@@ -0,0 +1,104 @@
1
+ # FuroToggle
2
+
3
+ Two-state pressed/unpressed button — bold/italic in an editor toolbar, a filter on/off switch, a view-mode flip. Built on a headless toggle primitive that owns the `aria-pressed` / `data-state` semantics. The pressed state is a boolean exposed through the value channel (`v-model:value`), the same surface as the other Furo form controls.
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Atom |
10
+ | **Path** | `lib/components/atoms/FuroToggle/` |
11
+ | **Base primitive** | Headless toggle button (`aria-pressed`, `data-state="on" / "off"`) |
12
+ | **Value** | `boolean` via `v-model:value` / `parcel.value` |
13
+ | **Emits** | `change-value`, `update:value` |
14
+ | **Building block for** | `FuroToggleGroup`, `FuroToolBar` |
15
+
16
+ ---
17
+
18
+ ## Public API
19
+
20
+ ### Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `parcel` | `FuroToggleParcel \| null` | `null` | Reactive data object holding the pressed value, variant, size, and disabled flag. |
25
+
26
+ ### Parcel keys
27
+
28
+ | Key | Type | Default | Description |
29
+ |---|---|---|---|
30
+ | `value` | `boolean` | — | Controlled pressed state. Present → controlled; absent → uncontrolled (starts released). |
31
+ | `variant` | `'default' \| 'outline'` | `'default'` | `default` is borderless and fills the accent surface when pressed; `outline` adds a border. |
32
+ | `size` | `'default' \| 'sm' \| 'lg'` | `'default'` | Control height and padding. |
33
+ | `disabled` | `boolean` | `false` | Blocks interaction; also honored from a fallthrough `disabled` attribute. |
34
+
35
+ ### Emits
36
+
37
+ | Event | Payload | When |
38
+ |---|---|---|
39
+ | `change-value` | `ToggleEmitPayload` | On every press/release. Wraps a `CustomEvent` exposing `value` (boolean) and the control element. |
40
+ | `update:value` | `boolean` | v-model sync of the pressed state. |
41
+
42
+ ### Slots
43
+
44
+ | Slot | Props | Description |
45
+ |---|---|---|
46
+ | default | `{ pressed: boolean }` | Toggle label / icon. `pressed` reflects the current state for conditional content. |
47
+
48
+ ---
49
+
50
+ ## Behavior
51
+
52
+ | Concern | Behavior |
53
+ |---|---|
54
+ | **Controlled vs uncontrolled** | When `parcel.value` is present the internal model mirrors it via a watcher; otherwise the headless primitive self-manages from a released start. |
55
+ | **Domain-agnostic key** | The public key is `value` (not the primitive's `pressed`/`modelValue`); the mapping to the primitive happens inside the component. |
56
+ | **Disabled** | `parcel.disabled` or a fallthrough `disabled` attr sets the disabled state and the `disabled` root class. |
57
+ | **Pressed styling** | The primitive's `data-state="on"` drives the pressed appearance — no JS branching in the template. |
58
+ | **Passthrough** | Non-Furo attributes (`aria-label`, `name`, …) fall through to the root (`inheritAttrs: false` + `v-bind="$attrs"`). |
59
+
60
+ ---
61
+
62
+ ## Usage
63
+
64
+ ```vue
65
+ <!-- Uncontrolled, starts released -->
66
+ <FuroToggle aria-label="Bold">
67
+ <Icon name="ph:text-b-bold" />
68
+ </FuroToggle>
69
+
70
+ <!-- Controlled via v-model:value -->
71
+ <FuroToggle
72
+ v-model:value="isItalic"
73
+ aria-label="Italic"
74
+ @change-value="onChangeValue({ payload: $event })"
75
+ >
76
+ Italic
77
+ </FuroToggle>
78
+
79
+ <!-- Outline variant, large -->
80
+ <FuroToggle :parcel="{ variant: 'outline', size: 'lg' }">
81
+ Grid view
82
+ </FuroToggle>
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Composition notes
88
+
89
+ - Imports nothing from the app — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
90
+ - All logic lives in `FuroToggleContext`: extract-only getters (`parcel`, `internalValueRef`), boolean methods (`isControlled()`, `isDisabled()`, `isPressed()`), and the change handler that emits both channels.
91
+ - CSS is wrapped in `@layer furo`; dimensions use `rem`, `gap` uses `--size-space-*`, and colors reference semantic tokens (`--color-accent`, `--color-border`, `--color-ring`) only.
92
+
93
+ ---
94
+
95
+ ## Testing
96
+
97
+ - `FuroToggleContext` is fully covered by jsdom unit tests: parcel defaults, `isControlled()` / `isDisabled()` / `isPressed()` split into truthy / falsy describe blocks, `resolveSyncedValue()`, class/attribute extraction, controlled-value sync in `setupComponent()`, and the dual emit in `onChangeValue()`.
98
+ - No SFC mount test: rendering is declarative bindings over the tested context plus the headless primitive's own semantics, verified by manual QA in the playground.
99
+
100
+ ---
101
+
102
+ ## Playground
103
+
104
+ `/samples/components/playground` → **Toggle** tab. Demonstrates uncontrolled, controlled (`v-model:value`), variants, sizes, and disabled states.
@@ -0,0 +1,88 @@
1
+ # FuroToolBar
2
+
3
+ Presentational container that groups interactive controls — buttons, toggles, separators — into a single keyboard-navigable region. Built on a headless toolbar primitive that provides roving focus (Arrow keys move between controls; Tab enters and leaves the toolbar as one stop). It carries no value and emits nothing.
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Molecule |
10
+ | **Path** | `lib/components/molecules/FuroToolBar/` |
11
+ | **Base primitive** | Headless toolbar (`role="toolbar"` with roving focus) |
12
+ | **Composes** | Slotted atoms/molecules (`FuroButton`, `FuroToggle`, `FuroToggleGroup`, `FuroSeparator`) |
13
+ | **Emits** | None |
14
+ | **Orientation** | `horizontal` (default) or `vertical` |
15
+
16
+ ---
17
+
18
+ ## Public API
19
+
20
+ ### Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `parcel` | `ToolBarParcel \| null` | `null` | Reactive data object holding orientation, text direction, and focus-wrap behavior. |
25
+
26
+ ### Parcel keys
27
+
28
+ | Key | Type | Default | Description |
29
+ |---|---|---|---|
30
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Layout axis. Also sets which Arrow keys move focus and the `aria-orientation`. |
31
+ | `textDirection` | `'toRight' \| 'toLeft'` | `'toRight'` | Reading direction for the contents (`toRight` → left-to-right, `toLeft` → right-to-left). |
32
+ | `loopFocus` | `boolean` | `false` | When `true`, roving focus wraps from the last control back to the first (and vice versa). |
33
+
34
+ ### Emits
35
+
36
+ None. The toolbar is presentational.
37
+
38
+ ### Slots
39
+
40
+ | Slot | Description |
41
+ |---|---|
42
+ | default | The controls to group — buttons, toggles, toggle groups, separators. |
43
+
44
+ ---
45
+
46
+ ## Behavior
47
+
48
+ | Concern | Behavior |
49
+ |---|---|
50
+ | **Orientation** | `parcel.orientation` maps to the `horizontal` / `vertical` root class, the primitive's keyboard axis, and `aria-orientation`. |
51
+ | **Roving focus** | The toolbar is a single Tab stop; Arrow keys move focus across controls. `loopFocus` decides whether focus wraps at the ends. |
52
+ | **Text direction** | `parcel.textDirection` (`toRight` / `toLeft`) maps to the primitive's reading direction; defaults to left-to-right. |
53
+ | **Passthrough** | Extra attributes fall through to the root element (`inheritAttrs: false` + `v-bind="$attrs"`). |
54
+
55
+ ---
56
+
57
+ ## Usage
58
+
59
+ ```vue
60
+ <!-- Editor toolbar with a divider between groups (button text via slot) -->
61
+ <FuroToolBar :parcel="{ orientation: 'horizontal', loopFocus: true }">
62
+ <FuroButton :parcel="{ variant: 'ghost', size: 'sm' }">Bold</FuroButton>
63
+ <FuroButton :parcel="{ variant: 'ghost', size: 'sm' }">Italic</FuroButton>
64
+ <FuroSeparator :parcel="{ orientation: 'vertical' }" />
65
+ <FuroButton :parcel="{ variant: 'ghost', size: 'sm' }">Link</FuroButton>
66
+ </FuroToolBar>
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Composition notes
72
+
73
+ - Imports nothing from the app — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
74
+ - All logic lives in `FuroToolBarContext`: an extract-only getter (`orientation`), the direction mapper (`extractTextDirectionValue()`, `toRight`/`toLeft` → `ltr`/`rtl`), one boolean method (`isLoopingFocus()`), and the root class map (`extractRootClassNames()`).
75
+ - CSS is wrapped in `@layer furo`; `gap` uses a `--size-space-*` token, other dimensions use `rem` / `--size-thinnest`, and colors reference semantic tokens only.
76
+
77
+ ---
78
+
79
+ ## Testing
80
+
81
+ - Context logic (`orientation`, `textDirection`, `isLoopingFocus()`, `extractRootClassNames()`) is fully covered by jsdom unit tests, with the boolean method split into truthy / falsy describe blocks.
82
+ - No SFC mount test: behavior is declarative bindings over the tested context plus the headless primitive's own roving-focus semantics. Rendered output is verified by manual QA in the playground.
83
+
84
+ ---
85
+
86
+ ## Playground
87
+
88
+ `/samples/components/playground` → **Tool Bar** tab. Demonstrates a horizontal toolbar with looping focus and a vertical toolbar.
@@ -0,0 +1,107 @@
1
+ # FuroTooltip
2
+
3
+ Hover/focus hint surface shown next to a trigger element — truncated cells, icon-button labels, and action hints. Built on a headless tooltip primitive that handles delay, positioning, dismissal, and `aria-describedby` wiring. It carries a short text (or a rich content slot) and reacts only to open-state changes.
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Molecule |
10
+ | **Path** | `lib/components/molecules/FuroTooltip/` |
11
+ | **Base primitive** | Headless tooltip (`Provider` + `Root` + `Trigger` + `Portal` + `Content`) |
12
+ | **Composes** | The trigger slot content plus a portaled content surface |
13
+ | **Emits** | `open:change` |
14
+ | **Controlled** | Optional via `parcel.open` |
15
+
16
+ ---
17
+
18
+ ## Public API
19
+
20
+ ### Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `parcel` | `TooltipParcel \| null` | `null` | Reactive data object holding text, positioning, delay, disabled, and optional controlled open state. |
25
+
26
+ ### Parcel keys
27
+
28
+ | Key | Type | Default | Description |
29
+ |---|---|---|---|
30
+ | `text` | `string` | `null` | Tooltip body shown when no `content` slot is supplied. |
31
+ | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'top'` | Edge of the trigger the content prefers. |
32
+ | `align` | `'start' \| 'center' \| 'end'` | `'center'` | Alignment along the chosen side. |
33
+ | `sideOffset` | `number` | `4` | Gap in pixels between trigger and content. |
34
+ | `delayDuration` | `number` | `200` | Delay in milliseconds before the tooltip opens on hover. |
35
+ | `disabled` | `boolean` | `false` | When `true`, the tooltip never opens. |
36
+ | `open` | `boolean \| null` | `null` | When set (non-null), open state is controlled by the consumer; otherwise the component manages it internally. |
37
+
38
+ ### Emits
39
+
40
+ | Event | Payload | Description |
41
+ |---|---|---|
42
+ | `open:change` | `{ open: boolean }` | Fired whenever the open state changes (hover, focus, or dismissal). |
43
+
44
+ ### Slots
45
+
46
+ | Slot | Description |
47
+ |---|---|
48
+ | `trigger` | The element the tooltip is anchored to. Focusable by default (rendered as a button). |
49
+ | `content` | Rich tooltip body. Falls back to `parcel.text` when omitted. |
50
+
51
+ ---
52
+
53
+ ## Behavior
54
+
55
+ | Concern | Behavior |
56
+ |---|---|
57
+ | **Open trigger** | Hover or keyboard focus on the trigger opens the tooltip after `delayDuration`. |
58
+ | **Controlled / uncontrolled** | When `parcel.open` is non-null, the consumer owns the state; otherwise an internal ref tracks it. Either way `open:change` fires. |
59
+ | **Disabled** | `parcel.disabled` forwards to the primitive root so the tooltip never opens. |
60
+ | **Positioning** | `side` / `align` / `sideOffset` bind to the portaled content surface; the primitive flips to stay in view. A pointer arrow is rendered and tracks the trigger. |
61
+ | **Motion** | Content fades + zooms (95% → 100%) on open, anchored to the trigger via the primitive's transform-origin, and fades out on close. Disabled under `prefers-reduced-motion: reduce`. |
62
+ | **Passthrough** | Extra attributes fall through to the trigger (`inheritAttrs: false` + `v-bind="$attrs"`), so consumers can pass `as-child` / `as` to wrap non-button content. |
63
+
64
+ ---
65
+
66
+ ## Usage
67
+
68
+ ```vue
69
+ <!-- Simple text hint -->
70
+ <FuroTooltip :parcel="{ text: 'Copy to clipboard' }">
71
+ <template #trigger>
72
+ <FuroButton :parcel="{ label: 'Copy' }" />
73
+ </template>
74
+ </FuroTooltip>
75
+
76
+ <!-- Rich content on the right edge -->
77
+ <FuroTooltip :parcel="{ side: 'right', delayDuration: 500 }">
78
+ <template #trigger>
79
+ Hover me
80
+ </template>
81
+ <template #content>
82
+ <strong>Last synced</strong> 2 minutes ago
83
+ </template>
84
+ </FuroTooltip>
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Composition notes
90
+
91
+ - Imports nothing from the app — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
92
+ - Public parcel keys are value-oriented; none expose the underlying primitive's API.
93
+ - All logic lives in `FuroTooltipContext`: extract-only getters (`text`, `delayDuration`), boolean methods (`isOpenControlled()`, `isDisabled()`), and the open-state resolver/handler (`extractOpenValue()`, `onOpenChange()`).
94
+ - CSS is wrapped in `@layer furo`; dimensions use `rem` and reference only semantic tokens. The surface uses the inverted `--color-primary` / `--color-primary-foreground` pair so it stays high-contrast against white or dark containers in both themes.
95
+
96
+ ---
97
+
98
+ ## Testing
99
+
100
+ - Context logic is fully covered by jsdom unit tests: `text` / `delayDuration` getters, `extractContentAttributes()`, `extractOpenValue()` (controlled + uncontrolled), and `onOpenChange()` (emits `open:change`, mutates the ref only when uncontrolled). Boolean methods are split into truthy / falsy describe blocks.
101
+ - No SFC mount test: behavior is declarative bindings over the tested context plus the headless primitive's own hover/focus/dismiss semantics. Rendered output is verified by manual QA in the playground.
102
+
103
+ ---
104
+
105
+ ## Playground
106
+
107
+ `/samples/components/playground` → **Tooltip** tab. Demonstrates default (top), delayed right-side, rich content slot, and disabled tooltips.
@@ -215,22 +215,22 @@ export default class FuroButtonContext extends BaseFuroContext {
215
215
  * Extract ARIA attributes for the button root.
216
216
  *
217
217
  * @returns {{
218
- * 'aria-disabled': 'true' | undefined
219
- * 'aria-busy': 'true' | undefined
218
+ * 'aria-disabled'?: 'true'
219
+ * 'aria-busy'?: 'true'
220
220
  * }}
221
221
  */
222
222
  extractAriaAttributes () {
223
223
  const ariaDisabled = this.isDisabled()
224
- ? 'true'
225
- : undefined
224
+ ? { 'aria-disabled': 'true' }
225
+ : {}
226
226
 
227
227
  const ariaBusy = this.isLoading()
228
- ? 'true'
229
- : undefined
228
+ ? { 'aria-busy': 'true' }
229
+ : {}
230
230
 
231
231
  return {
232
- 'aria-disabled': ariaDisabled,
233
- 'aria-busy': ariaBusy,
232
+ ...ariaDisabled,
233
+ ...ariaBusy,
234
234
  }
235
235
  }
236
236