@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,214 @@
1
+ # `<FuroTextarea>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ Read [`<FuroTextField>`](./furo-text-field.md) first. This page describes the **textarea-specific** contract and the few behavioral differences that follow from using a multiline control.
6
+
7
+ ---
8
+
9
+ ## Differences from `<FuroTextField>`
10
+
11
+ | Topic | `<FuroTextField>` | `<FuroTextarea>` |
12
+ |--------|-------------------|------------------|
13
+ | Native element | `<input type="text">` | `<textarea>` |
14
+ | CSS root class | `.unit-form-control.text` | `.unit-form-control.textarea` |
15
+ | `type` fallthrough | Excluded (always `text`) | N/A — `<textarea>` has no `type` |
16
+ | `@commit-value` | `blur` **or** `Enter` | **`blur` only** — `Enter` inserts a newline in a `<textarea>` and must not be treated as “commit” by default |
17
+ | `parcel.value` | `string \| number \| null` | `string \| null` only (multiline text; no numeric shortcut) |
18
+
19
+ ### Class and type naming
20
+
21
+ | Role | Name |
22
+ |------|------|
23
+ | Component | `FuroTextarea` |
24
+ | Context | `FuroTextareaContext` |
25
+ | Emit payload | `TextareaEmitPayload` |
26
+ | Parcel typedef | `FuroTextareaParcel` |
27
+
28
+ `TextareaEmitPayload` is defined for `<FuroTextarea>` only. It extends [`BaseEmitPayload`](../lib/emit-payload/BaseEmitPayload.js) and exposes the same helper surface as text-field payloads for consistency (`.value`, `.trimmedValue`, `.hasEmptyText()`, `.hasEmptyTrimmedText()`, `.hasEnteredText()`, `.hasEnteredTrimmedText()`). Trimming applies to the full string, including newlines; parent Context implements line-length or format rules if needed.
29
+
30
+ ---
31
+
32
+ ## Props
33
+
34
+ Same **two mechanisms** as `<FuroTextField>`: `parcel` for Furo behavior, fallthrough for native attributes on the `<textarea>`.
35
+
36
+ ### Rule: `parcel` vs fallthrough attributes
37
+
38
+ | Mechanism | When to use | Rendered as |
39
+ |-----------|-------------|-------------|
40
+ | `:parcel` | Value, invalid state, and other **component behavior** | Not direct public HTML attr names unless also bound internally |
41
+ | Fallthrough attributes | Native **HTML** attributes for `<textarea>` | On the underlying `<textarea>` |
42
+
43
+ ```html
44
+ <!-- ✅ Correct -->
45
+ <FuroTextarea
46
+ :parcel="context.descriptionParcel"
47
+ :disabled="context.descriptionParcel.disabled"
48
+ rows="5"
49
+ name="description"
50
+ />
51
+ ```
52
+
53
+ > **Why the same split as `<FuroTextField>`:** One reviewable `parcel` object for behavior; visible HTML attributes stay in the template where structure is clear.
54
+
55
+ ---
56
+
57
+ ### `parcel` shape
58
+
59
+ | Key | Type | Required | Default | Description |
60
+ |-----|------|----------|---------|-------------|
61
+ | `value` | `string \| null` | No | `null` | Controlled value; synced to internal ref. |
62
+ | `invalid` | `boolean` | No | `false` | Invalid / error visual state. |
63
+
64
+ > All values must be **JSON-serializable** (no functions, no class instances).
65
+
66
+ ---
67
+
68
+ ### Fallthrough attributes (recommended usage)
69
+
70
+ Vue forwards non-prop attributes to the underlying `<textarea>` (with `inheritAttrs: false`, the Context binds them explicitly — same pattern as other Furo atoms).
71
+
72
+ Commonly used native attributes:
73
+
74
+ | Attribute | Type | Description |
75
+ |-----------|------|-------------|
76
+ | `disabled` | `boolean` | Disables editing. |
77
+ | `readonly` | `boolean` | Read-only display. |
78
+ | `required` | `boolean` | Required for constraint validation. |
79
+ | `placeholder` | `string` | Hint text (not a substitute for a visible label). |
80
+ | `name` | `string` | Form field name. |
81
+ | `rows` | `number` | Visible row hint (HTML `rows`). |
82
+ | `cols` | `number` | Visible column hint (HTML `cols`). |
83
+ | `maxlength` | `number` | Maximum character length. |
84
+ | `minlength` | `number` | Minimum character length. |
85
+ | `wrap` | `'soft' \| 'hard'` | Line wrapping behavior when submitting forms. |
86
+ | `spellcheck` | `boolean \| 'true' \| 'false'` | Native spellcheck hint. |
87
+ | `autocomplete` | `string` | Rare on `<textarea>` but valid when applicable (e.g. street address sections per HTML guidelines). |
88
+
89
+ Attributes that are **out of scope for documentation examples** but remain valid when passed as fallthrough include `id`, `class`, `style`, `autofocus`, and form-associated attributes your project standardizes elsewhere.
90
+
91
+ Do **not** rely on fallthrough for “resize handle” behavior — prefer scoped CSS on `.unit-form-control.textarea` (e.g. `resize: vertical`) so UX stays consistent across apps.
92
+
93
+ ---
94
+
95
+ ## Emits
96
+
97
+ Same event names as `<FuroTextField>`. Payload instances are **`TextareaEmitPayload`** (not `TextFieldEmitPayload`):
98
+
99
+ | Event | When |
100
+ |-------|------|
101
+ | `@change-value` | Every `input` event (each edit). |
102
+ | `@commit-value` | **`blur` only** on `<textarea>`. |
103
+ | `@update:value` | Alongside the above; raw string for `v-model:value`. |
104
+
105
+ ```html
106
+ <FuroTextarea
107
+ :parcel="context.buildTextareaParcel()"
108
+ @change-value="context.onChangeValue({ payload: $event })"
109
+ @commit-value="context.onCommitValue({ payload: $event })"
110
+ />
111
+ ```
112
+
113
+ > **Why no Enter-to-commit:** In a multiline field, `Enter` is content. Reusing `keypress.enter` from `<FuroTextField>` would break expected editing. If a product needs “submit on Ctrl+Enter” (or similar), that belongs in the **parent** Context or a higher-level form pattern, not in the default Atom contract.
114
+
115
+ > **Emitter responsibility:** `<FuroTextarea>` **always emits** for the events above; the parent Context decides validation and side effects. The component does not suppress emits based on value state.
116
+
117
+ ---
118
+
119
+ ## Event handler style in parent Context
120
+
121
+ Same as [`<FuroTextField>`](./furo-text-field.md#event-handler-style-in-context): **single object parameter** with `{ payload }` destructuring.
122
+
123
+ ```js
124
+ onChangeValue ({
125
+ payload,
126
+ }) {
127
+ this.statusReactive.isDescriptionInvalid = !this.validatesDescription({
128
+ payload,
129
+ })
130
+ }
131
+ ```
132
+
133
+ ---
134
+
135
+ ## `TextareaEmitPayload` API
136
+
137
+ `@change-value` and `@commit-value` pass a **`TextareaEmitPayload`** instance as `$event`. Members match the behavioral contract documented for [`TextFieldEmitPayload`](./furo-text-field.md#textfieldemitpayload-api), but the **runtime type** for this component is `TextareaEmitPayload` only.
138
+
139
+ | Member | Type | Description |
140
+ |--------|------|-------------|
141
+ | `.value` | `string \| null` | Current raw string value of the `<textarea>`. |
142
+ | `.trimmedValue` | `string \| null` | Whitespace-trimmed `.value` (multiline strings use normal string trim semantics). |
143
+ | `.hasEmptyText()` | `boolean` | `true` if `.value` is empty or the control is missing. |
144
+ | `.hasEmptyTrimmedText()` | `boolean` | `true` if `.trimmedValue` is empty or the control is missing. |
145
+ | `.hasEnteredText()` | `boolean` | `true` if `.value` has at least one character (whitespace counts). |
146
+ | `.hasEnteredTrimmedText()` | `boolean` | `true` if `.trimmedValue` has at least one non-whitespace character. |
147
+
148
+ Inside `<FuroTextarea>`, emits use `TextareaEmitPayload.create({ rawEvent })` — parallel to `TextFieldEmitPayload` in `<FuroTextField>`, but a separate class so consumers and types stay textarea-specific.
149
+
150
+ ---
151
+
152
+ ## `v-model` Support
153
+
154
+ Same contract as `<FuroTextField>`: `v-model:value` is sugar over `:parcel` / `@update:value`.
155
+
156
+ ```html
157
+ <FuroTextarea v-model:value="context.descriptionRef.value" />
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Controlled vs. uncontrolled state
163
+
164
+ | Mode | How |
165
+ |------|-----|
166
+ | Controlled | `:parcel="{ value: ... }"` or `v-model:value` |
167
+ | Uncontrolled | Omit `value` in `parcel` |
168
+
169
+ Internal watcher order should match `<FuroTextField>`: prefer `parcel.value`, then fall back when documenting parity tests.
170
+
171
+ ---
172
+
173
+ ## Example
174
+
175
+ ```html
176
+ <FuroTextarea
177
+ :parcel="{
178
+ value: context.descriptionRef.value,
179
+ invalid: context.statusReactive.isDescriptionInvalid,
180
+ }"
181
+ :required="true"
182
+ rows="6"
183
+ name="description"
184
+ placeholder="Describe the issue in detail"
185
+ @change-value="context.onChangeDescription({ payload: $event })"
186
+ @commit-value="context.onCommitDescription({ payload: $event })"
187
+ />
188
+ ```
189
+
190
+ ```js
191
+ onChangeDescription ({
192
+ payload,
193
+ }) {
194
+ if (payload.hasEmptyTrimmedText()) {
195
+ this.statusReactive.isDescriptionInvalid = true
196
+
197
+ return
198
+ }
199
+
200
+ this.statusReactive.isDescriptionInvalid = !this.validatesDescription({
201
+ payload,
202
+ })
203
+ }
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Implementation notes (non-normative)
209
+
210
+ - **Height:** Use `rows` and/or CSS `min-height` on `.unit-form-control.textarea` so layouts do not collapse when `rows` is omitted.
211
+ - **Accessibility:** Keep an associated `<label>` (or `aria-labelledby`) in the parent; pass `id` through fallthrough when wiring `for` / `id`.
212
+ - **Testing:** Snapshot or e2e tests should assert that Enter **does not** fire `commit-value`, only `change-value` (input), whereas blur fires `commit-value`.
213
+
214
+ ---
@@ -0,0 +1,276 @@
1
+ # FuroTimeField
2
+
3
+ Single-time molecule built on a headless segmented time-field primitive. Renders an inline segmented time control (hour / minute, optionally seconds, optionally AM/PM). No popover or portal — segments are edited in place via type or arrow keys.
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Molecule |
10
+ | **Path** | `lib/components/molecules/FuroTimeField/` |
11
+ | **Base primitive** | Headless segmented time-field (`Root` + `Input`) |
12
+ | **Value type** | `HH:MM:SS` string or `null` |
13
+ | **Wire format** | Always `HH:MM:SS` (zero-padded via `Time#toString()`); accepts `HH:MM` on input |
14
+ | **Granularity** | `minute` (default), `hour`, or `second` via `parcel.granularity` |
15
+ | **Time format** | 12 or 24 via `parcel.timeFormat` (locale default when unset) |
16
+ | **Step** | Step amount for arrow edits and the ▲/▼ buttons via `parcel.step` (default `1`); the unit follows `granularity` (the smallest visible segment) |
17
+ | **Timezone** | Configurable via `parcel.timezone` (IANA); default `null` = naive wall-clock |
18
+
19
+ ---
20
+
21
+ ## Public API
22
+
23
+ ### Props
24
+
25
+ | Prop | Type | Default | Description |
26
+ |---|---|---|---|
27
+ | `parcel` | `TimeFieldParcel \| null` | `null` | Reactive data object. Furo-only keys are stripped before passthrough to the underlying primitive. |
28
+
29
+ ### Parcel keys
30
+
31
+ | Key | Type | Description |
32
+ |---|---|---|
33
+ | `value` | `string \| null` | Current time as `HH:MM:SS` (or `HH:MM`) or `null`. Context also accepts `Date` / `Time` at the boundary. |
34
+ | `invalid` | `boolean` | Forces error chrome and `aria-invalid` (external validation). |
35
+ | `placeholder` | `string \| null` | Placeholder text for the time field. |
36
+ | `disabled` | `boolean` | Disables all interaction. Also respected from fallthrough `disabled`, including the bare `<FuroTimeField disabled>` spelling. |
37
+ | `readonly` | `boolean` | Segments are focusable but not editable. Also respected from fallthrough `readonly`, including the bare `<FuroTimeField readonly>` spelling. |
38
+ | `decrementLabel` | `string` | Accessible name of the decrement button. Default `'Decrement time'`. |
39
+ | `incrementLabel` | `string` | Accessible name of the increment button. Default `'Increment time'`. |
40
+ | `minValue` | `string \| null` | Minimum time (`HH:MM:SS`). Out-of-range values are **flagged** (`aria-invalid` + error chrome), not clamped. |
41
+ | `maxValue` | `string \| null` | Maximum time (`HH:MM:SS`). Out-of-range values are **flagged**, not clamped. |
42
+ | `timeFormat` | `number \| null` | `12` or `24`. When unset, the locale default applies. |
43
+ | `granularity` | `'hour' \| 'minute' \| 'second' \| null` | Controls visible segments. When unset, it defaults to `minute`. |
44
+ | `step` | `number \| null` | Step amount for segment arrow edits and the ▲/▼ buttons. Default `1`. The button step unit follows `granularity` (`hour`→hours, `second`→seconds, otherwise minutes). Forwarded as the native `step`. |
45
+ | `stepSnapping` | `boolean \| null` | Optional `stepSnapping`. Forwarded only when explicitly set. |
46
+ | `timezone` | `string \| null` | IANA timezone identifier (e.g. `'Asia/Tokyo'`). When set, `Date` inputs are normalized to wall-clock parts in that zone. Default `null` = naive (local parts). **Furo-only — never forwarded to the underlying primitive.** |
47
+
48
+ All parcel keys above are **Furo-only** — they are stripped before the root v-bind spread. `step` (and `stepSnapping` when set) are re-applied to the root after stripping; `timezone` is never forwarded. Pass additional native props via `parcel` to forward them.
49
+
50
+ ### Emits
51
+
52
+ | Event | Payload | Description |
53
+ |---|---|---|
54
+ | `change-value` | `TimeFieldEmitPayload` | Fired when the user edits a segment. |
55
+ | `commit-value` | `TimeFieldEmitPayload` | Fired on the same edit event (inline single-region UX). |
56
+ | `update:value` | `string \| null` | `HH:MM:SS` string or `null`. Suitable for `v-model:value`. |
57
+
58
+ ---
59
+
60
+ ## Usage
61
+
62
+ ### Basic
63
+
64
+ ```vue
65
+ <FuroTimeField
66
+ :parcel="{ placeholder: 'Pick a time' }"
67
+ @update:value="onValueChange"
68
+ />
69
+ ```
70
+
71
+ ### v-model binding
72
+
73
+ ```vue
74
+ <FuroTimeField
75
+ v-model:value="selectedTime"
76
+ :parcel="{ placeholder: 'Pick a time' }"
77
+ />
78
+ ```
79
+
80
+ ### 12-hour cycle
81
+
82
+ ```vue
83
+ <FuroTimeField
84
+ :parcel="{
85
+ timeFormat: 12,
86
+ value: '14:05:00',
87
+ }"
88
+ @update:value="onValueChange"
89
+ />
90
+ ```
91
+
92
+ `update:value` is always emitted as a 24-hour `HH:MM:SS` string regardless of `timeFormat`.
93
+
94
+ ### Second granularity
95
+
96
+ ```vue
97
+ <FuroTimeField
98
+ :parcel="{
99
+ granularity: 'second',
100
+ value: '08:15:42',
101
+ }"
102
+ @update:value="onValueChange"
103
+ />
104
+ ```
105
+
106
+ ### Min / Max bounds
107
+
108
+ ```vue
109
+ <FuroTimeField
110
+ :parcel="{
111
+ minValue: '09:00:00',
112
+ maxValue: '17:00:00',
113
+ }"
114
+ @update:value="onValueChange"
115
+ />
116
+ ```
117
+
118
+ Bounds are **flag-only** for segment editing: out-of-range entries set `aria-invalid` and show error chrome. The control does not clamp or block out-of-range segments — combine with `parcel.invalid` for external validation. The increment/decrement buttons, however, **do clamp** their result to `[minValue, maxValue]`.
119
+
120
+ ### Customizable step + increment/decrement buttons
121
+
122
+ ```vue
123
+ <FuroTimeField
124
+ :parcel="{
125
+ step: 5,
126
+ value: '09:00:00',
127
+ }"
128
+ @update:value="onValueChange"
129
+ />
130
+ ```
131
+
132
+ `parcel.step` (default `1`) controls both the segment arrow-key increment and the two native ▲/▼ stepper buttons rendered inside the field. The step **unit follows `granularity`** — the smallest visible segment: `granularity: 'hour'` steps hours, `'second'` steps seconds, otherwise (default) minutes. The buttons:
133
+
134
+ - Add / subtract `step` of the smallest segment via `@internationalized/date` `Time.add` / `Time.subtract`.
135
+ - **Clamp** the result to `[minValue, maxValue]` when bounds are set (no overflow past a bound).
136
+ - Start from `00:00:00` when the current value is `null`.
137
+ - Route through the same emit path as segment edits (`change-value` → `update:value` → `commit-value`).
138
+ - Are disabled when `parcel.disabled` / `parcel.readonly`, when increment is at/over `maxValue`, or when decrement is at/under `minValue`.
139
+
140
+ ### Configurable timezone
141
+
142
+ ```vue
143
+ <FuroTimeField
144
+ :parcel="{
145
+ timezone: 'Asia/Tokyo',
146
+ value: someDateObject,
147
+ }"
148
+ @update:value="onValueChange"
149
+ />
150
+ ```
151
+
152
+ When `parcel.timezone` is an IANA identifier, a `Date` input is normalized to wall-clock time parts **in that zone** (via `Intl.DateTimeFormat` with `hourCycle: 'h23'`, each part zero-padded). When `timezone` is `null` (default), `Date` inputs use local `getHours()` / `getMinutes()` / `getSeconds()` (naive). An invalid timezone identifier normalizes to `null`.
153
+
154
+ The library stays **domain-agnostic** — it never hardcodes a default timezone. Consumers set the default: for example, CloudLink passes `timezone: 'Asia/Tokyo'` from its adapter.
155
+
156
+ > **Controlled-value note.** `timezone` only affects how a `Date` **input** is read into wall-clock parts; edits emit a plain `HH:MM:SS` string. If you bind `value` to a **fixed** `Date` (not a `ref` updated by `@update:value`), every edit re-normalizes that same `Date` back and the field appears frozen. Bind a controlled value instead — feed the emitted string back, falling back to the `Date` only initially:
157
+ >
158
+ > ```vue
159
+ > :parcel="{ timezone: 'Asia/Tokyo', value: emittedValueRef ?? initialDate }"
160
+ > @update:value="value => { emittedValueRef = value }"
161
+ > ```
162
+
163
+ ### Disabled / read-only
164
+
165
+ ```vue
166
+ <FuroTimeField :parcel="{ disabled: true }" />
167
+ <FuroTimeField :parcel="{ value: '11:20:00', readonly: true }" />
168
+ ```
169
+
170
+ ### Invalid state
171
+
172
+ ```vue
173
+ <FuroTimeField
174
+ :parcel="{
175
+ placeholder: 'Required field',
176
+ invalid: isFieldInvalid,
177
+ }"
178
+ />
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Value contract
184
+
185
+ | Concern | Decision |
186
+ |---|---|
187
+ | Wire format | `HH:MM:SS` string — always serialized via `Time#toString()` (zero-pads). `HH:MM` accepted on input and padded to `HH:MM:SS`. |
188
+ | Timezone | **Configurable** via `parcel.timezone` (IANA). Default `null` = naive wall-clock (`Date` inputs use local `getHours()` / `getMinutes()` / `getSeconds()`). When set, `Date` inputs are normalized to wall-clock parts in that zone. The library never hardcodes a default — consumers set it (e.g. CloudLink → `'Asia/Tokyo'`). |
189
+ | Range | Single time only; time-range support out of scope. |
190
+ | Clear | Pass `null` to `parcel.value`, or clear all segments via keyboard, to reset to `null`. |
191
+
192
+ The context accepts these input shapes at the boundary and normalizes to an internal `@internationalized/date` `Time`:
193
+
194
+ - `HH:MM` / `HH:MM:SS` string → `parseTime`
195
+ - `Date` object (no `timezone`) → `new Time(hours, minutes, seconds)` (local parts)
196
+ - `Date` object (with `timezone`) → wall-clock parts in that IANA zone via `Intl.DateTimeFormat`
197
+ - `Time` instance → passthrough
198
+ - `null` / invalid → `null`
199
+
200
+ ### Native mobile fallback
201
+
202
+ This molecule does not render a native `<input type="time">`. On mobile, the segmented control is still keyboard- and tap-editable; a native fallback is documented here as a future consideration but is out of scope for v1.
203
+
204
+ ---
205
+
206
+ ## CSS classes
207
+
208
+ ```text
209
+ .unit-form-control.time-field ← host div
210
+ .unit-form-control.time-field > .field ← root host element
211
+ .unit-form-control.time-field > .field.invalid ← invalid state (parcel.invalid OR out-of-range)
212
+ .unit-form-control.time-field > .field.disabled ← disabled state
213
+ .unit-form-control.time-field > .field > .field-input ← editable segment (hour/minute/second/dayPeriod)
214
+ .unit-form-control.time-field > .field > .field-input[data-placeholder] ← placeholder segment
215
+ .unit-form-control.time-field > .field > .field-input:focus-visible ← keyboard-active segment
216
+ ```
217
+
218
+ All CSS is scoped under `@layer furo` and uses semantic tokens only (`--color-*`, `--size-*`, `--font-size-*`).
219
+
220
+ ---
221
+
222
+ ## Context: `FuroTimeFieldContext`
223
+
224
+ `FuroTimeFieldContext` (extends `BaseFuroContext`) holds all reactive state and computed logic. The Vue component is a thin template wrapper.
225
+
226
+ Key methods:
227
+
228
+ | Method | Description |
229
+ |---|---|
230
+ | `normalizeToTime({ raw })` | Converts `HH:MM` / `HH:MM:SS` / `Date` / `Time` / `null` → `Time \| null`. |
231
+ | `normalizeStringToTime({ raw })` | Parses a time string via `parseTime`; returns `null` on failure. |
232
+ | `normalizeDateToTime({ raw })` | Converts a `Date` → `Time`. Uses local wall-clock parts, or `parcel.timezone` parts when set. |
233
+ | `normalizeDateToTimeInTimezone({ raw, timezone })` | Converts a `Date` → `Time` using wall-clock parts in a specific IANA timezone; `null` on invalid zone. |
234
+ | `extractTimePartsInTimezone({ raw, timezone })` (static) | Pulls zero-padded `hour` / `minute` / `second` strings from a `Date` via `Intl.DateTimeFormat` (`hourCycle: 'h23'`). |
235
+ | `normalizeToIsoString({ time })` | Converts `Time \| null` → `HH:MM:SS` string or `null` via `Time#toString()`. |
236
+ | `normalizeMinValue()` / `normalizeMaxValue()` | Returns `Time` from `parcel.minValue` / `parcel.maxValue`. |
237
+ | `extractRootAttributes()` | Builds the root attribute object (strips Furo-only keys, applies `modelValue`, `disabled`, `readonly`, `step`, and optional `stepSnapping` / `timeFormat` / `granularity` / `minValue` / `maxValue`). |
238
+ | `stripFuroParcelKeys()` | Removes `FURO_TIME_FIELD_PARCEL_KEYS` from parcel before spreading to the underlying primitive. |
239
+ | `onChangeValue({ time })` | Handles the primitive's `update:modelValue`; emits all three events. |
240
+ | `onIncrementValue()` / `onDecrementValue()` | Steps the value by `step` minutes, clamps to bounds, then routes through the emit trio. |
241
+ | `clampTime({ time })` | Clamps a `Time` into `[minValue, maxValue]`. |
242
+ | `isLiteralPart({ part })` | Whether a segment part is a literal separator (e.g. `:`). |
243
+ | `formatSegmentValue({ part, value })` | Zero-pads filled hour/minute/second segments to two digits (`9` → `09`); day period and literals pass through. With `tabular-nums` this keeps the field width constant as the value changes (no layout shift). |
244
+ | `isTwoDigitSegmentPart({ part })` | Whether a segment part displays zero-padded to two digits (hour, minute, second). |
245
+
246
+ ---
247
+
248
+ ## Files
249
+
250
+ ```text
251
+ lib/components/molecules/FuroTimeField/
252
+ ├── FuroTimeField.vue ← component
253
+ ├── FuroTimeFieldContext.js ← context
254
+ └── TimeFieldEmitPayload.js ← typed emit payload
255
+
256
+ pages/samples/components/playground/FuroTimeFieldPlayground/
257
+ ├── index.vue ← playground page
258
+ └── FuroTimeFieldPlaygroundContext.js
259
+
260
+ tests/__tests__/jsdom/lib/components/molecules/FuroTimeField/
261
+ ├── FuroTimeFieldContext.js ← context tests
262
+ └── TimeFieldEmitPayload.js ← payload tests
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Out of scope (v1)
268
+
269
+ - Combined datetime molecule (`FuroDateTimePicker` — Story 1.5)
270
+ - Time range (`TimeRangeField*`)
271
+ - Native `<input type="time">` mobile fallback
272
+ - i18n locale packs (prop-only)
273
+
274
+ ## Commit semantics note
275
+
276
+ `change-value` and `commit-value` fire together on every segment edit and on every stepper button press — there is no separate blur/enter distinction (inline single-region UX, identical to `FuroDatePicker`). Treat them as equivalent until cross-component commit semantics are unified.