@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,163 @@
|
|
|
1
|
+
# `<FuroFileField>` Interface Specification
|
|
2
|
+
|
|
3
|
+
`<FuroFileField>` follows the same **architectural split** as [`<FuroTextField>`](./furo-text-field.md): `:parcel` for Furo behavior, **fallthrough** for native `<input>` attributes. Read [`furo-text-field.md`](./furo-text-field.md) first for the `parcel` vs fallthrough rule and for **Event Handler Style in Context** (single object parameter destructuring).
|
|
4
|
+
|
|
5
|
+
This page documents **only** what differs for a file input atom. The component is specified here; implementation may land in the codebase separately.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Differences from `<FuroTextField>`
|
|
10
|
+
|
|
11
|
+
### `type` attribute
|
|
12
|
+
|
|
13
|
+
Always renders `type="file"`. The `type` fallthrough attribute is excluded — the underlying control is always an HTML file input (`<input type="file">`).
|
|
14
|
+
|
|
15
|
+
### CSS class
|
|
16
|
+
|
|
17
|
+
`.unit-form-control.file` instead of `.unit-form-control.text`.
|
|
18
|
+
|
|
19
|
+
### No `parcel.value`
|
|
20
|
+
|
|
21
|
+
`<FuroTextField>` uses `parcel.value` for a controlled string. **`<FuroFileField>` does not define `parcel.value`.**
|
|
22
|
+
|
|
23
|
+
Browsers do not allow scripts to set the file input’s value to a user-chosen path for security reasons. Selection state therefore **cannot** be mirrored the same way as text. The parent learns the current `FileList` through **`@change-value`** and `FileFieldEmitPayload` only.
|
|
24
|
+
|
|
25
|
+
### `parcel` shape
|
|
26
|
+
|
|
27
|
+
Behavior-controlling props passed as a single object. **No `value` key.**
|
|
28
|
+
|
|
29
|
+
| Key | Type | Required | Default | Description |
|
|
30
|
+
|-----------|-----------|----------|---------|-------------------------------------|
|
|
31
|
+
| `invalid` | `boolean` | No | `false` | Applies invalid/error visual state. |
|
|
32
|
+
|
|
33
|
+
> All values must be **JSON-serializable** (no functions, no class instances).
|
|
34
|
+
|
|
35
|
+
### `multiple` selection
|
|
36
|
+
|
|
37
|
+
Single or multiple files are supported according to the native **`multiple`** attribute. **`multiple` is only a fallthrough HTML attribute** on `<FuroFileField>` (forwarded to the `<input>`). It is **not** part of `parcel`.
|
|
38
|
+
|
|
39
|
+
On each `@change-value`, the emitted `FileFieldEmitPayload` exposes **`isMultiple()`**, which reads the same **`multiple`** flag on the underlying control (see [`FileFieldEmitPayload` API](#filefieldemitpayload-api) below).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Fallthrough attributes (recommended usage)
|
|
44
|
+
|
|
45
|
+
Same fallthrough rules as `<FuroTextField>` for attributes that apply to `<input type="file">`. Typical and optional attributes:
|
|
46
|
+
|
|
47
|
+
| Attribute | Type | Description |
|
|
48
|
+
|-------------|-----------|-------------|
|
|
49
|
+
| `name` | `string` | Form field name for submission. |
|
|
50
|
+
| `id` | `string` | Use with `<label for="...">` for accessibility. |
|
|
51
|
+
| `accept` | `string` | Hint for allowed file types (MIME types or extensions per HTML). |
|
|
52
|
+
| `multiple` | `boolean` | When true, the user may select more than one file. |
|
|
53
|
+
| `required` | `boolean` | Marks the field as required. |
|
|
54
|
+
| `disabled` | `boolean` | Disables user interaction. |
|
|
55
|
+
| `capture` | `string` | Media capture intent where supported (e.g. mobile). |
|
|
56
|
+
|
|
57
|
+
> **`placeholder`** and **`autocomplete`** are not listed for file inputs in the same way as text fields; use them only when product and browser behavior justify it.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Emits
|
|
62
|
+
|
|
63
|
+
Each emit passes a **`FileFieldEmitPayload` instance** directly as the event argument. In the template, `$event` is that `FileFieldEmitPayload` instance.
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
// Inside <FuroFileField>
|
|
67
|
+
emit('change-value', FileFieldEmitPayload.create({ rawEvent }))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<FuroFileField
|
|
72
|
+
:parcel="context.buildFileFieldParcel()"
|
|
73
|
+
@change-value="context.onChangeValue({ payload: $event })"
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `@change-value`
|
|
78
|
+
|
|
79
|
+
Emitted when the set of selected files **changes** (maps to the native **`change`** event on the file `<input>`).
|
|
80
|
+
|
|
81
|
+
> **Naming:** Same Furo intent as `<FuroTextField>` — parent reacts to “value changed,” not to raw DOM event names in the public API.
|
|
82
|
+
|
|
83
|
+
> **Emitter responsibility:** `<FuroFileField>` **always emits** when the user changes the selection. The parent (Context) decides validation and side effects. The component does not cancel an emit based on file content.
|
|
84
|
+
|
|
85
|
+
### No `@commit-value`
|
|
86
|
+
|
|
87
|
+
`<FuroTextField>` uses `@commit-value` for blur / Enter. **`<FuroFileField>` does not define `@commit-value`** — there is no equivalent “commit” gesture separate from choosing files.
|
|
88
|
+
|
|
89
|
+
### No `v-model:value` / `@update:value`
|
|
90
|
+
|
|
91
|
+
`<FuroTextField>` supports `v-model:value` via `parcel.value` and `@update:value`. **`<FuroFileField>` does not support that pattern** because there is no serializable `parcel.value` and no symmetric “controlled FileList from parent” contract. Future sugar, if any, would be a separate, explicit API and version decision.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Clearing the selection
|
|
96
|
+
|
|
97
|
+
Because there is no `parcel.value`, the parent cannot “set value to empty” through `parcel` the way a text field does. **Recommended pattern:** remount the component with a changing **`:key`** when the form or step must reset the file input. An implementation may later expose an imperative clear API; this specification does not require it.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Class naming
|
|
102
|
+
|
|
103
|
+
| Role | `FuroTextField` | `FuroFileField` |
|
|
104
|
+
|----------------|------------------------|----------------------------|
|
|
105
|
+
| Component | `FuroTextField` | `FuroFileField` |
|
|
106
|
+
| Context | `FuroTextFieldContext` | `FuroFileFieldContext` |
|
|
107
|
+
| EmitPayload | `TextFieldEmitPayload` | `FileFieldEmitPayload` |
|
|
108
|
+
| Parcel typedef | `FuroTextFieldParcel` | `FuroFileFieldParcel` |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## `FileFieldEmitPayload` API
|
|
113
|
+
|
|
114
|
+
`FileFieldEmitPayload` extends [`BaseEmitPayload`](../lib/emit-payload/BaseEmitPayload.js). It exposes file selection helpers derived from the underlying `<input type="file">` (`controlElement` from the raw event).
|
|
115
|
+
|
|
116
|
+
| Member | Type | Description |
|
|
117
|
+
|------------------------|------------------------------|-------------|
|
|
118
|
+
| `.files` | `FileList \| Array<File>` | `controlElement.files`, or an **empty array** if the control is missing. |
|
|
119
|
+
| `.firstFile` | `File \| null` | The first selected file, or `null` when the selection is empty or the control is missing. Equivalent to the first element of `.extractFiles()`, or `null`. |
|
|
120
|
+
| `.isMultiple()` | `boolean` | `true` when the control’s **`multiple`** property is set so the user may choose more than one file; `false` when the control is missing or single-file selection only. |
|
|
121
|
+
| `.extractFiles()` | `Array<File>` | Snapshot of selected files as a plain array (same order as `.files`). |
|
|
122
|
+
| `.hasEmptySelection()` | `boolean` | `true` if there is no selected file (or the control is missing). |
|
|
123
|
+
| `.hasSelectedFiles()` | `boolean` | `true` if at least one file is selected. |
|
|
124
|
+
|
|
125
|
+
Size limits, MIME checks, and server-side rules stay in the parent Context; `<FuroFileField>` only surfaces selection through the payload.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Example
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<FuroFileField
|
|
133
|
+
:key="context.fileFieldRemountKey"
|
|
134
|
+
:parcel="{
|
|
135
|
+
invalid: context.statusReactive.isFileInvalid,
|
|
136
|
+
}"
|
|
137
|
+
name="attachment"
|
|
138
|
+
accept="image/png,image/jpeg"
|
|
139
|
+
:multiple="true"
|
|
140
|
+
:required="true"
|
|
141
|
+
@change-value="context.onChangeValue({
|
|
142
|
+
payload: $event
|
|
143
|
+
})"
|
|
144
|
+
/>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
onChangeValue ({
|
|
149
|
+
payload
|
|
150
|
+
}) {
|
|
151
|
+
if (payload.hasEmptySelection()) {
|
|
152
|
+
this.statusReactive.isFileInvalid = true
|
|
153
|
+
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.statusReactive.isFileInvalid = !this.validatesSelectedFiles({
|
|
158
|
+
payload
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# FuroIcon
|
|
2
|
+
|
|
3
|
+
Icon atom rendering the glyphs the library ships plus the ones the consuming application registers, resolved offline. An application that installs its own renderer reaches every other name too.
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
| Field | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| **Layer** | Atom |
|
|
10
|
+
| **Path** | `lib/components/atoms/FuroIcon/` |
|
|
11
|
+
| **Base primitive** | Headless offline icon renderer |
|
|
12
|
+
| **Composes** | Nothing, a single glyph |
|
|
13
|
+
| **Emits** | None |
|
|
14
|
+
| **Built-in icon set** | 49 icons, one collection, registered at module load |
|
|
15
|
+
| **Consumer icons** | Registered through `furoIconRegistry` (`@openreachtech/furo-vue/icons`) |
|
|
16
|
+
| **Every other name** | Drawn by the renderer the application installs, registry first |
|
|
17
|
+
|
|
18
|
+
Every other Furo component renders its icons through this atom, including the ones whose icon name comes from the consumer, such as a sidebar item. A consumer reaches for the atom directly mainly when overriding another component's icon slot.
|
|
19
|
+
|
|
20
|
+
## Public API
|
|
21
|
+
|
|
22
|
+
### Props
|
|
23
|
+
|
|
24
|
+
| Prop | Type | Default | Description |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| `name` | `string` | required | Icon name including its collection prefix, for example `ph:check`. Resolvable when the library ships it, the application registered it, the installed resolver returns data for it, or the installed renderer draws it. |
|
|
27
|
+
| `size` | `string \| number \| null` | `null` | One CSS length applied to both axes. When unset, the glyph is `1em` and scales with the surrounding text. |
|
|
28
|
+
|
|
29
|
+
This atom takes `name` and `size` directly rather than a `parcel`, because it carries no value and has no state: a glyph identifier and a box are the whole input.
|
|
30
|
+
|
|
31
|
+
### Slots
|
|
32
|
+
|
|
33
|
+
None. `FuroIcon` is the leaf that renders the glyph; to swap an icon, override the slot on the component that renders it.
|
|
34
|
+
|
|
35
|
+
### Events
|
|
36
|
+
|
|
37
|
+
None.
|
|
38
|
+
|
|
39
|
+
## Registering the application's own icons
|
|
40
|
+
|
|
41
|
+
Resolution is offline, so an icon draws only from data the library holds. `furoIconRegistry` is the seam that lets an application hand over its own.
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { furoIconRegistry } from '@openreachtech/furo-vue/icons'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The same registry is re-exported from the package root, so `import { furoIconRegistry } from '@openreachtech/furo-vue'` works too. Register once at application start, before the first render. On a server the registry is a module singleton shared across requests, so never register per request.
|
|
48
|
+
|
|
49
|
+
### Register a whole icon set
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
furoIconRegistry.registerIconSet({
|
|
53
|
+
iconSet: applicationIconSet,
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
An icon set is a plain object, the interchange shape every published icon-data package already uses:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const applicationIconSet = {
|
|
61
|
+
prefix: 'app-icons',
|
|
62
|
+
width: 256,
|
|
63
|
+
height: 256,
|
|
64
|
+
icons: {
|
|
65
|
+
house: { body: '<path fill="currentColor" d="…" />' },
|
|
66
|
+
gear: { body: '<path fill="currentColor" d="…" />' },
|
|
67
|
+
},
|
|
68
|
+
aliases: {
|
|
69
|
+
home: { parent: 'house' },
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Every name the set carries becomes `<prefix>:<name>`, so the example above registers `app-icons:house`, `app-icons:gear` and `app-icons:home`. A set with no `prefix` registers its keys verbatim, which is how a set of already-prefixed names is handed over.
|
|
75
|
+
|
|
76
|
+
A set is stored whole or not at all: one icon carrying no `body`, or one alias whose `parent` is outside the set, costs the whole set its registration. Nothing it carried is recorded, so every one of its names fails loudly at render rather than drawing blank.
|
|
77
|
+
|
|
78
|
+
A published set can run to thousands of icons, and all of it reaches the bundle once it is registered. Register the names the application actually draws:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const drawnNames = ['house', 'users', 'envelope-simple', 'gear']
|
|
82
|
+
|
|
83
|
+
furoIconRegistry.registerIconSet({
|
|
84
|
+
iconSet: {
|
|
85
|
+
...publishedIconSet,
|
|
86
|
+
icons: Object.fromEntries(
|
|
87
|
+
drawnNames.map(drawnName => [drawnName, publishedIconSet.icons[drawnName]])
|
|
88
|
+
),
|
|
89
|
+
aliases: {},
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Register one icon
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
furoIconRegistry.registerIcon({
|
|
98
|
+
name: 'app-icons:logo',
|
|
99
|
+
icon: {
|
|
100
|
+
body: '<path fill="currentColor" d="…" />',
|
|
101
|
+
width: 256,
|
|
102
|
+
height: 256,
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A single icon carries its own `width` and `height`, since it has no set to take defaults from. One carrying no `body` registers nothing, since the renderer would draw nothing for it.
|
|
108
|
+
|
|
109
|
+
### Install a resolver
|
|
110
|
+
|
|
111
|
+
A resolver answers for any name no registration covers, which suits an application holding a large set it would rather not register up front.
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
furoIconRegistry.useIconResolver({
|
|
115
|
+
resolver: ({ name }) => applicationIconHash[name] ?? null,
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The resolver runs during render, so it must return synchronously: the icon data for a name it knows, `null` for one it does not. Passing `resolver: null` uninstalls it.
|
|
120
|
+
|
|
121
|
+
Keep it cheap and free of side effects. One render asks it more than once, because the atom decides the renderer, the attributes and the root class map separately, and no result is cached between those questions.
|
|
122
|
+
|
|
123
|
+
Data the renderer could not draw counts as no answer. A shape carrying no string `body`, or declaring a dimension that is not a number, is rejected exactly as `registerIcon()` rejects it, so a resolver returning junk still reports the name as unresolved rather than drawing a blank box.
|
|
124
|
+
|
|
125
|
+
### Ask whether a name draws
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
furoIconRegistry.hasIcon({ name: 'app-icons:house' })
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`hasIcon()` answers for the whole registry: it reads the registrations, then asks the resolver for a name none of them carries. Call `hasRegisteredIcon()` to read the registrations alone, leaving the resolver unasked. To get what the atom would draw rather than whether it would, call `findRenderableIcon()`, which returns the registered name, the resolved icon data, or `null`.
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
furoIconRegistry.findRenderableIcon({ name: 'app-icons:house' })
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Handing every other name to your own renderer
|
|
138
|
+
|
|
139
|
+
Registration scales with the library's surface, not with yours. An application drawing icons from data, a sidebar reading its items from an API for instance, cannot register a name it has not seen yet. Install a renderer and every name the registry lacks goes to it.
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
import { Icon } from '#components' // the application's own icon component
|
|
143
|
+
import { furoIconRegistry } from '@openreachtech/furo-vue/icons'
|
|
144
|
+
|
|
145
|
+
furoIconRegistry.useIconRenderer({
|
|
146
|
+
component: Icon,
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The renderer receives `name` and `size`, which are this atom's own props, so a component taking those two plugs in unchanged. A renderer naming them otherwise is wrapped by the application in a functional component.
|
|
151
|
+
|
|
152
|
+
`component` takes either the component itself or the name it is globally registered under, so `component: 'Icon'` works wherever `<Icon>` resolves. A name is what the Nuxt module installs, since that is what survives an application renaming its icon component. Pass an extensible object: a component run through `Object.freeze()` cannot be marked raw, and Vue's reactive state would then proxy it.
|
|
153
|
+
|
|
154
|
+
Resolution stays **registry first**: the registry answers every name it carries, and the installed renderer answers only the rest. The library's own 49 icons therefore keep drawing offline however the renderer is configured, and no application configuration can break them. Replace one built-in glyph with `registerIcon()`, which overwrites that name.
|
|
155
|
+
|
|
156
|
+
Pass `component: null` to uninstall the renderer and return to registry-only resolution.
|
|
157
|
+
|
|
158
|
+
### In a Nuxt application
|
|
159
|
+
|
|
160
|
+
The module wires the application's icon component for you.
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
// nuxt.config.js
|
|
164
|
+
export default defineNuxtConfig({
|
|
165
|
+
modules: [
|
|
166
|
+
'@nuxt/icon',
|
|
167
|
+
'@openreachtech/furo-vue/nuxt',
|
|
168
|
+
],
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
It leaves an icon module the application already lists alone, keeping that configuration untouched. It registers a merely installed one. It fails, naming what to install, when the application has neither. Coverage then belongs to that module's configuration, not to this library: a collection outside its bundle costs whatever its own fallback costs.
|
|
173
|
+
|
|
174
|
+
The module installs the icon component by name, reading `icon.componentName` so an application that renames the component keeps working. Point it somewhere else with its own option, which wins over what the icon module reports.
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
// nuxt.config.js
|
|
178
|
+
export default defineNuxtConfig({
|
|
179
|
+
furoVue: {
|
|
180
|
+
iconComponentName: 'AppIcon',
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### In a plain Vue application
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
import { createIconifyRenderer } from '@openreachtech/furo-vue/icon-renderers'
|
|
189
|
+
import { furoIconRegistry } from '@openreachtech/furo-vue/icons'
|
|
190
|
+
|
|
191
|
+
furoIconRegistry.useIconRenderer({
|
|
192
|
+
component: createIconifyRenderer(),
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
This renderer draws any name in the catalogue, fetching the data it holds none for. It lives at its own subpath so an application staying offline never pulls the fetching client into its bundle.
|
|
197
|
+
|
|
198
|
+
## An unresolvable name fails loudly
|
|
199
|
+
|
|
200
|
+
Outside a production build, a name neither registered nor resolved throws, naming the icon and the four ways to draw it. A typo cannot reach a reviewer.
|
|
201
|
+
|
|
202
|
+
This holds while no renderer is installed. Once one is, the library can no longer tell a typo from a valid name, so it reports nothing: `isUnresolvedIcon()` is false, the `unresolved` class stays off, and the throw stays silent. Silence there is honest, and the diagnosis belongs to the renderer.
|
|
203
|
+
|
|
204
|
+
In a production build the same name renders the built-in warning glyph, carries the `unresolved` class on the root host element, and colours itself with `--color-warning-text`. The fallback is deliberate. An icon that renders nothing is indistinguishable from an icon the consumer forgot to pass, so the failure hid in plain sight until someone compared the screen against the design.
|
|
205
|
+
|
|
206
|
+
A nullish `name` is the one case neither mode reports. Nothing was asked for, so nothing draws, no throw fires and the `unresolved` class stays off. `name` is a required prop, so Vue already warns about it, and throwing on top would crash a render for the common case of a name arriving from an API a tick later.
|
|
207
|
+
|
|
208
|
+
Register every icon set before the first render. An icon set registered after a `FuroIcon` has mounted throws in development, even though production would have drawn the glyph and recovered.
|
|
209
|
+
|
|
210
|
+
```vue
|
|
211
|
+
<!-- throws in development, draws the warning glyph in production -->
|
|
212
|
+
<FuroIcon name="app-icons:not-registered-yet" />
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Why the property is called `name`
|
|
216
|
+
|
|
217
|
+
Two properties are named differently by the two renderers, and passing the wrong one fails silently:
|
|
218
|
+
|
|
219
|
+
| Renderer | Icon property | Size property |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| The underlying offline renderer | `icon` | `width` and `height` |
|
|
222
|
+
| A framework icon module in a consumer app | `name` | `size` |
|
|
223
|
+
| `FuroIcon` | `name` | `size` |
|
|
224
|
+
|
|
225
|
+
`FuroIcon` deliberately exposes `name` and `size`, matching the framework module a consumer app is likely to have installed, not the renderer it wraps. `FuroIconContext.buildOfflineRendererAttributes()` holds the mapping for the atom, so the renderer's own property names never reach a call site. Its sibling `buildDelegatedRendererAttributes()` maps nothing, because an installed renderer already takes `name` and `size`. The adapter built by `createIconifyRenderer()` repeats the same mapping in `FuroIconifyRendererContext.extractRendererAttributes()`, being a standalone component rather than something the atom's context drives.
|
|
226
|
+
|
|
227
|
+
The size mapping matters more than it looks. The offline renderer has no `size` property: passing one would land on the `<svg>` as a meaningless attribute and the glyph would silently fall back to `1em` instead of the size asked for. `FuroIcon` maps `size` onto both `width` and `height`, and omits both when no size is given so the `1em` default still applies.
|
|
228
|
+
|
|
229
|
+
## Offline resolution
|
|
230
|
+
|
|
231
|
+
The library's own icon data is registered when the module loads, before the first render, so a built-in icon never flickers in while a lookup is in flight, and no request is made to a remote icon API. The renderer is imported from its offline entry point, which holds no code path that could fetch. The fetching entry point lives only behind the `icon-renderers` subpath, which an offline application never imports.
|
|
232
|
+
|
|
233
|
+
The consequence is that the library resolves **only** the data it holds: its own 49 icons, plus whatever the application registers or resolves. Registration must go through `furoIconRegistry`, because that is what the atom reads to tell a drawable name from an undrawable one.
|
|
234
|
+
|
|
235
|
+
An installed renderer changes what is offline, not what the library holds. Whether a delegated name costs a request is the renderer's business: a framework icon module bundling its collections draws offline too, while one falling back to a remote icon API does not.
|
|
236
|
+
|
|
237
|
+
The renderer binding itself, the one line of the template that chooses between the two components, is verified by manual UAT on the playground rather than by a test. The checklist and the reason are below.
|
|
238
|
+
|
|
239
|
+
## Adding an icon to the library
|
|
240
|
+
|
|
241
|
+
Icon data for the library's own components is generated, not hand-written.
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
npm run icons:data
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The generator scans `lib/` for icon names, resolves each against the icon collection, and rewrites `lib/components/atoms/FuroIcon/furoIconData.js`. Reference a new icon in a component and then regenerate. Skipping the second step fails the test suite, which asserts that every name referenced under `lib/` has registered data.
|
|
248
|
+
|
|
249
|
+
This step is for icons the library's own components render. An application's icons never enter this file; they go through `furoIconRegistry`.
|
|
250
|
+
|
|
251
|
+
## Manual UAT for the renderer binding
|
|
252
|
+
|
|
253
|
+
One line of `FuroIcon`'s template picks between the two renderers, and no test reaches it: a full SFC mount is infeasible under jest, because the SFC compiles to CommonJS and cannot load the ESM context module. Run this on the playground (`npm run dev`, then the FuroIcon tab) whenever that binding or the module changes.
|
|
254
|
+
|
|
255
|
+
| # | Step | Expected |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| 1 | Open the "Registered icons" section | All 49 glyphs draw, no network request for any of them |
|
|
258
|
+
| 2 | Open the "Delegated icons" section | `mdi:`, `lucide:`, `simple-icons:` and `material-symbols:` names all draw |
|
|
259
|
+
| 3 | Read the network panel while step 2 loads | The collections outside the server bundle arrive over the icon API, the library's own do not |
|
|
260
|
+
| 4 | Look at `ph:no-such-icon-anywhere` | Draws nothing and does not throw. The `unresolved` class stays off. Any console output comes from the icon module, not from this library |
|
|
261
|
+
| 5 | Remove `'~/nuxt/index.js'` from `nuxt.config.js` and reload | Step 4's name now throws in development, naming the icon. Every `ph:` name outside the shipped 49 throws with it, because the playground has no resolver of its own |
|
|
262
|
+
| 6 | With the module still removed, reload the "Registered icons" section | All 49 still draw, proving the registry never depended on the renderer |
|
|
263
|
+
| 7 | Restore the module, set `icon: { componentName: 'AppIcon' }` and reload | The delegated names still draw, proving the renderer follows the configured name rather than a hardcoded `Icon` |
|
|
264
|
+
| 8 | Drop `'@nuxt/icon'` from `modules` but keep the package installed | The build succeeds: the module registers the merely installed package. This is the second of its three branches |
|
|
265
|
+
| 9 | Uninstall the package too (`npm rm @nuxt/icon`) | The build fails naming the package to install. Dropping it from `modules` alone never reaches this branch, because the package stays resolvable |
|
|
266
|
+
| 10 | Install a resolver answering one name with undrawable data (`() => ({})`) | That name reports as unresolved and throws in development, proving junk data is not mistaken for an answer |
|
|
267
|
+
|
|
268
|
+
## Styling
|
|
269
|
+
|
|
270
|
+
The glyph is filled with `currentColor` and takes its box from `font-size`, so it is styled like text rather than through props.
|
|
271
|
+
|
|
272
|
+
```vue
|
|
273
|
+
<FuroIcon
|
|
274
|
+
name="ph:warning"
|
|
275
|
+
style="font-size: 1.5rem; color: var(--color-warning-text)"
|
|
276
|
+
/>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Accessibility
|
|
280
|
+
|
|
281
|
+
- An icon carries no accessible name of its own. Add `aria-hidden="true"` when it is decorative beside a text label.
|
|
282
|
+
- When the icon is the only content of a control, give the interactive ancestor an `aria-label`.
|
|
283
|
+
- Colour comes from the container through `currentColor`, so the icon needs no token of its own.
|
|
284
|
+
|
|
285
|
+
## Related
|
|
286
|
+
|
|
287
|
+
- [ADR 0031](adr/0031-library-owned-icons-via-offline-renderer.md): why the library owns its icons, and why the consumer app keeps its own icon module.
|
|
288
|
+
- [ADR 0037](adr/0037-icon-resolution-delegates-registry-first.md): why an installed renderer answers only the names the registry lacks, and what the delegated mode gives up.
|