@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,298 @@
1
+ # `<FuroDropdownMenu>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ `<FuroDropdownMenu>` is an action/navigation menu molecule in the Furo-Vue component system. Its **public contract is Furo-defined**: applications pass `parcel` and `triggerParcel`, listen to Furo emits, and optionally override the trigger slot — without importing any headless menu library.
6
+
7
+ Implementation uses a headless menu primitive inside `lib/components/molecules/FuroDropdownMenu/*` only. That dependency is **not** part of the application API.
8
+
9
+ **No `v-model`:** unlike `FuroSelect`, this menu does not represent a form value. It emits selection and checkbox events; parent code owns persistent checked state for checkbox rows.
10
+
11
+ **Default trigger:** the underlying primitive renders a native `<button>` trigger (no `as-child`). Appearance uses `triggerParcel` tokens (`variant`, `size`) aligned with `FuroButton` — the `FuroButton` SFC is **not** used as trigger because `as-child` composition breaks open behavior.
12
+
13
+ **Custom trigger slot:** Label content only (text, icons). Do not nest another `<button>`.
14
+
15
+ **Source files:**
16
+
17
+ - `lib/components/molecules/FuroDropdownMenu/FuroDropdownMenu.vue`
18
+ - `lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuOptionRows.vue`
19
+ - `lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuContext.js`
20
+ - `lib/components/molecules/FuroDropdownMenu/DropdownMenuOptionSelectEmitPayload.js`
21
+
22
+ **Live samples:** `pages/samples/components/playground/FuroDropdownMenuPlayground/index.vue`
23
+
24
+ ---
25
+
26
+ ## Public API at a glance
27
+
28
+ | Area | Status | Notes |
29
+ | ---- | ------ | ----- |
30
+ | **SFC surface** | | |
31
+ | Props `parcel`, `triggerParcel` | **Implemented** | See tables below |
32
+ | **Menu** | | |
33
+ | `parcel.options` declarative rows | **Implemented** | Action, checkbox, submenu, separator |
34
+ | Custom trigger slot | **Implemented** | `#trigger`; label inside native trigger button |
35
+ | `option-label` scoped slot | **Implemented** | Override row label rendering |
36
+ | Default slot | **Implemented** | Extra menu children (advanced) |
37
+ | **Emits** | | |
38
+ | `open:change` | **Implemented** | `{ open: boolean }` |
39
+ | `option:select` | **Implemented** | `DropdownMenuOptionSelectEmitPayload` |
40
+ | `option:checked-change` | **Implemented** | `{ option, checked }` |
41
+
42
+ ---
43
+
44
+ ## Props
45
+
46
+ ### `parcel` — `FuroDropdownMenuParcel`
47
+
48
+ | Key | Type | Default | Description |
49
+ | --- | ---- | ------- | ----------- |
50
+ | `open` | `boolean` | uncontrolled | Controlled open state; omit for internal state |
51
+ | `disabled` | `boolean` | `false` | Disables trigger and menu |
52
+ | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'bottom'` | Positioning control: which edge of the trigger the menu opens against |
53
+ | `align` | `'start' \| 'center' \| 'end'` | `'start'` | Positioning control: how the menu aligns along that edge |
54
+ | `sideOffset` | `number` | `4` | Positioning control: gap between trigger and menu, in `rem`-equivalent pixels |
55
+ | `options` | `Array<FuroDropdownMenuOption>` | `[]` | Declarative menu rows |
56
+
57
+ ### `FuroDropdownMenuOption`
58
+
59
+ | Key | Type | Description |
60
+ | --- | ---- | ----------- |
61
+ | `label` | `string` | Visible row label |
62
+ | `value` | `string` | Stable identifier for handlers |
63
+ | `disabled` | `boolean` | Non-interactive row |
64
+ | `separator` | `boolean` | Renders separator instead of a selectable row |
65
+ | `variant` | `'default' \| 'destructive'` | Destructive styling for delete actions |
66
+ | `type` | `'action' \| 'checkbox' \| 'submenu'` | Row kind; omit for action |
67
+ | `checked` | `boolean` | Checkbox row checked state (parent-owned) |
68
+ | `keepOpen` | `boolean` | Action row: keep menu open after select |
69
+ | `options` | `Array<FuroDropdownMenuOption>` | Nested rows for `type: 'submenu'` |
70
+
71
+ **Row kind resolution (priority):**
72
+
73
+ 1. `separator: true` → separator
74
+ 2. `type: 'submenu'` with `options` array → nested submenu
75
+ 3. `type: 'checkbox'` → checkbox row (menu stays open on toggle)
76
+ 4. default → action row (closes menu unless `keepOpen: true`)
77
+
78
+ ### `triggerParcel` — trigger appearance
79
+
80
+ | Key | Type | Default | Description |
81
+ | --- | ---- | ------- | ----------- |
82
+ | `variant` | `string` | — | Default trigger appearance token, aligned with `FuroButton` |
83
+ | `size` | `string` | — | Default trigger size token, aligned with `FuroButton` |
84
+ | `asChild` | `boolean` | `false` | When `true`, the `#trigger` slot's own element becomes the trigger — no wrapper `<button>` and no forced `.unit-button` styling — so a consumer can supply their own trigger element without a nested button |
85
+
86
+ By default (`asChild: false`) the menu renders the styled native button and the
87
+ `#trigger` slot fills its label content. Set `asChild: true` to hand the trigger
88
+ element to the consumer (for example a `<FuroButton>` or an `<a>`), letting the
89
+ slotted element itself become the trigger. See [Consumer-owned trigger](#consumer-owned-trigger-aschild).
90
+
91
+ ---
92
+
93
+ ## Emits
94
+
95
+ | Event | Payload | When |
96
+ | ----- | ------- | ---- |
97
+ | `open:change` | `{ open: boolean }` | Menu open state changes |
98
+ | `option:select` | `DropdownMenuOptionSelectEmitPayload` | User selects an enabled action row |
99
+ | `option:checked-change` | `{ option: FuroDropdownMenuOption, checked: boolean }` | User toggles a checkbox row |
100
+
101
+ ### Parent handler style (action rows)
102
+
103
+ ```js
104
+ runMenuAction ({
105
+ payload,
106
+ }) {
107
+ const value = payload.option.value
108
+
109
+ if (!value) {
110
+ return
111
+ }
112
+
113
+ this.runAction({
114
+ value,
115
+ })
116
+ }
117
+ ```
118
+
119
+ ### Parent handler style (checkbox rows)
120
+
121
+ Checkbox rows do **not** close the root menu. Parent code must update `parcel.options` (including nested submenu options) when receiving `option:checked-change`:
122
+
123
+ ```js
124
+ syncCheckboxOption ({
125
+ payload,
126
+ }) {
127
+ this.menuOptions = this.applyCheckedChangeToOptions({
128
+ options: this.menuOptions,
129
+ targetValue: payload.option.value,
130
+ checked: payload.checked,
131
+ })
132
+ }
133
+ ```
134
+
135
+ Use `keepOpen: true` on action rows when the menu should stay open after a one-shot action (for example “Reset filters” inside a submenu).
136
+
137
+ ---
138
+
139
+ ## Slots
140
+
141
+ | Slot | Scoped props | Default |
142
+ | ---- | ------------ | ------- |
143
+ | `trigger` | — | Native button labeled "Menu" (FuroButton-compatible styling) |
144
+ | `option-label` | `option`, `index` | `option.label` text |
145
+ | `default` | — | Empty — append custom menu content (advanced) |
146
+
147
+ ---
148
+
149
+ ## Accessibility
150
+
151
+ - Default trigger sets `aria-haspopup="menu"`.
152
+ - The underlying primitive manages `aria-expanded` on the trigger.
153
+ - Keyboard: Arrow keys navigate, Enter selects, Escape closes (primitive baseline).
154
+ - Checkbox rows expose checked state through the underlying primitive's checkbox row.
155
+ - Disabled options are skipped via the underlying primitive plus a Furo guard on emits.
156
+ - Menu rows take their minimum height from `--size-option-height`, so an app raises them to a larger touch target without overriding menu CSS.
157
+
158
+ ---
159
+
160
+ ## Examples
161
+
162
+ ### Declarative action options
163
+
164
+ ```html
165
+ <FuroDropdownMenu
166
+ :parcel="{
167
+ options: [
168
+ { label: 'Edit', value: 'edit' },
169
+ { separator: true },
170
+ { label: 'Delete', value: 'delete', variant: 'destructive' },
171
+ ],
172
+ }"
173
+ @option:select="context.runRowAction({ payload: $event })"
174
+ />
175
+ ```
176
+
177
+ ### Checkbox filters (multi-toggle, menu stays open)
178
+
179
+ ```html
180
+ <FuroDropdownMenu
181
+ :parcel="{
182
+ options: context.extractCheckboxFilterMenuOptions(),
183
+ }"
184
+ @option:checked-change="context.recordOptionCheckedChange({ payload: $event })"
185
+ />
186
+ ```
187
+
188
+ ```js
189
+ extractCheckboxFilterMenuOptions () {
190
+ return [
191
+ {
192
+ type: 'checkbox',
193
+ label: 'Show archived',
194
+ value: 'archived',
195
+ checked: this.showArchived,
196
+ },
197
+ {
198
+ type: 'checkbox',
199
+ label: 'Show drafts',
200
+ value: 'drafts',
201
+ checked: this.showDrafts,
202
+ },
203
+ ]
204
+ }
205
+ ```
206
+
207
+ ### Submenu with nested checkboxes
208
+
209
+ ```html
210
+ <FuroDropdownMenu
211
+ :parcel="{
212
+ options: [
213
+ { label: 'Export CSV', value: 'export-csv' },
214
+ {
215
+ type: 'submenu',
216
+ label: 'Column filters',
217
+ value: 'column-filters',
218
+ options: context.extractCheckboxFilterMenuOptions(),
219
+ },
220
+ { separator: true },
221
+ {
222
+ label: 'Reset filters',
223
+ value: 'reset-filters',
224
+ keepOpen: true,
225
+ },
226
+ ],
227
+ }"
228
+ @option:select="context.recordOptionSelect({ payload: $event })"
229
+ @option:checked-change="context.recordOptionCheckedChange({ payload: $event })"
230
+ />
231
+ ```
232
+
233
+ ### Custom trigger
234
+
235
+ Set `triggerParcel` for variant/size, then override `#trigger` slot content:
236
+
237
+ ```html
238
+ <FuroDropdownMenu
239
+ :parcel="{ options: context.extractActionMenuOptions() }"
240
+ :trigger-parcel="{
241
+ variant: 'secondary',
242
+ size: 'sm',
243
+ }"
244
+ @option:select="context.runRowAction({ payload: $event })"
245
+ >
246
+ <template #trigger>
247
+ <Icon name="ph:dots-three-outline" />
248
+ Actions
249
+ </template>
250
+ </FuroDropdownMenu>
251
+ ```
252
+
253
+ ### Consumer-owned trigger (`asChild`)
254
+
255
+ Set `triggerParcel.asChild: true` so the slotted element itself becomes the
256
+ trigger — no wrapper `<button>`, no forced `.unit-button` styling:
257
+
258
+ ```html
259
+ <FuroDropdownMenu
260
+ :parcel="{ options: context.extractActionMenuOptions() }"
261
+ :trigger-parcel="{ asChild: true }"
262
+ @option:select="context.runRowAction({ payload: $event })"
263
+ >
264
+ <template #trigger>
265
+ <FuroButton :parcel="{ variant: 'secondary' }">
266
+ Actions
267
+ </FuroButton>
268
+ </template>
269
+ </FuroDropdownMenu>
270
+ ```
271
+
272
+ The default (`asChild: false`) still renders the styled native button.
273
+
274
+ ---
275
+
276
+ ## Implementation status
277
+
278
+ | Deliverable | Status |
279
+ | ----------- | ------ |
280
+ | `FuroDropdownMenu.vue` + context + payloads | Shipped |
281
+ | `FuroDropdownMenuOptionRows.vue` (recursive rows) | Shipped |
282
+ | Submenus (`type: 'submenu'`) | Shipped |
283
+ | Checkbox options (`type: 'checkbox'`) | Shipped |
284
+ | `keepOpen` action rows | Shipped |
285
+ | Playground `FuroDropdownMenuPlayground` | Shipped |
286
+ | Unit tests (context + payloads) | Shipped |
287
+ | Default trigger via `<FuroButton>` SFC | **Deferred** | Blocked by atom polymorphic root — see below |
288
+ | Radio menu options | Out of scope |
289
+
290
+ ---
291
+
292
+ ## Deferred work: default trigger as `<FuroButton>`
293
+
294
+ **Status:** Intentionally not shipped. Default trigger is the primitive's native trigger button with `FuroButton`-compatible styling via `triggerParcel`.
295
+
296
+ **Why:** the primitive's `as-child` trigger + `<FuroButton>` does not open the menu — Vue component boundary and missing `ref` forward on `FuroButton`. Full analysis: `docs/furo-vue/performance-and-pitfalls.md` §11.
297
+
298
+ **When fixed on `FuroButton`:** Re-evaluate replacing duplicated `.unit-button` CSS in `FuroDropdownMenu.vue` with `<FuroButton>` as the default trigger. Until then, do not nest `<FuroButton>` under the primitive's `as-child` trigger in application code.
@@ -0,0 +1,391 @@
1
+ # `<FuroEditableField>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ `<FuroEditableField>` is a **shipped** editable molecule in the Furo-Vue component system. Applications use `parcel`, Furo emits, documented slots, and attribute fallthrough only — no internal implementation imports.
6
+
7
+ Implementation wraps a headless inline-edit primitive (`Root`, `Area`, `Preview`, `Input`, …) inside `lib/components/molecules/FuroEditableField/*`. The underlying primitive is **not** part of the application API.
8
+
9
+ Contract style aligns with [`FuroTextField`](./furo-text-field.md) and [`FuroCheckbox`](./furo-checkbox.md):
10
+
11
+ - Furo-owned behavior via `:parcel` (`FuroEditableFieldParcel`)
12
+ - primitive-owned edit lifecycle keys also live on `parcel` but are **forwarded** to the root (not interpreted by Furo)
13
+ - HTML / form attributes via fallthrough (`name`, `required`, `id`, `aria-*`, …)
14
+ - Furo emit: `commit-value` with `EditableEmitPayload`
15
+ - default **text** editor (`FuroTextField`) when `#editor` is omitted
16
+ - `#editor` slot for number, boolean, select, file, and other custom controls
17
+
18
+ **Public SFC prop:** `parcel` only (+ attribute fallthrough).
19
+
20
+ **Live samples:** `pages/samples/components/playground/FuroEditableFieldPlayground/index.vue`
21
+
22
+ ### Abstraction boundary
23
+
24
+ | Layer | Responsibility |
25
+ | ----- | -------------- |
26
+ | Application | `parcel`, slots, `@commit-value` listener; updates `parcel.value` after commit |
27
+ | Molecule (`FuroEditableField`) | Draft/commit sync, preview text, floating editor position, undo, `EditableEmitPayload` |
28
+ | Underlying primitive (maintainers) | Edit mode open/close, activation/submit gestures on default surface; internal only |
29
+
30
+ **Rules:**
31
+
32
+ 1. Documented **Furo-owned** `parcel` keys are stripped in `extractRootAttributes()` before optional passthrough to the underlying primitive.
33
+ 2. Do **not** import the underlying editable primitive in application code.
34
+ 3. Sync committed value in the parent on `@commit-value` — Furo does **not** emit `update:value`.
35
+
36
+ **Source files:**
37
+
38
+ - `lib/components/molecules/FuroEditableField/FuroEditableField.vue`
39
+ - `lib/components/molecules/FuroEditableField/FuroEditableFieldContext.js`
40
+ - `lib/components/molecules/FuroEditableField/EditableEmitPayload.js`
41
+ - `lib/components/molecules/FuroEditableField/processors/EditorCollisionProcessor.js`
42
+ - `lib/components/molecules/FuroEditableField/processors/ObjectProcessor.js`
43
+
44
+ ---
45
+
46
+ ## Public API at a glance
47
+
48
+ | Area | Status | Notes |
49
+ | ---- | ------ | ----- |
50
+ | **SFC surface** | | |
51
+ | Public prop `parcel` only (+ attribute fallthrough) | **Implemented** | See [`FuroEditableFieldParcel`](#furoeditablefieldparcel) |
52
+ | **Value** | | |
53
+ | `parcel.value` (controlled sync) | **Implemented** | Watched; updates `committedValueRef` and `draftValueRef` |
54
+ | `v-model:value` | **Not implemented** | Listen to `@commit-value` and assign `parcel.value` in the parent |
55
+ | **Furo-owned parcel** | | |
56
+ | `invalid`, `disabled`, `required`, `readonly` | **Implemented** | `disabled` / `required` / `readonly` also from fallthrough attrs |
57
+ | `placeholder` | **Implemented** | Default text editor + forwarded placeholder on the root |
58
+ | `emptyPreviewText` | **Implemented** | Default `'---'` when value is empty |
59
+ | `showActions` | **Implemented** | Default `false`; shows Edit trigger and enables action-button row |
60
+ | `showEditing` | **Implemented** | Default `false`; sets internal editing flag on mount |
61
+ | `showUndoChange` | **Implemented** | Default `false`; shows undo control when committed value diverges from mount snapshot |
62
+ | `collisionBoundary` | **Implemented** | `HTMLElement`, `Ref<HTMLElement>`, or `null`; clamps floating editor position |
63
+ | **Primitive-forwarded parcel** | | |
64
+ | `activationMode`, `submitMode`, `selectOnFocus`, `startWithEditMode`, `maxLength`, `autoResize` | **Implemented** | Passed through `extractRootAttributes()` to the root; the primitive's defaults apply when omitted |
65
+ | `variant`, `preserveLayout`, `editOverlay` | **Not implemented** | Editor uses `position: fixed` collision positioning when editing |
66
+ | `previewText`, `multiline`, `editing` / `v-model:editing` | **Not implemented** | Use `#preview` slot; `#editor` + `FuroTextarea`; the primitive's state is internal |
67
+ | **Emits** | | |
68
+ | `commit-value` | **Implemented** | On successful submit and on undo |
69
+ | `update-editing-state` | **Implemented** | the primitive's `@update:state` → `onUpdateEditingState()`; payload is `EdittingState` |
70
+ | `change-value`, `cancel-edit`, `update:value` | **Not implemented** | Draft changes are local until commit |
71
+ | **Slots** | | |
72
+ | `preview`, `editor` | **Implemented** | `editor` replaces default `FuroTextField` |
73
+ | `edit-trigger`, `submit-trigger`, `cancel-trigger`, `undo-trigger` | **Implemented** | Optional custom triggers |
74
+ | `#edit` (legacy name) | **Not implemented** | Use `#editor` |
75
+ | **Editor overlay** | | |
76
+ | Fixed collision-aware editor panel | **Implemented** | `EditorCollisionProcessor`; scroll/resize listeners while editing |
77
+ | Teleport to `document.body`, `editOverlay: 'inset'` | **Not implemented** | |
78
+ | Closed `parcel` (reject unknown keys) | **Not implemented** | Unknown keys may pass through to the underlying primitive |
79
+
80
+ ---
81
+
82
+ ## `FuroEditableFieldParcel`
83
+
84
+ Behavior-controlling props passed as `:parcel`. Furo-owned values must be **JSON-serializable** (no functions, no class instances). `collisionBoundary` may be a Vue `Ref`.
85
+
86
+ ### `parcel` vs fallthrough
87
+
88
+ | Mechanism | Use for |
89
+ | --------- | ------- |
90
+ | `:parcel` | Furo behavior and the root's options |
91
+ | Fallthrough attributes | Native HTML on the root (`name`, `required`, `id`, `aria-*`, `placeholder`, …) |
92
+
93
+ Merged defaults (Furo-owned keys only):
94
+
95
+ | Key | Default |
96
+ | --- | ------- |
97
+ | `emptyPreviewText` | `'---'` |
98
+ | `showActions` | `false` |
99
+ | `showEditing` | `false` |
100
+ | `showUndoChange` | `false` |
101
+
102
+ ### Furo-owned keys
103
+
104
+ Stripped before forwarding to the underlying primitive. Interpreted by `FuroEditableFieldContext`.
105
+
106
+ | Key | Type | Default | Description |
107
+ | --- | ---- | ------- | ----------- |
108
+ | `value` | `FuroEditableValue` | — | Committed value. Parent should update this after `@commit-value` |
109
+ | `invalid` | `boolean` | `false` | Passed to default `FuroTextField`; use `#editor` for custom invalid UI |
110
+ | `disabled` | `boolean` | `false` | Also respected from fallthrough `disabled`, including the bare `<FuroEditableField disabled>` spelling |
111
+ | `required` | `boolean` | `false` | Also respected from fallthrough `required`, including the bare `<FuroEditableField required>` spelling |
112
+ | `readonly` | `boolean` | `false` | Also respected from fallthrough `readonly`, including the bare `<FuroEditableField readonly>` spelling |
113
+ | `editLabel` | `string` | `'Edit'` | Accessible name of the edit trigger |
114
+ | `undoLabel` | `string` | `'Undo'` | Accessible name of the undo control |
115
+ | `submitLabel` | `string` | `'Submit'` | Accessible name of the submit control |
116
+ | `cancelLabel` | `string` | `'Cancel'` | Accessible name of the cancel control |
117
+ | `placeholder` | `string` \| `{ edit: string, preview: string }` | fallthrough `placeholder` or `''` | Default text editor placeholder |
118
+ | `emptyPreviewText` | `string` | `'---'` | Shown when `hasEmptyValue()` is true (no `#preview` slot) |
119
+ | `showActions` | `boolean` | `false` | Show Edit icon trigger (`v-show` on the edit-trigger part) |
120
+ | `showEditing` | `boolean` | `false` | On mount, sets internal `isShowEditing` used for editor positioning |
121
+ | `showUndoChange` | `boolean` | `false` | Show undo button when committed value changed since mount |
122
+ | `collisionBoundary` | `HTMLElement` \| `Ref<HTMLElement \| null>` \| `null` | — | Bounding rect for editor collision; viewport when omitted |
123
+
124
+ A fallthrough `disabled` or `required` is read by presence, the way HTML reads a valueless flag: every value turns it on except `false` and an absent attribute, so `:disabled="0"` disables. Bind a comparison, as in `:disabled="selectedCount === 0"`, when a number drives the flag. See [ADR 0036](adr/0036-boolean-attribute-presence-decides.md).
125
+
126
+ ### Primitive-forwarded keys
127
+
128
+ Not in `FURO_EDITABLE_PARCEL_KEYS`. Merged into `extractRootAttributes()` and passed to the root. Furo does not validate or default these — the underlying primitive's semantics apply.
129
+
130
+ | Key | Type (JSDoc) | Description |
131
+ | --- | ------------ | ----------- |
132
+ | `activationMode` | `'dblclick' \| 'focus' \| 'none'` | How preview enters edit mode |
133
+ | `submitMode` | `'blur' \| 'enter' \| 'both' \| 'none'` | When the primitive auto-submits the draft |
134
+ | `selectOnFocus` | `boolean` | Select all on focus in the primitive's default input path |
135
+ | `startWithEditMode` | `boolean` | Mount in edit mode (primitive) |
136
+ | `maxLength` | `number` | Max length (primitive default input path) |
137
+ | `autoResize` | `boolean` | Auto-resize (primitive default input path) |
138
+
139
+ Other unknown `parcel` keys are also forwarded to the underlying primitive unless added to `FURO_EDITABLE_PARCEL_KEYS` in a future release.
140
+
141
+ ---
142
+
143
+ ## Emits
144
+
145
+ Each `@commit-value` argument is an **`EditableEmitPayload` instance**. `$event.value` is the committed value.
146
+
147
+ ```html
148
+ <FuroEditableField
149
+ :parcel="{ value: context.titleRef.value, showActions: true }"
150
+ @commit-value="context.titleRef.value = $event.value"
151
+ @update-editing-state="context.onUpdateEditingState({ editingState: $event })"
152
+ />
153
+ ```
154
+
155
+ `$event` is an `EdittingState`: `'edit'` when the editor opens, `'submit'` or `'cancel'` when it closes.
156
+
157
+ | Event | Payload | When |
158
+ | ----- | ------- | ---- |
159
+ | `commit-value` | `EditableEmitPayload` | the primitive's `@submit` → `onSubmitCommittedValue()`; undo button → `onUndoChange()` |
160
+ | `update-editing-state` | `EdittingState` | the primitive's `@update:state` → `onUpdateEditingState()`; values: `'edit'`, `'cancel'`, `'submit'` |
161
+
162
+ Furo does **not** emit `change-value`, `cancel-edit`, or `update:value`. Draft updates via `updateDraftValue` in the `#editor` slot are internal until the primitive submits or the parent calls scoped `submit()` from the root's slot props.
163
+
164
+ ### `EditableEmitPayload`
165
+
166
+ | Member | Description |
167
+ | ------ | ----------- |
168
+ | `.value` | Committed value from event `detail.value` |
169
+ | `.controlElement` | `detail.target` from the raw `CustomEvent`, or `null` |
170
+ | `.hasEmptyValue()` | `true` for `null`, `''`, `{}`, `[]` |
171
+
172
+ Not on payload (removed from earlier spec drafts): `.draftValue`, `.committedValue`, `.reason`, `.isEditing()`, `.extractTrimmedText()`.
173
+
174
+ ---
175
+
176
+ ## Slots
177
+
178
+ | Slot | Scoped props | Notes |
179
+ | ---- | ------------- | ----- |
180
+ | `preview` | root props + `committedValue`, `draftValue`, `updateDraftValue`, `undoChange` | Used when `hasEmptyValue()` is false; overrides `resolvePreviewText()` |
181
+ | `editor` | Same as `preview` | Replaces default `FuroTextField`; hosts custom atoms |
182
+ | `edit-trigger` | Same | `as-child` when slot provided |
183
+ | `submit-trigger` | Same | Inside editor `.actions`; visible when `isEditing` |
184
+ | `cancel-trigger` | Same | Inside editor `.actions`; visible when `isEditing` |
185
+ | `undo-trigger` | Same | Shown when `showUndoChange` and value changed since mount |
186
+
187
+ The root's scoped props (`isEditing`, `submit`, `cancel`, `edit`, `modelValue`, `isEmpty`, …) are spread into every slot via `buildSlotProps()`.
188
+
189
+ ### `#editor` integration contract
190
+
191
+ 1. Read `draftValue` — synced from `parcel.value` when edit opens and on each `updateDraftValue`.
192
+ 2. On each change → `updateDraftValue({ value })` — updates draft only (no Furo emit).
193
+ 3. On save → call scoped `submit()` from the root's props, or rely on the primitive's `submitMode` (`'blur'`, `'enter'`, `'both'`) on the root.
194
+ 4. On discard → call scoped `cancel()` from the root's props.
195
+
196
+ For commit-on-pick controls (select, checkbox, file), set `submitMode: 'none'` on `parcel` and call `submit()` from the child handler, or sync draft and let blur submit if appropriate.
197
+
198
+ ---
199
+
200
+ ## Layout & action UI
201
+
202
+ | Concern | Behavior |
203
+ | ------- | -------- |
204
+ | Root layout | `.unit-form-control.editable` — flex row; preview ellipsis |
205
+ | Edit trigger | Shown when `parcel.showActions` |
206
+ | Submit / Cancel row | Shown when `showActions` **or** any of `edit-trigger` / `submit-trigger` / `cancel-trigger` slots exist; submit/cancel icons only while `isEditing` |
207
+ | Undo | Shown when `showUndoChange` and `hasOriginalValueChanged()` |
208
+ | Editor panel | `position: fixed` while editing; width matches anchor cell; `z-index: calc(var(--value-z-index-layer-overlay) + 0)` |
209
+
210
+ There is no `variant: 'inline' \| 'form'` flag. Use `showActions` and table-friendly `collisionBoundary` for inline grids.
211
+
212
+ ---
213
+
214
+ ## Edit lifecycle
215
+
216
+ ```mermaid
217
+ stateDiagram-v2
218
+ [*] --> Preview: mount
219
+ Preview --> Editing: primitive activation (click/focus/dblclick/trigger)
220
+ Editing --> Preview: submit() / blur+submitMode / cancel()
221
+ Editing --> Preview: scroll outside editor (primitive cancel)
222
+ Preview --> Preview: commit-value emit (parent updates parcel.value)
223
+ ```
224
+
225
+ | Stage | What happens |
226
+ | ----- | ------------ |
227
+ | Mount | `originalValueRef` ← `parcel.value`; `committedValueRef` / `draftValueRef` synced from `parcel.value` watch |
228
+ | Enter edit | the root opens; `@update:state` → `onUpdateEditingState` → `update-editing-state` emit; focus first `input` / `textarea` in editor |
229
+ | Draft | Default editor: `@change-value` on `FuroTextField` → `updateDraftValue`. Custom `#editor`: parent calls `updateDraftValue` |
230
+ | Commit | the primitive's `@submit` → `onSubmitCommittedValue` → `commit-value` with current draft |
231
+ | Cancel | the primitive's `cancel()` — no Furo emit |
232
+ | Scroll | Window scroll (capture) outside floating editor → `editableRootInstanceRef.cancel()` |
233
+ | Resize | Window `resize` while editing → recalculate editor position |
234
+ | Undo | `onUndoChange()` restores `originalValueRef` to committed + draft → `commit-value` |
235
+
236
+ `parcel.showEditing` only sets internal `isShowEditing` on mount for positioning; prefer the primitive's `startWithEditMode` to open edit on mount.
237
+
238
+ ---
239
+
240
+ ## Editor collision / overlay
241
+
242
+ While `isShowEditing` is true, `generateEditorPositionStyle()` runs `EditorCollisionProcessor`:
243
+
244
+ | Input | Role |
245
+ | ----- | ---- |
246
+ | Anchor rect | `.unit-form-control.editable` root `getBoundingClientRect()` |
247
+ | Editor size | Floating `.editor` `offsetWidth` / `offsetHeight` |
248
+ | Boundary | `parcel.collisionBoundary` rect, or viewport (`document.documentElement.clientWidth` / `clientHeight`) |
249
+
250
+ | Rule | Behavior |
251
+ | ---- | -------- |
252
+ | Horizontal overflow | Prefer anchor `left`; flip to `anchor.right - editorWidth` if needed; clamp inside boundary |
253
+ | Vertical overflow | Prefer anchor `top`; flip to `anchor.bottom - editorHeight` if needed; clamp inside boundary |
254
+ | Width | Editor panel width = anchor width |
255
+
256
+ **Table usage:** pass the table element as `collisionBoundary` so dropdowns stay inside the table box (see playground).
257
+
258
+ ```html
259
+ :parcel="{
260
+ value: row.title,
261
+ showActions: true,
262
+ collisionBoundary: context.tableElementRef.value,
263
+ }"
264
+ ```
265
+
266
+ Editor is **not** teleported to `document.body`; it is `position: fixed` relative to the viewport with coordinates derived from the anchor.
267
+
268
+ ---
269
+
270
+ ## Value types
271
+
272
+ `FuroEditableValue` — any value except `undefined`.
273
+
274
+ | Kind | Examples | Default preview (`resolvePreviewText`) | Empty (`hasEmptyValue`) |
275
+ | ---- | -------- | -------------------------------------- | ------------------------ |
276
+ | `null` | `null` | `''` (shows `emptyPreviewText`) | yes |
277
+ | `string` | `'Acme'`, `''` | `String(value)`; `''` is **not** empty | only `null` for primitives |
278
+ | `number` | `42`, `0` | `String(value)` | no |
279
+ | `boolean` | `true`, `false` | `String(value)` | no |
280
+ | Plain object | `{ label: 'US', value: 'us' }` | `JSON.stringify(value)` | yes when `{}` |
281
+ | Array | `['a', 'b']` | `String(value)` (comma-separated) | yes when `[]` |
282
+
283
+ For human-readable preview (options, booleans, masked passwords), use `#preview` — there is no `previewText` parcel key.
284
+
285
+ `updateDraftValue({ value })` assigns `value ?? null` — passing `undefined` coerces to `null`.
286
+
287
+ `ObjectProcessor` provides deep equality for undo comparison on objects and arrays.
288
+
289
+ ---
290
+
291
+ ## Usage examples
292
+
293
+ ### Default text editor
294
+
295
+ From playground — `showActions`, `showUndoChange`, commit via payload:
296
+
297
+ ```html
298
+ <FuroEditableField
299
+ :parcel="{
300
+ value: context.textValueRef.value,
301
+ showActions: true,
302
+ showUndoChange: true,
303
+ }"
304
+ name="playground-editable-default"
305
+ @commit-value="context.textValueRef.value = $event.value"
306
+ />
307
+ ```
308
+
309
+ ### Table row with `#editor` (textarea)
310
+
311
+ ```html
312
+ <FuroEditableField
313
+ :parcel="{
314
+ value: row.notes,
315
+ showActions: true,
316
+ showUndoChange: true,
317
+ collisionBoundary: context.tableElementRef.value,
318
+ }"
319
+ @commit-value="row.notes = $event.value"
320
+ >
321
+ <template #editor="{ draftValue, updateDraftValue }">
322
+ <FuroTextarea
323
+ :parcel="{ value: draftValue }"
324
+ @change-value="updateDraftValue({ value: $event.value })"
325
+ />
326
+ </template>
327
+ </FuroEditableField>
328
+ ```
329
+
330
+ ### Select with custom preview
331
+
332
+ ```html
333
+ <FuroEditableField
334
+ :parcel="{
335
+ value: row.territoryCode,
336
+ showActions: true,
337
+ collisionBoundary: context.tableElementRef.value,
338
+ submitMode: 'none',
339
+ }"
340
+ @commit-value="row.territoryCode = $event.value"
341
+ >
342
+ <template #preview>
343
+ {{ row.formatTerritoryPreview() }}
344
+ </template>
345
+
346
+ <template #editor="{ draftValue, updateDraftValue, submit }">
347
+ <FuroSelect
348
+ :parcel="{ value: draftValue, options: context.territoryOptions }"
349
+ @update:value="updateDraftValue({ value: $event })"
350
+ />
351
+ </template>
352
+ </FuroEditableField>
353
+ ```
354
+
355
+ ### Checkbox — draft sync only; commit on the primitive's submit or explicit `submit()`
356
+
357
+ ```html
358
+ <template #editor="{ draftValue, updateDraftValue }">
359
+ <FuroCheckbox
360
+ :parcel="{ value: draftValue }"
361
+ @update:value="updateDraftValue({ value: $event })"
362
+ />
363
+ </template>
364
+ ```
365
+
366
+ ---
367
+
368
+ ## Non-goals / limitations
369
+
370
+ - **No `v-model:value`** — parent owns `parcel.value`; listen to `@commit-value`.
371
+ - **No `v-model:editing`** — listen to `@update-editing-state` (`'edit' \| 'cancel' \| 'submit'`) if the parent needs edit-mode telemetry.
372
+ - **No per-keystroke Furo emit** — use child field `@change-value` inside `#editor` if the parent needs live draft telemetry.
373
+ - **No `cancel-edit` emit** — listen to the primitive's cancel only via future API; today cancel is silent at the Furo layer.
374
+ - **No built-in `variant` / `editOverlay` modes** — one floating editor positioning strategy.
375
+ - **Default preview is not option-aware** — objects render as JSON; use `#preview`.
376
+ - **Empty string is not “empty”** for preview placeholder — only `null`, `{}`, and `[]` show `emptyPreviewText`.
377
+ - **Do not import** `EditorCollisionProcessor`, `ObjectProcessor`, or the underlying editable primitive in application code.
378
+
379
+ ---
380
+
381
+ ## Tests
382
+
383
+ - `tests/__tests__/jsdom/lib/components/molecules/FuroEditableField/EditableEmitPayload.js`
384
+ - `tests/__tests__/jsdom/lib/components/molecules/FuroEditableField/FuroEditableFieldContext.js`
385
+ - `tests/__tests__/jsdom/lib/components/molecules/FuroEditableField/processors/EditorCollisionProcessor.js`
386
+
387
+ Run:
388
+
389
+ ```bash
390
+ npm test -- tests/__tests__/jsdom/lib/components/molecules/FuroEditableField/
391
+ ```