@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,443 @@
1
+ # `<FuroSelect>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ `<FuroSelect>` is a select molecule built on a headless select primitive. It follows the same contract style as the field atoms:
6
+
7
+ - behavior via `parcel` (and a few top-level attribute props)
8
+ - fallthrough attributes merged onto the root where applicable
9
+ - Furo emits (`change-value`, `commit-value`, `update:value`)
10
+ - `v-model:value` support (synced through fallthrough `value` / `@update:value`)
11
+
12
+ For a select, the selection gesture is the commit point, so `change-value` and `commit-value` fire together with the same payload (honoring the Furo form-control event contract).
13
+
14
+ Read [`FuroTextField`](./furo-text-field.md) first for the shared `parcel` vs fallthrough mental model and event handler style.
15
+
16
+ **Source files:**
17
+
18
+ - `lib/components/molecules/FuroSelect/FuroSelect.vue`
19
+ - `lib/components/molecules/FuroSelect/FuroSelectContext.js`
20
+ - `lib/components/molecules/FuroSelect/SelectEmitPayload.js`
21
+
22
+ ---
23
+
24
+ ## Props
25
+
26
+ ### Rule: `parcel` vs fallthrough vs top-level attributes
27
+
28
+ | Mechanism | When to use | Bound to |
29
+ | --- | --- | --- |
30
+ | `:parcel` | Furo behavior (`value`, `options`, `invalid`, `loading`, `placeholder`, root props such as `multiple`, `defaultValue`, `disabled`) | The root (via `extractRootAttributes()`) |
31
+ | `triggerParcel` / `portalParcel` / `contentParcel` | Extra attributes for the trigger, portal, or content parts | Matching part |
32
+ | Fallthrough attributes | HTML-like and root attrs (`name`, `required`, `id`, `aria-*`, …) | Merged into the root together with `parcel` |
33
+
34
+ ```html
35
+ <FuroSelect
36
+ v-model:value="context.territoryRef.value"
37
+ :parcel="{
38
+ placeholder: 'Select a territory',
39
+ options: context.territoryOptions,
40
+ invalid: context.statusReactive.isInvalidTerritory,
41
+ }"
42
+ name="territory"
43
+ :required="true"
44
+ />
45
+ ```
46
+
47
+ ### Top-level component props
48
+
49
+ | Prop | Type | Default | Description |
50
+ | --- | --- | --- | --- |
51
+ | `parcel` | root behavior object or `null` | `null` | Root behavior and Furo fields |
52
+ | `triggerParcel` | trigger attribute object or `null` | `null` | Extra trigger attributes |
53
+ | `portalParcel` | portal attribute object or `null` | `null` | Attributes of the portal the option list renders in, such as `to` or `disabled`. The list renders at `body` by default, so a transformed or clipping ancestor such as `FuroDialog` leaves it in place |
54
+ | `contentParcel` | content attribute object or `null` | `null` | Extra content attributes (`side-offset` defaults to `4` on the component) |
55
+
56
+ ### `parcel` shape (Furo fields)
57
+
58
+ These keys live on `parcel` (not on a nested `triggerParcel` inside `parcel`):
59
+
60
+ | Key | Type | Required | Default | Description |
61
+ | --- | --- | --- | --- | --- |
62
+ | `value` | `string \| number \| object \| null` (an acceptable value) or array when `multiple` | No | `null` | Controlled value; objects are supported — see [Object values](#object-values) |
63
+ | `defaultValue` | same as `value` | No | — | Uncontrolled initial value (root `defaultValue`) |
64
+ | `options` | `Array<FuroSelectOption>` | No | `[]` | Unified list; see [Unified options](#unified-options) |
65
+ | `optionLabel` | `string \| ((option) => string)` | No | uses `option.label` | Key name or function that selects each option's display label; see [Custom option shape](#custom-option-shape) |
66
+ | `optionValue` | `string \| ((option) => value)` | No | uses `option.value` | Key name or function that selects each option's model value — what `update:value` emits and what the bound value matches against; see [Custom option shape](#custom-option-shape) |
67
+ | `invalid` | `boolean` | No | `false` | Invalid visual state + `aria-invalid` on trigger |
68
+ | `loading` | `boolean` | No | `false` | Loading state on trigger (spinner icon); suppresses empty row |
69
+ | `placeholder` | `string` | No | `''` | Passed to the value display |
70
+ | `textDirection` | `toRight` or `toLeft` | No | `toRight` | Maps to the root `dir` (`ltr` / `rtl`) |
71
+ | `multiple` | `boolean` | No | `false` | Multi-select (root); see [Multiple selection](#multiple-selection) |
72
+ | `disabled` | `boolean` | No | `false` | May be set on `parcel` or as fallthrough; forwarded to the root |
73
+
74
+ Remaining `parcel` keys are forwarded to the root (for example `name`, `required`, `defaultValue`, `multiple`). Furo-specific keys above are consumed by the context/template.
75
+
76
+ `parcel` values should be JSON-serializable.
77
+
78
+ #### Object values
79
+
80
+ An option `value` may be an object, not just a `string` / `number`. The value
81
+ flows through the root unchanged, and the context resolves the matching
82
+ `selectedOption` with deep (`JSON.stringify`) equality in `onChangeValue()`, so
83
+ an object `value` correctly resolves its option, index, and payload.
84
+
85
+ Caveat: `SelectEmitPayload.matchesOption({ value })` uses strict `===`, so it
86
+ matches object values **by reference only**. When binding objects, compare by a
87
+ stable id/key rather than relying on `matchesOption()` with an object literal.
88
+
89
+ #### Custom option shape
90
+
91
+ By default an option renders `option.label` and carries `option.value` as its
92
+ model value. When options do not follow that shape (for example `{ id, name }`),
93
+ map them with `optionLabel` / `optionValue`:
94
+
95
+ - `optionLabel` — a key name (`'name'`) or a function (`option => option.name`)
96
+ that selects the **display label** for each option. When unset, `option.label`
97
+ is used.
98
+ - `optionValue` — a key name (`'id'`) or a function that selects the **model
99
+ value** — what `update:value` emits and what the bound `value` matches against.
100
+ When unset, `option.value` (a primitive) is used.
101
+
102
+ Both keys are backward-compatible: with neither set, behavior is exactly as
103
+ before (`label` = `option.label`, `value` = `option.value`).
104
+
105
+ ```html
106
+ <FuroSelect
107
+ :parcel="{
108
+ placeholder: 'Select a member',
109
+ optionLabel: 'name',
110
+ optionValue: 'id',
111
+ options: [
112
+ { id: 50001, name: 'Ada Lovelace' },
113
+ { id: 50002, name: 'Alan Turing' },
114
+ ],
115
+ }"
116
+ @change-value="context.onChangeMember({ payload: $event })"
117
+ />
118
+ ```
119
+
120
+ `textDirection` maps to the root `dir`:
121
+
122
+ - `toRight` → `ltr`
123
+ - `toLeft` → `rtl`
124
+
125
+ #### Unified `options`
126
+
127
+ Applications pass a **single** `parcel.options` array. The component renders either a **group** (group label + child rows) or a **leaf** (option row) per top-level entry.
128
+
129
+ **Classification (as implemented):** An entry is rendered as a **group** when it has a non-empty `options` array (`shouldShowGroupOption()`). Otherwise it is rendered as a **leaf** row. A group entry **may** include a `value` field (used only for flattening / payload matching, not as a selectable row in the template).
130
+
131
+ Nested `options` inside a group contain leaf rows only (no nested groups in v1).
132
+
133
+ **`selectedIndex` / payload matching:** `FuroSelectContext.generateFlattenedOptions()` builds the list used to resolve `selectedOption` and indices. For grouped data it includes the **group header object** as the first entry in each group block, then each child leaf. Flat top-level leaves use `groupIndex: null` and `optionIndex` equal to their index in `parcel.options`.
134
+
135
+ #### `FuroSelectOption`
136
+
137
+ | Key | Type | Required | Default |
138
+ | --- | --- | --- | --- |
139
+ | `value` | `string \| number \| object` (an acceptable value) | Yes (leaf) | — |
140
+ | `label` | `string` | Yes | — |
141
+ | `disabled` | `boolean` | No | `false` |
142
+ | `separator` | `boolean` | No | `false` | Renders a separator after the row when `true` |
143
+ | `options` | `Array<FuroSelectOption>` | No | — | When non-empty, entry is treated as a group |
144
+
145
+ Additional properties are allowed (icons, captions, foreign keys) as long as the whole `parcel` stays JSON-serializable. Preserve full option objects for slots and `SelectEmitPayload.selectedOption`.
146
+
147
+ #### Group entries
148
+
149
+ Group rows are elements of `parcel.options` with `label` and non-empty `options`:
150
+
151
+ | Key | Type | Required |
152
+ | --- | --- | --- |
153
+ | `label` | `string` | Yes |
154
+ | `options` | `Array<FuroSelectOption>` | Yes (non-empty for group UI) |
155
+ | `disabled` | `boolean` | No |
156
+ | `value` | `string \| number` | No | Optional; included in flatten list |
157
+
158
+ #### Disabled precedence
159
+
160
+ Per-option **effective** disabled on each option row:
161
+
162
+ - **Leaf row:** `option.disabled` (root `disabled` is on the root)
163
+ - **Grouped child:** `groupChildOption.disabled || group.disabled`
164
+
165
+ Root / parcel `disabled` disables the whole control via the root.
166
+
167
+ ### Fallthrough attributes
168
+
169
+ Consumed via `inheritAttrs: false` and merged in `extractRootAttributes()` (`...attrs`, `...parcel`, `dir`, `modelValue`).
170
+
171
+ Common attributes:
172
+
173
+ | Attribute | Behavior |
174
+ | --- | --- |
175
+ | `name` | Forwarded to the root |
176
+ | `disabled` | Forwarded to the root |
177
+ | `required` | Forwarded to the root |
178
+ | `id` | Forwarded to the root (not trigger-only) |
179
+ | `aria-label` / `aria-labelledby` / `aria-describedby` | Forwarded to the root |
180
+ | `value` | Watched with `parcel.value` for controlled sync (`v-model:value`) |
181
+
182
+ Prefer `parcel.placeholder` over a fallthrough `placeholder` (placeholder is read from `parcel` in `extractPlaceholder()`).
183
+
184
+ ---
185
+
186
+ ## Slots
187
+
188
+ | Slot | Scoped props | Default content |
189
+ | --- | --- | --- |
190
+ | `trigger-content` | — | Value display + trigger icon area |
191
+ | `trigger-text` | — | Inside the value display (optional extra trigger label) |
192
+ | `trigger-icon` | — | Caret icon when not loading |
193
+ | `trigger-loading-icon` | — | Spinner when `parcel.loading` |
194
+ | `content-before` | — | Before scroll up button |
195
+ | `content-after` | — | After scroll down button |
196
+ | `scroll-up-button` | — | Caret up icon |
197
+ | `scroll-down-button` | — | Caret down icon |
198
+ | `group` | `{ group, groupIndex }` | Group label + child options |
199
+ | `group-label` | `{ group, groupIndex }` | Group `label` text |
200
+ | `option` | `{ option, optionIndex, group?, groupIndex? }` | Option text + indicator |
201
+ | `option-text` | `{ option, optionIndex }` | Option `label` |
202
+ | `option-indicator` | `{ option, optionIndex }` | Check icon |
203
+ | `separator` | `{ option, optionIndex }` | Separator when `option.separator` |
204
+ | `empty` | — (no scoped props passed today) | Shown when `hasEmptyOptions()`; default text `No options` |
205
+
206
+ **Empty / loading:** `hasEmptyOptions()` is true when `options` is empty **and** `loading` is false. While `loading` is true, the trigger shows the loading icon and the empty row is not shown.
207
+
208
+ ---
209
+
210
+ ## Emits
211
+
212
+ | Event | Payload | When |
213
+ | --- | --- | --- |
214
+ | `change-value` | `SelectEmitPayload` | On selection (root `@update:model-value`) |
215
+ | `commit-value` | `SelectEmitPayload` | Same gesture; selection is the commit point |
216
+ | `update:value` | `value \| Array<value> \| null` | Same gesture; a single value in single mode, the full array of selected values in multiple mode (values reflect `optionValue` when set) |
217
+
218
+ `@update:open` is not exposed.
219
+
220
+ ### Parent handler style
221
+
222
+ ```js
223
+ onChangeValue ({
224
+ payload,
225
+ }) {
226
+ this.statusReactive.isInvalidTerritory = payload.hasEmptyValue()
227
+ }
228
+ ```
229
+
230
+ ---
231
+
232
+ ## `SelectEmitPayload` API
233
+
234
+ `SelectEmitPayload` extends [`BaseEmitPayload`](../lib/emit-payload/BaseEmitPayload.js).
235
+
236
+ | Member | Type | Description |
237
+ | --- | --- | --- |
238
+ | `value` | `string \| number \| object \| null` | Primary selected value (`selectedOption.value`) |
239
+ | `values` | `Array<string \| number \| object> \| null` | All selected values in multi-select |
240
+ | `controlElement` | `HTMLElement \| null` | Trigger element from custom event `detail.target` |
241
+ | `selectedOption` | `FuroSelectOption \| null` | Matched row from flatten list |
242
+ | `selectedIndex` | `number \| null` | `optionIndex` on matched row |
243
+ | `selectedOptions` | `Array<FuroSelectOption> \| null` | Multi-select matches |
244
+ | `selectedIndices` | `Array<number> \| null` | Multi-select indices |
245
+ | `hasEmptyValue()` | `boolean` | `value === null` |
246
+ | `hasSelectedValue()` | `boolean` | `value !== null` |
247
+ | `matchesOption({ value })` | `boolean` | Strict (`===`) equality with `value`. Matches object values by **reference only** — for object values compare by id/key instead |
248
+
249
+ Custom event `detail` also includes `selectedGroupIndex` / `selectedGroupIndices` (used internally; not exposed as payload getters today).
250
+
251
+ ---
252
+
253
+ ## `v-model:value` and state handling
254
+
255
+ ```html
256
+ <FuroSelect
257
+ v-model:value="context.territoryRef.value"
258
+ :parcel="{ options: context.territoryOptions, placeholder: 'Select a territory' }"
259
+ />
260
+ ```
261
+
262
+ Equivalent to fallthrough `value` + `@update:value` with internal sync in `setupComponent()`.
263
+
264
+ ### Controlled vs uncontrolled
265
+
266
+ | Mode | How |
267
+ | --- | --- |
268
+ | Controlled | `parcel.value` and/or `v-model:value` / fallthrough `value` |
269
+ | Uncontrolled | `parcel.defaultValue` (omit `value`) |
270
+
271
+ ### State matrix
272
+
273
+ | State | Source |
274
+ | --- | --- |
275
+ | Selected value | `parcel.value` / `v-model:value` / `defaultValue` |
276
+ | Whole control `disabled` | `parcel.disabled` or fallthrough `disabled` on root |
277
+ | Row `disabled` | `option.disabled` and/or `group.disabled` |
278
+ | `invalid` | `parcel.invalid` |
279
+ | `placeholder` | `parcel.placeholder` |
280
+ | `options` | `parcel.options` |
281
+ | `loading` | `parcel.loading` (trigger UI; hides empty state) |
282
+
283
+ ### Multiple selection
284
+
285
+ Set `parcel.multiple: true`. Use array `defaultValue` / internal value. `change-value` exposes `payload.value` as the **first** selected value; use `payload.values` / `payload.selectedOptions` for the full set. `update:value` now reliably emits the **full array** of selected values, and the selection **persists across re-open** (previously it emitted only a single value). In single mode `update:value` emits a single value.
286
+
287
+ ---
288
+
289
+ ## Internal structure
290
+
291
+ ### Underlying primitive and Furo option naming
292
+
293
+ The underlying headless select primitive uses item/group terminology. Furo’s public API uses **`option`** (slot and `parcel.options`); the component maps options to the primitive's item rows internally.
294
+
295
+ ### Composition
296
+
297
+ ```text
298
+ div.unit-form-control.select
299
+ root
300
+ trigger(.trigger)
301
+ slot trigger-content → value display + icon
302
+ portal
303
+ content(.unit-select-content)
304
+ slot content-before
305
+ scroll-up-button
306
+ viewport(.viewport)
307
+ group* / option* # group vs leaf from parcel.options
308
+ optional empty option
309
+ scroll-down-button
310
+ slot content-after
311
+ ```
312
+
313
+ Use `data-*` states in styles: `data-placeholder`, `data-disabled`, `data-highlighted`, etc.
314
+
315
+ ---
316
+
317
+ ## Accessibility requirements
318
+
319
+ - Trigger remains a semantic `button`.
320
+ - Provide a visible label (`label` + `for` / `id`, or `aria-label` / `aria-labelledby`).
321
+ - `aria-invalid` is set on the trigger from `parcel.invalid`.
322
+ - Keyboard / focus behavior is delegated to the underlying primitive.
323
+ - Forward `name` and `required` on root for form integration.
324
+
325
+ ---
326
+
327
+ ## Styling convention
328
+
329
+ Inside the SFC, styles use `@layer furo`:
330
+
331
+ - Root wrapper: `.unit-form-control.select`
332
+ - Trigger: `.trigger` (invalid `.invalid`, loading `.loading`)
333
+ - Dropdown: `.unit-select-content`, `.viewport`, `.group`, `.option`, `.separator`
334
+ - Tokens align with other field atoms (`--color-input`, `--size-input-height`, etc.)
335
+ - Option rows take their minimum height from `--size-option-height`
336
+
337
+ ---
338
+
339
+ ## Examples
340
+
341
+ ### Basic (controlled)
342
+
343
+ ```html
344
+ <FuroSelect
345
+ :parcel="{
346
+ value: context.statusReactive.territory,
347
+ invalid: context.statusReactive.isInvalidTerritory,
348
+ placeholder: 'Select a territory',
349
+ options: [
350
+ { value: 'JP', label: 'Japan' },
351
+ { value: 'VN', label: 'Vietnam' },
352
+ { value: 'TW', label: 'Taiwan' },
353
+ { value: 'US', label: 'United States' },
354
+ ],
355
+ }"
356
+ name="territory"
357
+ :required="true"
358
+ @change-value="context.onChangeTerritory({ payload: $event })"
359
+ />
360
+ ```
361
+
362
+ ### Grouped options
363
+
364
+ ```html
365
+ <FuroSelect
366
+ :parcel="{
367
+ value: context.statusReactive.fruit,
368
+ placeholder: 'Pick a fruit',
369
+ options: [
370
+ {
371
+ label: 'Citrus',
372
+ options: [
373
+ { value: 'orange', label: 'Orange' },
374
+ { value: 'lemon', label: 'Lemon' },
375
+ ],
376
+ },
377
+ {
378
+ label: 'Berries',
379
+ disabled: true,
380
+ options: [
381
+ { value: 'strawberry', label: 'Strawberry' },
382
+ { value: 'blueberry', label: 'Blueberry', disabled: true },
383
+ ],
384
+ },
385
+ ],
386
+ }"
387
+ />
388
+ ```
389
+
390
+ ### Uncontrolled default
391
+
392
+ ```html
393
+ <FuroSelect
394
+ name="territory"
395
+ :parcel="{
396
+ defaultValue: 'JP',
397
+ placeholder: 'Select a territory',
398
+ options: context.territoryOptions,
399
+ }"
400
+ />
401
+ ```
402
+
403
+ ### Empty slot
404
+
405
+ ```html
406
+ <FuroSelect
407
+ :parcel="{
408
+ placeholder: 'Assignee',
409
+ options: [],
410
+ }"
411
+ >
412
+ <template #empty>
413
+ <span>No assignees available</span>
414
+ </template>
415
+ </FuroSelect>
416
+ ```
417
+
418
+ ### Custom option row
419
+
420
+ ```html
421
+ <FuroSelect
422
+ :parcel="{
423
+ placeholder: 'Choose row',
424
+ options: context.customRowOptions,
425
+ }"
426
+ >
427
+ <template #option-text="{ option }">
428
+ <strong>{{ option.label }}</strong>
429
+ <small>{{ option.caption }}</small>
430
+ </template>
431
+ </FuroSelect>
432
+ ```
433
+
434
+ Live samples: `pages/samples/components/playground/FuroSelectPlayground/index.vue`.
435
+
436
+ ---
437
+
438
+ ## Known gaps and follow-ups
439
+
440
+ - **Multi-select:** `update:value` now emits the full array of selected values and the selection persists across re-open; `payload.value` still surfaces the primary value (use `payload.values` for the full set).
441
+ - **`empty` slot:** typedef allows `{ loading }` but the template does not pass scoped props yet.
442
+ - **Group classification:** spec previously used “no `value` on group”; implementation uses non-empty `options` only.
443
+ - **Open state:** `@update:open` / `open` on `parcel` not exposed.
@@ -0,0 +1,87 @@
1
+ # FuroSeparator
2
+
3
+ Presentational divider drawn between sibling content — toolbar groups, menu sections, and section boundaries. Built on a headless separator primitive that handles the correct `role` / `aria-orientation` semantics. It carries no value and emits nothing.
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Molecule |
10
+ | **Path** | `lib/components/molecules/FuroSeparator/` |
11
+ | **Base primitive** | Headless separator (`role="separator"` with `aria-orientation`, or `role="none"` when decorative) |
12
+ | **Composes** | Nothing — a single styled rule |
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` | `SeparatorParcel \| null` | `null` | Reactive data object holding orientation and the decorative flag. |
25
+
26
+ ### Parcel keys
27
+
28
+ | Key | Type | Default | Description |
29
+ |---|---|---|---|
30
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direction the divider runs. `horizontal` spans the full inline width; `vertical` stretches to the parent's height. |
31
+ | `decorative` | `boolean` | `false` | When `true`, the divider is purely visual and drops the `separator` role so assistive tech ignores it. Use for dividers that repeat content already conveyed by layout. |
32
+
33
+ ### Emits
34
+
35
+ None. The separator is presentational.
36
+
37
+ ### Slots
38
+
39
+ None.
40
+
41
+ ---
42
+
43
+ ## Behavior
44
+
45
+ | Concern | Behavior |
46
+ |---|---|
47
+ | **Orientation** | `parcel.orientation` maps to the `horizontal` / `vertical` root class and to the primitive's `aria-orientation`; no JS branching. |
48
+ | **Semantics** | Non-decorative separators expose `role="separator"`. Decorative separators expose `role="none"` and no orientation semantics. |
49
+ | **Sizing** | Thickness is `--size-thinnest`. Horizontal fills `width: 100%`; vertical uses `align-self: stretch` so it matches a flex row's height. |
50
+ | **Passthrough** | Extra attributes fall through to the rule element (`inheritAttrs: false` + `v-bind="$attrs"`). |
51
+
52
+ ---
53
+
54
+ ## Usage
55
+
56
+ ```vue
57
+ <!-- Horizontal section divider -->
58
+ <FuroSeparator :parcel="{ orientation: 'horizontal' }" />
59
+
60
+ <!-- Vertical divider between toolbar groups -->
61
+ <div class="toolbar">
62
+ <FuroButton :parcel="{ label: 'Bold' }" />
63
+ <FuroSeparator :parcel="{ orientation: 'vertical' }" />
64
+ <FuroButton :parcel="{ label: 'Link' }" />
65
+ </div>
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Composition notes
71
+
72
+ - Imports nothing from the app — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
73
+ - All logic lives in `FuroSeparatorContext` as an extract-only getter (`orientation`) and one boolean method (`isDecorative()`).
74
+ - CSS is wrapped in `@layer furo`; dimensions use `rem` / `--size-thinnest` and the rule color references the semantic `--color-border` token only.
75
+
76
+ ---
77
+
78
+ ## Testing
79
+
80
+ - Context logic (`orientation` getter, `isDecorative()`) is fully covered by jsdom unit tests, with the boolean method split into truthy / falsy describe blocks.
81
+ - No SFC mount test: behavior is declarative bindings over the tested context plus the headless primitive's own semantics. Rendered output (orientation class, role) is verified by manual QA in the playground.
82
+
83
+ ---
84
+
85
+ ## Playground
86
+
87
+ `/samples/components/playground` → **Separator** tab. Demonstrates horizontal, vertical (between inline items), and decorative dividers.