@cfasim-ui/components 0.7.8 → 0.8.1
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/dist/Box/Box.d.ts +7 -11
- package/dist/Button/Button.d.ts +7 -11
- package/dist/ButtonGroup/ButtonGroup.d.ts +8 -12
- package/dist/Container/Container.d.ts +7 -11
- package/dist/Expander/Expander.d.ts +12 -14
- package/dist/Grid/Grid.d.ts +6 -5
- package/dist/Hint/Hint.d.ts +2 -1
- package/dist/Icon/Icon.d.ts +3 -2
- package/dist/LightDarkToggle/LightDarkToggle.d.ts +2 -1
- package/dist/MultiSelect/MultiSelect.d.ts +6 -6
- package/dist/NumberInput/NumberInput.d.ts +13 -11
- package/dist/ParamEditor/ParamEditor.d.ts +3 -2
- package/dist/ParamEditor/ParamEditorImpl.d.ts +3 -5
- package/dist/{ParamEditorImpl-D0xZTyAN.js → ParamEditorImpl-CkElC6PI.js} +3075 -3041
- package/dist/SelectBox/SelectBox.d.ts +8 -6
- package/dist/{SelectBox-CByXZfaC.js → SelectBox-Df8dE2r-.js} +6 -6
- package/dist/SidebarLayout/SidebarLayout.d.ts +21 -20
- package/dist/Spinner/Spinner.d.ts +3 -2
- package/dist/TextInput/TextInput.d.ts +7 -5
- package/dist/Toggle/Toggle.d.ts +8 -6
- package/dist/ToggleGroup/ToggleGroup.d.ts +8 -6
- package/dist/_internal/FieldLabel.d.ts +2 -1
- package/dist/index.js +20 -20
- package/docs/Box.md +49 -0
- package/docs/Button.md +67 -0
- package/docs/ButtonGroup.md +64 -0
- package/docs/Container.md +103 -0
- package/docs/Expander.md +34 -0
- package/docs/Grid.md +170 -0
- package/docs/Hint.md +29 -0
- package/docs/Icon.md +188 -0
- package/docs/MultiSelect.md +143 -0
- package/docs/NumberInput.md +485 -0
- package/docs/ParamEditor.md +97 -0
- package/docs/SelectBox.md +182 -0
- package/docs/SidebarLayout.md +106 -0
- package/docs/Spinner.md +51 -0
- package/docs/TextInput.md +83 -0
- package/docs/Toggle.md +81 -0
- package/docs/ToggleGroup.md +163 -0
- package/docs/index.json +194 -0
- package/package.json +24 -20
- package/src/Box/Box.md +41 -0
- package/src/Box/Box.vue +52 -0
- package/src/Button/Button.md +59 -0
- package/src/Button/Button.vue +81 -0
- package/src/ButtonGroup/ButtonGroup.md +62 -0
- package/src/ButtonGroup/ButtonGroup.vue +91 -0
- package/src/Container/Container.md +99 -0
- package/src/Container/Container.vue +62 -0
- package/src/Expander/Expander.md +23 -0
- package/src/Expander/Expander.vue +95 -0
- package/src/Grid/Grid.md +175 -0
- package/src/Grid/Grid.vue +145 -0
- package/src/Hint/Hint.md +24 -0
- package/src/Hint/Hint.vue +83 -0
- package/src/Icon/Icon.md +176 -0
- package/src/Icon/Icon.vue +104 -0
- package/src/Icon/defaultIcons.ts +92 -0
- package/src/Icon/github.svg +1 -0
- package/src/Icon/registry.ts +68 -0
- package/src/LightDarkToggle/LightDarkToggle.vue +49 -0
- package/src/MultiSelect/MultiSelect.md +142 -0
- package/src/MultiSelect/MultiSelect.vue +279 -0
- package/src/NumberInput/NumberInput.md +455 -0
- package/src/NumberInput/NumberInput.vue +575 -0
- package/src/ParamEditor/ParamEditor.md +87 -0
- package/src/ParamEditor/ParamEditor.vue +43 -0
- package/src/ParamEditor/ParamEditorImpl.vue +358 -0
- package/src/SelectBox/SelectBox.md +169 -0
- package/src/SelectBox/SelectBox.vue +260 -0
- package/src/SidebarLayout/SidebarLayout.md +106 -0
- package/src/SidebarLayout/SidebarLayout.vue +468 -0
- package/src/Spinner/Spinner.md +45 -0
- package/src/Spinner/Spinner.vue +55 -0
- package/src/TextInput/TextInput.md +68 -0
- package/src/TextInput/TextInput.vue +54 -0
- package/src/Toggle/Toggle.md +68 -0
- package/src/Toggle/Toggle.vue +81 -0
- package/src/ToggleGroup/ToggleGroup.md +158 -0
- package/src/ToggleGroup/ToggleGroup.vue +138 -0
- package/src/_internal/FieldLabel.vue +27 -0
- package/src/_internal/field.ts +27 -0
- package/src/_internal/gap.ts +17 -0
- package/src/_internal/input.css +54 -0
- package/src/_internal/listbox.css +53 -0
- package/src/env.d.ts +4 -0
- package/src/index.ts +33 -0
- package/src/svg.d.ts +5 -0
package/src/Icon/Icon.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Icon
|
|
2
|
+
|
|
3
|
+
Renders a [Material Symbols](https://fonts.google.com/icons) icon as an inline
|
|
4
|
+
SVG. Icons are resolved from a registry by name, so you reference them exactly as
|
|
5
|
+
before — `<Icon icon="help" />` — but no webfont is loaded: only the SVGs you
|
|
6
|
+
actually use end up in your bundle.
|
|
7
|
+
|
|
8
|
+
A [base set](#base-set) of common icons is pre-registered and works with zero
|
|
9
|
+
setup. These SVGs are bundled inside `cfasim-ui`, so they need no extra install
|
|
10
|
+
or config — just `<Icon icon="help" />`. To use any other icon,
|
|
11
|
+
[register it](#registering-icons).
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### Sizes
|
|
16
|
+
|
|
17
|
+
<ComponentDemo>
|
|
18
|
+
<Icon icon="help" size="sm" aria-label="help" />
|
|
19
|
+
<Icon icon="help" size="md" aria-label="help" />
|
|
20
|
+
<Icon icon="help" size="lg" aria-label="help" />
|
|
21
|
+
|
|
22
|
+
<template #code>
|
|
23
|
+
|
|
24
|
+
```vue
|
|
25
|
+
<Icon icon="help" size="sm" aria-label="help" />
|
|
26
|
+
<Icon icon="help" size="md" aria-label="help" />
|
|
27
|
+
<Icon icon="help" size="lg" aria-label="help" />
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
</template>
|
|
31
|
+
</ComponentDemo>
|
|
32
|
+
|
|
33
|
+
### Filled
|
|
34
|
+
|
|
35
|
+
<ComponentDemo>
|
|
36
|
+
<Icon icon="favorite" size="lg" aria-label="favorite" />
|
|
37
|
+
<Icon icon="favorite" size="lg" :fill="true" aria-label="favorite filled" />
|
|
38
|
+
|
|
39
|
+
<template #code>
|
|
40
|
+
|
|
41
|
+
```vue
|
|
42
|
+
<Icon icon="favorite" size="lg" aria-label="favorite" />
|
|
43
|
+
<Icon icon="favorite" size="lg" :fill="true" aria-label="favorite filled" />
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
</template>
|
|
47
|
+
</ComponentDemo>
|
|
48
|
+
|
|
49
|
+
### Inline in text
|
|
50
|
+
|
|
51
|
+
<ComponentDemo>
|
|
52
|
+
<p style="margin: 0">Click the <Icon icon="help" size="sm" :inline="true" aria-label="help" /> icon for more info.</p>
|
|
53
|
+
|
|
54
|
+
<template #code>
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<p>Click the <Icon icon="help" size="sm" :inline="true" aria-label="help" /> icon for more info.</p>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
</template>
|
|
61
|
+
</ComponentDemo>
|
|
62
|
+
|
|
63
|
+
## Base set
|
|
64
|
+
|
|
65
|
+
These icons are bundled with `cfasim-ui` and need no install or registration —
|
|
66
|
+
just pass the name to `icon`. Anything not listed here must be
|
|
67
|
+
[registered](#registering-icons).
|
|
68
|
+
|
|
69
|
+
<script setup>
|
|
70
|
+
const baseIcons = [
|
|
71
|
+
"help", "favorite", "dark_mode", "light_mode",
|
|
72
|
+
"chevron_left", "chevron_right", "keyboard_arrow_up", "keyboard_arrow_down",
|
|
73
|
+
"keyboard_double_arrow_left", "keyboard_double_arrow_right",
|
|
74
|
+
"arrow_upward", "arrow_downward", "arrow_back", "arrow_forward",
|
|
75
|
+
"menu", "more_vert", "more_horiz",
|
|
76
|
+
"add", "edit", "delete", "close", "check", "search", "tune", "download",
|
|
77
|
+
"github",
|
|
78
|
+
];
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<Grid class="base-icons" :cols="4" :colsSmall="2" gap="sm">
|
|
82
|
+
<div v-for="name in baseIcons" :key="name" class="base-icons__cell">
|
|
83
|
+
<Icon :icon="name" size="lg" :aria-label="name" />
|
|
84
|
+
<code>{{ name }}</code>
|
|
85
|
+
</div>
|
|
86
|
+
</Grid>
|
|
87
|
+
|
|
88
|
+
<style>
|
|
89
|
+
.base-icons__cell {
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 0.5rem;
|
|
94
|
+
padding: 0.75rem 0.5rem;
|
|
95
|
+
border: 1px solid var(--vp-c-divider);
|
|
96
|
+
border-radius: 8px;
|
|
97
|
+
text-align: center;
|
|
98
|
+
}
|
|
99
|
+
.base-icons__cell code {
|
|
100
|
+
font-size: 0.65rem;
|
|
101
|
+
word-break: break-word;
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
104
|
+
|
|
105
|
+
`favorite` also has a filled variant via `:fill="true"`.
|
|
106
|
+
|
|
107
|
+
## Registering icons
|
|
108
|
+
|
|
109
|
+
The base set above is bundled with `cfasim-ui` and needs none of the steps below
|
|
110
|
+
— this section is only for additional icons. `cfasim-ui` does not depend on
|
|
111
|
+
`@material-symbols/svg-400`, so you install the icons yourself; each one you
|
|
112
|
+
import is inlined into _your_ bundle (still no webfont).
|
|
113
|
+
|
|
114
|
+
### 1. Install the icons and the Vite SVG loader
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
pnpm add -D @material-symbols/svg-400 vite-svg-loader
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2. Add `svgLoader` to your Vite config
|
|
121
|
+
|
|
122
|
+
Add it to your existing `plugins` array — leave the other plugins in place:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// vite.config.ts
|
|
126
|
+
import svgLoader from "vite-svg-loader";
|
|
127
|
+
|
|
128
|
+
export default defineConfig({
|
|
129
|
+
plugins: [
|
|
130
|
+
// ...your existing plugins
|
|
131
|
+
svgLoader(),
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3. Register the icons once at startup
|
|
137
|
+
|
|
138
|
+
Import each SVG from the `outlined` style with the `?component` query, then pass
|
|
139
|
+
them to `registerIcons`. The key you register is the name you pass to `icon`.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// main.ts
|
|
143
|
+
import rocket from "@material-symbols/svg-400/outlined/rocket.svg?component";
|
|
144
|
+
import settings from "@material-symbols/svg-400/outlined/settings.svg?component";
|
|
145
|
+
import { registerIcons } from "@cfasim-ui/components";
|
|
146
|
+
|
|
147
|
+
registerIcons({ rocket, settings });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 4. Use them by name
|
|
151
|
+
|
|
152
|
+
```vue
|
|
153
|
+
<Icon icon="rocket" />
|
|
154
|
+
<Icon icon="settings" size="lg" />
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
An unregistered name renders nothing and logs a one-time console warning with the
|
|
158
|
+
exact code to register it.
|
|
159
|
+
|
|
160
|
+
### Filled variants
|
|
161
|
+
|
|
162
|
+
To support `:fill="true"` on a registered icon, register both the outline and
|
|
163
|
+
`-fill` files:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import favorite from "@material-symbols/svg-400/outlined/favorite.svg?component";
|
|
167
|
+
import favoriteFill from "@material-symbols/svg-400/outlined/favorite-fill.svg?component";
|
|
168
|
+
|
|
169
|
+
registerIcons({ favorite: { outline: favorite, fill: favoriteFill } });
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
> The `weight` and `grade` props are deprecated no-ops — static SVGs ship at
|
|
173
|
+
> weight 400, grade 0. `size`, `fill`, `inline`, and color (`currentColor`) work
|
|
174
|
+
> as before.
|
|
175
|
+
|
|
176
|
+
<!--@include: ./_api/icon.md-->
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CSSProperties } from "vue";
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import { getIconComponent, warnMissingIcon } from "./registry";
|
|
5
|
+
import { registerDefaultIcons } from "./defaultIcons";
|
|
6
|
+
|
|
7
|
+
// Driven from setup (a retained code path) so the base set survives
|
|
8
|
+
// tree-shaking; idempotent, so the per-instance call is effectively free.
|
|
9
|
+
registerDefaultIcons();
|
|
10
|
+
|
|
11
|
+
export type IconSize = "sm" | "md" | "lg" | "xl";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
icon: string;
|
|
15
|
+
size?: IconSize | number;
|
|
16
|
+
fill?: boolean;
|
|
17
|
+
/** @deprecated No effect — SVG icons ship at weight 400. */
|
|
18
|
+
weight?: number;
|
|
19
|
+
/** @deprecated No effect — SVG icons ship at grade 0. */
|
|
20
|
+
grade?: number;
|
|
21
|
+
decorative?: boolean;
|
|
22
|
+
ariaLabel?: string;
|
|
23
|
+
inline?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
27
|
+
size: "md",
|
|
28
|
+
fill: false,
|
|
29
|
+
decorative: true,
|
|
30
|
+
inline: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const sizePreset = computed(() =>
|
|
34
|
+
typeof props.size === "string" ? props.size : undefined,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const inlineStyle = computed<CSSProperties>(() =>
|
|
38
|
+
typeof props.size === "number"
|
|
39
|
+
? { width: `${props.size}px`, height: `${props.size}px` }
|
|
40
|
+
: {},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const svg = computed(() => {
|
|
44
|
+
const component = getIconComponent(props.icon, props.fill);
|
|
45
|
+
if (!component) warnMissingIcon(props.icon);
|
|
46
|
+
return component;
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<span
|
|
52
|
+
class="Icon"
|
|
53
|
+
:data-size="sizePreset"
|
|
54
|
+
:data-inline="inline ? 'true' : undefined"
|
|
55
|
+
:style="inlineStyle"
|
|
56
|
+
:aria-hidden="decorative ? true : undefined"
|
|
57
|
+
:aria-label="decorative ? undefined : ariaLabel"
|
|
58
|
+
:role="decorative ? undefined : 'img'"
|
|
59
|
+
>
|
|
60
|
+
<component :is="svg" v-if="svg" />
|
|
61
|
+
</span>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<style>
|
|
65
|
+
.Icon {
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
flex: none;
|
|
70
|
+
width: 24px;
|
|
71
|
+
height: 24px;
|
|
72
|
+
color: inherit;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.Icon svg {
|
|
76
|
+
display: block;
|
|
77
|
+
width: 100%;
|
|
78
|
+
height: 100%;
|
|
79
|
+
fill: currentColor;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.Icon[data-size="sm"] {
|
|
83
|
+
width: 20px;
|
|
84
|
+
height: 20px;
|
|
85
|
+
}
|
|
86
|
+
.Icon[data-size="md"] {
|
|
87
|
+
width: 24px;
|
|
88
|
+
height: 24px;
|
|
89
|
+
}
|
|
90
|
+
.Icon[data-size="lg"] {
|
|
91
|
+
width: 28px;
|
|
92
|
+
height: 28px;
|
|
93
|
+
}
|
|
94
|
+
.Icon[data-size="xl"] {
|
|
95
|
+
width: 32px;
|
|
96
|
+
height: 32px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.Icon[data-inline="true"] {
|
|
100
|
+
width: 1em;
|
|
101
|
+
height: 1em;
|
|
102
|
+
vertical-align: -0.125em;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The base icon set, registered on first use of <Icon> so `icon="..."` works
|
|
2
|
+
// with zero setup. These are bundled into the published package (each import
|
|
3
|
+
// inlines a ~0.3-0.6 KB SVG at build time), so consumers need no install.
|
|
4
|
+
//
|
|
5
|
+
// Registration is driven from Icon.vue's setup (a used code path) rather than a
|
|
6
|
+
// bare side-effect import, so it survives tree-shaking of this side-effect-free
|
|
7
|
+
// package.
|
|
8
|
+
//
|
|
9
|
+
// Keep this list and the "Base set" table in Icon.md in sync.
|
|
10
|
+
import help from "@material-symbols/svg-400/outlined/help.svg?component";
|
|
11
|
+
import favorite from "@material-symbols/svg-400/outlined/favorite.svg?component";
|
|
12
|
+
import favorite_fill from "@material-symbols/svg-400/outlined/favorite-fill.svg?component";
|
|
13
|
+
import dark_mode from "@material-symbols/svg-400/outlined/dark_mode.svg?component";
|
|
14
|
+
import light_mode from "@material-symbols/svg-400/outlined/light_mode.svg?component";
|
|
15
|
+
// chevrons / navigation
|
|
16
|
+
import chevron_left from "@material-symbols/svg-400/outlined/chevron_left.svg?component";
|
|
17
|
+
import chevron_right from "@material-symbols/svg-400/outlined/chevron_right.svg?component";
|
|
18
|
+
import keyboard_arrow_up from "@material-symbols/svg-400/outlined/keyboard_arrow_up.svg?component";
|
|
19
|
+
import keyboard_arrow_down from "@material-symbols/svg-400/outlined/keyboard_arrow_down.svg?component";
|
|
20
|
+
import keyboard_double_arrow_left from "@material-symbols/svg-400/outlined/keyboard_double_arrow_left.svg?component";
|
|
21
|
+
import keyboard_double_arrow_right from "@material-symbols/svg-400/outlined/keyboard_double_arrow_right.svg?component";
|
|
22
|
+
// arrows (all directions)
|
|
23
|
+
import arrow_upward from "@material-symbols/svg-400/outlined/arrow_upward.svg?component";
|
|
24
|
+
import arrow_downward from "@material-symbols/svg-400/outlined/arrow_downward.svg?component";
|
|
25
|
+
import arrow_back from "@material-symbols/svg-400/outlined/arrow_back.svg?component";
|
|
26
|
+
import arrow_forward from "@material-symbols/svg-400/outlined/arrow_forward.svg?component";
|
|
27
|
+
// menus
|
|
28
|
+
import menu from "@material-symbols/svg-400/outlined/menu.svg?component";
|
|
29
|
+
import more_vert from "@material-symbols/svg-400/outlined/more_vert.svg?component";
|
|
30
|
+
import more_horiz from "@material-symbols/svg-400/outlined/more_horiz.svg?component";
|
|
31
|
+
// actions
|
|
32
|
+
import add from "@material-symbols/svg-400/outlined/add.svg?component";
|
|
33
|
+
import edit from "@material-symbols/svg-400/outlined/edit.svg?component";
|
|
34
|
+
import deleteIcon from "@material-symbols/svg-400/outlined/delete.svg?component";
|
|
35
|
+
import close from "@material-symbols/svg-400/outlined/close.svg?component";
|
|
36
|
+
import check from "@material-symbols/svg-400/outlined/check.svg?component";
|
|
37
|
+
import search from "@material-symbols/svg-400/outlined/search.svg?component";
|
|
38
|
+
import tune from "@material-symbols/svg-400/outlined/tune.svg?component";
|
|
39
|
+
import download from "@material-symbols/svg-400/outlined/download.svg?component";
|
|
40
|
+
// brand logos (not part of Material Symbols)
|
|
41
|
+
import github from "./github.svg?component";
|
|
42
|
+
import type { IconRegistration } from "./registry";
|
|
43
|
+
import { registerIcons, hasIcon } from "./registry";
|
|
44
|
+
|
|
45
|
+
const defaults: Record<string, IconRegistration> = {
|
|
46
|
+
// status / theme
|
|
47
|
+
help,
|
|
48
|
+
favorite: { outline: favorite, fill: favorite_fill },
|
|
49
|
+
dark_mode,
|
|
50
|
+
light_mode,
|
|
51
|
+
// chevrons / navigation
|
|
52
|
+
chevron_left,
|
|
53
|
+
chevron_right,
|
|
54
|
+
keyboard_arrow_up,
|
|
55
|
+
keyboard_arrow_down,
|
|
56
|
+
keyboard_double_arrow_left,
|
|
57
|
+
keyboard_double_arrow_right,
|
|
58
|
+
// arrows (all directions)
|
|
59
|
+
arrow_upward,
|
|
60
|
+
arrow_downward,
|
|
61
|
+
arrow_back,
|
|
62
|
+
arrow_forward,
|
|
63
|
+
// menus
|
|
64
|
+
menu,
|
|
65
|
+
more_vert,
|
|
66
|
+
more_horiz,
|
|
67
|
+
// actions
|
|
68
|
+
add,
|
|
69
|
+
edit,
|
|
70
|
+
delete: deleteIcon,
|
|
71
|
+
close,
|
|
72
|
+
check,
|
|
73
|
+
search,
|
|
74
|
+
tune,
|
|
75
|
+
download,
|
|
76
|
+
// brand logos
|
|
77
|
+
github,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let registered = false;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register the base icon set once. A name already registered by the consumer is
|
|
84
|
+
* left untouched, so explicit `registerIcons` calls always win over defaults.
|
|
85
|
+
*/
|
|
86
|
+
export function registerDefaultIcons(): void {
|
|
87
|
+
if (registered) return;
|
|
88
|
+
registered = true;
|
|
89
|
+
for (const [name, reg] of Object.entries(defaults)) {
|
|
90
|
+
if (!hasIcon(name)) registerIcons({ [name]: reg });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Component } from "vue";
|
|
2
|
+
|
|
3
|
+
/** A registered icon. Provide a bare component (outline only) or both variants. */
|
|
4
|
+
export interface IconVariants {
|
|
5
|
+
outline: Component;
|
|
6
|
+
fill?: Component;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type IconRegistration = Component | IconVariants;
|
|
10
|
+
|
|
11
|
+
const registry = new Map<string, IconVariants>();
|
|
12
|
+
|
|
13
|
+
function toVariants(reg: IconRegistration): IconVariants {
|
|
14
|
+
return typeof reg === "object" && reg !== null && "outline" in reg
|
|
15
|
+
? (reg as IconVariants)
|
|
16
|
+
: { outline: reg as Component };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register icons for use with `<Icon icon="name" />`. Call once at app startup.
|
|
21
|
+
* Re-registering a name overwrites the previous entry.
|
|
22
|
+
*/
|
|
23
|
+
export function registerIcons(icons: Record<string, IconRegistration>): void {
|
|
24
|
+
for (const [name, reg] of Object.entries(icons)) {
|
|
25
|
+
registry.set(name, toVariants(reg));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolve the component for an icon name, preferring the fill variant when asked. */
|
|
30
|
+
export function getIconComponent(
|
|
31
|
+
name: string,
|
|
32
|
+
fill = false,
|
|
33
|
+
): Component | undefined {
|
|
34
|
+
const entry = registry.get(name);
|
|
35
|
+
if (!entry) return undefined;
|
|
36
|
+
return fill && entry.fill ? entry.fill : entry.outline;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether an icon name has been registered. */
|
|
40
|
+
export function hasIcon(name: string): boolean {
|
|
41
|
+
return registry.has(name);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const warned = new Set<string>();
|
|
45
|
+
|
|
46
|
+
function importSnippet(name: string): string {
|
|
47
|
+
const validIdent = /^[a-zA-Z_$][\w$]*$/.test(name);
|
|
48
|
+
const ident = validIdent ? name : `icon_${name.replace(/[^\w$]/g, "_")}`;
|
|
49
|
+
const reg = validIdent ? `{ ${name} }` : `{ "${name}": ${ident} }`;
|
|
50
|
+
return (
|
|
51
|
+
` import ${ident} from "@material-symbols/svg-400/outlined/${name}.svg?component";\n` +
|
|
52
|
+
` import { registerIcons } from "@cfasim-ui/components";\n` +
|
|
53
|
+
` registerIcons(${reg});`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Warn once per unregistered icon name with copy-pasteable registration code. */
|
|
58
|
+
export function warnMissingIcon(name: string): void {
|
|
59
|
+
if (warned.has(name)) return;
|
|
60
|
+
warned.add(name);
|
|
61
|
+
console.warn(
|
|
62
|
+
`[cfasim-ui] Icon "${name}" is not registered — nothing will render.\n` +
|
|
63
|
+
`Register it once at startup:\n\n` +
|
|
64
|
+
`${importSnippet(name)}\n\n` +
|
|
65
|
+
`Requires dev deps "@material-symbols/svg-400" + "vite-svg-loader".\n` +
|
|
66
|
+
`Docs: https://cdcgov.github.io/cfa-simulator/docs/cfasim-ui/components/icon#registering-icons`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from "vue";
|
|
3
|
+
import Icon from "../Icon/Icon.vue";
|
|
4
|
+
|
|
5
|
+
const isDark = ref(window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
6
|
+
|
|
7
|
+
function apply(dark: boolean) {
|
|
8
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
9
|
+
document.documentElement.classList.toggle("light", !dark);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
apply(isDark.value);
|
|
13
|
+
|
|
14
|
+
watch(isDark, apply);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<button
|
|
19
|
+
class="light-dark-toggle"
|
|
20
|
+
type="button"
|
|
21
|
+
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
22
|
+
@click="isDark = !isDark"
|
|
23
|
+
>
|
|
24
|
+
<Icon :icon="isDark ? 'dark_mode' : 'light_mode'" size="sm" />
|
|
25
|
+
</button>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<style scoped>
|
|
29
|
+
.light-dark-toggle {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
width: 32px;
|
|
34
|
+
height: 32px;
|
|
35
|
+
border: 1px solid var(--color-border);
|
|
36
|
+
border-radius: 0.25em;
|
|
37
|
+
background: var(--color-bg-0);
|
|
38
|
+
color: var(--color-text-secondary);
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
transition:
|
|
41
|
+
color var(--transition-fast),
|
|
42
|
+
background-color var(--transition-fast);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.light-dark-toggle:hover {
|
|
46
|
+
color: var(--color-text);
|
|
47
|
+
background: var(--color-bg-1);
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
keywords:
|
|
3
|
+
[
|
|
4
|
+
multiselect,
|
|
5
|
+
multi-select,
|
|
6
|
+
autocomplete,
|
|
7
|
+
combobox,
|
|
8
|
+
tags,
|
|
9
|
+
chips,
|
|
10
|
+
filter,
|
|
11
|
+
search,
|
|
12
|
+
typeahead,
|
|
13
|
+
]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# MultiSelect
|
|
17
|
+
|
|
18
|
+
A multi-select combobox with autocomplete. Built on reka-ui's Combobox: type to
|
|
19
|
+
filter the options, click to add them, and selected values appear as removable
|
|
20
|
+
chips. Binds to a `string[]` via `v-model`.
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
<script setup>
|
|
25
|
+
import { ref } from 'vue'
|
|
26
|
+
const states = ref(['ca'])
|
|
27
|
+
const stationery = ref([])
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<ComponentDemo>
|
|
31
|
+
<div style="width: 280px">
|
|
32
|
+
<MultiSelect
|
|
33
|
+
v-model="states"
|
|
34
|
+
label="States"
|
|
35
|
+
placeholder="Search states…"
|
|
36
|
+
:options="[
|
|
37
|
+
{ value: 'ca', label: 'California' },
|
|
38
|
+
{ value: 'ny', label: 'New York' },
|
|
39
|
+
{ value: 'tx', label: 'Texas' },
|
|
40
|
+
{ value: 'wa', label: 'Washington' },
|
|
41
|
+
{ value: 'fl', label: 'Florida' },
|
|
42
|
+
]"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<template #code>
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<script setup>
|
|
50
|
+
import { ref } from "vue";
|
|
51
|
+
const states = ref(["ca"]);
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<MultiSelect
|
|
55
|
+
v-model="states"
|
|
56
|
+
label="States"
|
|
57
|
+
placeholder="Search states…"
|
|
58
|
+
:options="[
|
|
59
|
+
{ value: 'ca', label: 'California' },
|
|
60
|
+
{ value: 'ny', label: 'New York' },
|
|
61
|
+
{ value: 'tx', label: 'Texas' },
|
|
62
|
+
{ value: 'wa', label: 'Washington' },
|
|
63
|
+
{ value: 'fl', label: 'Florida' },
|
|
64
|
+
]"
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
</template>
|
|
69
|
+
</ComponentDemo>
|
|
70
|
+
|
|
71
|
+
Type in the field to filter, click an option to add it, and use a chip's `✕`
|
|
72
|
+
button (or press Backspace in an empty input) to remove a selection.
|
|
73
|
+
|
|
74
|
+
### Hidden label
|
|
75
|
+
|
|
76
|
+
Use `hide-label` to visually hide the label while keeping it available to screen
|
|
77
|
+
readers. Prefer this over `aria-label` whenever you have label text.
|
|
78
|
+
|
|
79
|
+
<ComponentDemo>
|
|
80
|
+
<div style="width: 280px">
|
|
81
|
+
<MultiSelect
|
|
82
|
+
v-model="stationery"
|
|
83
|
+
label="Supplies"
|
|
84
|
+
hide-label
|
|
85
|
+
placeholder="Add supplies…"
|
|
86
|
+
:options="[
|
|
87
|
+
{ value: 'pen', label: 'Pen' },
|
|
88
|
+
{ value: 'pencil', label: 'Pencil' },
|
|
89
|
+
{ value: 'marker', label: 'Marker' },
|
|
90
|
+
{ value: 'eraser', label: 'Eraser' },
|
|
91
|
+
]"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<template #code>
|
|
96
|
+
|
|
97
|
+
```vue
|
|
98
|
+
<MultiSelect
|
|
99
|
+
v-model="supplies"
|
|
100
|
+
label="Supplies"
|
|
101
|
+
hide-label
|
|
102
|
+
placeholder="Add supplies…"
|
|
103
|
+
:options="[
|
|
104
|
+
{ value: 'pen', label: 'Pen' },
|
|
105
|
+
{ value: 'pencil', label: 'Pencil' },
|
|
106
|
+
{ value: 'marker', label: 'Marker' },
|
|
107
|
+
{ value: 'eraser', label: 'Eraser' },
|
|
108
|
+
]"
|
|
109
|
+
/>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
</template>
|
|
113
|
+
</ComponentDemo>
|
|
114
|
+
|
|
115
|
+
## Accessibility
|
|
116
|
+
|
|
117
|
+
MultiSelect is built on reka-ui's Combobox, which implements the
|
|
118
|
+
[ARIA combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/):
|
|
119
|
+
|
|
120
|
+
- The text field has `role="combobox"` with `aria-expanded`, `aria-controls`,
|
|
121
|
+
`aria-autocomplete="list"`, and `aria-activedescendant` tracking the
|
|
122
|
+
highlighted option as you arrow through the list.
|
|
123
|
+
- The popup is a `role="listbox"` with `aria-multiselectable="true"`; each option
|
|
124
|
+
is a `role="option"` and reflects its state via `aria-selected`.
|
|
125
|
+
- Keyboard support is handled for you: type to filter, ↑/↓ to move, Enter to
|
|
126
|
+
toggle the highlighted option, Escape to close, and Backspace in an empty input
|
|
127
|
+
removes the last chip.
|
|
128
|
+
- Pass `label` so the field is named by a real `<label>` (associated via
|
|
129
|
+
`aria-labelledby`); use `hide-label` to keep it for screen readers while hiding
|
|
130
|
+
it visually, or `aria-label` when there's no visible label text.
|
|
131
|
+
- Each chip's remove button has an `aria-label` (e.g. "Remove California").
|
|
132
|
+
|
|
133
|
+
<!--@include: ./_api/multi-select.md-->
|
|
134
|
+
|
|
135
|
+
### MultiSelectOption
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
interface MultiSelectOption {
|
|
139
|
+
value: string;
|
|
140
|
+
label: string;
|
|
141
|
+
}
|
|
142
|
+
```
|