@openreachtech/furo-vue 1.6.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 (79) 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 +19 -17
  73. package/public/furo-vue/components.json +1 -1
  74. package/types/generated/components/atoms/FuroButton/FuroButtonContext.d.ts +4 -4
  75. package/types/generated/components/molecules/FuroSelect/FuroSelect.vue.d.ts +23 -0
  76. package/types/generated/components/molecules/FuroSelect/FuroSelectContext.d.ts +14 -0
  77. package/types/generated/components/molecules/FuroSelect/FuroSelectOptionText.vue.d.ts +50 -0
  78. package/types/generated/components/molecules/FuroSelect/FuroSelectOptionTextContext.d.ts +141 -0
  79. package/types/generated/components/organisms/FuroTable/FuroTableContext.d.ts +2 -2
@@ -0,0 +1,384 @@
1
+ # `<FuroCheckbox>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ `<FuroCheckbox>` is a **shipped** checkbox atom in the Furo-Vue component system. Its **public contract is Furo-defined**: application developers pass `parcel`, listen to Furo emits, and use documented slots — without importing the underlying primitive library.
6
+
7
+ Implementation uses a headless checkbox primitive inside `lib/components/atoms/FuroCheckbox/*` only. That dependency is **not** part of the application API.
8
+
9
+ Contract style aligns with [`FuroTextField`](./furo-text-field.md) and [`FuroAutocompleteField`](./furo-autocomplete-field.md):
10
+
11
+ - behavior via `:parcel` (`FuroCheckboxParcel`)
12
+ - HTML / form attributes via fallthrough (`name`, `id`, `aria-*`, …)
13
+ - Furo emits (`change-value`, `update:value`, `update:indeterminate`)
14
+ - `v-model:value` syncs checked state (`boolean`); `v-model:indeterminate` syncs the [-] UI flag
15
+
16
+ **Public SFC prop:** `parcel` only.
17
+
18
+ **Model shape:** **`boolean` checked** (`parcel.value` / `v-model:value`) plus **`boolean` indeterminate** (`parcel.indeterminate` / `v-model:indeterminate`). Tri-state is not encoded in `value`.
19
+
20
+ `@change-value` fires on every committed toggle from the checkbox root (`@update:model-value`).
21
+
22
+ Read [`FuroTextField`](./furo-text-field.md) for the shared `parcel` mental model.
23
+
24
+ ### Abstraction boundary
25
+
26
+ | Layer | Responsibility |
27
+ | ----- | -------------- |
28
+ | Application | `parcel`, listeners, slots; controls `parcel.value` / `parcel.indeterminate` |
29
+ | Atom (`FuroCheckbox`) | Maps parcel → the underlying checkbox primitive; DOM, emits, default indicator |
30
+ | Underlying primitive (maintainers) | Internal only; not part of the application API |
31
+
32
+ **Rules:**
33
+
34
+ 1. Documented `parcel` keys are the **Furo contract**. Furo-owned keys are stripped in `extractRootAttributes()` before optional passthrough to the underlying primitive.
35
+ 2. Do **not** import the underlying checkbox primitive in application code.
36
+ 3. Use `parcel.indeterminate` (or `v-model:indeterminate`) for the [-] state — not `null` or ad-hoc DOM attributes on `value`.
37
+
38
+ **Source files:**
39
+
40
+ - `lib/components/atoms/FuroCheckbox/FuroCheckbox.vue`
41
+ - `lib/components/atoms/FuroCheckbox/FuroCheckboxContext.js`
42
+ - `lib/components/atoms/FuroCheckbox/CheckboxEmitPayload.js`
43
+
44
+ **Live samples:** `pages/samples/components/playground/FuroCheckboxPlayground/index.vue`
45
+
46
+ ---
47
+
48
+ ## Public API at a glance
49
+
50
+ | Area | Status | Notes |
51
+ | ---- | ------ | ----- |
52
+ | **SFC surface** | | |
53
+ | Public prop `parcel` only (+ attribute fallthrough) | **Implemented** | See [`FuroCheckboxParcel`](#furocheckboxparcel) |
54
+ | **Value & state** | | |
55
+ | Controlled `parcel.value` + `v-model:value` | **Implemented** | `Object.hasOwn(parcel, 'value')` |
56
+ | Uncontrolled (no `parcel.value`) | **Implemented** | Internal ref starts `false`; parent toggle without `value` key |
57
+ | Indeterminate (`parcel.indeterminate` + `v-model:indeterminate`) | **Implemented** | Separate boolean channel; default minus/check icons |
58
+ | `invalid`, `disabled`, `required` | **Implemented** | `.invalid` class + `aria-invalid` / `disabled` / `required` on the checkbox root |
59
+ | `parcel.size` | **Implemented** | CSS length (e.g. `1rem`, `1.5rem`) via `--furo-checkbox-size` |
60
+ | **Emits** | | |
61
+ | `change-value`, `update:value`, `update:indeterminate` | **Implemented** | No `commit-value` (no commit gesture) |
62
+ | **Slots** | | |
63
+ | `indicator`, `indeterminate-indicator` | **Implemented** | No built-in label — use external `<label>` |
64
+ | Built-in `label` slot | **Not implemented** | Label must live outside `<FuroCheckbox>` |
65
+ | **Spec debt (not in runtime)** | | |
66
+ | Checkbox group fieldset API | **Not implemented** | v1 single checkbox only |
67
+ | Closed `parcel` (reject unknown primitive keys) | **Not implemented** | Extra keys may pass via `extractRootAttributes()` |
68
+
69
+ ---
70
+
71
+ ## Props
72
+
73
+ ### Rule: `parcel` vs attributes
74
+
75
+ | Mechanism | When to use | Application sees |
76
+ | --------- | ----------- | ---------------- |
77
+ | `:parcel` | Furo checkbox behavior | `FuroCheckboxParcel` (+ merged defaults) |
78
+ | Attributes on `<FuroCheckbox>` | e.g. `name`, `id` | Vue fallthrough to the checkbox root (merged with `extractRootAttributes()`) |
79
+
80
+ > **`parcel` must be JSON-serializable** — no functions inside `parcel`.
81
+
82
+ ```html
83
+ <div class="unit-checkbox-field">
84
+ <FuroCheckbox
85
+ id="terms"
86
+ v-model:value="context.termsAcceptedRef.value"
87
+ :parcel="{
88
+ value: context.termsAcceptedRef.value,
89
+ invalid: context.statusReactive.isInvalidTerms,
90
+ }"
91
+ name="terms"
92
+ @change-value="context.onChangeTerms({ payload: $event })"
93
+ />
94
+ <label for="terms">
95
+ I accept the terms
96
+ </label>
97
+ </div>
98
+ ```
99
+
100
+ ### Component props (public)
101
+
102
+ | Prop | Type | Default | Description |
103
+ | ---- | ---- | ------- | ----------- |
104
+ | `parcel` | `FuroCheckboxParcel` or `null` | `null` | Furo behavior (see table below) |
105
+
106
+ ### `FuroCheckboxValue`
107
+
108
+ `boolean` — checked state only (`true` / `false`). Indeterminate UI uses `parcel.indeterminate` (see below).
109
+
110
+ ### `FuroCheckboxParcel`
111
+
112
+ Keys read in `FuroCheckboxContext` (defaults from `parcel` getter):
113
+
114
+ | Key | Type | Default (getter) | Description |
115
+ | --- | ---- | ---------------- | ----------- |
116
+ | `value` | `FuroCheckboxValue` | — | Controlled checked state. Presence of this key (`Object.hasOwn`) enables controlled mode. |
117
+ | `indeterminate` | `boolean` | `false` | When `true`, shows [-] indicator; `value` still reflects checked boolean (often `false` for partial “select all”) |
118
+ | `invalid` | `boolean` | `false` | `.invalid` class + `aria-invalid` on the checkbox root |
119
+ | `disabled` | `boolean` | `false` | Disables interaction (also respects fallthrough `disabled`) |
120
+ | `required` | `boolean` | `false` | Forwarded to root (also respects fallthrough `required`) |
121
+ | `size` | `string` | `1.125rem` (`DEFAULT_CHECKBOX_SIZE`) | Box width/height; icon uses `8/9` of box (`DEFAULT_CHECKBOX_ICON_SIZE` = `1rem`) |
122
+
123
+ Furo-owned keys above are **not** forwarded via `extractRootAttributes()`.
124
+
125
+ #### Size
126
+
127
+ Pass any valid CSS length on `parcel.size` (`rem`, `px`, or `em`). The root sets `--furo-checkbox-size` and `--furo-checkbox-icon-size` (box × `8/9`). Default icons receive matching `Icon` `size` via `extractCheckboxIconSize()`.
128
+
129
+ ```html
130
+ <FuroCheckbox
131
+ :parcel="{
132
+ value: context.flagRef.value,
133
+ size: '1.5rem',
134
+ }"
135
+ />
136
+ ``` Other JSON-serializable keys on `parcel` may pass through to the underlying primitive (unsupported for apps).
137
+
138
+ #### Indeterminate state
139
+
140
+ | Mechanism | Behavior |
141
+ | --------- | -------- |
142
+ | `parcel.indeterminate: true` | the underlying primitive's internal model is indeterminate; `parcel.value` stays boolean |
143
+ | `v-model:indeterminate` | Two-way sync for `parcel.indeterminate` on user toggle |
144
+ | `update:indeterminate` | Emits `boolean` on each toggle (with `update:value`) |
145
+ | Default `#indeterminate-indicator` / `#indicator` slots | `ph:minus-bold` when indeterminate; `ph:check-bold` when checked and not indeterminate |
146
+
147
+ Toggling from indeterminate follows the underlying checkbox primitive's rules (typically moves to checked / unchecked). After the user clicks, clear `parcel.indeterminate` when the parent adopts a definite selection (`update:indeterminate` emits `false`).
148
+
149
+ > **Maintainers:** the underlying primitive uses an internal `'indeterminate'` sentinel (`FuroCheckboxContext.INDETERMINATE_VALUE`). Applications must not put that string in `parcel.value` or compare `v-model:value` to it.
150
+
151
+ #### Migration (breaking)
152
+
153
+ | Before | After |
154
+ | ------ | ----- |
155
+ | `v-model:value === 'indeterminate'` | `v-model:indeterminate === true` (keep `value` boolean, often `false`) |
156
+ | `parcel.value: 'indeterminate'` | `parcel: { value: false, indeterminate: true }` |
157
+ | `payload.value === 'indeterminate'` | `payload.isIndeterminate()` or `payload.indeterminate` |
158
+
159
+ ---
160
+
161
+ ## Controlled & uncontrolled
162
+
163
+ ```html
164
+ <!-- Controlled -->
165
+ <FuroCheckbox
166
+ v-model:value="context.flagRef.value"
167
+ :parcel="{ value: context.flagRef.value }"
168
+ />
169
+
170
+ <!-- Uncontrolled (starts unchecked) -->
171
+ <FuroCheckbox :parcel="{}" />
172
+ ```
173
+
174
+ | Mode | Detection | Sync |
175
+ | ---- | --------- | ---- |
176
+ | Controlled | `'value' in parcel` (including `value: false`) | `watch` on `resolveSyncedValue()` updates `internalValueRef` |
177
+ | Uncontrolled | no `value` key on `parcel` | Starts `false`; user toggles update internal ref only |
178
+ | Fallthrough `value` attr | Used when controlled via attrs only | Same as `FuroTextField` fallback in `resolveSyncedValue()` |
179
+
180
+ ### State matrix
181
+
182
+ | State | Source |
183
+ | ----- | ------ |
184
+ | Underlying primitive `model-value` | `internalValueRef` (may use internal indeterminate sentinel) |
185
+ | Checked (boolean) | `parcel.value` / `v-model:value` |
186
+ | Uncontrolled seed | `false` |
187
+ | Indeterminate UI | `parcel.indeterminate` / `v-model:indeterminate` |
188
+ | Invalid UI | `parcel.invalid` |
189
+ | Disabled | `parcel.disabled` or fallthrough `disabled` |
190
+
191
+ ---
192
+
193
+ ## Emits
194
+
195
+ | Event | Payload | When | Implemented |
196
+ | ----- | ------- | ---- | ----------- |
197
+ | `change-value` | `CheckboxEmitPayload` | Each primitive's model update | Yes |
198
+ | `update:value` | `FuroCheckboxValue` | Checked boolean after toggle (`v-model:value`) | Yes |
199
+ | `update:indeterminate` | `boolean` | Indeterminate flag after toggle (`v-model:indeterminate`) | Yes |
200
+
201
+ ### Parent handler style
202
+
203
+ ```js
204
+ onChangeTerms ({
205
+ payload,
206
+ }) {
207
+ this.statusReactive.isInvalidTerms = payload.isUnchecked()
208
+ }
209
+ ```
210
+
211
+ ```html
212
+ @change-value="context.onChangeTerms({ payload: $event })"
213
+ ```
214
+
215
+ ---
216
+
217
+ ## `CheckboxEmitPayload` API
218
+
219
+ `CheckboxEmitPayload` extends [`BaseEmitPayload`](../lib/emit-payload/BaseEmitPayload.js). Built from `CustomEvent` `detail` on toggle.
220
+
221
+ | Member | Type | Description |
222
+ | ------ | ---- | ----------- |
223
+ | `value` | `FuroCheckboxValue \| null` | Checked state after change |
224
+ | `indeterminate` | `boolean` | Indeterminate flag after change |
225
+ | `controlElement` | `HTMLElement \| null` | Checkbox root element when ref assigned |
226
+ | `isIndeterminate()` | `boolean` | Same as `indeterminate` |
227
+ | `isChecked()` | `boolean \| null` | `true` when `value` is boolean `true`; `null` when `value` is not a boolean |
228
+ | `isUnchecked()` | `boolean \| null` | `true` when `value` is boolean `false`; `null` when `value` is not a boolean |
229
+
230
+ ---
231
+
232
+ ## `v-model` and state handling
233
+
234
+ ```html
235
+ <FuroCheckbox
236
+ v-model:value="context.flagRef.value"
237
+ :parcel="{
238
+ value: context.flagRef.value,
239
+ }"
240
+ />
241
+ ```
242
+
243
+ | Binding | Implemented | Notes |
244
+ | ------- | ----------- | ----- |
245
+ | `v-model:value` | **Yes** | Emits `true` / `false` only |
246
+ | `v-model:indeterminate` | **Yes** | Emits `true` / `false` for [-] UI |
247
+
248
+ ---
249
+
250
+ ## Slots
251
+
252
+ | Slot | Scoped props | Default |
253
+ | ---- | ------------ | ------- |
254
+ | `indicator` | — | `ph:check-bold` when `value === true` |
255
+ | `indeterminate-indicator` | — | `ph:minus-bold` when `parcel.indeterminate` / indeterminate UI |
256
+
257
+ The checkbox root and indicator are not slot-replaceable.
258
+
259
+ There is **no** `label` slot. Associate copy with an external `<label for="…">` pointing at the checkbox `id` (fallthrough attribute on the checkbox root).
260
+
261
+ ---
262
+
263
+ ## Internal implementation (maintainers only)
264
+
265
+ ### Template composition
266
+
267
+ ```text
268
+ checkbox root .unit-form-control.checkbox (model-value, aria-*, id for external label)
269
+ checkbox indicator
270
+ [#indeterminate-indicator] | [#indicator]
271
+ ```
272
+
273
+ ### Primitive mapping
274
+
275
+ | Furo | Underlying primitive |
276
+ | ---- | -------------------- |
277
+ | `internalValueRef` | `model-value` / `@update:model-value` |
278
+ | `parcel.indeterminate` | internal indeterminate sentinel (not public `value`) |
279
+
280
+ ---
281
+
282
+ ## Accessibility requirements
283
+
284
+ - Provide an external `<label for="control-id">` (or wrap the atom in `<label>`) — set matching `id` on `<FuroCheckbox>`.
285
+ - `aria-invalid` reflects `parcel.invalid`.
286
+ - Checkbox roles and keyboard support are delegated to the underlying primitive ([checkbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/)).
287
+ - Set `name` on the component for forms.
288
+
289
+ ---
290
+
291
+ ## Styling convention
292
+
293
+ Styles in SFC `@layer furo`:
294
+
295
+ - Root: the checkbox root `.unit-form-control.checkbox`
296
+ - Parts: `.indicator`, `.indicator-icon`
297
+ - Size: `--furo-checkbox-size`, `--furo-checkbox-icon-size` (from `parcel.size`, default box `1.125rem` / icon `1rem`)
298
+ - Tokens: `--color-input`, `--color-background`, `--color-primary`, `--color-destructive`
299
+
300
+ Use `data-state` / `data-disabled` from the underlying primitive for state styling.
301
+
302
+ ---
303
+
304
+ ## Examples
305
+
306
+ ### Basic controlled
307
+
308
+ ```html
309
+ <div class="unit-checkbox-field">
310
+ <FuroCheckbox
311
+ id="accepted"
312
+ v-model:value="context.acceptedRef.value"
313
+ :parcel="{
314
+ value: context.acceptedRef.value,
315
+ }"
316
+ name="accepted"
317
+ @change-value="context.onChangeAccepted({ payload: $event })"
318
+ />
319
+ <label for="accepted">
320
+ Accept policy
321
+ </label>
322
+ </div>
323
+ ```
324
+
325
+ ### Indeterminate (parent-driven)
326
+
327
+ ```html
328
+ <div class="unit-checkbox-field">
329
+ <FuroCheckbox
330
+ id="select-all"
331
+ v-model:value="context.selectAllStateReactive.value"
332
+ v-model:indeterminate="context.selectAllStateReactive.isIndeterminate"
333
+ :parcel="{
334
+ value: context.selectAllStateReactive.value,
335
+ indeterminate: context.selectAllStateReactive.isIndeterminate,
336
+ }"
337
+ name="select-all"
338
+ />
339
+ <label for="select-all">
340
+ Select all
341
+ </label>
342
+ </div>
343
+ ```
344
+
345
+ ### Invalid
346
+
347
+ ```html
348
+ <FuroCheckbox
349
+ :parcel="{
350
+ value: false,
351
+ invalid: context.statusReactive.isInvalidTerms,
352
+ }"
353
+ name="terms"
354
+ />
355
+ ```
356
+
357
+ ---
358
+
359
+ ## Implementation status
360
+
361
+ | Deliverable | Status |
362
+ | ----------- | ------ |
363
+ | `FuroCheckbox.vue` + context + payload | Shipped |
364
+ | Public prop: `parcel` only | Shipped |
365
+ | Indeterminate + boolean model + controlled/uncontrolled | Shipped |
366
+ | Playground `FuroCheckboxPlayground` | Shipped |
367
+ | Unit tests `FuroCheckboxContext.js`, `CheckboxEmitPayload.js` (jsdom) | Shipped |
368
+ | Checkbox group | Pending |
369
+ | Closed `parcel` | Pending |
370
+
371
+ ---
372
+
373
+ ## Future / spec debt
374
+
375
+ - Checkbox group API for related checkbox groups
376
+ - `commit-value` (not applicable unless blur/commit semantics added)
377
+ - Strip unknown keys on `v-bind` to the underlying primitive
378
+
379
+ ---
380
+
381
+ ## Known gaps and follow-ups
382
+
383
+ - **Indeterminate toggle:** after user clicks, the underlying primitive decides next state; bind `v-model:indeterminate` or clear `parcel.indeterminate` when adopting a definite selection.
384
+ - **Group semantics:** multiple related checkboxes need separate instances or future group API.
@@ -0,0 +1,104 @@
1
+ # FuroCollapsible
2
+
3
+ Single expand/collapse region. A trigger toggles the visibility of one associated content block. Built on a headless collapsible primitive that manages the open state, ARIA wiring, and the measured content height for smooth animation.
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Molecule |
10
+ | **Path** | `lib/components/molecules/FuroCollapsible/` |
11
+ | **Base primitive** | Headless collapsible (`Root` + `Trigger` + `Content`) |
12
+ | **Composes** | Nothing — one trigger + one content region |
13
+ | **Model** | `value` (open boolean) via `v-model:value` |
14
+ | **Emits** | `change-value`, `update:value` |
15
+
16
+ ---
17
+
18
+ ## Public API
19
+
20
+ ### Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `parcel` | `FuroCollapsibleParcel \| null` | `null` | Reactive data object holding the open value and disabled flag. |
25
+
26
+ ### Parcel keys
27
+
28
+ | Key | Type | Default | Description |
29
+ |---|---|---|---|
30
+ | `value` | `boolean` | unset (`false`) | Open state. When present, the component is **controlled** — the parent owns the value. When absent, the component manages its own state (**uncontrolled**). |
31
+ | `disabled` | `boolean` | `false` | Disables the trigger; the region stays in its current state. |
32
+
33
+ ### Emits
34
+
35
+ | Event | Payload | Description |
36
+ |---|---|---|
37
+ | `change-value` | `CollapsibleEmitPayload` | Fired when the open state changes. `payload.value` is the new boolean; `payload.isOpen()` is a convenience predicate. |
38
+ | `update:value` | `boolean` | `v-model:value` sync of the open state. |
39
+
40
+ ### Slots
41
+
42
+ | Slot | Slot props | Description |
43
+ |---|---|---|
44
+ | `trigger` | `{ open: boolean }` | Trigger content (label + optional indicator). Rendered inside the toggle button. |
45
+ | default | `{ open: boolean }` | The collapsible content revealed when open. |
46
+
47
+ ---
48
+
49
+ ## Behavior
50
+
51
+ | Concern | Behavior |
52
+ |---|---|
53
+ | **Controlled vs uncontrolled** | `isControlled()` returns true when `parcel` owns a `value` key. A watcher syncs the controlled value into the internal open ref; uncontrolled instances start closed and track their own state. |
54
+ | **Value mapping** | The public `value` boolean maps to the primitive's own open prop in the template (`:open` / `@update:open`); the primitive's prop name is not exposed in the public API. |
55
+ | **Disabled** | `isDisabled()` reads `parcel.disabled` or a fallthrough `disabled` attribute. |
56
+ | **Animation** | The content wrapper animates between `0` and the primitive's measured `--reka-collapsible-content-height` for a smooth expand/collapse. |
57
+ | **Passthrough** | Non-Furo parcel keys and extra attributes fall through to the root (`inheritAttrs: false` + `v-bind="$attrs"`). |
58
+
59
+ ---
60
+
61
+ ## Usage
62
+
63
+ ```vue
64
+ <!-- Uncontrolled -->
65
+ <FuroCollapsible>
66
+ <template #trigger="{ open }">
67
+ <span>What is furo-vue?</span>
68
+ <Icon :name="open ? 'ph:caret-up' : 'ph:caret-down'" />
69
+ </template>
70
+
71
+ <p>furo-vue is a domain-agnostic Vue component library.</p>
72
+ </FuroCollapsible>
73
+
74
+ <!-- Controlled -->
75
+ <FuroCollapsible
76
+ :parcel="{ value: isOpen }"
77
+ @update:value="isOpen = $event"
78
+ >
79
+ <template #trigger>Release notes</template>
80
+ <p>...</p>
81
+ </FuroCollapsible>
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Composition notes
87
+
88
+ - Imports nothing from the app — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
89
+ - All logic lives in `FuroCollapsibleContext`: extract-only getters, the controlled/uncontrolled sync, and the `extractRootClassNames()` / `extractRootAttributes()` mappers. The component template only binds context methods.
90
+ - The public `value` key is value-oriented; the primitive's open prop name appears only in the template binding.
91
+ - CSS is in `@layer furo`, scoped, with `unit-collapsible` root. Dimensions use `rem`; `gap` uses `--size-space-*`; colors reference semantic tokens only.
92
+
93
+ ---
94
+
95
+ ## Testing
96
+
97
+ - Context logic is fully covered by jsdom unit tests: `parcel` defaults, `isControlled()`, `resolveSyncedValue()`, `isDisabled()` and `isOpen()` (split into truthy / falsy describe blocks), `extractRootClassNames()`, `extractRootAttributes()` passthrough, the controlled/uncontrolled `setupComponent()` sync, and `onChangeValue()` emit behavior.
98
+ - No SFC mount test: behavior is declarative bindings over the tested context plus the headless primitive's own open handling, verified by manual QA in the playground.
99
+
100
+ ---
101
+
102
+ ## Playground
103
+
104
+ `/samples/components/playground` → **Collapsible** tab. Demonstrates uncontrolled, controlled (with an outside toggle button), and disabled regions.
@@ -0,0 +1,102 @@
1
+ # FuroControlBlock
2
+
3
+ Presentational wrapper that frames a single form control with a **label** and **error messages**. The control is supplied through the default slot, so the block is domain-agnostic — it never knows which atom it wraps and emits nothing; the slotted control owns its own events. Intended as the per-field frame for the consumer-built ListView and DetailView forms (in `crm-kit-frontend`).
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Molecule |
10
+ | **Path** | `lib/components/molecules/FuroControlBlock/` |
11
+ | **Composes** | Nothing — wraps an arbitrary control via the default slot |
12
+ | **Emits** | None (the slotted control bubbles its own events to the consumer) |
13
+ | **Orientation** | `vertical` (default) or `horizontal`, via a CSS modifier |
14
+
15
+ ---
16
+
17
+ ## Public API
18
+
19
+ ### Props
20
+
21
+ | Prop | Type | Default | Description |
22
+ |---|---|---|---|
23
+ | `parcel` | `ControlBlockParcel \| null` | `null` | Reactive data object holding the label, control id, error messages, required flag, and orientation. |
24
+
25
+ ### Parcel keys
26
+
27
+ | Key | Type | Default | Description |
28
+ |---|---|---|---|
29
+ | `label` | `string \| null` | `null` | Label text. When `null`, no `<label>` is rendered. |
30
+ | `controlId` | `string \| null` | `null` | Id of the slotted control. Rendered as `<label for>` for a11y association; the consumer passes the same id to the control. When `null`, the label has no `for`. |
31
+ | `errorMessages` | `Array<string>` | `[]` | Error messages. A non-empty array marks the block invalid and renders one line per message. |
32
+ | `required` | `boolean` | `false` | When `true`, renders a required mark (`*`) after the label. |
33
+ | `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout of label vs control. `vertical` stacks; `horizontal` places the label beside the control. |
34
+
35
+ ### Emits
36
+
37
+ None. The block is presentational; the control inside the slot emits its own `change-value` / `commit-value` / `update:value` events straight to the consumer.
38
+
39
+ ### Slots
40
+
41
+ | Slot | Description |
42
+ |---|---|
43
+ | default | The control to frame (e.g. `FuroTextField`). Pass the same `id` as the parcel's `controlId` so `<label for>` resolves. |
44
+
45
+ ---
46
+
47
+ ## Behavior
48
+
49
+ | Concern | Behavior |
50
+ |---|---|
51
+ | **Label** | Rendered only when `parcel.label` is set. Associated with the control via `<label :for="controlId">` when `controlId` is provided. |
52
+ | **Required mark** | The `*` carries `aria-hidden="true"` — it is a visual cue only; convey required state on the control itself. |
53
+ | **Invalid state** | When `errorMessages` is non-empty, the root gets the `invalid` modifier and the error list renders. |
54
+ | **Error region** | Rendered as a `<ul role="alert">`, one `<li>` per message. |
55
+ | **Orientation** | `parcel.orientation` maps to the `vertical` / `horizontal` root class; no JS branching. |
56
+ | **Fallthrough class / style** | A consumer-supplied `class` and `style` compose onto the block's root element, so a layout selector like `.control.name { flex: 1 }` reaches the wrapper. The block uses `inheritAttrs: false` and explicitly composes `$attrs.class` / `$attrs.style` onto the root. |
57
+
58
+ > **a11y note (deliberate):** the error region carries `role="alert"` only. The block does **not** auto-wire `aria-describedby` onto the slotted control, because it cannot set attributes on a slot child. Consumers that need the stronger association wire it themselves. Upgrade path: expose an `errorId` getter for consumers to bind.
59
+
60
+ ---
61
+
62
+ ## Usage
63
+
64
+ ```vue
65
+ <FuroControlBlock
66
+ :parcel="{
67
+ label: 'Email address',
68
+ controlId: 'field-email',
69
+ required: true,
70
+ errorMessages: emailErrors,
71
+ orientation: 'vertical',
72
+ }"
73
+ >
74
+ <FuroTextField
75
+ id="field-email"
76
+ :parcel="{ value: email }"
77
+ @update:value="onEmailChange({ value: $event })"
78
+ />
79
+ </FuroControlBlock>
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Composition notes
85
+
86
+ - The block imports nothing — no atoms, no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports. It is a pure slot wrapper.
87
+ - All logic lives in `FuroControlBlockContext` as extract-only getters (`label`, `controlId`, `errorMessages`, `orientation`) and two boolean methods (`isInvalid()`, `isRequired()`).
88
+ - CSS is wrapped in `@layer furo`; `gap` uses `--size-space-*` tokens and all other dimensions use `rem`. Only semantic tokens are referenced (`--color-foreground`, `--color-destructive-text`, `--font-label-small`, `--font-body-small`). The label and the error text each carry a composite text style through the `font` shorthand per [ADR 0032](./adr/0032-composite-text-style-tokens-via-font-shorthand.md), so the label renders at weight `500` and no longer inherits the host page's typography.
89
+ - `inheritAttrs: false`; a consumer-supplied `class` / `style` is composed onto the block root (via `$attrs.class` / `$attrs.style`), so a parent layout selector such as `.control.name { flex: 1 }` reaches the wrapper.
90
+
91
+ ---
92
+
93
+ ## Testing
94
+
95
+ - Context logic (every getter and both boolean methods) is fully covered by jsdom unit tests.
96
+ - No SFC mount test: this repo has no jsdom mount precedent for molecules, and the block's behavior is entirely declarative bindings over the tested context. The rendered output (label show/hide, required mark, error list, orientation class) is verified by **manual QA in the playground**.
97
+
98
+ ---
99
+
100
+ ## Playground
101
+
102
+ `/samples/components/playground` → **Control Block** tab. Demonstrates labeled (vertical), required, invalid (multiple error messages), and horizontal layouts wrapping `FuroTextField`.