@openreachtech/furo-vue 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +2 -0
- package/README.md +2 -0
- package/docs/design-tokens.md +419 -0
- package/docs/furo-accordion.md +109 -0
- package/docs/furo-alert-dialog.md +107 -0
- package/docs/furo-attachment.md +133 -0
- package/docs/furo-autocomplete-field.md +719 -0
- package/docs/furo-avatar-group.md +87 -0
- package/docs/furo-avatar.md +101 -0
- package/docs/furo-badge.md +126 -0
- package/docs/furo-breadcrumb.md +89 -0
- package/docs/furo-button.md +263 -0
- package/docs/furo-checkbox.md +384 -0
- package/docs/furo-collapsible.md +104 -0
- package/docs/furo-control-block.md +102 -0
- package/docs/furo-date-picker.md +270 -0
- package/docs/furo-date-time-picker.md +110 -0
- package/docs/furo-dialog.md +88 -0
- package/docs/furo-drawer.md +122 -0
- package/docs/furo-dropdown-menu.md +298 -0
- package/docs/furo-editable-field.md +391 -0
- package/docs/furo-editor.md +304 -0
- package/docs/furo-email-field.md +93 -0
- package/docs/furo-empty-state.md +97 -0
- package/docs/furo-error-state.md +97 -0
- package/docs/furo-file-field.md +163 -0
- package/docs/furo-icon.md +288 -0
- package/docs/furo-number-field.md +397 -0
- package/docs/furo-pagination.md +101 -0
- package/docs/furo-password-field.md +75 -0
- package/docs/furo-popover.md +114 -0
- package/docs/furo-radio.md +125 -0
- package/docs/furo-scroll-area.md +112 -0
- package/docs/furo-search-field.md +94 -0
- package/docs/furo-section-header.md +135 -0
- package/docs/furo-select.md +443 -0
- package/docs/furo-separator.md +87 -0
- package/docs/furo-sidebar.md +178 -0
- package/docs/furo-skeleton.md +92 -0
- package/docs/furo-splitter.md +103 -0
- package/docs/furo-stepper.md +127 -0
- package/docs/furo-switch.md +107 -0
- package/docs/furo-table.md +233 -0
- package/docs/furo-tabs.md +173 -0
- package/docs/furo-text-field.md +249 -0
- package/docs/furo-textarea.md +214 -0
- package/docs/furo-time-field.md +276 -0
- package/docs/furo-toast.md +217 -0
- package/docs/furo-toggle-group.md +123 -0
- package/docs/furo-toggle.md +104 -0
- package/docs/furo-tool-bar.md +88 -0
- package/docs/furo-tooltip.md +107 -0
- package/lib/assets/css/furo/0030.variables-semantic-color.css +15 -1
- package/lib/components/atoms/FuroButton/FuroButtonContext.js +8 -8
- package/lib/components/atoms/FuroCheckbox/FuroCheckboxContext.js +1 -1
- package/lib/components/atoms/FuroIcon/FuroIcon.vue +1 -1
- package/lib/components/atoms/FuroSwitch/FuroSwitchContext.js +1 -1
- package/lib/components/atoms/FuroToggle/FuroToggleContext.js +1 -1
- package/lib/components/molecules/FuroAccordion/FuroAccordionContext.js +1 -1
- package/lib/components/molecules/FuroAttachment/FuroAttachment.vue +1 -1
- package/lib/components/molecules/FuroBreadcrumb/FuroBreadcrumb.vue +1 -1
- package/lib/components/molecules/FuroCollapsible/FuroCollapsibleContext.js +1 -1
- package/lib/components/molecules/FuroControlBlock/FuroControlBlock.vue +2 -2
- package/lib/components/molecules/FuroDropdownMenu/FuroDropdownMenu.vue +1 -1
- package/lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuContext.js +1 -2
- package/lib/components/molecules/FuroErrorState/FuroErrorState.vue +1 -1
- package/lib/components/molecules/FuroPopover/FuroPopoverContext.js +1 -1
- package/lib/components/molecules/FuroSearchField/FuroSearchField.vue +1 -1
- package/lib/components/molecules/FuroSelect/FuroSelect.vue +8 -6
- package/lib/components/molecules/FuroSelect/FuroSelectContext.js +35 -0
- package/lib/components/molecules/FuroSelect/FuroSelectOptionText.vue +40 -0
- package/lib/components/molecules/FuroSelect/FuroSelectOptionTextContext.js +275 -0
- package/lib/components/molecules/FuroSplitter/FuroSplitterContext.js +1 -1
- package/lib/components/molecules/FuroToggleGroup/FuroToggleGroupContext.js +1 -1
- package/lib/components/molecules/FuroTooltip/FuroTooltipContext.js +1 -1
- package/lib/components/organisms/FuroSidebar/FuroSidebar.vue +3 -3
- package/lib/components/organisms/FuroTable/FuroTable.vue +1 -1
- package/lib/components/organisms/FuroTable/FuroTableContext.js +3 -3
- package/lib/components/organisms/FuroToast/FuroToastContext.js +6 -2
- package/lib/components/organisms/FuroToast/ToastQueue.js +4 -4
- package/nuxt/index.js +9 -9
- package/package.json +23 -22
- package/public/furo-vue/components.json +1 -1
- package/types/generated/components/atoms/FuroButton/FuroButtonContext.d.ts +4 -4
- package/types/generated/components/molecules/FuroSelect/FuroSelect.vue.d.ts +23 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectContext.d.ts +14 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectOptionText.vue.d.ts +50 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectOptionTextContext.d.ts +141 -0
- package/types/generated/components/organisms/FuroTable/FuroTableContext.d.ts +2 -2
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# `<FuroRadio>` Interface Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`<FuroRadio>` is a **shipped** radio atom in the Furo-Vue component system. Its **public contract is Furo-defined**: application developers pass `parcel`, listen to Furo emits, and use documented slots — without importing the underlying primitive library.
|
|
6
|
+
|
|
7
|
+
Implementation uses a headless radio-group primitive inside `lib/components/atoms/FuroRadio/*` only. That dependency is **not** part of the application API.
|
|
8
|
+
|
|
9
|
+
Contract style aligns with [`FuroCheckbox`](./furo-checkbox.md) and [`FuroTextField`](./furo-text-field.md):
|
|
10
|
+
|
|
11
|
+
- behavior via `:parcel` (`FuroRadioParcel`)
|
|
12
|
+
- HTML / form attributes via fallthrough (`name`, `id`, `aria-*`, …)
|
|
13
|
+
- Furo emits (`change-value`, `update:value`)
|
|
14
|
+
- `v-model:value` syncs the chosen option's value
|
|
15
|
+
|
|
16
|
+
**Public SFC prop:** `parcel` only.
|
|
17
|
+
|
|
18
|
+
**Model shape:** the **value of the chosen option** (`string | number`), or `null` when nothing is chosen. Unlike `FuroCheckbox`, the model is not a boolean: a radio group answers "which one", not "yes or no".
|
|
19
|
+
|
|
20
|
+
### One component, not one per button
|
|
21
|
+
|
|
22
|
+
`FuroRadio` is the whole group, not a single button. A lone radio button is not a usable control — the exclusivity that distinguishes a radio from a checkbox only exists across a set — so the group is the smallest thing worth shipping. Options are declared as data through `parcel.options`, the same way `FuroSelect` declares its choices.
|
|
23
|
+
|
|
24
|
+
`@change-value` fires on every committed choice.
|
|
25
|
+
|
|
26
|
+
Read [`FuroTextField`](./furo-text-field.md) for the shared `parcel` mental model.
|
|
27
|
+
|
|
28
|
+
### Abstraction boundary
|
|
29
|
+
|
|
30
|
+
| Layer | Responsibility |
|
|
31
|
+
| ----- | -------------- |
|
|
32
|
+
| Application | `parcel`, listeners, slots; controls `parcel.value` |
|
|
33
|
+
| Atom (`FuroRadio`) | Maps parcel → the underlying radio-group primitive; DOM, emits, default indicator |
|
|
34
|
+
| Underlying primitive (maintainers) | Internal only; not part of the application API |
|
|
35
|
+
|
|
36
|
+
**Rules:**
|
|
37
|
+
|
|
38
|
+
1. Documented `parcel` keys are the **Furo contract**. Furo-owned keys are stripped in `extractRootAttributes()` before optional passthrough to the underlying primitive.
|
|
39
|
+
2. Do **not** import the underlying radio-group primitive in application code.
|
|
40
|
+
3. An option's own `disabled` is additive to the group's: a group that is disabled disables every option regardless of what each option says.
|
|
41
|
+
|
|
42
|
+
**Source files:**
|
|
43
|
+
|
|
44
|
+
- `lib/components/atoms/FuroRadio/FuroRadio.vue`
|
|
45
|
+
- `lib/components/atoms/FuroRadio/FuroRadioContext.js`
|
|
46
|
+
- `lib/components/atoms/FuroRadio/RadioEmitPayload.js`
|
|
47
|
+
|
|
48
|
+
**Live samples:** `pages/samples/components/playground/FuroRadioPlayground/index.vue`
|
|
49
|
+
|
|
50
|
+
## Summary
|
|
51
|
+
|
|
52
|
+
| Field | Value |
|
|
53
|
+
|---|---|
|
|
54
|
+
| **Layer** | Atom |
|
|
55
|
+
| **Path** | `lib/components/atoms/FuroRadio/` |
|
|
56
|
+
| **Base primitive** | Headless radio group (`Root` + option + indicator) |
|
|
57
|
+
| **Value** | `string \| number \| null` via `v-model:value` / `parcel.value` |
|
|
58
|
+
| **Emits** | `change-value`, `update:value` |
|
|
59
|
+
|
|
60
|
+
## `parcel` keys
|
|
61
|
+
|
|
62
|
+
| Key | Type | Default | Meaning |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `value` | `string \| number \| null` | — | The chosen option's value. Supplying this key at all makes the group **controlled**. |
|
|
65
|
+
| `options` | `Array<FuroRadioOption>` | `[]` | The choices to render, in order. |
|
|
66
|
+
| `invalid` | `boolean` | `false` | Renders the error appearance and sets `aria-invalid`. |
|
|
67
|
+
| `disabled` | `boolean` | `false` | Disables every option. |
|
|
68
|
+
| `required` | `boolean` | `false` | Marks the group required. |
|
|
69
|
+
| `size` | `string` | `'1.125rem'` | Diameter of one radio circle. The indicator dot is derived from it. |
|
|
70
|
+
|
|
71
|
+
### `FuroRadioOption`
|
|
72
|
+
|
|
73
|
+
| Key | Type | Default | Meaning |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| `label` | `string` | — | Text shown beside the circle. |
|
|
76
|
+
| `value` | `string \| number` | — | The value reported when this option is chosen. |
|
|
77
|
+
| `disabled` | `boolean` | `false` | Withdraws this one option while leaving the rest usable. |
|
|
78
|
+
|
|
79
|
+
## Controlled and uncontrolled
|
|
80
|
+
|
|
81
|
+
The group is **controlled** when `parcel` owns a `value` key, even when that key holds `null`. A controlled group never follows its own interaction: it emits, and re-renders only when the parent sends a new `value` back. This is the same rule `FuroCheckbox` follows, so the two controls behave alike when composed in one form.
|
|
82
|
+
|
|
83
|
+
Omit the key entirely for an **uncontrolled** group, which starts with nothing chosen and then tracks its own state.
|
|
84
|
+
|
|
85
|
+
## Emits
|
|
86
|
+
|
|
87
|
+
| Event | Payload | When |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `change-value` | `RadioEmitPayload` | The user chooses an option. |
|
|
90
|
+
| `update:value` | `string \| number \| null` | Same moment; this is the `v-model:value` channel. |
|
|
91
|
+
|
|
92
|
+
### `RadioEmitPayload`
|
|
93
|
+
|
|
94
|
+
| Member | Returns | Meaning |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `value` | `string \| number \| null` | The chosen option's value. |
|
|
97
|
+
| `controlElement` | `HTMLElement \| null` | The group's root element. |
|
|
98
|
+
| `isSelected()` | `boolean` | Whether an option is chosen. A group with nothing chosen reports `false`. |
|
|
99
|
+
|
|
100
|
+
## Slots
|
|
101
|
+
|
|
102
|
+
| Slot | Scope | Purpose |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `option-label` | `{ option }` | Replaces the default label text for every option, so an application can render a description, a price, or an icon beside the choice. |
|
|
105
|
+
|
|
106
|
+
## Labels
|
|
107
|
+
|
|
108
|
+
Each option's circle and text are wrapped in one `<label>`. A `<button>` is a labelable element, so clicking the text activates the option without the component having to mint an `id` per option.
|
|
109
|
+
|
|
110
|
+
## Styling
|
|
111
|
+
|
|
112
|
+
| Part | Class | Notes |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| Group root | `.unit-form-control.radio-group` | Gains `.invalid` and `.disabled`. |
|
|
115
|
+
| One option | `.option` | Gains `.disabled`. |
|
|
116
|
+
| The circle | `.radio` | Carries `data-state="checked"` when chosen. |
|
|
117
|
+
| The dot | `.indicator` | Sized from `--furo-radio-dot-size`. |
|
|
118
|
+
| Label text | `.label` | Only rendered by the default slot content. |
|
|
119
|
+
|
|
120
|
+
Colour comes from the `--color-selected` family, the role that means "the user chose this". It is deliberately distinct from `--color-primary`, which means "this is the action" and is a dark neutral in the shipped theme. Override `--color-selected` to re-theme every selection control at once.
|
|
121
|
+
|
|
122
|
+
| Custom property | Default | Meaning |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `--furo-radio-size` | `1.125rem` | Diameter of the circle; set from `parcel.size`. |
|
|
125
|
+
| `--furo-radio-dot-size` | `0.5rem` | Diameter of the dot; derived from the circle. |
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# FuroScrollArea
|
|
2
|
+
|
|
3
|
+
Presentational scroll container that wraps overflowing content with a styled, cross-browser scrollbar. Built on a headless scroll-area primitive that hides the native scrollbar and renders a consistent custom one across browsers. It carries no value and emits nothing.
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
| Field | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| **Layer** | Molecule |
|
|
10
|
+
| **Path** | `lib/components/molecules/FuroScrollArea/` |
|
|
11
|
+
| **Base primitive** | Headless scroll-area (`Root` + `Viewport` + `Scrollbar` + `Thumb` + `Corner`) |
|
|
12
|
+
| **Composes** | Nothing — a single styled scroll region |
|
|
13
|
+
| **Emits** | None |
|
|
14
|
+
| **Orientation** | `vertical` (default), `horizontal`, or `both` |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Public API
|
|
19
|
+
|
|
20
|
+
### Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `parcel` | `FuroScrollAreaParcel \| null` | `null` | Reactive data object holding orientation and scrollbar visibility. |
|
|
25
|
+
|
|
26
|
+
### Parcel keys
|
|
27
|
+
|
|
28
|
+
| Key | Type | Default | Description |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `orientation` | `'vertical' \| 'horizontal' \| 'both'` | `'vertical'` | Which scrollbars render, and which axes are scrollable. `both` also renders the corner where they meet. |
|
|
31
|
+
| `scrollbarVisibility` | `'hover' \| 'scroll' \| 'glimpse' \| 'auto' \| 'always'` | unset (`hover`) | **When the scrollbar appears** — it does not affect whether the content scrolls (see Behavior). When unset, the primitive default (`hover`) applies. |
|
|
32
|
+
| `scrollHideDelay` | `number` | unset | Milliseconds before the scrollbar hides after the trigger ends (applies to `hover` / `scroll` / `glimpse`). Forwarded to the primitive. |
|
|
33
|
+
|
|
34
|
+
#### `scrollbarVisibility` values
|
|
35
|
+
|
|
36
|
+
| Value | Scrollbar shown |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `hover` (default) | While the pointer is over the scroll area; hides after `scrollHideDelay`. |
|
|
39
|
+
| `scroll` | While scrolling; hides after `scrollHideDelay`. |
|
|
40
|
+
| `glimpse` | Briefly revealed on pointer enter, then hidden. |
|
|
41
|
+
| `auto` | Whenever the axis overflows (native-like overlay), hidden otherwise. |
|
|
42
|
+
| `always` | Always on screen. Best for wide tables / lists where the overflow must be discoverable at a glance. |
|
|
43
|
+
|
|
44
|
+
### Emits
|
|
45
|
+
|
|
46
|
+
None. The scroll area is presentational.
|
|
47
|
+
|
|
48
|
+
### Slots
|
|
49
|
+
|
|
50
|
+
| Slot | Description |
|
|
51
|
+
|---|---|
|
|
52
|
+
| default | The overflowing content placed inside the scroll viewport. |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Behavior
|
|
57
|
+
|
|
58
|
+
| Concern | Behavior |
|
|
59
|
+
|---|---|
|
|
60
|
+
| **Orientation** | `parcel.orientation` drives which `Scrollbar` (and `Corner`) renders via `showsVerticalScrollbar()` / `showsHorizontalScrollbar()` / `showsCorner()` — no template branching beyond `v-if`. |
|
|
61
|
+
| **Scrolling vs. visibility** | An axis is scrollable (wheel / touch / keyboard) whenever its scrollbar is rendered — i.e. whenever `orientation` includes that axis. This is **independent of `scrollbarVisibility`**: with the default `hover`, content still scrolls before you hover; only the bar is hidden until you do. An axis not in `orientation` is clipped (`overflow: hidden`) and not scrollable. |
|
|
62
|
+
| **Visibility** | The public `scrollbarVisibility` key maps to the primitive's visibility prop inside `buildOptionalRootAttributes()`; omitted entirely when unset. It changes only *when the bar is shown*, never whether the content scrolls. |
|
|
63
|
+
| **Native scrollbar** | The primitive hides the native scrollbar and renders the styled thumb, so appearance is consistent across browsers. |
|
|
64
|
+
| **Passthrough** | Non-Furo parcel keys (e.g. `scrollHideDelay`, `aria-*`) and extra attributes fall through to the root (`inheritAttrs: false` + `v-bind="$attrs"`). |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
```vue
|
|
71
|
+
<!-- Vertical long list -->
|
|
72
|
+
<FuroScrollArea :parcel="{ orientation: 'vertical' }">
|
|
73
|
+
<ul>
|
|
74
|
+
<li v-for="tag in tags" :key="tag">{{ tag }}</li>
|
|
75
|
+
</ul>
|
|
76
|
+
</FuroScrollArea>
|
|
77
|
+
|
|
78
|
+
<!-- Wide table row, scrollbar always visible -->
|
|
79
|
+
<FuroScrollArea :parcel="{ orientation: 'horizontal', scrollbarVisibility: 'always' }">
|
|
80
|
+
<table>...</table>
|
|
81
|
+
</FuroScrollArea>
|
|
82
|
+
|
|
83
|
+
<!-- Both axes -->
|
|
84
|
+
<FuroScrollArea :parcel="{ orientation: 'both' }">
|
|
85
|
+
<div class="wide-tall-content">...</div>
|
|
86
|
+
</FuroScrollArea>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The scroll area fills its parent (`width: 100%; height: 100%`). Give the parent a bounded size so content can overflow.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Composition notes
|
|
94
|
+
|
|
95
|
+
- Imports nothing from the app — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
|
|
96
|
+
- All logic lives in `FuroScrollAreaContext` as extract-only getters (`orientation`, `scrollbarVisibility`), boolean rendering methods, and the `extractRootClassNames()` / `extractRootAttributes()` mappers.
|
|
97
|
+
- The public `scrollbarVisibility` key is value-oriented; the primitive's own prop name appears only in `buildOptionalRootAttributes()`.
|
|
98
|
+
- CSS is in `@layer furo` and **unscoped**: the underlying viewport is a multi-root (Fragment) component, so a scoped `data-v-*` attribute would not reach its element and the `> .viewport` height rule would not match — collapsing the viewport and breaking scroll. All selectors are namespaced under `.unit-scroll-area`.
|
|
99
|
+
- Dimensions use `rem`; colors reference semantic tokens only (`--color-border` thumb, `--color-ring` on hover). The thumb carries a `::before` that widens its pointer hit-target to ~44px without enlarging the visual bar.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Testing
|
|
104
|
+
|
|
105
|
+
- Context logic is fully covered by jsdom unit tests: the `orientation` / `scrollbarVisibility` getters, the three boolean rendering methods (split into truthy / falsy describe blocks), `extractRootClassNames()`, and `extractRootAttributes()` (visibility-to-`type` mapping plus passthrough).
|
|
106
|
+
- No SFC mount test: behavior is declarative bindings over the tested context plus the headless primitive's own scroll handling. Rendered scrollbars are verified by manual QA in the playground.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Playground
|
|
111
|
+
|
|
112
|
+
`/samples/components/playground` → **Scroll Area** tab. Demonstrates a vertical long list, a wide horizontal table row (always-visible scrollbar), and a both-axes grid.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# `<FuroSearchField>` Interface Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`<FuroSearchField>` is a **shipped** search field molecule in the Furo-Vue component system. It makes every search entry point in an application look and behave the same: one bordered box holding a leading magnifier and a single-line text control.
|
|
6
|
+
|
|
7
|
+
It composes the text field atom rather than restyling an input, so the value contract is the one every other field already uses: `parcel` plus `v-model:value`, with `change-value` and `commit-value` carrying the same payload.
|
|
8
|
+
|
|
9
|
+
**Public SFC prop:** `parcel` only. `class` and `style` land on the wrapper; every other HTML attribute passes through to the inner input.
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
| Field | Value |
|
|
14
|
+
|---|---|
|
|
15
|
+
| **Layer** | Molecule |
|
|
16
|
+
| **Path** | `lib/components/molecules/FuroSearchField/` |
|
|
17
|
+
| **Base primitive** | None; a `div` wrapping the text field atom |
|
|
18
|
+
| **Value** | `string \| number \| null` |
|
|
19
|
+
| **Emits** | `change-value`, `commit-value`, `update:value` |
|
|
20
|
+
| **Composes** | `FuroTextField` for the value, `FuroIcon` for the magnifier |
|
|
21
|
+
|
|
22
|
+
## Why the attributes split
|
|
23
|
+
|
|
24
|
+
The wrapper is the box a consumer lays out, so `class` and `style` stay on it: a layout selector such as `.filters .search { flex: 1 }` has to match something with a width. Everything else describes the control, not the box, so `placeholder`, `disabled`, `name`, `aria-label` and the rest reach the inner input untouched. A consumer therefore writes the same attributes they would write on `<FuroTextField>`, and they land where they already expect.
|
|
25
|
+
|
|
26
|
+
## `parcel` keys
|
|
27
|
+
|
|
28
|
+
| Key | Type | Default | Meaning |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `value` | `string \| number \| null` | `null` | The search text. |
|
|
31
|
+
| `invalid` | `boolean` | `false` | Draws the field in its error tone. |
|
|
32
|
+
|
|
33
|
+
The parcel is deliberately the text field's parcel, minus nothing and plus nothing. A consumer who already knows one field knows this one.
|
|
34
|
+
|
|
35
|
+
## Events
|
|
36
|
+
|
|
37
|
+
| Event | Payload | When |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `change-value` | Field payload | On every keystroke. |
|
|
40
|
+
| `commit-value` | Field payload | On blur and on Enter. |
|
|
41
|
+
| `update:value` | `string \| null` | Alongside both of the above, which is what makes `v-model:value` work. |
|
|
42
|
+
|
|
43
|
+
The payload is the same object the text field atom emits, so `payload.value`, `payload.trimmedValue`, `payload.hasEnteredText()` and `payload.hasEnteredTrimmedText()` all read the same as anywhere else. `hasEnteredTrimmedText()` is the one to reach for before firing a query, because a field holding only spaces is an empty search.
|
|
44
|
+
|
|
45
|
+
## Slots
|
|
46
|
+
|
|
47
|
+
| Slot | Scope | Purpose |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `icon` | — | Replaces the leading mark. Default: a magnifier icon. |
|
|
50
|
+
| `trailing` | — | Content after the control, for a clear button, a shortcut hint, or a result count. |
|
|
51
|
+
|
|
52
|
+
## States
|
|
53
|
+
|
|
54
|
+
| State | What changes |
|
|
55
|
+
|---|---|
|
|
56
|
+
| Rest | Border in the input tone, placeholder in the caption tone. |
|
|
57
|
+
| Focus | Border turns the focus-ring tone. The whole wrapper reacts, because focus lands on the control inside it. |
|
|
58
|
+
| Filled | Nothing structural; the text simply reads in the body tone instead of the caption tone. |
|
|
59
|
+
| Invalid | Border turns the destructive tone, and stays destructive while focused. |
|
|
60
|
+
| Disabled | The wrapper dims and takes a not-allowed cursor. |
|
|
61
|
+
|
|
62
|
+
## Styling
|
|
63
|
+
|
|
64
|
+
| Part | Class | Notes |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| Root | `.unit-form-control.search` | Gains `.invalid` and `.disabled` |
|
|
67
|
+
| The leading mark | `.leading` | `aria-hidden`, so the magnifier is not announced |
|
|
68
|
+
| The magnifier | `.leading-icon` | Only rendered by the default slot content |
|
|
69
|
+
| The control | `.unit-form-control.text` | The text field atom, stripped of its own border and padding |
|
|
70
|
+
|
|
71
|
+
The wrapper draws the border, the radius, the height and the background; the control inside draws only text. That is the one place this molecule reaches into the atom it composes, and it is unavoidable: a shared box around an icon and an input cannot have two borders.
|
|
72
|
+
|
|
73
|
+
## Read from the design
|
|
74
|
+
|
|
75
|
+
| Part | Design value | Token |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| The box | `#ffffff` | `--color-background` |
|
|
78
|
+
| Its border | `1px` `#c4c7cf` | `--size-thinnest`, `--color-input` |
|
|
79
|
+
| Its border, focused | `#3b82f6` | `--color-ring` |
|
|
80
|
+
| Its corners | `8px` | `--size-border-radius-medium` |
|
|
81
|
+
| Its height | `32px` | `--size-input-height` |
|
|
82
|
+
| Between the mark and the text | `8px` | `--size-space-small` |
|
|
83
|
+
| Its inline padding | `12px` | `0.75rem` |
|
|
84
|
+
| The magnifier | `16px`, `#8b93a2` | `1rem`, `--color-foreground-glyph` |
|
|
85
|
+
| The placeholder | `#8b93a2`, regular 13.5px/20 | `--color-foreground-tertiary`, `--font-body-medium` |
|
|
86
|
+
| The entered text | `#424752`, regular 13.5px/20 | `--color-foreground`, `--font-body-medium` |
|
|
87
|
+
|
|
88
|
+
The design's `8px` block padding is carried by the `32px` height plus vertical centring rather than by a `padding-block`, which is the same `32px` every other single-line control in the library stands at.
|
|
89
|
+
|
|
90
|
+
The design draws no clear affordance and no hover state for the field itself, so neither is invented here; the `trailing` slot is where a clear button goes. It also draws the field appearing from a magnifier button and disappearing on a click outside. That is a disclosure the consumer composes, not a behaviour of the field: a molecule that opened and closed itself would be deciding when an application searches.
|
|
91
|
+
|
|
92
|
+
## Accessibility
|
|
93
|
+
|
|
94
|
+
The wrapper is a plain `div` and carries no landmark role, because a landmark is a claim about the page rather than about the control. Give the input an `aria-label` (or an associated `<label>` through `FuroControlBlock`) so the field is announced; the magnifier is `aria-hidden` and says nothing.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# `<FuroSectionHeader>` Interface Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`<FuroSectionHeader>` is a **shipped** section-header molecule in the Furo-Vue component system. It names a region of a long form or a detail panel and closes it off from what follows with a rule, so every group boundary in an application is drawn the same way.
|
|
6
|
+
|
|
7
|
+
It is purely presentational: it carries no value and emits nothing.
|
|
8
|
+
|
|
9
|
+
**Public SFC prop:** `parcel` only. HTML attributes pass through to the root.
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
| Field | Value |
|
|
14
|
+
|---|---|
|
|
15
|
+
| **Layer** | Molecule |
|
|
16
|
+
| **Path** | `lib/components/molecules/FuroSectionHeader/` |
|
|
17
|
+
| **Base primitive** | None; a heading beside a trailing region |
|
|
18
|
+
| **Value** | None |
|
|
19
|
+
| **Emits** | None |
|
|
20
|
+
| **Composes** | `FuroSeparator` |
|
|
21
|
+
|
|
22
|
+
## `parcel` keys
|
|
23
|
+
|
|
24
|
+
| Key | Type | Default | Meaning |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| `title` | `string` | `''` | Text the heading renders. |
|
|
27
|
+
| `headingLevel` | `number` | `2` | Depth of the heading in the consumer's document outline. Values outside one to six are clamped to the nearest level HTML defines. |
|
|
28
|
+
| `size` | `'large' \| 'medium'` | `'large'` | Type step the title renders at, matching the two the design draws. |
|
|
29
|
+
| `divided` | `boolean` | `true` | Whether a rule closes the header off from what follows it. |
|
|
30
|
+
|
|
31
|
+
## The heading is a real heading
|
|
32
|
+
|
|
33
|
+
The title renders as `h1` through `h6`, chosen by `headingLevel`, not as a styled `div`. A consumer therefore keeps a document outline a screen-reader user can navigate by heading, and the library never forces markup that reads as decoration.
|
|
34
|
+
|
|
35
|
+
The level is a parcel key rather than a fixed choice because only the consumer knows how deep the section sits. The default of two suits a section inside a page that already carries an `h1`.
|
|
36
|
+
|
|
37
|
+
## The rule is the separator, not a border
|
|
38
|
+
|
|
39
|
+
`divided` renders `FuroSeparator` across the block-end edge rather than a `border-block-end` on the root, so every rule in the library is the same object and changes in one place. The rule is marked decorative: the heading already tells a screen reader where the boundary is, and announcing it twice adds nothing.
|
|
40
|
+
|
|
41
|
+
`divided` defaults to `true` because the divider is what the component is for. A header sitting at the top of a surface whose content draws its own top border passes `divided: false`.
|
|
42
|
+
|
|
43
|
+
## Slots
|
|
44
|
+
|
|
45
|
+
| Slot | Scope | Purpose |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `leading` | none | Content before the heading, for example the back control the design draws on a sub-section. |
|
|
48
|
+
| `title` | none | Replaces the heading's text. Default: `parcel.title`. |
|
|
49
|
+
| `actions` | none | The trailing region. The region is dropped entirely when the slot is empty, so it takes no space. |
|
|
50
|
+
|
|
51
|
+
The trailing region is a slot rather than a list of button parcels. A header carries whatever controls the screen needs, and a parcel key per control would grow one key per screen while still not covering the next one.
|
|
52
|
+
|
|
53
|
+
## Styling
|
|
54
|
+
|
|
55
|
+
| Part | Class | Notes |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| Root | `.unit-section-header` | Also carries `size-large` or `size-medium` |
|
|
58
|
+
| The row | `.row` | Heading and trailing region, spaced apart |
|
|
59
|
+
| Heading side | `.lead` | The `leading` slot beside the heading |
|
|
60
|
+
| The heading | `.title` | The `h1` to `h6` element |
|
|
61
|
+
| Trailing region | `.actions` | Only rendered when the `actions` slot carries content |
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```vue
|
|
66
|
+
<!-- A list panel header, its rule left to the table below it -->
|
|
67
|
+
<FuroSectionHeader
|
|
68
|
+
:parcel="{
|
|
69
|
+
title: 'Employees',
|
|
70
|
+
divided: false,
|
|
71
|
+
}"
|
|
72
|
+
>
|
|
73
|
+
<template #actions>
|
|
74
|
+
<FuroButton :parcel="{ variant: 'ghost' }">New Program</FuroButton>
|
|
75
|
+
<FuroSeparator :parcel="{ orientation: 'vertical' }" />
|
|
76
|
+
<FuroButton :parcel="{ variant: 'ghost' }">New Product</FuroButton>
|
|
77
|
+
</template>
|
|
78
|
+
</FuroSectionHeader>
|
|
79
|
+
|
|
80
|
+
<!-- A sub-section of a detail panel, with a back control -->
|
|
81
|
+
<FuroSectionHeader
|
|
82
|
+
:parcel="{
|
|
83
|
+
title: 'Customize View',
|
|
84
|
+
size: 'medium',
|
|
85
|
+
headingLevel: 3,
|
|
86
|
+
}"
|
|
87
|
+
>
|
|
88
|
+
<template #leading>
|
|
89
|
+
<FuroButton
|
|
90
|
+
:parcel="{ variant: 'ghost', size: 'icon' }"
|
|
91
|
+
@click="context.goBack()"
|
|
92
|
+
>
|
|
93
|
+
<FuroIcon name="ph:caret-left" />
|
|
94
|
+
</FuroButton>
|
|
95
|
+
</template>
|
|
96
|
+
</FuroSectionHeader>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Read from the design
|
|
100
|
+
|
|
101
|
+
Every value is a semantic token. Nothing is transcribed as a hex or a raw size:
|
|
102
|
+
|
|
103
|
+
| Design value | Token |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `Grayscale/Text/Title` | `--color-foreground-title` |
|
|
106
|
+
| `white` surface | `--color-card` |
|
|
107
|
+
| `Corner/3xl (16)`, top corners | `--size-border-radius-2x-large` |
|
|
108
|
+
| `Spacing/lg (20)`, inline padding | `1.25rem` |
|
|
109
|
+
| `Spacing/sm (12)`, block padding | `0.75rem` |
|
|
110
|
+
| `Spacing/xxs (4)`, beside the heading | `--size-space-tiny` |
|
|
111
|
+
| `Spacing/sm (12)`, between trailing controls | `--size-space-compact` |
|
|
112
|
+
| `Label/Large` | `--font-label-large` |
|
|
113
|
+
| `Label/Medium` | `--font-label-medium` |
|
|
114
|
+
|
|
115
|
+
Two values are recorded rather than matched:
|
|
116
|
+
|
|
117
|
+
- The row is 56px tall in both instances the design draws. There is no height token for it, so it is a plain `3.5rem` minimum, the same treatment the other non-scale dimensions get.
|
|
118
|
+
- The design draws the rule in `Grayscale/Border/Disabled` (`#f1f2f4`), one step lighter than `Grayscale/Border/Default` (`#e0e2e6`) which `--color-border` carries. `FuroSeparator` has no colour channel, so the rule renders at the library's default border. Giving the separator a colour channel is a change to that component and is recorded here rather than made from this one.
|
|
119
|
+
|
|
120
|
+
The design draws no supporting text under the title, so no key carries one.
|
|
121
|
+
|
|
122
|
+
## Composition notes
|
|
123
|
+
|
|
124
|
+
- Imports nothing from a higher layer and nothing from the app: no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
|
|
125
|
+
- The root is a `div` rather than a `header`. A `header` placed outside sectioning content becomes a `banner` landmark, and a library component cannot know how a consumer nests it.
|
|
126
|
+
- All logic lives in `FuroSectionHeaderContext`; the template only calls context members.
|
|
127
|
+
- CSS is wrapped in `@layer furo`, references semantic tokens only, and uses `--size-space-*` for `gap` and `rem` everywhere else.
|
|
128
|
+
|
|
129
|
+
## Testing
|
|
130
|
+
|
|
131
|
+
`FuroSectionHeaderContext` is covered by jsdom unit tests: the title, size and heading-level getters including the clamp at both ends, `isDivided()` and `hasActions()` split into truthy and falsy blocks, the heading tag, the separator parcel, the root class map, and the passthrough filter that withholds the Furo-owned keys.
|
|
132
|
+
|
|
133
|
+
## Playground
|
|
134
|
+
|
|
135
|
+
`/samples/components/playground` → **Section Header** tab. Demonstrates both type steps, the header with and without its rule, a trailing action region, and a leading control.
|