@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,719 @@
1
+ # `<FuroAutocompleteField>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ `<FuroAutocompleteField>` is a **shipped** autocomplete molecule 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 combobox primitive inside `lib/components/molecules/FuroAutocompleteField/`* only. That dependency is **not** part of the application API.
8
+
9
+ Contract style aligns with `[FuroTextField](./furo-text-field.md)` (Furo-first `parcel` + fallthrough where applicable):
10
+
11
+ - behavior via `:parcel` (`FuroAutocompleteFieldParcel`)
12
+ - HTML / form attributes via fallthrough (`name`, `required`, `id`, `aria-`*, `disabled`, …)
13
+ - Furo emits (`change-value`, `update:value`, `search-keyword`, …)
14
+ - `v-model:value` syncs when the parent controls `parcel.value`
15
+
16
+ **Public SFC props:** `parcel` (behavior) and `inputParcel` (attributes of the inner `<input role="combobox">`).
17
+
18
+ **Model shape:** the combobox stores **full option objects** (leaf rows from `parcel.options`), not only `option.value`. `@update:value` and `parcel.value` use that same shape (`FuroAutocompleteOption`, primitive leaf, or arrays thereof in multi mode).
19
+
20
+ **Input display text:** the input part uses `extractDisplayValue()` → `option.label` (or `String(option)` for primitive leaves). There is no public display callback prop.
21
+
22
+ `@change-value` fires on **committed gestures** (list selection, select-all toggle, clear) — not on every keystroke. Use `@search-keyword` for debounced parent-driven search.
23
+
24
+ Read `[FuroTextField](./furo-text-field.md)` for the shared `parcel` mental model.
25
+
26
+ ### Abstraction boundary
27
+
28
+
29
+ | Layer | Responsibility |
30
+ | ------------------------------ | -------------------------------------------------------------------------- |
31
+ | Application | `parcel`, listeners, slots; replaces `parcel.options` for API-driven lists |
32
+ | Molecule (`FuroAutocompleteField`) | Maps parcel → the underlying combobox primitive; DOM, debounce, client filter, emits |
33
+ | Underlying primitive (maintainers) | Internal only; not part of the application API |
34
+
35
+
36
+ **Rules:**
37
+
38
+ 1. Documented `parcel` keys are the **Furo contract**. The getter merges defaults, then `...props.parcel`, then the template spreads `v-bind="context.parcel"` onto the root (additional keys may reach the underlying primitive but are **not** supported for apps).
39
+ 2. Do **not** use the legacy `triggerParcel` / `portalParcel` / `contentParcel` props — they are **not** on the SFC. The one part-level prop is `inputParcel`, which carries plain input attributes only (see [Accessible name and form wiring](#accessible-name-and-form-wiring)).
40
+ 3. `FuroSelect` still uses the older primitive passthrough pattern; autocomplete is Furo-first.
41
+
42
+ **Source files:**
43
+
44
+ - `lib/components/molecules/FuroAutocompleteField/FuroAutocompleteField.vue`
45
+ - `lib/components/molecules/FuroAutocompleteField/FuroAutocompleteFieldContext.js`
46
+ - `lib/components/molecules/FuroAutocompleteField/AutocompleteEmitPayload.js`
47
+ - `app/shares/TimerClerk.js`
48
+
49
+ **Live samples:** `pages/samples/components/playground/FuroAutocompleteFieldPlayground/index.vue`
50
+
51
+ ---
52
+
53
+ ## Public API at a glance
54
+
55
+
56
+ | Area | Status | Notes |
57
+ | ----------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------- |
58
+ | **SFC surface** | | |
59
+ | Public props `parcel` and `inputParcel` (+ attribute fallthrough) | **Implemented** | See `[FuroAutocompleteFieldParcel](#furoautocompletefieldparcel)` |
60
+ | `inputParcel` → inner `<input role="combobox">` (`id`, `name`, `aria-*`, `required`, …) | **Implemented** | Gives the combobox an accessible name; see [Accessible name and form wiring](#accessible-name-and-form-wiring) |
61
+ | **Selection & options** | | |
62
+ | Single + multi; groups, separators, option `icon` | **Implemented** | Model = full leaf option object(s), not bare `value` |
63
+ | `parcel.value` + `v-model:value` | **Implemented** | Watches `parcel.value` and fallthrough `value` |
64
+ | Client filter (`localCompare: true`) | **Implemented** | `filteredOptions()` + the primitive's `useFilter` collator |
65
+ | Parent-owned list (`localCompare: false`) | **Implemented** | Full `parcel.options`; parent filters (e.g. API) |
66
+ | `loading`, `invalid`, `disabled`, `multiple` | **Implemented** | |
67
+ | `showClear`, `showSelectAll`, `selectAllText`, `emptyText` | **Implemented** | Defaults merged in context `parcel` getter |
68
+ | **Search & popover** | | |
69
+ | Debounced `search-keyword` (`string`) | **Implemented** | `TimerClerk`; only while popover open; also `''` on clear |
70
+ | `parcel.searchKeyword` → input | **Partial** | One-way watch into `internalSearchKeywordRef`; no `update:search-keyword` |
71
+ | `v-model:search-keyword` | **Not implemented** | |
72
+ | Popover open/close | **Internal only** | `internalOpenPopoverRef`; keyword cleared when popover opens |
73
+ | `parcel.open` / `v-model:open` | **Not implemented** | Template `:open` overrides `parcel.open` from `v-bind` |
74
+ | **Emits** | | |
75
+ | `update:value`, `change-value` | **Implemented** | Select row, select-all, clear, **chip remove** |
76
+ | `search-keyword` | **Implemented** | Payload is a **string** (not `{ searchKeyword }`) |
77
+ | `update:search-keyword`, `update:open`, `commit-value` | **Not implemented** | Listed on SFC `emits` only; never fired from context |
78
+ | **Slots** | | |
79
+ | Documented slots (`option-text`, `group-label`, `empty`, …) | **Implemented** | No `input` / root `trigger` slots |
80
+ | `select-all` scoped `{ checked, indeterminate, disabled }` | **Not implemented** | Typedef only; slot has no scoped props wired |
81
+ | **Spec debt (not in runtime)** | | |
82
+ | Free-form (`allowFreeForm`, `commit-value` on Enter/blur) | **Not implemented** | |
83
+ | Closed `parcel` (reject unknown keys before the primitive) | **Not implemented** | Extra keys still spread to the root |
84
+
85
+
86
+ ---
87
+
88
+ ## Props
89
+
90
+ ### Rule: `parcel` vs attributes
91
+
92
+
93
+ | Mechanism | When to use | Application sees |
94
+ | --------------------------------------- | -------------------------- | --------------------------------------------------------------- |
95
+ | `:parcel` | Furo autocomplete behavior | `FuroAutocompleteFieldParcel` (+ merged defaults) |
96
+ | `:input-parcel` | `id`, `name`, `aria-label`, `aria-labelledby`, `aria-describedby`, `required`, … | Bound onto the inner `<input role="combobox">` |
97
+ | Attributes on `<FuroAutocompleteField>` | e.g. `class`, `style` | Vue fallthrough to the root wrapper (default `inheritAttrs`); **not** the input |
98
+
99
+
100
+ > `**parcel` must be JSON-serializable** — no functions inside `parcel`.
101
+
102
+ ```html
103
+ <FuroAutocompleteField
104
+ v-model:value="context.assigneeRef.value"
105
+ :parcel="{
106
+ placeholder: 'Search assignee',
107
+ options: context.assigneeOptions,
108
+ localCompare: false,
109
+ debounceMilliseconds: 300,
110
+ loading: context.statusReactive.isLoadingAssignees,
111
+ invalid: context.statusReactive.isInvalidAssignee,
112
+ }"
113
+ name="assignee"
114
+ @search-keyword="context.onSearchAssignees({ searchKeyword: $event })"
115
+ />
116
+ ```
117
+
118
+ > `**@search-keyword` payload is a `string**`, not `{ searchKeyword }`. Handlers may accept either for forward compatibility (see playground).
119
+
120
+ ### Component props (public)
121
+
122
+
123
+ | Prop | Type | Default | Description |
124
+ | -------- | --------------------------------------- | ------- | ------------------------------- |
125
+ | `parcel` | `FuroAutocompleteFieldParcel` or `null` | `null` | Furo behavior (see table below) |
126
+ | `inputParcel` | `FuroAutocompleteFieldInputParcel` or `null` | `null` | Attributes bound onto the inner `<input role="combobox">`. Mirrors `FuroSelect`'s `triggerParcel` |
127
+
128
+ ### Accessible name and form wiring
129
+
130
+ The real control is the inner `<input role="combobox">`, not the root wrapper (a `div`). A `label[for]`, `aria-label`, `aria-labelledby` or `aria-describedby` on the wrapper never reaches it, so route them through `inputParcel`:
131
+
132
+ ```html
133
+ <label id="assignee-label" for="assignee-input">Assignee</label>
134
+ <FuroAutocompleteField
135
+ v-model:value="context.assigneeRef.value"
136
+ :parcel="{ options: context.assigneeOptions }"
137
+ :input-parcel="{
138
+ id: 'assignee-input',
139
+ name: 'assignee',
140
+ 'aria-describedby': 'assignee-hint',
141
+ required: true,
142
+ }"
143
+ />
144
+ ```
145
+
146
+ `FuroControlBlock` emits `<label :for="controlId">`; pass the same id as `inputParcel.id`.
147
+
148
+ `inputParcel` is `InputHTMLAttributes` without `placeholder` and `value`. The component keeps the placeholder (`parcel.placeholder`), the display text and the search keyword for itself, and the primitive keeps `role`, `aria-expanded`, `aria-controls` and `aria-autocomplete`.
149
+
150
+
151
+ ### `FuroAcceptableValue`
152
+
153
+ Primitive or object carried in option `value` and payloads:
154
+
155
+ `string | number | bigint | Record<string, unknown> | null`
156
+
157
+ The **model** for the field is usually a `**FuroAutocompleteOption` leaf object** (or array of them), not bare `FuroAcceptableValue`, unless options are primitive leaves.
158
+
159
+ ### `FuroAutocompleteFieldParcel`
160
+
161
+ Keys read in `FuroAutocompleteFieldContext` and/or merged in the `parcel` getter (defaults shown).
162
+
163
+
164
+ | Key | Type | Default (getter) | Description |
165
+ | ---------------------- | ---------------------------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------- |
166
+ | `value` | `FuroAutocompleteOption | FuroAcceptableValue | Array<…> | null` | — | Controlled selection; synced to `internalValueRef` (with fallthrough `value` as fallback) |
167
+ | `options` | `Array<FuroAutocompleteOption | FuroAcceptableValue>` | `[]` | List rows (groups + leaves) |
168
+ | `optionLabel` | `string \| ((option) => string)` | uses `option.label` | Key name or function that selects each option's display label; also drives local filtering. See [Custom option shape](#custom-option-shape) |
169
+ | `optionValue` | `string \| ((option) => value)` | whole option object | Key name or function that selects each option's model value. When unset, `update:value` emits the whole option object (historical default); when set, it emits that key's value or the function's result |
170
+ | `maxVisibleChips` | `number` | `null` | Multi only: render only the first N selected chips, then a `+{count}` overflow indicator. `null` means no cap |
171
+ | `chipOverflow` | `'wrap' \| 'ellipsis'` | `'wrap'` | How selected chips behave when they exceed the trigger width: `wrap` flows onto multiple lines; `ellipsis` keeps a single line and truncates |
172
+ | `placeholder` | `string` | `'Select an option'` | Input placeholder |
173
+ | `loading` | `boolean` | `false` | Input spinner; `filteredOptions()` returns `[]`; suppresses empty state |
174
+ | `invalid` | `boolean` | `false` | Root `.invalid` class |
175
+ | `disabled` | `boolean` | `false` | Disables combobox |
176
+ | `textDirection` | `'toRight' | 'toLeft'` | `'toRight'` | Maps to the primitive's `dir` (`ltr` / `rtl`) |
177
+ | `multiple` | `boolean` | `false` | Multi-select; chips in anchor |
178
+ | `showClear` | `boolean` | `true` | Clear control shown when value selected |
179
+ | `localCompare` | `boolean` | `true` | `true` → client filter via `useFilter` collator; `false` → parent-owned `options` |
180
+ | `debounceMilliseconds` | `number` | `300` | Debounce for `search-keyword` (`TimerClerk`) |
181
+ | `openOnClick` | `boolean` | `true` | Passed through to the root |
182
+ | `showSelectAll` | `boolean` | `false` | Multi only; sentinel select-all row |
183
+ | `selectAllText` | `string` | `'Select All'` | Select-all row label |
184
+ | `emptyText` | `string` | `'No options.'` | Default `#empty` copy |
185
+ | `creatable` | `boolean` | `false` | Show a "create new" row when the trimmed keyword has no exact label/value match (case-insensitive) |
186
+ | `createOptionText` | `string` | `'Create "{keyword}"'` | Create-row text template; `{keyword}` is replaced with the trimmed keyword. Override for i18n / word order |
187
+ | `searchKeyword` | `string` | — | **Partial** — parent → internal input via watch; no `update:search-keyword` |
188
+ | `open` | `boolean` | — | **Not implemented** — template `:open` uses `internalOpenPopoverRef` |
189
+
190
+
191
+ Additional keys on `props.parcel` are spread to the root (primitive passthrough). Only the keys above are the **supported Furo contract**.
192
+
193
+ #### `FuroAutocompleteOption` (leaf / group)
194
+
195
+
196
+ | Key | Type | Description |
197
+ | ----------- | --------------------- | ------------------------------------------------------------------- |
198
+ | `value` | `FuroAcceptableValue` | Required on leaf rows |
199
+ | `label` | `string` | Display, filter text, and input display via `extractDisplayValue()` |
200
+ | `disabled` | `boolean` | Row disabled; skipped in select-all when `true` |
201
+ | `separator` | `boolean` | Renders `#separator` after row |
202
+ | `icon` | `IconProps` | Optional `#option-icon` |
203
+ | `options` | `Array<…>` | Non-empty → **group** (`isGroupOption()`) |
204
+
205
+
206
+ Extra JSON-serializable keys (e.g. `department`) are allowed for slots and parent logic.
207
+
208
+ #### Option equality (`compareOption`)
209
+
210
+ Selection matching uses `compareOption()`:
211
+
212
+ - Object leaves with `value` key: `JSON.stringify(optionA.value) === JSON.stringify(optionB.value)`
213
+ - Otherwise: strict equality (`===`)
214
+
215
+ #### Custom option shape
216
+
217
+ By default the field reads `option.label` for display / filter text and stores
218
+ the **whole option object** as the model value. When options do not follow that
219
+ shape (for example `{ id, name }`), map them with `optionLabel` / `optionValue`:
220
+
221
+ - `optionLabel` — a key name (`'name'`) or a function (`option => option.name`)
222
+ that selects the **display / filter label** for each option. When unset,
223
+ `option.label` is used.
224
+ - `optionValue` — a key name (`'id'`) or a function that selects the **model
225
+ value**. When unset, `update:value` emits the whole option object (the
226
+ historical default); when set, it emits that key's value or the function's
227
+ result.
228
+
229
+ Both keys are backward-compatible: with neither set, behavior is exactly as
230
+ before (label = `option.label`, value = the whole option object).
231
+
232
+ ```html
233
+ <FuroAutocompleteField
234
+ v-model:value="context.memberRef.value"
235
+ :parcel="{
236
+ placeholder: 'Search member',
237
+ optionLabel: 'name',
238
+ optionValue: 'id',
239
+ options: [
240
+ { id: 50001, name: 'Ada Lovelace' },
241
+ { id: 50002, name: 'Alan Turing' },
242
+ ],
243
+ }"
244
+ @change-value="context.onChangeMember({ payload: $event })"
245
+ />
246
+ ```
247
+
248
+ #### Chip overflow (multiple mode)
249
+
250
+ In `multiple` mode, `maxVisibleChips` caps how many selected chips render before
251
+ a `+{count}` overflow indicator; `chipOverflow` controls layout when chips
252
+ exceed the trigger width (`wrap` flows onto multiple lines, `ellipsis` keeps a
253
+ single line and truncates). Defaults are backward-compatible: no cap and `wrap`.
254
+
255
+ ```html
256
+ <FuroAutocompleteField
257
+ v-model:value="context.selectedTagsRef.value"
258
+ :parcel="{
259
+ multiple: true,
260
+ maxVisibleChips: 3,
261
+ chipOverflow: 'ellipsis',
262
+ placeholder: 'Search tags',
263
+ options: context.tagOptions,
264
+ }"
265
+ @change-value="context.onChangeTags({ payload: $event })"
266
+ />
267
+ ```
268
+
269
+ #### Unified options
270
+
271
+
272
+ | Shape | Rendered as |
273
+ | ---------------------------- | --------------------------------------- |
274
+ | `option.options?.length > 0` | Group (group part + `group-label`) |
275
+ | Otherwise | Leaf (group part with `as-child`) |
276
+
277
+
278
+ List template iterates `context.filteredOptions()` — top-level entries after client filter (groups stay whole; children filtered inside `filteredOptions()` for object groups).
279
+
280
+ #### Disabled precedence
281
+
282
+ - Leaf: `option.disabled`
283
+ - Group child: `shouldDisableOption({ option: childOption })` checks the **child** only
284
+ - Root: `parcel.disabled` on the root
285
+
286
+ ---
287
+
288
+ ## Filtering
289
+
290
+ List visibility is computed in `filteredOptions()` (context).
291
+
292
+
293
+ | `parcel.localCompare` | `parcel.loading` | Visible list |
294
+ | --------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
295
+ | `true` (default) | `false` | Subset of `parcel.options` where any group child / leaf matches `internalSearchKeyword` (trimmed) via `useFilter().contains` on `label` and `String(value)` |
296
+ | `true` | `true` | `[]` (empty list while loading) |
297
+ | `false` | any | Full `parcel.options` (parent filters, e.g. API) |
298
+
299
+
300
+ ### `hasEmptyOptions()`
301
+
302
+ ```text
303
+ filteredOptions().length === 0 && !loading
304
+ ```
305
+
306
+ While `loading`, `#empty` is hidden (`hasEmptyOptions()` is false) and `#loading` can show inside the viewport.
307
+
308
+ ---
309
+
310
+ ## Search & debounce
311
+
312
+ Uses `[TimerClerk](../app/shares/TimerClerk.js)` in `setupComponent()` (`debounceMilliseconds` from merged parcel, default **300**).
313
+
314
+
315
+ | Step | Behavior |
316
+ | ------------- | -------------------------------------------------------------------------------- |
317
+ | User types | `onUpdateSearchKeyword` sets `internalSearchKeywordRef` |
318
+ | Debounce | `startTimer({ searchKeyword })` **only if** `internalOpenPopoverRef` is **open** |
319
+ | Tick | `emit('search-keyword', searchKeyword)` — **string** argument |
320
+ | Popover opens | `onUpdateOpenPopover({ isOpen: true })` resets `internalSearchKeyword` to `''` |
321
+ | Clear button | Also `emit('search-keyword', '')` |
322
+
323
+
324
+ **Not implemented:** `emit('update:search-keyword')`, controlled `parcel.searchKeyword`.
325
+
326
+ ```text
327
+ Input change (popover open)
328
+ → internalSearchKeywordRef
329
+ → TimerClerk.startTimer
330
+ → search-keyword "<string>"
331
+ → parent updates parcel.options + parcel.loading
332
+ ```
333
+
334
+ ---
335
+
336
+ ## Slots
337
+
338
+
339
+ | Slot | Scoped props | Default |
340
+ | ------------------ | ------------------------------------------------ | -------------------------------------------------------------------------------------- |
341
+ | `trigger-left` | — | Optional leading anchor content |
342
+ | `trigger-right` | — | Caret icon inside the trigger part |
343
+ | `cancel` | — | Clear icon in the clear control |
344
+ | `content-before` | — | Before viewport |
345
+ | `content-after` | — | After viewport |
346
+ | `select-all` | — | Select-all label text only (scoped `checked` / `indeterminate` / `disabled` not wired) |
347
+ | `group` | `group`, `groupIndex` | Group + children |
348
+ | `group-label` | `group`, `groupIndex` | Group header text |
349
+ | `option` | `option`, `optionIndex`, `group?`, `groupIndex?` | Row + indicator |
350
+ | `option-icon` | `option`, `optionIndex` | Icon when `option.icon` set |
351
+ | `option-text` | `option`, `optionIndex` | Label text |
352
+ | `option-indicator` | `option`, `optionIndex` | Check icon |
353
+ | `separator` | `option`, `optionIndex` | the separator part |
354
+ | `empty` | `searchKeyword`, `loading` | `emptyText` |
355
+ | `loading` | `searchKeyword` | Spinner in list |
356
+ | `create-option` | `searchKeyword` | `createOptionText` with `{keyword}` replaced — shown only when `creatable` and no exact match |
357
+
358
+
359
+ **Not present:** `input`, `trigger` (use `trigger-left` / `trigger-right`).
360
+
361
+ The input part is always rendered in the anchor (not slot-replaceable).
362
+
363
+ ---
364
+
365
+ ## Emits
366
+
367
+
368
+ | Event | Payload | When | Implemented |
369
+ | ----------------------- | ------------------------------- | ----------------------------------------------------- | ----------- |
370
+ | `change-value` | `AutocompleteEmitPayload` | Select option, select-all, clear | Yes |
371
+ | `update:value` | Option object, array, or `null` | Same as above | Yes |
372
+ | `search-keyword` | `string` | After debounce while popover open; also `''` on clear | Yes |
373
+ | `update:search-keyword` | `string` | — | **No** |
374
+ | `update:open` | `boolean` | — | **No** |
375
+ | `commit-value` | `AutocompleteEmitPayload` | — | **No** |
376
+ | `create-option` | `AutocompleteEmitPayload` | Select the create row (`creatable`); read the typed value via `payload.extractTrimmedSearchKeyword()` | Yes |
377
+
378
+
379
+ List selection closes the popover and clears `internalSearchKeyword`. Multi toggle on a row updates value without separate commit emit.
380
+
381
+ ### Creatable (parent-owns)
382
+
383
+ When `parcel.creatable` is `true` and the trimmed keyword has no exact label/value match, a create row appears (text from `createOptionText`, `{keyword}` interpolated; override the string or the `create-option` slot for i18n). Selecting it emits **`create-option`** with an `AutocompleteEmitPayload`, then clears the keyword and closes the popover. The field never mutates its own `options`: the **parent owns** appending the new option to `parcel.options` and to the model.
384
+
385
+ ```js
386
+ onCreateOption ({
387
+ payload,
388
+ }) {
389
+ const label = payload.extractTrimmedSearchKeyword()
390
+
391
+ if (!label) {
392
+ return
393
+ }
394
+
395
+ const newOption = {
396
+ value: label,
397
+ label,
398
+ }
399
+
400
+ this.tagsReactive.options = [
401
+ ...this.tagsReactive.options,
402
+ newOption,
403
+ ]
404
+ this.tagsReactive.selectedValue = [
405
+ ...this.tagsReactive.selectedValue,
406
+ newOption,
407
+ ]
408
+ }
409
+ ```
410
+
411
+ ```html
412
+ @create-option="context.onCreateOption({ payload: $event })"
413
+ ```
414
+
415
+ ### Parent handler style
416
+
417
+ ```js
418
+ onChangeValue ({
419
+ payload,
420
+ }) {
421
+ this.statusReactive.isInvalidAssignee = payload.hasEmptyValue()
422
+ }
423
+
424
+ onSearchAssignees ({
425
+ searchKeyword,
426
+ }) {
427
+ this.fetchAssignees({ searchKeyword })
428
+ }
429
+ ```
430
+
431
+ ```html
432
+ @search-keyword="context.onSearchAssignees({ searchKeyword: $event })"
433
+ ```
434
+
435
+ ---
436
+
437
+ ## `AutocompleteEmitPayload` API
438
+
439
+ `AutocompleteEmitPayload` extends `[BaseEmitPayload](../lib/emit-payload/BaseEmitPayload.js)`. Built from `CustomEvent` `detail` on selection/clear.
440
+
441
+
442
+ | Member | Type | Description |
443
+ | ------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- |
444
+ | `searchKeyword` | `string` | From `detail.searchKeyword` |
445
+ | `controlElement` | `HTMLElement | null` | `detail.target` |
446
+ | `selectedOption` | `FuroAutocompleteOption | FuroAcceptableValue | null` | `detail.selectedOption` |
447
+ | `selectedIndex` | `number | null` | `detail.selectedIndex` |
448
+ | `selectedOptions` | `Array<…>` | `detail.selectedOptions` |
449
+ | `selectedGroupIndex` | `number | null` | `detail.selectedGroupIndex` |
450
+ | `extractValue({ option })` | `FuroAcceptableValue` | `option.value` if object with `value`, else option |
451
+ | `extractValues()` | `Array<FuroAcceptableValue>` | Maps `selectedOptions` through `extractValue` |
452
+ | `hasEmptyValue()` | `boolean` | `extractValue()` is `null` or `extractValues()` empty |
453
+ | `hasSelectedValue()` | `boolean` | Negation of `hasEmptyValue()` |
454
+ | `extractTrimmedSearchKeyword()` | `string` | Trimmed `searchKeyword` |
455
+ | `matchesOption({ value })` | `boolean` | Strict equality of `extractValue` results |
456
+
457
+
458
+ **Not on class (spec debt):** `value`, `selectedIndices`, `selectedGroupIndices`, `isFreeFormCommit()`.
459
+
460
+ Use `payload.extractValue()` / `payload.extractValues()` for primitive `value` fields inside options.
461
+
462
+ ---
463
+
464
+ ## `v-model` and state handling
465
+
466
+ ```html
467
+ <FuroAutocompleteField
468
+ v-model:value="context.assigneeRef.value"
469
+ :parcel="{
470
+ value: context.assigneeRef.value,
471
+ options: context.assigneeOptions,
472
+ placeholder: 'Search…',
473
+ }"
474
+ />
475
+ ```
476
+
477
+
478
+ | Binding | Implemented | Notes |
479
+ | ------------------------ | ----------- | --------------------------------------------------------------------------- |
480
+ | `v-model:value` | **Yes** | Watches `parcel.value` and fallthrough `value`; emits full option object(s) |
481
+ | `v-model:search-keyword` | **No** | `parcel.searchKeyword` syncs in only; no `update:search-keyword` |
482
+ | `v-model:open` | **No** | `internalOpenPopoverRef` bound on root; `parcel.open` ignored |
483
+
484
+
485
+ ### State matrix
486
+
487
+
488
+ | State | Source |
489
+ | ----------------- | ------------------------------------------------------------------------ |
490
+ | Selected value(s) | `parcel.value` / `@update:value` |
491
+ | Filter string | `internalSearchKeywordRef`; optional one-way from `parcel.searchKeyword` |
492
+ | Popover open | `internalOpenPopoverRef` only (not `parcel.open`) |
493
+ | Disabled | `parcel.disabled` + fallthrough |
494
+ | Visible options | `filteredOptions()` from `parcel.options` + `localCompare` + `loading` |
495
+ | Loading UI | `parcel.loading` |
496
+
497
+
498
+ ---
499
+
500
+ ## Multiple selection + select all
501
+
502
+ Set `parcel.multiple: true`. Model is an **array of option objects** (can include primitive leaves if options are primitives).
503
+
504
+
505
+ | Topic | Behavior |
506
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------ |
507
+ | `update:value` | Full array (or `[]` when cleared / deselect all) |
508
+ | `change-value` | Same gesture; payload lists current selection in `detail` |
509
+ | Row click | Toggles membership via `compareOption()` |
510
+ | Select all | `showSelectAll: true`; value `FURO_AUTOCOMPLETE_FIELD_SELECT_ALL_VALUE` (`__furo_autocomplete_field_select_all__`) |
511
+ | Select-all action | Toggles all non-disabled leaves from flattened `options`; closes popover |
512
+ | Chip remove | `onRemoveOption` emits `update:value` + `change-value` with updated array |
513
+
514
+
515
+ ---
516
+
517
+ ## Internal implementation (maintainers only)
518
+
519
+ ### Template composition
520
+
521
+ ```text
522
+ Root (v-bind parcel, model-value, :open internal)
523
+ Anchor.trigger
524
+ [trigger-left]
525
+ [multiple chips]
526
+ Input (display-value → extractDisplayValue / label)
527
+ Cancel? / loading icon / Trigger [trigger-right]
528
+ Portal
529
+ Content
530
+ [content-before]
531
+ Viewport
532
+ select-all option?
533
+ filteredOptions → groups / leaves / slots
534
+ Empty?
535
+ loading div?
536
+ [content-after]
537
+ ```
538
+
539
+ ### Primitive mapping notes
540
+
541
+
542
+ | Furo | Underlying primitive / behavior |
543
+ | ---------------------- | ------------------------------------------ |
544
+ | `textDirection` | `dir` `ltr` / `rtl` |
545
+ | `localCompare` | Furo-side filtering in `filteredOptions()` |
546
+ | `debounceMilliseconds` | `TimerClerk` only (not a primitive prop) |
547
+
548
+
549
+ ---
550
+
551
+ ## Accessibility requirements
552
+
553
+ - Provide a visible label (`<label for>`, `aria-label`, or `aria-labelledby`).
554
+ - Set `textValue` on options when slot content is not plain text (underlying-primitive a11y; **filter still uses `label` / `value` in context**).
555
+ - Combobox roles delegated to the underlying primitive ([combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/)).
556
+ - `name` on component for forms; multi-value serialization is application-defined.
557
+
558
+ ---
559
+
560
+ ## Styling convention
561
+
562
+ Styles in SFC `@layer furo`:
563
+
564
+ - Root: `.unit-form-control.autocomplete`
565
+ - Anchor: `.trigger`, `.input`, `.multiple`, `.trigger-right`
566
+ - Content: `.unit-autocomplete-content`, `.viewport`, `.group`, `.option`, `.empty`, `.loading`
567
+ - Tokens: field atoms (`--color-input`, `--size-input-height`, …)
568
+
569
+ Use `data-highlighted`, `data-disabled`, `data-state` from the underlying primitive for state styling.
570
+
571
+ ---
572
+
573
+ ## Examples
574
+
575
+ ### Basic (controlled, client filter)
576
+
577
+ ```html
578
+ <FuroAutocompleteField
579
+ v-model:value="context.countryRef.value"
580
+ :parcel="{
581
+ placeholder: 'Search country',
582
+ options: [
583
+ { value: 'JP', label: 'Japan' },
584
+ { value: 'VN', label: 'Vietnam' },
585
+ { value: 'US', label: 'United States' },
586
+ ],
587
+ invalid: context.statusReactive.isInvalidCountry,
588
+ }"
589
+ name="country"
590
+ @change-value="context.onChangeCountry({ payload: $event })"
591
+ />
592
+ ```
593
+
594
+ ### Grouped options
595
+
596
+ ```html
597
+ <FuroAutocompleteField
598
+ :parcel="{
599
+ placeholder: 'Search fruit',
600
+ options: [
601
+ {
602
+ label: 'Citrus',
603
+ options: [
604
+ { value: 'orange', label: 'Orange' },
605
+ { value: 'lemon', label: 'Lemon' },
606
+ ],
607
+ },
608
+ ],
609
+ }"
610
+ />
611
+ ```
612
+
613
+ ### API-driven options (`localCompare: false`)
614
+
615
+ ```html
616
+ <FuroAutocompleteField
617
+ v-model:value="context.userRef.value"
618
+ :parcel="{
619
+ debounceMilliseconds: 300,
620
+ loading: context.statusReactive.isSearchingUsers,
621
+ localCompare: false,
622
+ options: context.userOptions,
623
+ placeholder: 'Search users',
624
+ }"
625
+ @search-keyword="context.fetchUsers({ searchKeyword: $event })"
626
+ />
627
+ ```
628
+
629
+ ### Object-valued options (model = full option row)
630
+
631
+ ```html
632
+ <FuroAutocompleteField
633
+ v-model:value="context.personRef.value"
634
+ :parcel="{
635
+ options: context.peopleOptions,
636
+ placeholder: 'Search person',
637
+ }"
638
+ @update:value="context.personRef.value = $event"
639
+ />
640
+ ```
641
+
642
+ ```js
643
+ // peopleOptions example: { value: { id: 50001, name: 'Ada' }, label: 'Ada' }
644
+ // personRef holds the selected option object; use option.label for display (built-in)
645
+ ```
646
+
647
+ ### Multi-select + select all
648
+
649
+ ```html
650
+ <FuroAutocompleteField
651
+ v-model:value="context.selectedTagsRef.value"
652
+ :parcel="{
653
+ multiple: true,
654
+ showSelectAll: true,
655
+ selectAllText: 'Select all tags',
656
+ placeholder: 'Search tags',
657
+ options: context.tagOptions,
658
+ }"
659
+ @change-value="context.onChangeTags({ payload: $event })"
660
+ />
661
+ ```
662
+
663
+ ### Custom option slot
664
+
665
+ ```html
666
+ <FuroAutocompleteField
667
+ :parcel="{
668
+ placeholder: 'Search employee',
669
+ options: context.employeeOptions,
670
+ }"
671
+ >
672
+ <template #option-text="{ option }">
673
+ <strong>{{ option.label }}</strong>
674
+ <small>{{ option.department }}</small>
675
+ </template>
676
+ </FuroAutocompleteField>
677
+ ```
678
+
679
+ ---
680
+
681
+ ## Implementation status
682
+
683
+
684
+ | Deliverable | Status |
685
+ | --------------------------------------------------------- | ----------------- |
686
+ | `FuroAutocompleteField.vue` + context + payload | Shipped |
687
+ | Public prop: `parcel` only | Shipped |
688
+ | Playground `FuroAutocompleteFieldPlayground` | Shipped |
689
+ | Unit tests `FuroAutocompleteFieldContext.js` (jsdom) | Shipped |
690
+ | `parcel.searchKeyword` one-way + `v-model:search-keyword` | Partial / pending |
691
+ | `v-model:open` / controlled `parcel.open` | Pending |
692
+ | Free-form + `commit-value` | Pending |
693
+ | Emit on chip remove | Shipped |
694
+ | Closed `parcel` (strip unknown primitive keys) | Pending |
695
+
696
+
697
+ ---
698
+
699
+ ## Future / spec debt
700
+
701
+ Planned items **not** in runtime yet:
702
+
703
+ - `allowFreeForm`, `commit-value`, `generateFreeFormOptionFromKeyword`
704
+ - `resetSearchKeywordOnBlur`, `resetSearchKeywordOnSelect`, `openOnFocus`, `highlightOnHover`, `hideContentWhenEmpty`
705
+ - `update:search-keyword`, `update:open`, controlled `parcel.searchKeyword` / `parcel.open`
706
+ - `select-all` slot scoped `{ checked, indeterminate, disabled }`
707
+ - `AutocompleteEmitPayload.value`, `isFreeFormCommit()`, index arrays
708
+ - Remove pass-through of unknown keys on `v-bind="context.parcel"`
709
+
710
+ ---
711
+
712
+ ## Known gaps and follow-ups
713
+
714
+ - `**search-keyword` type:** runtime emits `string`; JSDoc on `EMIT_EVENT_HASH` still types `payload: { searchKeyword: string }` — align in code later.
715
+ - `**parcel.searchKeyword`:** inbound sync only; typing in the field does not emit `update:search-keyword` for parents.
716
+ - **Group `disabled`:** not propagated to children in `shouldDisableOption`.
717
+ - **Select-all:** no indeterminate UI state; toggle is all-or-none of enabled leaves.
718
+ - **API race:** parent should cancel in-flight fetches when a new keyword fires.
719
+ - **Form submit with `multiple`:** native `name` may not match array serialization expectations.