@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,270 @@
1
+ # FuroDatePicker
2
+
3
+ Single-date molecule built on a headless date-picker primitive. Features an inline date field (segments for month/day/year) plus a calendar popover with dropdown selects for month and year navigation, the usual date-of-birth picker pattern.
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Molecule |
10
+ | **Path** | `lib/components/molecules/FuroDatePicker/` |
11
+ | **Base primitive** | Headless date-picker (`Root` + calendar popover) |
12
+ | **Value type** | `YYYY-MM-DD` ISO string or `null` |
13
+ | **Granularity** | `day` (date-only; no time) |
14
+ | **Timezone** | Configurable via `timezone` parcel key (default: timezone-naive) |
15
+ | **Calendar nav** | Dropdown selects for month and year (+ Prev/Next buttons) |
16
+
17
+ ---
18
+
19
+ ## Public API
20
+
21
+ ### Props
22
+
23
+ | Prop | Type | Default | Description |
24
+ |---|---|---|---|
25
+ | `parcel` | `DatePickerParcel \| null` | `null` | Reactive data object. Furo-only keys are stripped before passthrough to the underlying primitive. |
26
+ | `triggerParcel` | `Record<string, unknown> \| null` | `null` | Passed to the date field / trigger via v-bind. |
27
+ | `contentParcel` | `Record<string, unknown> \| null` | `null` | Passed to the popover content via v-bind. |
28
+
29
+ ### Parcel keys
30
+
31
+ | Key | Type | Description |
32
+ |---|---|---|
33
+ | `value` | `string \| null` | Currently selected date as `YYYY-MM-DD` or `null`. |
34
+ | `invalid` | `boolean` | Shows error chrome and sets `aria-invalid`. |
35
+ | `placeholder` | `string \| null` | Placeholder text for the date field. |
36
+ | `disabled` | `boolean` | Disables all interaction. |
37
+ | `minValue` | `string \| null` | Minimum selectable date (`YYYY-MM-DD`). Earlier dates are non-interactive. |
38
+ | `maxValue` | `string \| null` | Maximum selectable date (`YYYY-MM-DD`). Later dates are non-interactive. |
39
+ | `displayFormat` | `string \| null` | Reserved for v1.1 localized display format (not yet applied). |
40
+ | `timezone` | `string \| null` | IANA timezone identifier (e.g. `'Asia/Tokyo'`, `'UTC'`). When set, `Date` object inputs are interpreted in this timezone before normalization. Has no effect when `value` is a `YYYY-MM-DD` string (already date-only). Defaults to `null` (timezone-naive: uses local date parts). |
41
+ | `closeOnSelect` | `boolean \| null` | Whether the popover closes when a date is selected. Defaults to `true`. `FuroDateTimePicker` passes `false` to keep the popover open for time selection. |
42
+ | `locale` | `string \| null` | Locale that drives the date field's segment **order** (e.g. `dd/mm/yyyy` vs `mm/dd/yyyy`). Defaults to the browser locale (`navigator.language`), falling back to `'en'` outside the browser. Filled day/month segments are always shown 2-digit regardless of locale (see `formatSegmentValue`). |
43
+ | `preventDeselect` | `boolean \| null` | When `true` (default), re-selecting the already-selected date **keeps** it instead of clearing it. Pass `false` to restore the toggle-off behavior. |
44
+ | `todayButtonText` | `string \| null` | Label for the footer **Today** button. Defaults to `'Today'`; pass `null` to hide the button. |
45
+ | `clearButtonText` | `string \| null` | Label for the footer **Clear** button. Defaults to `'Clear'`; pass `null` to hide the button. |
46
+
47
+ All parcel keys above are **Furo-only** — they are stripped before the v-bind spread onto the root. Pass additional primitive props via `parcel` to forward them.
48
+
49
+ ### Emits
50
+
51
+ | Event | Payload | Description |
52
+ |---|---|---|
53
+ | `change-value` | `DatePickerEmitPayload` | Fired when the user selects a date. |
54
+ | `commit-value` | `DatePickerEmitPayload` | Fired on the same selection event (single-date close-on-select UX). |
55
+ | `update:value` | `string \| null` | ISO `YYYY-MM-DD` string or `null`. Suitable for `v-model:value`. |
56
+
57
+ ### Slots
58
+
59
+ | Slot | Description |
60
+ |---|---|
61
+ | `trigger-icon` | Replaces the default calendar icon inside the trigger. |
62
+ | `prev-icon` | Replaces the left-arrow icon in the calendar header. |
63
+ | `next-icon` | Replaces the right-arrow icon in the calendar header. |
64
+ | `field-suffix` | Renders custom content inside the trigger field, after the date segments and before the trigger icon. A no-op when empty. Used by `FuroDateTimePicker` to host an inline, directly-editable time field in the closed field. |
65
+ | `content-footer` | Renders custom content inside the popover, below the calendar. A no-op when empty. Used by `FuroDateTimePicker` to inject a time field into the same popover. |
66
+
67
+ ---
68
+
69
+ ## Usage
70
+
71
+ ### Basic
72
+
73
+ ```vue
74
+ <FuroDatePicker
75
+ :parcel="{ placeholder: 'Pick a date' }"
76
+ @update:value="onValueChange"
77
+ />
78
+ ```
79
+
80
+ ### v-model binding
81
+
82
+ ```vue
83
+ <FuroDatePicker
84
+ v-model:value="selectedDate"
85
+ :parcel="{ placeholder: 'Pick a date' }"
86
+ />
87
+ ```
88
+
89
+ ### Min / Max constraints
90
+
91
+ ```vue
92
+ <FuroDatePicker
93
+ :parcel="{
94
+ placeholder: 'Booking date',
95
+ minValue: '2024-06-01',
96
+ maxValue: '2024-12-31',
97
+ }"
98
+ @update:value="onValueChange"
99
+ />
100
+ ```
101
+
102
+ When no value is set, the calendar view and the date segment placeholders default to **today clamped into `[minValue, maxValue]`** (`resolveCalendarPlaceholder()`). This keeps the year segment within the allowed range when bounds are set but today falls outside them — otherwise the year segment would render blank.
103
+
104
+ ### Invalid state
105
+
106
+ ```vue
107
+ <FuroDatePicker
108
+ :parcel="{
109
+ placeholder: 'Required field',
110
+ invalid: isFieldInvalid,
111
+ }"
112
+ />
113
+ ```
114
+
115
+ ### Disabled state
116
+
117
+ ```vue
118
+ <FuroDatePicker
119
+ :parcel="{
120
+ placeholder: 'Not editable',
121
+ disabled: true,
122
+ }"
123
+ />
124
+ ```
125
+
126
+ ### Japan Standard Time (CloudLink spec)
127
+
128
+ When the consumer always operates in JST (UTC+9) — e.g. CloudLink CRM — pass `timezone: 'Asia/Tokyo'`
129
+ so that `Date` object inputs (which are UTC-based internally) are interpreted on the correct calendar day.
130
+
131
+ ```vue
132
+ <!-- String value: already date-only, timezone has no effect -->
133
+ <FuroDatePicker
134
+ :parcel="{
135
+ value: '2024-01-15',
136
+ timezone: 'Asia/Tokyo',
137
+ placeholder: 'Select date',
138
+ }"
139
+ @update:value="onValueChange"
140
+ />
141
+
142
+ <!-- Date object: extracted in JST before normalization -->
143
+ <!-- new Date('2024-01-14T15:30:00Z') → JST 2024-01-15 00:30 → shown as 2024-01-15 -->
144
+ <FuroDatePicker
145
+ :parcel="{
146
+ value: utcDateObject,
147
+ timezone: 'Asia/Tokyo',
148
+ placeholder: 'Select date',
149
+ }"
150
+ @update:value="onValueChange"
151
+ />
152
+ ```
153
+
154
+ The emitted `update:value` is always a `YYYY-MM-DD` string (date-only). The timezone only affects how incoming `Date` objects are mapped to a calendar day.
155
+
156
+ ---
157
+
158
+ ## Value contract
159
+
160
+ | Concern | Decision |
161
+ |---|---|
162
+ | Wire format | `YYYY-MM-DD` string — context accepts `Date` objects and `CalendarDate` at boundary and normalizes |
163
+ | Timezone | Configurable via `parcel.timezone` (IANA string). When `null` (default), `Date` objects use local date parts. When set, date parts are extracted via `Intl.DateTimeFormat` in that timezone. YYYY-MM-DD string inputs are always timezone-neutral. |
164
+ | Range | Single-date only; range support deferred to v2 |
165
+ | Clear | Pass `null` to `parcel.value` to clear the field programmatically |
166
+
167
+ ---
168
+
169
+ ## CSS classes
170
+
171
+ ```text
172
+ Field (inline date input)
173
+ .unit-form-control.date-picker ← host div
174
+ .unit-form-control.date-picker > .field ← date field
175
+ .unit-form-control.date-picker > .field.invalid ← invalid state
176
+ .unit-form-control.date-picker > .field.disabled ← disabled state
177
+ .unit-form-control.date-picker > .field > .field-input ← segment (mm/dd/yyyy)
178
+ .unit-form-control.date-picker > .field > .field-input[data-placeholder] ← placeholder segment
179
+ .unit-form-control.date-picker > .field > .field-input[data-focused] ← keyboard-active segment
180
+ .unit-form-control.date-picker > .field > .trigger ← calendar icon button
181
+
182
+ Popover (portaled)
183
+ .unit-date-picker-content ← popover content
184
+ .unit-date-picker-content > .calendar ← calendar
185
+ .unit-date-picker-content > .calendar-actions ← Today/Clear footer bar
186
+ .unit-date-picker-content > .calendar-actions > .calendar-action.today ← Today button
187
+ .unit-date-picker-content > .calendar-actions > .calendar-action.clear ← Clear button
188
+
189
+ Calendar header
190
+ .unit-date-picker-content > .calendar > .calendar-header ← header row
191
+ .unit-date-picker-content > .calendar > .calendar-header > .calendar-nav-button ← Prev / Next
192
+ .unit-date-picker-content > .calendar > .calendar-header > .calendar-caption ← dropdown wrapper
193
+ .unit-date-picker-content > .calendar > .calendar-header > .calendar-caption > .caption-select ← month/year <select>
194
+
195
+ Calendar grid
196
+ .unit-date-picker-content > .calendar > .calendar-grids ← grid container
197
+ .unit-date-picker-content > .calendar > .calendar-grids > .calendar-grid ← <table>
198
+ … > .calendar-head-row > .calendar-head-cell ← weekday labels
199
+ … > .calendar-row > .calendar-cell ← date cell
200
+ … > .calendar-row > .calendar-cell > .calendar-cell-trigger ← date button
201
+ … > .calendar-cell-trigger[data-selected] ← selected date
202
+ … > .calendar-cell-trigger[data-today] ← today (dot indicator)
203
+ … > .calendar-cell-trigger[data-focused] ← keyboard-focused date
204
+ … > .calendar-cell-trigger[data-disabled] ← out-of-range date
205
+ … > .calendar-cell-trigger[data-outside-view] ← adjacent month dates
206
+ ```
207
+
208
+ All CSS is scoped under `@layer furo` and uses semantic tokens only (`--color-*`, `--size-*`, `--font-size-*`).
209
+
210
+ ---
211
+
212
+ ## Context: `FuroDatePickerContext`
213
+
214
+ `FuroDatePickerContext` (extends `BaseFuroContext`) holds all reactive state and computed logic. The Vue component is a thin template wrapper.
215
+
216
+ Key methods:
217
+
218
+ | Method | Description |
219
+ |---|---|
220
+ | `normalizeToCalendarDate({ raw })` | Converts `YYYY-MM-DD` / `Date` / `CalendarDate` / `null` → `CalendarDate \| null`. When `parcel.timezone` is set, delegates `Date` objects to `normalizeDateToCalendarDateInTimezone`. |
221
+ | `normalizeDateToCalendarDate({ raw })` | Converts `Date` → `CalendarDate`. Uses `parcel.timezone` when set; otherwise uses local date parts. |
222
+ | `normalizeDateToCalendarDateInTimezone({ raw, timezone })` | Converts `Date` → `CalendarDate` in a specific IANA timezone via `Intl.DateTimeFormat`. Returns `null` for invalid timezone strings. |
223
+ | `extractDatePartsInTimezone({ raw, timezone })` | Returns `{ year, month, day }` string parts extracted by `Intl.DateTimeFormat` for a given timezone. |
224
+ | `normalizeToIsoString({ calendarDate })` | Converts `CalendarDate \| null` → `YYYY-MM-DD` string or `null` |
225
+ | `normalizeMinValue()` | Returns `CalendarDate` from `parcel.minValue` or `undefined` |
226
+ | `normalizeMaxValue()` | Returns `CalendarDate` from `parcel.maxValue` or `undefined` |
227
+ | `extractRootAttributes()` | Builds the root attribute object (strips Furo-only keys, sets `granularity: 'day'`, `closeOnSelect: true`, injects `placeholder` for calendar nav) |
228
+ | `extractCalendarMonths()` | Returns the 12-month array (`{ value, label }[]`) for the month dropdown |
229
+ | `extractCalendarYears()` | Returns year options (`{ value }[]`) bounded by `minValue`/`maxValue` (defaults: −100 / +50 from today) |
230
+ | `stripFuroParcelKeys()` | Removes `FURO_DATE_PICKER_PARCEL_KEYS` from parcel before spreading to the underlying primitive (includes `timezone`) |
231
+ | `onChangeValue({ calendarDate })` | Handles the primitive's `update:modelValue`; emits all three events |
232
+ | `onPlaceholderChange({ calendarDate })` | Syncs `placeholderRef` when Prev/Next navigation fires `update:placeholder` |
233
+ | `onMonthChange({ changeEvent, currentYear })` | Navigates calendar to selected month; reads value from `HTMLSelectElement` |
234
+ | `onYearChange({ changeEvent, currentMonth })` | Navigates calendar to selected year; reads value from `HTMLSelectElement` |
235
+ | `formatSegmentValue({ part, value })` | Zero-pads filled day/month segments to two digits (`3` → `03`, `5` → `05`); year, placeholders, and literals pass through. The underlying primitive renders filled day/month as single digits, so this restores the 2-digit display. |
236
+ | `resolveCloseOnSelect()` | Resolves `parcel.closeOnSelect` (default `true`) — the popover's close-on-select behavior. |
237
+ | `resolveLocale()` | Resolves `parcel.locale`, defaulting to the browser locale, then `'en'` — drives segment order. |
238
+ | `resolvePreventDeselect()` | Resolves `parcel.preventDeselect` (default `true`) — keeps the date when its cell is re-clicked. |
239
+ | `onSelectToday()` | Footer **Today** button handler — selects today clamped into `[minValue, maxValue]` and emits the change trio. |
240
+ | `onClearValue()` | Footer **Clear** button handler — clears the value and emits the change trio with `null`. |
241
+
242
+ ---
243
+
244
+ ## Files
245
+
246
+ ```text
247
+ lib/components/molecules/FuroDatePicker/
248
+ ├── FuroDatePicker.vue ← component
249
+ ├── FuroDatePickerContext.js ← context
250
+ └── DatePickerEmitPayload.js ← typed emit payload
251
+
252
+ pages/samples/components/playground/FuroDatePickerPlayground/
253
+ ├── index.vue ← playground page
254
+ └── FuroDatePickerPlaygroundContext.js
255
+
256
+ tests/__tests__/jsdom/lib/components/molecules/FuroDatePicker/
257
+ ├── FuroDatePickerContext.js ← context tests
258
+ └── DatePickerEmitPayload.js ← payload tests
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Out of scope (v1)
264
+
265
+ - Time segments (`FuroTimeField` — Story 1.4)
266
+ - Combined datetime molecule (`FuroDateTimePicker` — Story 1.5)
267
+ - Date range / dual-calendar picker
268
+ - CRM adapter wiring
269
+ - Today/Clear localized button bar
270
+ - i18n locale packs (format string is prop-only v1)
@@ -0,0 +1,110 @@
1
+ # FuroDateTimePicker
2
+
3
+ Combined date + time form control. Composes `FuroDatePicker` (calendar) and `FuroTimeField` (segmented time) into a **single popover** and exposes one combined `v-model:value` as a canonical `YYYY-MM-DDTHH:MM:SS` string. Intended as the `datetime` field editor for the consumer-built ListView / DetailView / FormSequence templates (in `crm-kit-frontend`).
4
+
5
+ The molecule renders `FuroDatePicker` and injects `FuroTimeField` into the date picker's `content-footer` slot, so the calendar stays single-sourced — there is no duplicated calendar logic.
6
+
7
+ ## Summary
8
+
9
+ | Field | Value |
10
+ |---|---|
11
+ | **Layer** | Molecule |
12
+ | **Path** | `lib/components/molecules/FuroDateTimePicker/` |
13
+ | **Composes** | `FuroDatePicker` + `FuroTimeField` (the time field is injected into the calendar popover) |
14
+ | **Value type** | `YYYY-MM-DDTHH:MM:SS` ISO string or `null` |
15
+ | **Wire format** | Always second-precision; seconds default to `00`. `granularity` controls only the visible time segments. |
16
+ | **Timezone** | Naive v1; `timezone` parcel key is forwarded to both atoms (the atoms own `Date` normalization). |
17
+
18
+ ---
19
+
20
+ ## Public API
21
+
22
+ ### Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ |---|---|---|---|
26
+ | `parcel` | `DateTimePickerParcel \| null` | `null` | Reactive data object. Molecule-only keys are consumed here and used to derive the child parcels. |
27
+
28
+ ### Parcel keys
29
+
30
+ | Key | Type | Description |
31
+ |---|---|---|
32
+ | `value` | `string \| null` | Combined value as `YYYY-MM-DDTHH:MM:SS` or `null`. The context splits it into date/time parts and recombines on change. |
33
+ | `invalid` | `boolean` | Shows error chrome and sets `aria-invalid` on the unit shell; also forwarded to both atoms. |
34
+ | `placeholder` | `string \| null` | Placeholder text for the date field. |
35
+ | `disabled` | `boolean` | Disables all interaction. |
36
+ | `minValue` | `string \| null` | Minimum selectable datetime (`YYYY-MM-DDTHH:MM:SS`). See cross-field rule below. |
37
+ | `maxValue` | `string \| null` | Maximum selectable datetime (`YYYY-MM-DDTHH:MM:SS`). See cross-field rule below. |
38
+ | `timezone` | `string \| null` | IANA timezone identifier forwarded to both atoms. Default `null` (timezone-naive). |
39
+ | `granularity` | `'hour' \| 'minute' \| 'second' \| null` | Visible time segments. Does not change the wire format (always second precision). |
40
+ | `step` | `number \| null` | Step amount for the time field's increment/decrement buttons. |
41
+ | `timeFormat` | `number \| null` | `12` or `24`. When unset, the locale default applies. |
42
+
43
+ Consumer passthrough keys (e.g. `name`) that are not in the list above are forwarded to **both** child atoms unchanged.
44
+
45
+ ### Emits
46
+
47
+ | Event | Payload | Description |
48
+ |---|---|---|
49
+ | `change-value` | `DateTimePickerEmitPayload` | Fired when either the date or time part changes. |
50
+ | `commit-value` | `DateTimePickerEmitPayload` | Fired on the same change event. |
51
+ | `update:value` | `string \| null` | Combined `YYYY-MM-DDTHH:MM:SS` string. Selecting a date fabricates a default time (`12:00:00`) so a complete value is produced; `null` only while no date is set. Suitable for `v-model:value`. |
52
+
53
+ `DateTimePickerEmitPayload` exposes `value`, `datePart`, `timePart`, `hasValue()`, and `hasEmptyValue()`.
54
+
55
+ ---
56
+
57
+ ## Value contract
58
+
59
+ | Concern | Behavior |
60
+ |---|---|
61
+ | **Partial emit** | Selecting a date fabricates a default time of `12:00:00` (UAT decision), so the combined value is complete as soon as a date exists and the popover stays open for the user to refine the time. `null` is emitted only while no date is set. |
62
+ | **Popover stays open on date select** | The molecule sets `closeOnSelect: false` on the date atom, so picking a date keeps the popover open for time selection. (Standalone `FuroDatePicker` still closes on select.) |
63
+ | **Cross-field min/max** | The date range is always enforced. The **minimum time** applies only when the selected date equals the minimum date; the **maximum time** applies only when it equals the maximum date. On any other date the full day is allowed. |
64
+ | **Seconds** | The wire value is always `YYYY-MM-DDTHH:MM:SS` (seconds default `00`). |
65
+ | **Preserve-on-change** | Changing the date keeps the current time part; changing the time keeps the current date part. |
66
+ | **Timezone** | Naive v1: `parcel.timezone` is forwarded to both atoms, which own normalization. |
67
+
68
+ ---
69
+
70
+ ## Usage
71
+
72
+ ```vue
73
+ <FuroDateTimePicker
74
+ :parcel="{
75
+ placeholder: 'Pick a date & time',
76
+ value: scheduledAt,
77
+ minValue: '2024-06-01T08:00:00',
78
+ maxValue: '2024-12-31T18:00:00',
79
+ granularity: 'second',
80
+ }"
81
+ @update:value="onScheduledAtChange({ value: $event })"
82
+ />
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Composition notes
88
+
89
+ - The time field is rendered inside the date picker popover via the additive `content-footer` slot on `FuroDatePicker`. Because the time field lives **inside** the popover content (not outside it), editing the time does not trigger an outside-focus/pointer event and the popover stays open.
90
+ - Picking a date does **not** close the popover (`extractDateParcel()` sets `closeOnSelect: false`), and the molecule fabricates a default time of `12:00:00` on first date selection (`applyDefaultTimeOnDateSelection()`). The user can then refine the time without reopening the popover.
91
+ - Re-selecting the already-selected date keeps it (the date atom defaults to `preventDeselect: true`). The atom's footer **Today** / **Clear** buttons and their `todayButtonText` / `clearButtonText` labels pass through the molecule unchanged — **Today** sets the date (and fabricates the noon default), **Clear** resets **both** the date and time parts to `null` (clearing the date with `preventDeselect` on clears the time too — `syncTimeWithDateSelection()`).
92
+ - On open, the calendar is auto-focused by the underlying popover primitive. The molecule forces the date picker into **modal** mode (`extractDateParcel()` sets `modal: true`), so the popover traps focus (the underlying popover primitive's focus scope). Tab then cycles within the popover — calendar → footer time field — instead of escaping to the inline time field in the trigger (the teleported popover sits late in the DOM, so without trapping the focus order is wrong).
93
+ - The trigger field carries a **directly editable** time field: the molecule injects a `FuroTimeField` into `FuroDatePicker`'s `field-suffix` slot, beside the date segments. Users can type the time inline without opening the popover, and the calendar icon still opens the picker for date selection. The inline copy is styled borderless with its steppers hidden so it blends into the single trigger field; the full stepper version lives in the popover footer.
94
+ - The inline field (trigger) and the footer field (popover) are **two views of the same time part**: both bind `value` to `timePart` via `extractTimeParcel()` and route edits through `onTimeChange()`, so editing either keeps both — and the combined `v-model:value` — in sync.
95
+ - Stepping the time before a date is chosen no longer resets itself. Because the combined value emits `null` until both parts are set (Q2), the parent's `v-model` echoes `null` back; `syncFromExternalValue()` skips re-splitting when the external value already equals the current parts' combination, preserving the in-progress part.
96
+ - The popover does not shift when the time changes. The date/time fields use `font-variant-numeric: tabular-nums` (equal-width digits), and the popover is **start-aligned** (`align="start"` on the date content) so a width change in the trigger's inline time field never re-centers/moves the popover.
97
+ - The molecule imports the two atoms only — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
98
+
99
+ ---
100
+
101
+ ## Testing
102
+
103
+ - Context and payload logic are covered by jsdom unit tests (`splitValue` / `combineValue`, cross-field bounds, preserve-on-change, partial/null emit, parcel derivation).
104
+ - The full popover open-cycle ("open → pick date → edit time inside → combined value updates, popover stays open") is verified by **manual real-browser UAT** in the playground. A full SFC mount is infeasible in jsdom because the composed atoms import the headless primitive's namespaced entry + Nuxt `#components`, which do not resolve under the jest module map; the integration test is therefore a documented partial driving the context through the same handlers the SFC binds.
105
+
106
+ ---
107
+
108
+ ## Playground
109
+
110
+ `/samples/components/playground` → **Date Time** tab. Demonstrates basic, pre-filled, bounded window (cross-field time), programmatic set/clear, timezone passthrough, disabled, invalid, and the emit trio event log.
@@ -0,0 +1,88 @@
1
+ # FuroDialog
2
+
3
+ Organism — a modal (or non-modal) dialog built on a headless dialog primitive
4
+ (`Root` + `Portal` + `Overlay` + `Content` + `Title` + `Description` + `Close`).
5
+ Composes overlay, focus trap, Esc/overlay-click dismissal, and an accessible
6
+ heading. Domain-agnostic: no GraphQL, router, or Nuxt imports.
7
+
8
+ Location: `lib/components/organisms/FuroDialog/`
9
+
10
+ ## Props
11
+
12
+ | Prop | Type | Default | Notes |
13
+ |------|------|---------|-------|
14
+ | `parcel` | `FuroDialogParcel \| null` | `null` | Reactive behavior object (below) |
15
+
16
+ ### `parcel`
17
+
18
+ | Key | Type | Default | Notes |
19
+ |-----|------|---------|-------|
20
+ | `open` | `boolean` | — | Controlled open state. When set, the parent owns open/close. When omitted, the dialog manages its own state. |
21
+ | `disabled` | `boolean` | `false` | Disables the trigger. |
22
+ | `modal` | `boolean` | `true` | `false` renders without the overlay scrim and keeps the page interactive. |
23
+ | `title` | `string \| null` | `null` | Heading text. Provide a title (or the `title` slot) for accessibility. |
24
+ | `description` | `string \| null` | `null` | Sub-heading text below the title. |
25
+ | `closeText` | `string` | `'Close'` | Accessible label for the built-in close button. |
26
+ | `closeOnOutsideClick` | `boolean` | `true` | `false` ignores overlay / outside-click dismissal — only the close button or an explicit action closes it. |
27
+ | `closeOnEscape` | `boolean` | `true` | `false` ignores the Escape key. |
28
+ | `size` | `'sm' \| 'default' \| 'lg' \| 'fullscreen'` | `'default'` | Content width variant. `fullscreen` fills the viewport. |
29
+ | `busy` | `boolean` | `false` | While `true`, all dismissal (outside / Esc / close button / `close()`) is blocked — use during async work. |
30
+ | `initialFocus` | `string \| null` | `null` | CSS selector (scoped to the dialog content) of the element to focus on open. When unset, the first focusable element is focused. |
31
+
32
+ ## Events
33
+
34
+ | Event | Payload | When |
35
+ |-------|---------|------|
36
+ | `update:open` | `boolean` | Open state changed — enables `v-model:open`. |
37
+ | `open:change` | `{ open: boolean }` | Open state changed (semantic form). |
38
+
39
+ ## Slots
40
+
41
+ | Slot | Purpose |
42
+ |------|---------|
43
+ | `trigger` | Optional element that opens the dialog. Rendered as the trigger via `as-child` — supply your own button/element. Omit when the dialog is opened via controlled `open`. |
44
+ | `title` | Overrides the heading (falls back to `parcel.title`). |
45
+ | `description` | Overrides the description (falls back to `parcel.description`). |
46
+ | default | Dialog body content. Receives a `close()` slot prop to dismiss from inside. |
47
+ | `footer` | Footer region (e.g. action buttons). Receives a `close()` slot prop so action buttons can dismiss the dialog (after running their own logic). |
48
+ | `close` | Content of the built-in close button (defaults to `✕`). |
49
+
50
+ ## Usage
51
+
52
+ ### Uncontrolled (own trigger)
53
+
54
+ ```vue
55
+ <FuroDialog :parcel="{ title: 'Delete record', description: 'This cannot be undone.' }">
56
+ <template #trigger>
57
+ <button type="button">Delete…</button>
58
+ </template>
59
+ <p>The record will be permanently removed.</p>
60
+ <template #footer>
61
+ <button type="button">Cancel</button>
62
+ <button type="button">Delete</button>
63
+ </template>
64
+ </FuroDialog>
65
+ ```
66
+
67
+ ### Controlled (`v-model:open`)
68
+
69
+ ```vue
70
+ <FuroDialog
71
+ :parcel="{ open: isDialogOpen, title: 'Settings' }"
72
+ @open:change="isDialogOpen = $event.open"
73
+ >
74
+ <p>Open state is owned by the parent.</p>
75
+ </FuroDialog>
76
+ ```
77
+
78
+ ## Notes
79
+
80
+ - The dialog content is portaled to the document body; overlay and content carry
81
+ `unit-dialog-overlay` / `unit-dialog-content` classes inside `@layer furo`.
82
+ - Always provide a title (text or slot) — it is the dialog's accessible name.
83
+ - A description is optional. Without `parcel.description` or a `description` slot the content declares no described-by target, so the headless primitive logs no accessibility warning.
84
+ - By default the built-in close button, Esc key, and overlay click all dismiss the dialog.
85
+ Set `closeOnOutsideClick` / `closeOnEscape` to `false` to require an explicit close
86
+ (e.g. a footer action calling the `close()` slot prop).
87
+ - Overlay scrim color is the `--furo-dialog-overlay-background` component token.
88
+ - The built-in close button takes its size from `--size-close-button-medium`.
@@ -0,0 +1,122 @@
1
+ # FuroDrawer
2
+
3
+ Organism — a slide-in panel docked to a viewport edge, built on a headless
4
+ drawer primitive (`Root` + `Portal` + `Overlay` + `Content` + `Handle` +
5
+ `Title` + `Description` + `Close` + `SwipeArea`). Supports swipe-to-dismiss with
6
+ momentum, optional swipe-to-open, snap points for partially-open resting
7
+ positions, modal / focus-trapped non-modal / fully non-modal modes, controlled
8
+ or uncontrolled open state, and built-in Title/Description + Esc handling.
9
+ Domain-agnostic: no GraphQL, router, or Nuxt imports.
10
+
11
+ Location: `lib/components/organisms/FuroDrawer/`
12
+
13
+ ## Props
14
+
15
+ | Prop | Type | Default | Notes |
16
+ |------|------|---------|-------|
17
+ | `parcel` | `FuroDrawerParcel \| null` | `null` | Reactive behavior object (below) |
18
+
19
+ ### `parcel`
20
+
21
+ | Key | Type | Default | Notes |
22
+ |-----|------|---------|-------|
23
+ | `open` | `boolean` | — | Controlled open state. When set, the parent owns open/close. When omitted, the drawer manages its own state. |
24
+ | `disabled` | `boolean` | `false` | Disables the trigger. |
25
+ | `modal` | `boolean \| 'trap-focus'` | `true` | `true`: overlay scrim + focus trap + scroll lock. `'trap-focus'`: focus trapped but the page stays interactive (side-panel mode). `false`: fully non-modal. |
26
+ | `title` | `string \| null` | `null` | Heading text. Provide a title (or the `title` slot) for accessibility. |
27
+ | `description` | `string \| null` | `null` | Sub-heading text below the title. |
28
+ | `closeText` | `string` | `'Close'` | Accessible label for the built-in close button. |
29
+ | `closeOnOutsideClick` | `boolean` | `true` | `false` ignores overlay / outside-click dismissal. |
30
+ | `closeOnEscape` | `boolean` | `true` | `false` ignores the Escape key. |
31
+ | `side` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'right'` | Edge the panel docks to, slides in from, and is swiped toward to dismiss. |
32
+ | `size` | `'sm' \| 'default' \| 'lg' \| 'fullscreen'` | `'default'` | Cross-axis extent: width for `left`/`right`, height for `top`/`bottom`. `fullscreen` fills the docking axis. |
33
+ | `snapPoints` | `Array<number \| string> \| null` | `null` | Preset resting positions: fractions `0–1`, pixels `> 1`, or `'148px'`/`'30rem'` strings. |
34
+ | `activeSnapPoint` | `number \| string \| null` | `null` | Controlled active snap point. When set, the parent owns it (`update:activeSnapPoint`). |
35
+ | `sequentialSnap` | `boolean` | `false` | `true` snaps one point at a time; `false` snaps to the nearest by distance. |
36
+ | `swipeToOpen` | `boolean` | `false` | Renders an edge grab strip so the panel can be swiped open from closed. |
37
+ | `dragHandle` | `boolean` | `false` | Renders a drag handle (grab indicator) on the docked edge. |
38
+ | `busy` | `boolean` | `false` | While `true`, all dismissal (outside / Esc / close button / `close()`) is blocked — use during async work. |
39
+ | `initialFocus` | `string \| null` | `null` | CSS selector (scoped to the drawer content) of the element to focus on open. |
40
+
41
+ ## Events
42
+
43
+ | Event | Payload | When |
44
+ |-------|---------|------|
45
+ | `update:open` | `boolean` | Open state changed — enables `v-model:open`. |
46
+ | `open:change` | `{ open: boolean }` | Open state changed (semantic form). |
47
+ | `update:activeSnapPoint` | `number \| string \| null` | Active snap point changed — enables `v-model:activeSnapPoint`. |
48
+ | `snap:change` | `{ snapPoint: number \| string \| null }` | Active snap point changed (semantic form). |
49
+
50
+ ## Slots
51
+
52
+ | Slot | Purpose |
53
+ |------|---------|
54
+ | `trigger` | Optional element that opens the drawer (rendered via `as-child`). Omit when opened via controlled `open` or swipe-to-open. |
55
+ | `title` | Overrides the heading (falls back to `parcel.title`). |
56
+ | `description` | Overrides the description (falls back to `parcel.description`). |
57
+ | default | Drawer body content. Receives a `close()` slot prop to dismiss from inside. |
58
+ | `footer` | Footer region (e.g. action buttons). Receives a `close()` slot prop. |
59
+ | `close` | Content of the built-in close button (defaults to `✕`). |
60
+
61
+ ## Usage
62
+
63
+ ### Uncontrolled (own trigger)
64
+
65
+ ```vue
66
+ <FuroDrawer :parcel="{ side: 'right', title: 'Filters', dragHandle: true }">
67
+ <template #trigger>
68
+ <button type="button">Filters…</button>
69
+ </template>
70
+ <p>Filter controls go here. Swipe right or press Esc to close.</p>
71
+ <template #footer="{ close }">
72
+ <button type="button" @click="close()">Apply</button>
73
+ </template>
74
+ </FuroDrawer>
75
+ ```
76
+
77
+ ### Snap points (bottom sheet)
78
+
79
+ ```vue
80
+ <FuroDrawer
81
+ :parcel="{
82
+ side: 'bottom',
83
+ size: 'fullscreen',
84
+ dragHandle: true,
85
+ snapPoints: [0.4, 0.75, 1],
86
+ }"
87
+ @snap:change="onSnap($event.snapPoint)"
88
+ >
89
+ <template #trigger>
90
+ <button type="button">Open sheet</button>
91
+ </template>
92
+ <p>Drag the handle to rest at 40%, 75%, or 100%.</p>
93
+ </FuroDrawer>
94
+ ```
95
+
96
+ ### Controlled (`v-model:open`)
97
+
98
+ ```vue
99
+ <FuroDrawer
100
+ :parcel="{ open: isOpen, side: 'left', title: 'Navigation' }"
101
+ @open:change="isOpen = $event.open"
102
+ >
103
+ <p>Open state is owned by the parent.</p>
104
+ </FuroDrawer>
105
+ ```
106
+
107
+ ## Notes
108
+
109
+ - The drawer content is portaled to the document body; overlay and content carry
110
+ `unit-drawer-overlay` / `unit-drawer-content` classes inside `@layer furo`.
111
+ - Position composes the active snap-point offset (on the docking axis) with the
112
+ live swipe-drag offset the primitive exposes via CSS variables; enter/exit use
113
+ the independent `translate` property so they compose with the drag transform.
114
+ Both are suppressed under `prefers-reduced-motion: reduce`.
115
+ - `size` controls the cross-axis extent only. `fullscreen` fills the docking axis.
116
+ - `'trap-focus'` mode still renders the overlay element but keeps the page
117
+ interactive; `false` renders no overlay at all.
118
+ - Always provide a title (text or slot) — it is the drawer's accessible name.
119
+ - Overlay scrim reuses the `--furo-dialog-overlay-background` component token.
120
+ - The built-in close button takes its size from `--size-close-button-medium`.
121
+ - Swipe, momentum, and snap behaviors are pointer-driven and require manual
122
+ browser QA (the playground "Drawer" tab exercises every mode).