@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,397 @@
1
+ # `<FuroNumberField>` Interface Specification
2
+
3
+ ## Overview
4
+
5
+ `<FuroNumberField>` is a numeric input Atom in the Furo-Vue component system. It is an opinionated abstraction over a headless number-field primitive — it does not mirror the raw primitive API but encapsulates structure, accessibility, stepper controls, and controlled/uncontrolled state management internally.
6
+
7
+ Unlike `<FuroTextField>`, which wraps a single `<input>` element, `<FuroNumberField>` renders a composite structure consisting of a decrement button, a formatted input, and an increment button. Keyboard interactions (Arrow Up/Down, Page Up/Page Down) and mouse wheel control are handled by the underlying primitive automatically.
8
+
9
+ Adheres to the [Spinbutton WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton).
10
+
11
+ ---
12
+
13
+ ## Props
14
+
15
+ `<FuroNumberField>` uses **two distinct mechanisms** for receiving data from the parent, each with a different responsibility.
16
+
17
+ ### Rule: `parcel` vs Fallthrough Attributes
18
+
19
+ | Mechanism | When to use | Rendered as |
20
+ |------------------------|---------------------------------------------------------------------|----------------|
21
+ | `:parcel` | Props that **control component behavior** (value, bounds, formatting, nested primitive parcels, …) | Bound to the root and nested primitive parts |
22
+ | Fallthrough attributes | Non-prop attributes on `<FuroNumberField>` (e.g. `name`, `placeholder`, `disabled`) | Applied per Vue fallthrough rules; see below |
23
+
24
+ The distinction maps cleanly to responsibilities: numeric behavior and primitive configuration live in `parcel`. Declarative HTML-like hints used with native forms often stay on the component tag for readability.
25
+
26
+ ```html
27
+ <!-- ✅ Correct -->
28
+ <FuroNumberField
29
+ :parcel="context.quantityParcel"
30
+ placeholder="Quantity"
31
+ name="quantity"
32
+ />
33
+
34
+ <!-- context.quantityParcel = { value: 1, invalid: false, min: 0, max: 100 } -->
35
+ ```
36
+
37
+ > **Why `parcel` exists:** Components with many individual props make it hard to review the wiring between HTML template and Context members. `parcel` consolidates behavior-controlling props into one reviewable object.
38
+ > **Why fallthrough exists:** Attributes such as `name`, `placeholder`, or `disabled` stay visible on the component element where the usage is declared.
39
+
40
+ ---
41
+
42
+ ### `parcel` shape
43
+
44
+ The parcel is **`v-bind` spread onto the root** (via `extractRootAttributes()`), with Furo-specific extensions. It therefore includes every root prop except `modelValue` / `defaultValue` (those are managed internally), plus the fields below.
45
+
46
+ | Key | Type | Required | Default | Description |
47
+ |---------------------|-------------------------|----------|-------------|-------------|
48
+ | `value` | `number \| null` | No | `null` | Controlled value. Watched and synced into an internal ref. |
49
+ | `invalid` | `boolean` | No | `false` | Applies invalid/error visual state (`invalid` class and `aria-invalid`). |
50
+ | `min` | `number` | No | — | Smallest value allowed (the underlying primitive). Also reflected on the inner `<input>` as the `min` attribute. |
51
+ | `max` | `number` | No | — | Largest value allowed (the underlying primitive). Also reflected on the inner `<input>` as the `max` attribute. |
52
+ | `step` | `number` | No | (primitive default) | Step for increment/decrement. |
53
+ | `stepSnapping` | `boolean` | No | — | When `false`, avoids snapping to `step` increments (primitive). |
54
+ | `formatOptions` | `Intl.NumberFormatOptions` | No | — | Formatting for the displayed value (currency, percent, decimals, …). |
55
+ | `locale` | `string` | No | — | BCP 47 locale tag for formatting (e.g. `'en-US'`, `'ja-JP'`). |
56
+ | `inputParcel` | input part props & input HTML attrs | No | — | Passed to the input part (e.g. `autofocus`, `readonly`, `disabled` when binding at the input layer). |
57
+ | `decrementParcel` | decrement part props | No | — | Passed to the decrement part. |
58
+ | `incrementParcel` | increment part props | No | — | Passed to the increment part. |
59
+
60
+ > All parcel values must be **JSON-serializable** (no functions, no class instances).
61
+
62
+ #### `parcel.formatOptions` examples
63
+
64
+ ```js
65
+ // Decimal with minimum fraction digits
66
+ formatOptions: {
67
+ minimumFractionDigits: 2,
68
+ }
69
+
70
+ // Percentage (often paired with step: 0.01)
71
+ formatOptions: {
72
+ style: 'percent',
73
+ }
74
+
75
+ // Currency
76
+ formatOptions: {
77
+ style: 'currency',
78
+ currency: 'USD',
79
+ currencyDisplay: 'symbol',
80
+ }
81
+ ```
82
+
83
+ ---
84
+
85
+ ### Fallthrough Attributes (non-prop attributes)
86
+
87
+ `<FuroNumberField>` only declares the **`parcel`** prop. Any other attributes (`name`, `placeholder`, `disabled`, `class`, `style`, `autofocus`, …) are available on **`componentContext.attrs`**.
88
+
89
+ Two attributes are **read explicitly** for the inner native `<input>`:
90
+
91
+ | Attribute | Behavior |
92
+ |---------------|----------|
93
+ | `name` | Applied to the inner `<input>` (`extractNameAttribute()`). |
94
+ | `placeholder` | Applied to the inner `<input>` (`extractPlaceholderAttribute()`). |
95
+
96
+ Remaining fallthrough attributes follow Vue’s default behavior (typically the **single root** is the primitive root). For anything that must attach specifically to the native input or to the input part's props, prefer **`parcel.inputParcel`** (see playground: `inputParcel: { autofocus: true }`).
97
+
98
+ ```html
99
+ <FuroNumberField
100
+ :parcel="context.quantityParcel"
101
+ placeholder="0–100"
102
+ name="quantity"
103
+ disabled
104
+ />
105
+ ```
106
+
107
+ > The native `type` attribute is not used as a raw `type="number"` — the underlying primitive implements the spinbutton pattern (`role="spinbutton"`) with locale-aware formatting.
108
+
109
+ ---
110
+
111
+ ## Slots
112
+
113
+ Optional **`decrement`** and **`increment`** slots replace the default Phosphor minus/plus icons. Both receive a scoped prop:
114
+
115
+ | Slot | Scoped props | Default content |
116
+ |-------------|----------------|-----------------|
117
+ | `decrement` | `{ value }` — current numeric model (may be `null`) | `<Icon name="ph:minus" />` |
118
+ | `increment` | `{ value }` — current numeric model (may be `null`) | `<Icon name="ph:plus" />` |
119
+
120
+ ---
121
+
122
+ ## Emits
123
+
124
+ Each emit passes a **`NumberFieldEmitPayload` instance** directly as the event argument — except **`update:value`**, which emits a bare `number | null`. In the template, `$event` is that payload instance for `change-value` and `commit-value`.
125
+
126
+ ```js
127
+ // Conceptually inside <FuroNumberField> / context
128
+ emit('change-value', payload)
129
+ emit('update:value', payload.value)
130
+ ```
131
+
132
+ ```html
133
+ <!-- ✅ Correct — $event is the NumberFieldEmitPayload instance -->
134
+ <FuroNumberField
135
+ :parcel="context.buildNumberFieldParcel()"
136
+ @change-value="context.onChangeValue({ payload: $event })"
137
+ />
138
+ ```
139
+
140
+ > **Event handler style:** In the parent Context, always use **single object parameter destructuring**. The template wraps `$event` into `{ payload: $event }` when calling Context methods.
141
+
142
+ ---
143
+
144
+ ### `@change-value`
145
+
146
+ Emitted when the underlying primitive updates the model — typing, stepping (buttons), arrow keys, or wheel, via `@update:model-value` on the root.
147
+
148
+ ```html
149
+ <FuroNumberField
150
+ :parcel="context.buildNumberFieldParcel()"
151
+ @change-value="context.onChangeValue({ payload: $event })"
152
+ />
153
+ ```
154
+
155
+ ### `@commit-value`
156
+
157
+ Emitted on **`blur`** of the inner `<input>` or **`Enter`** (`@keypress.enter`).
158
+
159
+ ```html
160
+ <FuroNumberField
161
+ :parcel="context.buildNumberFieldParcel()"
162
+ @commit-value="context.onCommitValue({ payload: $event })"
163
+ />
164
+ ```
165
+
166
+ ### `@update:value`
167
+
168
+ Emitted whenever **`change-value`** or **`commit-value`** fires, with the raw **`number | null`** (not a payload instance). Intended for **`v-model:value`**.
169
+
170
+ > **Naming rationale:**
171
+ > `@change-value` replaces the potentially confusing `@input` native name, making developer intent explicit.
172
+ > `@commit-value` replaces `@change` to avoid collision with native `change` event semantics and is more readable.
173
+ > Furo names are based on what the parent component *wants*, not on internal browser events.
174
+
175
+ > **Emitter responsibility:** `<FuroNumberField>` **always emits** regardless of validity or range constraints. The parent (Context) decides how to respond. The component never cancels an emit based on value state.
176
+
177
+ ---
178
+
179
+ ## Event Handler Style in Context
180
+
181
+ All event handlers use **single object parameter destructuring** — no exceptions. This keeps Context code consistent with the rest of the ORT coding style.
182
+
183
+ ```js
184
+ // ✅ Correct
185
+ onChangeValue ({
186
+ payload
187
+ }) {
188
+ const isValid = this.validatesQuantityRange({
189
+ payload
190
+ })
191
+
192
+ this.statusReactive.isQuantityInvalid = !isValid
193
+ }
194
+
195
+ // ❌ Wrong — single positional argument breaks consistency
196
+ onChangeValue (payload) {
197
+ ...
198
+ }
199
+ ```
200
+
201
+ ---
202
+
203
+ ## `NumberFieldEmitPayload` API
204
+
205
+ The payload object received in **`change-value`** and **`commit-value`** exposes the following members:
206
+
207
+ | Member | Type | Description |
208
+ |------------------------------|------------------|-------------|
209
+ | `.value` | `number \| null` | Current numeric value. `null` when empty or cleared. |
210
+ | `.controlElement` | `HTMLElement \| null` | The inner input from the event detail (`detail.target`). |
211
+ | `.hasEmptyValue()` | `boolean` | `true` if `.value` is `null`. |
212
+ | `.hasEnteredValue()` | `boolean` | `true` if `.value` is a finite number. |
213
+ | `.extractMinValueAttribute()` | `number \| null` | Parses the **`min`** attribute from `.controlElement`. |
214
+ | `.extractMaxValueAttribute()` | `number \| null` | Parses the **`max`** attribute from `.controlElement`. |
215
+ | `.isGreaterThanOrEqualMin()` | `boolean` | `true` if `.value` >= parsed `min` attribute (both must be valid numbers). |
216
+ | `.isLessThanOrEqualMax()` | `boolean` | `true` if `.value` <= parsed `max` attribute (both must be valid numbers). |
217
+ | `.isWithinRange()` | `boolean` | `true` if both `min` and `max` attributes exist, parse as numbers, and the value lies between them (inclusive). Otherwise `false`. |
218
+
219
+ Range helpers use the **DOM `min` / `max` attributes** on the control (mirrored from `parcel.min` / `parcel.max` on the `<input>`), not free parameters.
220
+
221
+ ```js
222
+ onCommitValue ({
223
+ payload
224
+ }) {
225
+ if (payload.hasEmptyValue()) {
226
+ this.statusReactive.isQuantityInvalid = true
227
+
228
+ return
229
+ }
230
+
231
+ this.statusReactive.isQuantityInvalid = !payload.isWithinRange()
232
+ }
233
+ ```
234
+
235
+ ---
236
+
237
+ ## `v-model` Support
238
+
239
+ `v-model:value` is supported as syntactic sugar over `:parcel` value binding + `@update:value`.
240
+
241
+ ```html
242
+ <!-- These two are equivalent -->
243
+ <FuroNumberField v-model:value="context.quantityRef.value" />
244
+
245
+ <FuroNumberField
246
+ :parcel="{
247
+ value: context.quantityRef.value
248
+ }"
249
+ @update:value="context.quantityRef.value = $event"
250
+ />
251
+ ```
252
+
253
+ > A watcher **inside** `<FuroNumberField>` synchronizes `parcel.value` with the internal ref when `parcel.value` changes. No watcher is required in the parent component for that sync.
254
+
255
+ ---
256
+
257
+ ## Controlled vs. Uncontrolled State
258
+
259
+ `<FuroNumberField>` supports **both modes**:
260
+
261
+ | Mode | How |
262
+ |--------------|---------------------------------------------------------------------|
263
+ | Controlled | Pass `:parcel="{ value: ... }"` or `v-model:value`. Internal ref tracks updates from the underlying primitive and from `parcel.value`. |
264
+ | Uncontrolled | Omit `value` in `parcel`. Initial behavior follows the underlying primitive's defaults for missing model value. |
265
+
266
+ ---
267
+
268
+ ## Differences from `<FuroTextField>`
269
+
270
+ ### Structure
271
+
272
+ `<FuroTextField>` renders a single `<input>` element. `<FuroNumberField>` renders a composite structure:
273
+
274
+ ```
275
+ [root]
276
+ ├── [decrement part] ← stepper button (–), optional `decrement` slot
277
+ ├── [input part] ← formatted input (`as-child` → native `<input>`)
278
+ └── [increment part] ← stepper button (+), optional `increment` slot
279
+ ```
280
+
281
+ ### `type` attribute
282
+
283
+ `<FuroTextField>` renders `type="text"`. `<FuroNumberField>` does not rely on `type="number"` — it uses the underlying primitive's spinbutton pattern (`role="spinbutton"`), which is accessible and locale-aware.
284
+
285
+ ### CSS class
286
+
287
+ `.unit-form-control.number` instead of `.unit-form-control.text`.
288
+
289
+ ### `parcel.value` type
290
+
291
+ | Component | `parcel.value` type |
292
+ |-----------------------|----------------------------|
293
+ | `<FuroTextField>` | `string \| number \| null` |
294
+ | `<FuroPasswordField>` | `string \| null` |
295
+ | `<FuroNumberField>` | `number \| null` |
296
+
297
+ Number fields only accept `number | null`. String values are not accepted.
298
+
299
+ ### EmitPayload API
300
+
301
+ | Component | EmitPayload class | Value type | Key methods (examples) |
302
+ |-----------------------|-----------------------------|------------------|-------------------------|
303
+ | `<FuroTextField>` | `TextFieldEmitPayload` | `string \| null` | `.hasEmptyText()`, `.hasEmptyTrimmedText()`, … |
304
+ | `<FuroPasswordField>` | `PasswordFieldEmitPayload` | `string \| null` | `.hasEmptyText()`, `.hasEmptyTrimmedText()`, … |
305
+ | `<FuroNumberField>` | `NumberFieldEmitPayload` | `number \| null` | `.hasEmptyValue()`, `.hasEnteredValue()`, `.isWithinRange()`, … |
306
+
307
+ ### Class naming
308
+
309
+ All class and type names are prefixed with `Number` instead of `Text`:
310
+
311
+ | Role | `FuroTextField` | `FuroNumberField` |
312
+ |-----------------|--------------------------|------------------------------|
313
+ | Component | `FuroTextField` | `FuroNumberField` |
314
+ | Context | `FuroTextFieldContext` | `FuroNumberFieldContext` |
315
+ | EmitPayload | `TextFieldEmitPayload` | `NumberFieldEmitPayload` |
316
+ | Parcel typedef | `FuroTextFieldParcel` | `FuroNumberFieldParcel` |
317
+
318
+ ---
319
+
320
+ ## Example
321
+
322
+ ### Basic usage
323
+
324
+ ```html
325
+ <FuroNumberField
326
+ :parcel="{
327
+ value: context.statusReactive.quantity,
328
+ invalid: context.statusReactive.isQuantityInvalid,
329
+ min: 1,
330
+ max: 999,
331
+ step: 1,
332
+ }"
333
+ placeholder="1–999"
334
+ name="quantity"
335
+ @commit-value="context.onCommitValue({ payload: $event })"
336
+ />
337
+ ```
338
+
339
+ ```js
340
+ onCommitValue ({
341
+ payload
342
+ }) {
343
+ if (payload.hasEmptyValue()) {
344
+ this.statusReactive.isQuantityInvalid = true
345
+
346
+ return
347
+ }
348
+
349
+ this.statusReactive.isQuantityInvalid = !payload.isWithinRange()
350
+ }
351
+ ```
352
+
353
+ ---
354
+
355
+ ### Currency input
356
+
357
+ ```html
358
+ <FuroNumberField
359
+ :parcel="{
360
+ value: context.priceRef.value,
361
+ invalid: context.statusReactive.isPriceInvalid,
362
+ min: 0,
363
+ step: 0.01,
364
+ locale: 'en-US',
365
+ formatOptions: {
366
+ style: 'currency',
367
+ currency: 'USD',
368
+ },
369
+ }"
370
+ name="price"
371
+ @change-value="context.onChangeValue({ payload: $event })"
372
+ @commit-value="context.onCommitValue({ payload: $event })"
373
+ />
374
+ ```
375
+
376
+ ---
377
+
378
+ ### Percentage input
379
+
380
+ ```html
381
+ <FuroNumberField
382
+ :parcel="{
383
+ value: context.rateRef.value,
384
+ invalid: context.statusReactive.isRateInvalid,
385
+ min: 0,
386
+ max: 1,
387
+ step: 0.01,
388
+ formatOptions: {
389
+ style: 'percent',
390
+ },
391
+ }"
392
+ name="rate"
393
+ @commit-value="context.onCommitValue({ payload: $event })"
394
+ />
395
+ ```
396
+
397
+ ---
@@ -0,0 +1,101 @@
1
+ # FuroPagination
2
+
3
+ Molecule — page navigation built on a headless pagination primitive
4
+ (`Root` + `Prev` + `List` + `ListItem` + `Ellipsis` + `Next`). Derives the page
5
+ range, ellipsis gaps, and prev/next disabled bounds from the record count, and
6
+ emits the selected page. Domain-agnostic: no GraphQL, router, or Nuxt imports —
7
+ the consumer wires the emitted page to its own routing or fetching.
8
+
9
+ Location: `lib/components/molecules/FuroPagination/`
10
+
11
+ ## Props
12
+
13
+ | Prop | Type | Default | Notes |
14
+ |------|------|---------|-------|
15
+ | `parcel` | `FuroPaginationParcel \| null` | `null` | Reactive behavior object (below) |
16
+
17
+ ### `parcel`
18
+
19
+ | Key | Type | Default | Notes |
20
+ |-----|------|---------|-------|
21
+ | `page` | `number \| null` | `null` | Controlled current page (1-based). When set, the parent owns the page. When omitted, the component manages its own page. |
22
+ | `offset` | `number \| null` | `null` | Controlled current page expressed as a zero-based record offset (offset/limit-style state, e.g. GraphQL). The page is derived via `floor(offset / limit) + 1`. When both `page` and `offset` are set and they disagree (`offset !== (page - 1) * limit`), development throws and production lets `page` win. Provide only one. |
23
+ | `limit` | `number` | `20` | Records per page. Combined with `totalRecords` to compute the page count. |
24
+ | `totalRecords` | `number` | `0` | Total record count across all pages. |
25
+ | `siblingCount` | `number` | `2` | Pages shown on each side of the current page before collapsing into an ellipsis. |
26
+ | `showEdges` | `boolean` | `true` | When `true`, the first and last pages stay pinned with ellipsis gaps. `false` renders a plain contiguous range. |
27
+ | `disabled` | `boolean` | `false` | Disables the whole control. |
28
+
29
+ ## Events
30
+
31
+ | Event | Payload | When |
32
+ |-------|---------|------|
33
+ | `update:page` | `number` | Selected page changed — enables `v-model:page`. |
34
+ | `page:change` | `{ page: number, offset: number, limit: number }` | Selected page changed. `offset` (`(page - 1) * limit`) and `limit` are provided so offset/limit data sources (e.g. GraphQL pagination) can use the payload directly. |
35
+
36
+ ## Slots
37
+
38
+ | Slot | Purpose |
39
+ |------|---------|
40
+ | `previous` | Content of the previous-page button (defaults to a left-caret icon). |
41
+ | `next` | Content of the next-page button (defaults to a right-caret icon). |
42
+ | `ellipsis` | Content of the gap marker (defaults to a three-dots icon). |
43
+ | `page` | Custom content for each page entry, receiving `{ page }`. Rendered via `as-child`, so the slot may supply an anchor (`<a :href>`) for SEO — the click still drives `page:change`. When omitted, each page renders as a plain button labeled with the page number. |
44
+
45
+ ## Usage
46
+
47
+ ### Uncontrolled
48
+
49
+ ```vue
50
+ <FuroPagination
51
+ :parcel="{ limit: 10, totalRecords: 240 }"
52
+ @page:change="onPageChange($event.page)"
53
+ />
54
+ ```
55
+
56
+ ### Controlled (`v-model:page`)
57
+
58
+ ```vue
59
+ <FuroPagination
60
+ :parcel="{ page: currentPage, limit: 10, totalRecords: 240 }"
61
+ @page:change="currentPage = $event.page"
62
+ />
63
+ ```
64
+
65
+ ### Offset / limit data source (e.g. GraphQL)
66
+
67
+ ```vue
68
+ <FuroPagination
69
+ :parcel="{ page: currentPage, limit, totalRecords }"
70
+ @page:change="fetchRecords({ offset: $event.offset, limit: $event.limit })"
71
+ />
72
+ ```
73
+
74
+ ### Controlled by offset (offset-only state)
75
+
76
+ When the parent holds its pagination state purely as `offset` (no page number),
77
+ feed `offset` in and read it back from the payload:
78
+
79
+ ```vue
80
+ <FuroPagination
81
+ :parcel="{ offset: pagination.offset, limit: pagination.limit, totalRecords }"
82
+ @page:change="pagination.offset = $event.offset"
83
+ />
84
+ ```
85
+
86
+ ### Link items (SEO anchors)
87
+
88
+ ```vue
89
+ <FuroPagination :parcel="{ limit: 10, totalRecords: 240 }">
90
+ <template #page="{ page }">
91
+ <a :href="`/articles?page=${page}`">{{ page }}</a>
92
+ </template>
93
+ </FuroPagination>
94
+ ```
95
+
96
+ ## Notes
97
+
98
+ - The current page is marked with `data-selected` for styling; the previous/next
99
+ buttons are auto-disabled at the first/last page.
100
+ - Root CSS class: `unit-pagination` (inside `@layer furo`). The `disabled`
101
+ modifier class is applied when the control is disabled.
@@ -0,0 +1,75 @@
1
+ # `<FuroPasswordField>` Interface Specification
2
+
3
+ `<FuroPasswordField>` follows the same architecture as [`<FuroTextField>`](https://chiho-internal.openreach.tech/documents/7762). Read that document first — this page only lists the differences.
4
+
5
+ ---
6
+
7
+ ## Differences from `<FuroTextField>`
8
+
9
+ ### `type` attribute
10
+
11
+ Always renders `type="password"`. The `type` fallthrough attribute is excluded.
12
+
13
+ ### CSS class
14
+
15
+ `.unit-form-control.password` instead of `.unit-form-control.text`.
16
+
17
+ ### `parcel.value` type
18
+
19
+ | Component | `parcel.value` type |
20
+ |---------------------|---------------------------|
21
+ | `<FuroTextField>` | `string \| number \| null` |
22
+ | `<FuroPasswordField>` | `string \| null` |
23
+
24
+ Password fields do not accept `number` values.
25
+
26
+ ### Class naming
27
+
28
+ All class and type names are prefixed with `Password` instead of `Text`:
29
+
30
+ | Role | `FuroTextField` | `FuroPasswordField` |
31
+ |---------------|--------------------------|--------------------------------|
32
+ | Component | `FuroTextField` | `FuroPasswordField` |
33
+ | Context | `FuroTextFieldContext` | `FuroPasswordFieldContext` |
34
+ | EmitPayload | `TextFieldEmitPayload` | `PasswordFieldEmitPayload` |
35
+ | Parcel typedef | `FuroTextFieldParcel` | `FuroPasswordFieldParcel` |
36
+
37
+ ### `PasswordFieldEmitPayload`
38
+
39
+ `PasswordFieldEmitPayload` extends [`BaseEmitPayload`](../lib/emit-payload/BaseEmitPayload.js) with no additional members beyond the shared text-field helpers. The API (`.value`, `.extractTrimmedValue()`, `.hasEmptyText()`, `.hasEnteredTrimmedText()`, etc.) matches [`TextFieldEmitPayload`](./furo-text-field.md#textfieldemitpayload-api), which also extends `BaseEmitPayload`.
40
+
41
+ ---
42
+
43
+ ## Example
44
+
45
+ ```html
46
+ <FuroPasswordField
47
+ :parcel="{
48
+ value: null,
49
+ invalid: context.statusReactive.isPasswordInvalid,
50
+ }"
51
+ :required="true"
52
+ placeholder="Enter your password"
53
+ name="password"
54
+ autocomplete="current-password"
55
+ @commit-value="context.onCommitValue({
56
+ payload: $event
57
+ })"
58
+ />
59
+ ```
60
+
61
+ ```js
62
+ onCommitValue ({
63
+ payload
64
+ }) {
65
+ if (payload.hasEmptyTrimmedText()) {
66
+ this.statusReactive.isPasswordInvalid = true
67
+
68
+ return
69
+ }
70
+
71
+ this.statusReactive.isPasswordInvalid = !this.validatesPasswordLength({ payload })
72
+ }
73
+ ```
74
+
75
+ ---
@@ -0,0 +1,114 @@
1
+ # FuroPopover
2
+
3
+ Click-triggered overlay anchored to a trigger element — inline cell / field edit surfaces, action panels, and detail popouts. Built on a headless popover primitive that handles positioning, focus management, and dismissal (outside click, Escape). Unlike a tooltip it is click-activated and holds arbitrary interactive content, and it reacts only to open-state changes.
4
+
5
+ ## Summary
6
+
7
+ | Field | Value |
8
+ |---|---|
9
+ | **Layer** | Molecule |
10
+ | **Path** | `lib/components/molecules/FuroPopover/` |
11
+ | **Base primitive** | Headless popover (`Root` + `Trigger` + `Portal` + `Content`) |
12
+ | **Composes** | The trigger slot content plus a portaled content surface |
13
+ | **Emits** | `open:change` |
14
+ | **Controlled** | Optional via `parcel.open` |
15
+
16
+ ---
17
+
18
+ ## Public API
19
+
20
+ ### Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `parcel` | `PopoverParcel \| null` | `null` | Reactive data object holding positioning, disabled, modal, and optional controlled open state. |
25
+
26
+ ### Parcel keys
27
+
28
+ | Key | Type | Default | Description |
29
+ |---|---|---|---|
30
+ | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'bottom'` | Edge of the trigger the content prefers. |
31
+ | `align` | `'start' \| 'center' \| 'end'` | `'center'` | Alignment along the chosen side. |
32
+ | `sideOffset` | `number` | `4` | Gap in pixels between trigger and content. |
33
+ | `disabled` | `boolean` | `false` | When `true`, the trigger never opens the popover. |
34
+ | `modal` | `boolean` | `false` | When `true`, focus is trapped and interaction outside the content is blocked while open. |
35
+ | `open` | `boolean \| null` | `null` | When set (non-null), open state is controlled by the consumer; otherwise the component manages it internally. |
36
+
37
+ ### Emits
38
+
39
+ | Event | Payload | Description |
40
+ |---|---|---|
41
+ | `open:change` | `{ open: boolean }` | Fired whenever the open state changes (trigger click, outside click, or Escape). |
42
+
43
+ ### Slots
44
+
45
+ | Slot | Description |
46
+ |---|---|
47
+ | `trigger` | The element the popover is anchored to. Focusable by default (rendered as a button). |
48
+ | `content` | The popover body. Holds arbitrary interactive content. |
49
+
50
+ ---
51
+
52
+ ## Behavior
53
+
54
+ | Concern | Behavior |
55
+ |---|---|
56
+ | **Open trigger** | Clicking (or activating via keyboard) the trigger toggles the popover. |
57
+ | **Controlled / uncontrolled** | When `parcel.open` is non-null, the consumer owns the state; otherwise an internal ref tracks it. Either way `open:change` fires. |
58
+ | **Disabled** | `parcel.disabled` forwards to the trigger so it never opens the popover. |
59
+ | **Modal** | `parcel.modal` traps focus inside the content and blocks outside interaction while open. |
60
+ | **Positioning** | `side` / `align` / `sideOffset` bind to the portaled content surface; the primitive flips to stay in view. |
61
+ | **Dismissal** | Outside click and Escape close the popover; focus returns to the trigger. |
62
+ | **Motion** | Content fades + zooms (95% → 100%) on open, anchored to the trigger via the primitive's transform-origin, and fades out on close, then stays hidden (`animation-fill-mode: forwards` ending in `visibility: hidden`) so a closed node that outlives the primitive's unmount is neither drawn nor clickable. Disabled under `prefers-reduced-motion: reduce`. |
63
+ | **Passthrough** | Extra attributes fall through to the trigger (`inheritAttrs: false` + `v-bind="$attrs"`), so consumers can pass `as-child` / `as` to wrap non-button content. |
64
+
65
+ ---
66
+
67
+ ## Usage
68
+
69
+ ```vue
70
+ <!-- Inline field edit -->
71
+ <FuroPopover :parcel="{ side: 'bottom' }">
72
+ <template #trigger>
73
+ <FuroButton :parcel="{ label: 'Edit' }" />
74
+ </template>
75
+ <template #content>
76
+ <FuroTextField v-model:value="draft.name" :parcel="draft" />
77
+ </template>
78
+ </FuroPopover>
79
+
80
+ <!-- Controlled open state -->
81
+ <FuroPopover
82
+ :parcel="{ open: context.statusReactive.isEditOpen }"
83
+ @open:change="context.onOpenChange({ payload: $event })"
84
+ >
85
+ <template #trigger>
86
+ <FuroButton :parcel="{ label: 'Open' }" />
87
+ </template>
88
+ <template #content>
89
+ Panel body
90
+ </template>
91
+ </FuroPopover>
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Composition notes
97
+
98
+ - Imports nothing from the app — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
99
+ - Public parcel keys are value-oriented; none expose the underlying primitive's API.
100
+ - All logic lives in `FuroPopoverContext`: boolean methods (`isOpenControlled()`, `isDisabled()`, `isModal()`), the positioning extractor (`extractContentAttributes()`), and the open-state resolver/handler (`extractOpenValue()`, `onOpenChange()`).
101
+ - CSS is wrapped in `@layer furo`; dimensions use `rem` and reference only semantic tokens. The surface uses `--color-background` / `--color-border` / `--color-text` so it stays readable against either theme.
102
+
103
+ ---
104
+
105
+ ## Testing
106
+
107
+ - Context logic is fully covered by jsdom unit tests: boolean methods (`isOpenControlled()`, `isDisabled()`, `isModal()`) split into truthy / falsy describe blocks, `extractContentAttributes()` defaults, `extractOpenValue()` (controlled + uncontrolled), and `onOpenChange()` (emits `open:change`, mutates the ref only when uncontrolled).
108
+ - No SFC mount test: behavior is declarative bindings over the tested context plus the headless primitive's own open/dismiss/focus semantics. Rendered output is verified by manual QA in the playground.
109
+
110
+ ---
111
+
112
+ ## Playground
113
+
114
+ `/samples/components/playground` → **Popover** tab. Demonstrates default (bottom), right-side start-aligned, modal, and disabled popovers.