@vobs/ui 0.2.0 → 1.0.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/LICENSE +1 -1
- package/README.md +60 -0
- package/package.json +52 -102
- package/src/activity-rail.ts +125 -0
- package/src/alert.ts +65 -0
- package/src/assets/icons/alert-circle.svg +1 -0
- package/src/assets/icons/alert-triangle.svg +1 -0
- package/src/assets/icons/alert.d27e24.svg +1 -0
- package/src/assets/icons/alert.svg +1 -0
- package/src/assets/icons/apple.svg +1 -0
- package/src/assets/icons/arrow-down.svg +1 -0
- package/src/assets/icons/arrow-expand.svg +1 -0
- package/src/assets/icons/arrow-left.svg +1 -0
- package/src/assets/icons/arrow-minimize.svg +1 -0
- package/src/assets/icons/arrow-right-to-line.svg +1 -0
- package/src/assets/icons/arrow-right.svg +1 -0
- package/src/assets/icons/arrow-up-right.svg +1 -0
- package/src/assets/icons/arrow-up.0c0c0d.svg +1 -0
- package/src/assets/icons/arrow-up.svg +1 -0
- package/src/assets/icons/at.svg +1 -0
- package/src/assets/icons/atom.0c0c0d.svg +1 -0
- package/src/assets/icons/atom.svg +1 -0
- package/src/assets/icons/bar.svg +1 -0
- package/src/assets/icons/bell.svg +1 -0
- package/src/assets/icons/box.svg +1 -0
- package/src/assets/icons/bug.svg +1 -0
- package/src/assets/icons/calendar.svg +1 -0
- package/src/assets/icons/check-circle.33c192.svg +1 -0
- package/src/assets/icons/check-circle.svg +1 -0
- package/src/assets/icons/check.d1d3db.svg +1 -0
- package/src/assets/icons/check.svg +1 -0
- package/src/assets/icons/chevron-down.svg +1 -0
- package/src/assets/icons/chevron-right.svg +1 -0
- package/src/assets/icons/chevron-up.svg +1 -0
- package/src/assets/icons/circle.svg +1 -0
- package/src/assets/icons/clock.svg +1 -0
- package/src/assets/icons/cmd.svg +1 -0
- package/src/assets/icons/code.svg +1 -0
- package/src/assets/icons/columns.svg +1 -0
- package/src/assets/icons/copy.svg +1 -0
- package/src/assets/icons/cpu.svg +1 -0
- package/src/assets/icons/dashboard.0c0c0d.svg +1 -0
- package/src/assets/icons/dashboard.svg +1 -0
- package/src/assets/icons/dollar.svg +1 -0
- package/src/assets/icons/download.svg +1 -0
- package/src/assets/icons/edit.svg +1 -0
- package/src/assets/icons/extensions.svg +1 -0
- package/src/assets/icons/external.svg +1 -0
- package/src/assets/icons/eye.svg +1 -0
- package/src/assets/icons/file-text.svg +1 -0
- package/src/assets/icons/file.svg +1 -0
- package/src/assets/icons/files.svg +1 -0
- package/src/assets/icons/filter.svg +1 -0
- package/src/assets/icons/folder.svg +1 -0
- package/src/assets/icons/gear.svg +1 -0
- package/src/assets/icons/git.svg +1 -0
- package/src/assets/icons/github.svg +1 -0
- package/src/assets/icons/globe.svg +1 -0
- package/src/assets/icons/grid-2x2.svg +1 -0
- package/src/assets/icons/hash.svg +1 -0
- package/src/assets/icons/heart.svg +1 -0
- package/src/assets/icons/help.svg +1 -0
- package/src/assets/icons/home.svg +1 -0
- package/src/assets/icons/image.svg +1 -0
- package/src/assets/icons/info-circle.svg +1 -0
- package/src/assets/icons/info-square.svg +1 -0
- package/src/assets/icons/info.387bff.svg +1 -0
- package/src/assets/icons/info.svg +1 -0
- package/src/assets/icons/key.svg +1 -0
- package/src/assets/icons/layers.svg +1 -0
- package/src/assets/icons/layout.svg +1 -0
- package/src/assets/icons/link.svg +1 -0
- package/src/assets/icons/list.svg +1 -0
- package/src/assets/icons/lock.svg +1 -0
- package/src/assets/icons/log-out.svg +1 -0
- package/src/assets/icons/logo.svg +1 -0
- package/src/assets/icons/mail.svg +1 -0
- package/src/assets/icons/menu.svg +1 -0
- package/src/assets/icons/message-circle.svg +1 -0
- package/src/assets/icons/message-plus.svg +1 -0
- package/src/assets/icons/message-square.svg +1 -0
- package/src/assets/icons/mic.svg +1 -0
- package/src/assets/icons/minus.svg +1 -0
- package/src/assets/icons/moon.svg +1 -0
- package/src/assets/icons/more-h.svg +1 -0
- package/src/assets/icons/mouse.svg +1 -0
- package/src/assets/icons/palette.svg +1 -0
- package/src/assets/icons/panel-bottom.svg +1 -0
- package/src/assets/icons/pause.svg +1 -0
- package/src/assets/icons/play.svg +1 -0
- package/src/assets/icons/plug.svg +1 -0
- package/src/assets/icons/plus.0c0c0d.svg +1 -0
- package/src/assets/icons/plus.svg +1 -0
- package/src/assets/icons/refresh.svg +1 -0
- package/src/assets/icons/ruler.svg +1 -0
- package/src/assets/icons/scroll-text.svg +1 -0
- package/src/assets/icons/search-menu.svg +1 -0
- package/src/assets/icons/search.svg +1 -0
- package/src/assets/icons/send.0c0c0d.svg +1 -0
- package/src/assets/icons/send.svg +1 -0
- package/src/assets/icons/settings.0c0c0d.svg +1 -0
- package/src/assets/icons/settings.svg +1 -0
- package/src/assets/icons/shield.svg +1 -0
- package/src/assets/icons/sidebar-left.svg +1 -0
- package/src/assets/icons/sidebar-right.svg +1 -0
- package/src/assets/icons/sliders.svg +1 -0
- package/src/assets/icons/sparkles.32f08c.svg +1 -0
- package/src/assets/icons/sparkles.svg +1 -0
- package/src/assets/icons/square.svg +1 -0
- package/src/assets/icons/star.svg +1 -0
- package/src/assets/icons/sun.svg +1 -0
- package/src/assets/icons/terminal.svg +1 -0
- package/src/assets/icons/trash.svg +1 -0
- package/src/assets/icons/trending-down.svg +1 -0
- package/src/assets/icons/trending-up.svg +1 -0
- package/src/assets/icons/type.svg +1 -0
- package/src/assets/icons/upload.svg +1 -0
- package/src/assets/icons/user-circle.svg +1 -0
- package/src/assets/icons/user.svg +1 -0
- package/src/assets/icons/users.svg +1 -0
- package/src/assets/icons/wrench.svg +1 -0
- package/src/assets/icons/x-circle.svg +1 -0
- package/src/assets/icons/x.svg +1 -0
- package/src/assets/icons/zap.0c0c0d.svg +1 -0
- package/src/assets/icons/zap.svg +1 -0
- package/src/avatar.ts +64 -0
- package/src/button.ts +88 -0
- package/src/card.ts +59 -0
- package/src/chat-composer.ts +156 -0
- package/src/dialog.ts +127 -0
- package/src/drawer.ts +131 -0
- package/src/editor-tabs.ts +184 -0
- package/src/file-tree.ts +214 -0
- package/src/forms.ts +270 -0
- package/src/icon.ts +117 -0
- package/src/icons.js +165 -0
- package/src/index.test.ts +213 -0
- package/src/index.ts +94 -0
- package/src/kbd.ts +72 -0
- package/src/menu.ts +119 -0
- package/src/nav-list.ts +157 -0
- package/src/overlay.test.ts +66 -0
- package/src/overlay.ts +205 -0
- package/src/page-header.ts +97 -0
- package/src/pagination.ts +163 -0
- package/src/second-batch.test.ts +205 -0
- package/src/stat-card.ts +92 -0
- package/src/status-bar.ts +108 -0
- package/src/styles/base.css +240 -0
- package/src/styles/components.css +1234 -0
- package/{dist → src/styles}/styles.css +2 -0
- package/src/styles/toast.css +57 -0
- package/src/styles/tokens.css +438 -0
- package/src/styles/vui.css +47 -0
- package/src/table.ts +191 -0
- package/src/tabs.ts +100 -0
- package/src/tag.ts +33 -0
- package/src/third-batch.test.ts +137 -0
- package/src/toast.test.ts +55 -0
- package/src/toast.ts +79 -0
- package/src/types.ts +25 -0
- package/src/utils.ts +221 -0
- package/src/workbench-titlebar.ts +137 -0
- package/dist/app-shell-dom.d.ts +0 -24
- package/dist/app-shell-dom.js +0 -573
- package/dist/app-shell.d.ts +0 -199
- package/dist/app-shell.js +0 -370
- package/dist/base.css +0 -52
- package/dist/calendar-picker.d.ts +0 -45
- package/dist/calendar-picker.js +0 -217
- package/dist/calendar.d.ts +0 -95
- package/dist/calendar.js +0 -194
- package/dist/complex-inputs.d.ts +0 -300
- package/dist/complex-inputs.js +0 -862
- package/dist/components.css +0 -4568
- package/dist/data-display.d.ts +0 -1063
- package/dist/data-display.js +0 -2298
- package/dist/feedback.d.ts +0 -88
- package/dist/feedback.js +0 -281
- package/dist/forms.d.ts +0 -378
- package/dist/forms.js +0 -1015
- package/dist/icons-config.d.ts +0 -25
- package/dist/icons-config.js +0 -14
- package/dist/icons.d.ts +0 -41
- package/dist/icons.js +0 -185
- package/dist/index.d.ts +0 -27
- package/dist/index.js +0 -34
- package/dist/locale/en-US.d.ts +0 -31
- package/dist/locale/en-US.js +0 -20
- package/dist/locale/zh-CN.d.ts +0 -31
- package/dist/locale/zh-CN.js +0 -20
- package/dist/navigation.d.ts +0 -609
- package/dist/navigation.js +0 -1707
- package/dist/overlay.d.ts +0 -304
- package/dist/overlay.js +0 -969
- package/dist/primitives.d.ts +0 -268
- package/dist/primitives.js +0 -764
- package/dist/theme-data.d.ts +0 -363
- package/dist/theme-data.js +0 -374
- package/dist/theme.d.ts +0 -79
- package/dist/theme.js +0 -262
- package/dist/tokens.css +0 -449
- package/dist/tree.d.ts +0 -144
- package/dist/tree.js +0 -469
- package/dist/virtual-list.d.ts +0 -190
- package/dist/virtual-list.js +0 -521
- package/dist/workbench.d.ts +0 -136
- package/dist/workbench.js +0 -156
package/src/file-tree.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { state } from '@vobs/reactivity'
|
|
2
|
+
import {
|
|
3
|
+
addEventListener,
|
|
4
|
+
createElement,
|
|
5
|
+
createFragment,
|
|
6
|
+
createText,
|
|
7
|
+
insertBefore,
|
|
8
|
+
insertDynamic,
|
|
9
|
+
setAttribute,
|
|
10
|
+
setProperty,
|
|
11
|
+
type VobsNode
|
|
12
|
+
} from '@vobs/vobs'
|
|
13
|
+
import {
|
|
14
|
+
bindClassList,
|
|
15
|
+
bindCommonAttributes,
|
|
16
|
+
bindUserStyle,
|
|
17
|
+
hasProp,
|
|
18
|
+
mountSlot,
|
|
19
|
+
readProp,
|
|
20
|
+
setOptionalAttribute
|
|
21
|
+
} from './utils'
|
|
22
|
+
import type { VuiChildren, VuiCommonProps } from './types'
|
|
23
|
+
|
|
24
|
+
export type FileTreeItemKind = 'file' | 'folder'
|
|
25
|
+
|
|
26
|
+
export interface FileTreeItem {
|
|
27
|
+
readonly id: string
|
|
28
|
+
readonly label: string
|
|
29
|
+
readonly kind?: FileTreeItemKind
|
|
30
|
+
readonly icon?: VuiChildren
|
|
31
|
+
readonly chevron?: VuiChildren
|
|
32
|
+
readonly children?: readonly FileTreeItem[]
|
|
33
|
+
readonly expanded?: boolean
|
|
34
|
+
readonly active?: boolean
|
|
35
|
+
readonly disabled?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FileTreeProps extends VuiCommonProps {
|
|
39
|
+
readonly items?: readonly FileTreeItem[]
|
|
40
|
+
readonly value?: string
|
|
41
|
+
readonly defaultValue?: string
|
|
42
|
+
readonly expanded?: readonly string[]
|
|
43
|
+
readonly defaultExpanded?: readonly string[]
|
|
44
|
+
readonly onSelect?: (id: string, event: MouseEvent) => void
|
|
45
|
+
readonly onToggle?: (id: string, expanded: boolean, event: MouseEvent) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface VisibleFileTreeItem {
|
|
49
|
+
readonly item: FileTreeItem
|
|
50
|
+
readonly depth: number
|
|
51
|
+
readonly expanded: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function FileTree(props: FileTreeProps = {}): VobsNode {
|
|
55
|
+
const items = readProp<readonly FileTreeItem[]>(props, 'items', [])
|
|
56
|
+
const internalExpanded = state<readonly string[]>(initialExpanded(props, items))
|
|
57
|
+
const root = createElement('div')
|
|
58
|
+
bindClassList(root, props, () => ['vui-filetree'])
|
|
59
|
+
bindCommonAttributes(root, props, [
|
|
60
|
+
'items',
|
|
61
|
+
'value',
|
|
62
|
+
'defaultValue',
|
|
63
|
+
'expanded',
|
|
64
|
+
'defaultExpanded',
|
|
65
|
+
'onSelect',
|
|
66
|
+
'onToggle'
|
|
67
|
+
])
|
|
68
|
+
bindUserStyle(root, props)
|
|
69
|
+
if (!hasProp(props, 'role')) setAttribute(root, 'role', 'tree')
|
|
70
|
+
if (!hasProp(props, 'aria-label')) setAttribute(root, 'aria-label', 'Files')
|
|
71
|
+
insertDynamic(root, null, () => createTreeRows(props, internalExpanded))
|
|
72
|
+
if (hasProp(props, 'children')) mountSlot(root, props, 'children')
|
|
73
|
+
return root
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createTreeRows(
|
|
77
|
+
props: FileTreeProps,
|
|
78
|
+
internalExpanded: { value: readonly string[] }
|
|
79
|
+
): VobsNode {
|
|
80
|
+
const items = readProp<readonly FileTreeItem[]>(props, 'items', [])
|
|
81
|
+
const expanded = new Set(readProp<readonly string[] | undefined>(props, 'expanded', undefined) ?? internalExpanded.value)
|
|
82
|
+
const visible = flattenVisible(items, expanded)
|
|
83
|
+
const selected = activeId(props, items)
|
|
84
|
+
return createFragment((parent, anchor) => {
|
|
85
|
+
for (const entry of visible) {
|
|
86
|
+
insertBefore(parent, createTreeRow(entry, props, internalExpanded, selected, expanded), anchor)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createTreeRow(
|
|
92
|
+
entry: VisibleFileTreeItem,
|
|
93
|
+
props: FileTreeProps,
|
|
94
|
+
internalExpanded: { value: readonly string[] },
|
|
95
|
+
selected: string | undefined,
|
|
96
|
+
expanded: ReadonlySet<string>
|
|
97
|
+
): VobsNode {
|
|
98
|
+
const { item } = entry
|
|
99
|
+
const isFolder = item.kind === 'folder'
|
|
100
|
+
const hasChildren = isFolder && Boolean(item.children?.length)
|
|
101
|
+
const active = item.id === selected
|
|
102
|
+
const row = createElement('div')
|
|
103
|
+
setAttribute(row, 'class', `vui-filetree__row${active ? ' is-active' : ''}`)
|
|
104
|
+
setAttribute(row, 'role', 'treeitem')
|
|
105
|
+
setAttribute(row, 'data-depth', String(entry.depth))
|
|
106
|
+
setAttribute(row, 'data-file-id', item.id)
|
|
107
|
+
setProperty(row, 'tabIndex', item.disabled === true ? -1 : 0)
|
|
108
|
+
if (active) setAttribute(row, 'aria-selected', 'true')
|
|
109
|
+
if (isFolder) setAttribute(row, 'aria-expanded', entry.expanded ? 'true' : 'false')
|
|
110
|
+
if (item.disabled === true) setAttribute(row, 'aria-disabled', 'true')
|
|
111
|
+
|
|
112
|
+
const chevron = createElement('span')
|
|
113
|
+
setAttribute(chevron, 'class', `vui-filetree__chevron${hasChildren ? '' : ' vui-filetree__chevron--leaf'}`)
|
|
114
|
+
if (hasChildren) {
|
|
115
|
+
if (item.chevron !== undefined) mountSlot(chevron, item, 'chevron')
|
|
116
|
+
else insertBefore(chevron, createText(entry.expanded ? 'v' : '>'), null)
|
|
117
|
+
}
|
|
118
|
+
insertBefore(row, chevron, null)
|
|
119
|
+
|
|
120
|
+
if (item.icon !== undefined) {
|
|
121
|
+
const icon = createElement('span')
|
|
122
|
+
setAttribute(icon, `class`, `vui-filetree__icon ${fileIconClass(item)}`.trim())
|
|
123
|
+
mountSlot(icon, item, 'icon')
|
|
124
|
+
insertBefore(row, icon, null)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const label = createElement('span')
|
|
128
|
+
setAttribute(label, 'class', 'vui-filetree__label')
|
|
129
|
+
setOptionalAttribute(label, 'title', item.label)
|
|
130
|
+
insertBefore(label, createText(item.label), null)
|
|
131
|
+
insertBefore(row, label, null)
|
|
132
|
+
|
|
133
|
+
addEventListener(row, 'click', event => {
|
|
134
|
+
if (item.disabled === true) return
|
|
135
|
+
if (isFolder && hasChildren) {
|
|
136
|
+
const nextExpanded = !expanded.has(item.id)
|
|
137
|
+
if (readProp<readonly string[] | undefined>(props, 'expanded', undefined) === undefined) {
|
|
138
|
+
const next = new Set(internalExpanded.value)
|
|
139
|
+
if (nextExpanded) next.add(item.id)
|
|
140
|
+
else next.delete(item.id)
|
|
141
|
+
internalExpanded.value = [...next]
|
|
142
|
+
}
|
|
143
|
+
const onToggle = readProp<unknown>(props, 'onToggle', undefined)
|
|
144
|
+
if (typeof onToggle === 'function') {
|
|
145
|
+
(onToggle as FileTreeProps['onToggle'])!(item.id, nextExpanded, event as MouseEvent)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const onSelect = readProp<unknown>(props, 'onSelect', undefined)
|
|
149
|
+
if (typeof onSelect === 'function') (onSelect as FileTreeProps['onSelect'])!(item.id, event as MouseEvent)
|
|
150
|
+
})
|
|
151
|
+
addEventListener(row, 'keydown', event => {
|
|
152
|
+
const keyboardEvent = event as KeyboardEvent
|
|
153
|
+
if (keyboardEvent.key !== 'Enter' && keyboardEvent.key !== ' ') return
|
|
154
|
+
keyboardEvent.preventDefault?.()
|
|
155
|
+
row.dispatchEvent?.(new MouseEvent('click', { bubbles: true }))
|
|
156
|
+
})
|
|
157
|
+
return row
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function flattenVisible(
|
|
161
|
+
items: readonly FileTreeItem[],
|
|
162
|
+
expanded: ReadonlySet<string>,
|
|
163
|
+
depth = 0
|
|
164
|
+
): VisibleFileTreeItem[] {
|
|
165
|
+
const visible: VisibleFileTreeItem[] = []
|
|
166
|
+
for (const item of items) {
|
|
167
|
+
const folder = item.kind === 'folder'
|
|
168
|
+
const open = folder && expanded.has(item.id)
|
|
169
|
+
visible.push({ item, depth, expanded: open })
|
|
170
|
+
if (open && item.children) visible.push(...flattenVisible(item.children, expanded, depth + 1))
|
|
171
|
+
}
|
|
172
|
+
return visible
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function initialExpanded(props: FileTreeProps, items: readonly FileTreeItem[]): readonly string[] {
|
|
176
|
+
const ids = new Set(readProp<readonly string[]>(props, 'defaultExpanded', []))
|
|
177
|
+
collectExpanded(items, ids)
|
|
178
|
+
return [...ids]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function collectExpanded(items: readonly FileTreeItem[], ids: Set<string>): void {
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
if (item.expanded === true) ids.add(item.id)
|
|
184
|
+
if (item.children) collectExpanded(item.children, ids)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function activeId(props: FileTreeProps, items: readonly FileTreeItem[]): string | undefined {
|
|
189
|
+
const controlled = readProp<string | undefined>(props, 'value', undefined)
|
|
190
|
+
if (controlled !== undefined) return controlled
|
|
191
|
+
const defaultValue = readProp<string | undefined>(props, 'defaultValue', undefined)
|
|
192
|
+
if (defaultValue !== undefined) return defaultValue
|
|
193
|
+
return findActive(items)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function findActive(items: readonly FileTreeItem[]): string | undefined {
|
|
197
|
+
for (const item of items) {
|
|
198
|
+
if (item.active === true) return item.id
|
|
199
|
+
if (item.children) {
|
|
200
|
+
const nested = findActive(item.children)
|
|
201
|
+
if (nested !== undefined) return nested
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function fileIconClass(item: FileTreeItem): string {
|
|
208
|
+
if (item.kind === 'folder') return 'vui-filetree__icon--folder'
|
|
209
|
+
const extension = item.label.slice(item.label.lastIndexOf('.') + 1).toLowerCase()
|
|
210
|
+
if (extension === 'ts' || extension === 'tsx') return 'vui-filetree__icon--ts'
|
|
211
|
+
if (extension === 'css' || extension === 'scss') return 'vui-filetree__icon--css'
|
|
212
|
+
if (extension === 'json') return 'vui-filetree__icon--json'
|
|
213
|
+
return ''
|
|
214
|
+
}
|
package/src/forms.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { effect } from '@vobs/reactivity'
|
|
2
|
+
import {
|
|
3
|
+
createElement,
|
|
4
|
+
insertBefore,
|
|
5
|
+
setAttribute,
|
|
6
|
+
type VobsNode
|
|
7
|
+
} from '@vobs/vobs'
|
|
8
|
+
import {
|
|
9
|
+
bindClassList,
|
|
10
|
+
bindCommonAttributes,
|
|
11
|
+
bindPropertyValue,
|
|
12
|
+
bindUserStyle,
|
|
13
|
+
hasProp,
|
|
14
|
+
listen,
|
|
15
|
+
mountSlot,
|
|
16
|
+
readProp,
|
|
17
|
+
setOptionalAttribute,
|
|
18
|
+
setOptionalProperty
|
|
19
|
+
} from './utils'
|
|
20
|
+
import type { VuiCommonProps, VuiEventHandler } from './types'
|
|
21
|
+
|
|
22
|
+
export type InputType = 'date' | 'email' | 'number' | 'password' | 'search' | 'text' | 'url'
|
|
23
|
+
|
|
24
|
+
export interface InputProps extends VuiCommonProps {
|
|
25
|
+
readonly type?: InputType
|
|
26
|
+
readonly name?: string
|
|
27
|
+
readonly value?: string | number
|
|
28
|
+
readonly placeholder?: string
|
|
29
|
+
readonly disabled?: boolean
|
|
30
|
+
readonly readOnly?: boolean
|
|
31
|
+
readonly required?: boolean
|
|
32
|
+
readonly icon?: VobsNode | (() => VobsNode | null | undefined)
|
|
33
|
+
readonly onInput?: VuiEventHandler<InputEvent>
|
|
34
|
+
readonly onChange?: VuiEventHandler<Event>
|
|
35
|
+
readonly onBlur?: VuiEventHandler<FocusEvent>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function Input(props: InputProps = {}): VobsNode {
|
|
39
|
+
const root = createElement('div')
|
|
40
|
+
const control = createElement('input')
|
|
41
|
+
|
|
42
|
+
bindClassList(root, props, () => [
|
|
43
|
+
'vui-input',
|
|
44
|
+
readProp<InputType>(props, 'type', 'text') === 'search' ? 'vui-search' : undefined
|
|
45
|
+
])
|
|
46
|
+
bindUserStyle(root, props)
|
|
47
|
+
bindCommonAttributes(control, props, [
|
|
48
|
+
'type',
|
|
49
|
+
'name',
|
|
50
|
+
'value',
|
|
51
|
+
'placeholder',
|
|
52
|
+
'disabled',
|
|
53
|
+
'readOnly',
|
|
54
|
+
'required',
|
|
55
|
+
'icon',
|
|
56
|
+
'onInput',
|
|
57
|
+
'onChange',
|
|
58
|
+
'onBlur'
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
effect(() => {
|
|
62
|
+
setOptionalAttribute(control, 'type', readProp<InputType>(props, 'type', 'text'))
|
|
63
|
+
setOptionalAttribute(control, 'name', readProp<string | undefined>(props, 'name', undefined))
|
|
64
|
+
setOptionalAttribute(control, 'placeholder', readProp<string | undefined>(props, 'placeholder', undefined))
|
|
65
|
+
setOptionalProperty(control, 'disabled', readProp(props, 'disabled', false))
|
|
66
|
+
setOptionalProperty(control, 'readOnly', readProp(props, 'readOnly', false))
|
|
67
|
+
setOptionalProperty(control, 'required', readProp(props, 'required', false))
|
|
68
|
+
})
|
|
69
|
+
if (hasProp(props, 'value')) {
|
|
70
|
+
bindPropertyValue(control, 'value', () => readProp<string | number | undefined>(props, 'value', undefined) ?? '')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (hasProp(props, 'icon')) {
|
|
74
|
+
const icon = createElement('span')
|
|
75
|
+
setAttribute(icon, 'class', 'vui-input__icon')
|
|
76
|
+
mountSlot(icon, props, 'icon')
|
|
77
|
+
insertBefore(root, icon, null)
|
|
78
|
+
}
|
|
79
|
+
insertBefore(root, control, null)
|
|
80
|
+
if (hasProp(props, 'children')) mountSlot(root, props, 'children')
|
|
81
|
+
|
|
82
|
+
listen(control, 'input', props, 'onInput', () => readProp(props, 'disabled', false))
|
|
83
|
+
listen(control, 'change', props, 'onChange', () => readProp(props, 'disabled', false))
|
|
84
|
+
listen(control, 'blur', props, 'onBlur', () => false)
|
|
85
|
+
return root
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface TextareaProps extends VuiCommonProps {
|
|
89
|
+
readonly value?: string | number
|
|
90
|
+
readonly placeholder?: string
|
|
91
|
+
readonly disabled?: boolean
|
|
92
|
+
readonly readOnly?: boolean
|
|
93
|
+
readonly required?: boolean
|
|
94
|
+
readonly rows?: number
|
|
95
|
+
readonly name?: string
|
|
96
|
+
readonly onInput?: VuiEventHandler<InputEvent>
|
|
97
|
+
readonly onChange?: VuiEventHandler<Event>
|
|
98
|
+
readonly onBlur?: VuiEventHandler<FocusEvent>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function Textarea(props: TextareaProps = {}): VobsNode {
|
|
102
|
+
const root = createElement('textarea')
|
|
103
|
+
bindClassList(root, props, () => ['vui-textarea'])
|
|
104
|
+
bindCommonAttributes(root, props, [
|
|
105
|
+
'value',
|
|
106
|
+
'placeholder',
|
|
107
|
+
'disabled',
|
|
108
|
+
'readOnly',
|
|
109
|
+
'required',
|
|
110
|
+
'rows',
|
|
111
|
+
'name',
|
|
112
|
+
'onInput',
|
|
113
|
+
'onChange',
|
|
114
|
+
'onBlur'
|
|
115
|
+
])
|
|
116
|
+
effect(() => {
|
|
117
|
+
setOptionalAttribute(root, 'name', readProp<string | undefined>(props, 'name', undefined))
|
|
118
|
+
setOptionalAttribute(root, 'placeholder', readProp<string | undefined>(props, 'placeholder', undefined))
|
|
119
|
+
setOptionalProperty(root, 'disabled', readProp(props, 'disabled', false))
|
|
120
|
+
setOptionalProperty(root, 'readOnly', readProp(props, 'readOnly', false))
|
|
121
|
+
setOptionalProperty(root, 'required', readProp(props, 'required', false))
|
|
122
|
+
setOptionalAttribute(root, 'rows', readProp<number | undefined>(props, 'rows', undefined))
|
|
123
|
+
})
|
|
124
|
+
if (hasProp(props, 'value')) {
|
|
125
|
+
bindPropertyValue(root, 'value', () => readProp<string | number | undefined>(props, 'value', undefined) ?? '')
|
|
126
|
+
}
|
|
127
|
+
listen(root, 'input', props, 'onInput', () => readProp(props, 'disabled', false))
|
|
128
|
+
listen(root, 'change', props, 'onChange', () => readProp(props, 'disabled', false))
|
|
129
|
+
listen(root, 'blur', props, 'onBlur', () => false)
|
|
130
|
+
return root
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface SelectProps extends VuiCommonProps {
|
|
134
|
+
readonly name?: string
|
|
135
|
+
readonly value?: string | number
|
|
136
|
+
readonly disabled?: boolean
|
|
137
|
+
readonly required?: boolean
|
|
138
|
+
readonly multiple?: boolean
|
|
139
|
+
readonly size?: number
|
|
140
|
+
readonly onChange?: VuiEventHandler<Event>
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function Select(props: SelectProps = {}): VobsNode {
|
|
144
|
+
const root = createElement('select')
|
|
145
|
+
bindClassList(root, props, () => ['vui-select'])
|
|
146
|
+
bindCommonAttributes(root, props, [
|
|
147
|
+
'name',
|
|
148
|
+
'value',
|
|
149
|
+
'disabled',
|
|
150
|
+
'required',
|
|
151
|
+
'multiple',
|
|
152
|
+
'size',
|
|
153
|
+
'onChange'
|
|
154
|
+
])
|
|
155
|
+
effect(() => {
|
|
156
|
+
setOptionalAttribute(root, 'name', readProp<string | undefined>(props, 'name', undefined))
|
|
157
|
+
setOptionalProperty(root, 'disabled', readProp(props, 'disabled', false))
|
|
158
|
+
setOptionalProperty(root, 'required', readProp(props, 'required', false))
|
|
159
|
+
setOptionalProperty(root, 'multiple', readProp(props, 'multiple', false))
|
|
160
|
+
setOptionalAttribute(root, 'size', readProp<number | undefined>(props, 'size', undefined))
|
|
161
|
+
})
|
|
162
|
+
if (hasProp(props, 'children')) mountSlot(root, props, 'children')
|
|
163
|
+
if (hasProp(props, 'value')) {
|
|
164
|
+
bindPropertyValue(root, 'value', () => readProp<string | number | undefined>(props, 'value', undefined) ?? '')
|
|
165
|
+
}
|
|
166
|
+
listen(root, 'change', props, 'onChange', () => readProp(props, 'disabled', false))
|
|
167
|
+
return root
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ChoiceProps extends VuiCommonProps {
|
|
171
|
+
readonly name?: string
|
|
172
|
+
readonly value?: string
|
|
173
|
+
readonly checked?: boolean
|
|
174
|
+
readonly disabled?: boolean
|
|
175
|
+
readonly required?: boolean
|
|
176
|
+
readonly onChange?: VuiEventHandler<Event>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function Checkbox(props: ChoiceProps = {}): VobsNode {
|
|
180
|
+
return createChoice('checkbox', props)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function Radio(props: ChoiceProps = {}): VobsNode {
|
|
184
|
+
return createChoice('radio', props)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function createChoice(type: 'checkbox' | 'radio', props: ChoiceProps): VobsNode {
|
|
188
|
+
const root = createElement('label')
|
|
189
|
+
const control = createElement('input')
|
|
190
|
+
const visual = createElement('span')
|
|
191
|
+
|
|
192
|
+
bindClassList(root, props, () => [`vui-${type}`])
|
|
193
|
+
bindUserStyle(root, props)
|
|
194
|
+
bindCommonAttributes(control, props, [
|
|
195
|
+
'name',
|
|
196
|
+
'value',
|
|
197
|
+
'checked',
|
|
198
|
+
'disabled',
|
|
199
|
+
'required',
|
|
200
|
+
'onChange'
|
|
201
|
+
])
|
|
202
|
+
setAttribute(visual, 'class', type === 'checkbox' ? 'vui-check__box' : 'vui-radio__dot')
|
|
203
|
+
|
|
204
|
+
effect(() => {
|
|
205
|
+
setOptionalAttribute(control, 'type', type)
|
|
206
|
+
setOptionalAttribute(control, 'name', readProp<string | undefined>(props, 'name', undefined))
|
|
207
|
+
setOptionalAttribute(control, 'value', readProp<string | undefined>(props, 'value', undefined))
|
|
208
|
+
setOptionalProperty(control, 'disabled', readProp(props, 'disabled', false))
|
|
209
|
+
setOptionalProperty(control, 'required', readProp(props, 'required', false))
|
|
210
|
+
})
|
|
211
|
+
if (hasProp(props, 'checked')) {
|
|
212
|
+
bindPropertyValue(control, 'checked', () => readProp(props, 'checked', false))
|
|
213
|
+
}
|
|
214
|
+
insertBefore(root, control, null)
|
|
215
|
+
insertBefore(root, visual, null)
|
|
216
|
+
if (hasProp(props, 'children')) mountSlot(root, props, 'children')
|
|
217
|
+
listen(control, 'change', props, 'onChange', () => readProp(props, 'disabled', false))
|
|
218
|
+
return root
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface SwitchProps extends VuiCommonProps {
|
|
222
|
+
readonly checked?: boolean
|
|
223
|
+
readonly disabled?: boolean
|
|
224
|
+
readonly readOnly?: boolean
|
|
225
|
+
readonly size?: 'sm' | 'md'
|
|
226
|
+
readonly name?: string
|
|
227
|
+
readonly value?: string
|
|
228
|
+
readonly onChange?: VuiEventHandler<Event>
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function Switch(props: SwitchProps = {}): VobsNode {
|
|
232
|
+
const root = createElement('label')
|
|
233
|
+
const control = createElement('input')
|
|
234
|
+
const thumb = createElement('span')
|
|
235
|
+
|
|
236
|
+
bindClassList(root, props, () => [
|
|
237
|
+
'vui-switch',
|
|
238
|
+
readProp<'sm' | 'md'>(props, 'size', 'md') === 'sm' ? 'vui-switch--sm' : undefined
|
|
239
|
+
])
|
|
240
|
+
bindUserStyle(root, props)
|
|
241
|
+
bindCommonAttributes(root, props, ['role'])
|
|
242
|
+
bindCommonAttributes(control, props, [
|
|
243
|
+
'checked',
|
|
244
|
+
'disabled',
|
|
245
|
+
'readOnly',
|
|
246
|
+
'name',
|
|
247
|
+
'value',
|
|
248
|
+
'onChange'
|
|
249
|
+
])
|
|
250
|
+
setAttribute(thumb, 'class', 'vui-switch__thumb')
|
|
251
|
+
setAttribute(root, 'role', 'switch')
|
|
252
|
+
|
|
253
|
+
effect(() => {
|
|
254
|
+
setOptionalAttribute(control, 'type', 'checkbox')
|
|
255
|
+
setOptionalAttribute(control, 'name', readProp<string | undefined>(props, 'name', undefined))
|
|
256
|
+
setOptionalAttribute(control, 'value', readProp<string | undefined>(props, 'value', undefined))
|
|
257
|
+
setOptionalProperty(control, 'disabled', readProp(props, 'disabled', false))
|
|
258
|
+
setOptionalProperty(control, 'readOnly', readProp(props, 'readOnly', false))
|
|
259
|
+
setOptionalAttribute(root, 'aria-checked', readProp(props, 'checked', false) ? 'true' : 'false')
|
|
260
|
+
setOptionalAttribute(root, 'aria-disabled', readProp(props, 'disabled', false) ? 'true' : undefined)
|
|
261
|
+
})
|
|
262
|
+
if (hasProp(props, 'checked')) {
|
|
263
|
+
bindPropertyValue(control, 'checked', () => readProp(props, 'checked', false))
|
|
264
|
+
}
|
|
265
|
+
insertBefore(root, control, null)
|
|
266
|
+
insertBefore(root, thumb, null)
|
|
267
|
+
if (hasProp(props, 'children')) mountSlot(root, props, 'children')
|
|
268
|
+
listen(control, 'change', props, 'onChange', () => readProp(props, 'disabled', false))
|
|
269
|
+
return root
|
|
270
|
+
}
|
package/src/icon.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createIcon,
|
|
3
|
+
createSvgIconNode,
|
|
4
|
+
type IconDefinition,
|
|
5
|
+
type SvgIconDefinition,
|
|
6
|
+
type VobsIconProps
|
|
7
|
+
} from '@vobs/icon-core'
|
|
8
|
+
import { hasProp, mountSlot, readProp } from './utils'
|
|
9
|
+
import type { VobsNode } from '@vobs/vobs'
|
|
10
|
+
import type { VuiCommonProps } from './types'
|
|
11
|
+
|
|
12
|
+
export type { IconDefinition } from '@vobs/icon-core'
|
|
13
|
+
|
|
14
|
+
export interface IconProps extends VuiCommonProps, VobsIconProps {
|
|
15
|
+
readonly name?: string
|
|
16
|
+
readonly icon?: IconDefinition
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Icon(props: IconProps = {}): VobsNode {
|
|
20
|
+
const root = createSvgIconNode(() => {
|
|
21
|
+
const definition = resolveIcon(
|
|
22
|
+
readProp<IconDefinition | string | undefined>(props, 'icon', undefined)
|
|
23
|
+
?? readProp<string | undefined>(props, 'name', undefined)
|
|
24
|
+
)
|
|
25
|
+
if (!definition) {
|
|
26
|
+
if (hasProp(props, 'children')) return undefined
|
|
27
|
+
throw new Error('Vobs UI: Icon requires a registered `name` or an `icon` definition')
|
|
28
|
+
}
|
|
29
|
+
return iconDefinitionToSvg(definition)
|
|
30
|
+
}, props, {
|
|
31
|
+
class: 'vui-icon icon',
|
|
32
|
+
defaultSvgAttributes: {
|
|
33
|
+
fill: 'none',
|
|
34
|
+
stroke: 'currentColor',
|
|
35
|
+
'stroke-width': 2,
|
|
36
|
+
'stroke-linecap': 'butt',
|
|
37
|
+
'stroke-linejoin': 'miter'
|
|
38
|
+
}
|
|
39
|
+
}) as Element
|
|
40
|
+
|
|
41
|
+
if (hasProp(props, 'children')) mountSlot(root, props, 'children')
|
|
42
|
+
return root
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function iconDefinitionToSvg(definition: IconDefinition): SvgIconDefinition {
|
|
46
|
+
return {
|
|
47
|
+
name: definition.name,
|
|
48
|
+
body: definition.path,
|
|
49
|
+
viewBox: definition.viewBox
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const VUI_ICON_PATHS = {
|
|
54
|
+
search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
|
|
55
|
+
check: '<polyline points="4 12 10 18 20 6"/>',
|
|
56
|
+
x: '<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>',
|
|
57
|
+
plus: '<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>',
|
|
58
|
+
minus: '<line x1="5" y1="12" x2="19" y2="12"/>',
|
|
59
|
+
'chevron-right': '<polyline points="9 6 15 12 9 18"/>',
|
|
60
|
+
'chevron-down': '<polyline points="6 9 12 15 18 9"/>',
|
|
61
|
+
'chevron-up': '<polyline points="6 15 12 9 18 15"/>',
|
|
62
|
+
'chevron-left': '<polyline points="15 6 9 12 15 18"/>',
|
|
63
|
+
'arrow-left': '<line x1="20" y1="12" x2="4" y2="12"/><polyline points="10 6 4 12 10 18"/>',
|
|
64
|
+
'arrow-right': '<line x1="4" y1="12" x2="20" y2="12"/><polyline points="14 6 20 12 14 18"/>',
|
|
65
|
+
'arrow-up': '<line x1="12" y1="20" x2="12" y2="4"/><polyline points="6 10 12 4 18 10"/>',
|
|
66
|
+
'arrow-down': '<line x1="12" y1="4" x2="12" y2="20"/><polyline points="6 14 12 20 18 14"/>',
|
|
67
|
+
atom: '<circle cx="12" cy="12" r="2"/><ellipse cx="12" cy="12" rx="10" ry="4"/><ellipse cx="12" cy="12" rx="10" ry="4" transform="rotate(60 12 12)"/><ellipse cx="12" cy="12" rx="10" ry="4" transform="rotate(120 12 12)"/>',
|
|
68
|
+
globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/>',
|
|
69
|
+
'arrow-expand': '<polyline points="14 4 20 4 20 10"/><line x1="14" y1="10" x2="20" y2="4"/><polyline points="10 20 4 20 4 14"/><line x1="10" y1="14" x2="4" y2="20"/>',
|
|
70
|
+
'arrow-minimize': '<polyline points="20 4 14 10 20 10"/><line x1="14" y1="10" x2="14" y2="4"/><polyline points="4 20 10 14 4 14"/><line x1="10" y1="14" x2="10" y2="20"/>',
|
|
71
|
+
'check-circle': '<circle cx="12" cy="12" r="9"/><polyline points="8 12 11 15 16 9"/>',
|
|
72
|
+
'alert-circle': '<circle cx="12" cy="12" r="9"/><line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="16" x2="12" y2="16.01"/>',
|
|
73
|
+
'alert-triangle': '<path d="M12 3 22 20 2 20 Z"/><line x1="12" y1="10" x2="12" y2="15"/><line x1="12" y1="18" x2="12" y2="18.01"/>',
|
|
74
|
+
info: '<circle cx="12" cy="12" r="9"/><line x1="12" y1="11" x2="12" y2="16"/><line x1="12" y1="8" x2="12" y2="8.01"/>',
|
|
75
|
+
home: '<polygon points="3 11 12 3 21 11 21 21 14 21 14 14 10 14 10 21 3 21 3 11"/>',
|
|
76
|
+
user: '<path d="M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1"/><circle cx="12" cy="8" r="4"/>',
|
|
77
|
+
settings: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>',
|
|
78
|
+
menu: '<line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/>',
|
|
79
|
+
'more-h': '<circle cx="5" cy="12" r="1.5" fill="currentColor"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/><circle cx="19" cy="12" r="1.5" fill="currentColor"/>',
|
|
80
|
+
calendar: '<rect x="3" y="5" width="18" height="16"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="8" y1="3" x2="8" y2="7"/><line x1="16" y1="3" x2="16" y2="7"/>',
|
|
81
|
+
filter: '<polygon points="3 4 21 4 14 12 14 20 10 18 10 12 3 4"/>',
|
|
82
|
+
sort: '<path d="M8 20V4"/><polyline points="4 8 8 4 12 8"/><path d="M16 4v16"/><polyline points="12 16 16 20 20 16"/>',
|
|
83
|
+
edit: '<path d="M4 20h4l10-10-4-4L4 16v4z"/><path d="M14 6l4 4"/>',
|
|
84
|
+
trash: '<polyline points="4 6 20 6"/><path d="M6 6v14h12V6"/><path d="M9 6V4h6v2"/><line x1="10" y1="10" x2="10" y2="17"/><line x1="14" y1="10" x2="14" y2="17"/>',
|
|
85
|
+
download: '<path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><line x1="3" y1="21" x2="21" y2="21"/>',
|
|
86
|
+
upload: '<path d="M12 21V6"/><polyline points="7 11 12 6 17 11"/><line x1="3" y1="21" x2="21" y2="21"/>',
|
|
87
|
+
pause: '<line x1="9" y1="5" x2="9" y2="19"/><line x1="15" y1="5" x2="15" y2="19"/>',
|
|
88
|
+
play: '<polygon points="8 5 19 12 8 19 8 5"/>',
|
|
89
|
+
refresh: '<polyline points="21 4 21 10 15 10"/><polyline points="3 20 3 14 9 14"/><path d="M20.5 9A9 9 0 0 0 5 5.5L3 7M3.5 15A9 9 0 0 0 19 18.5L21 17"/>',
|
|
90
|
+
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 8 3 8H3s3-1 3-8z"/><path d="M10 21a2 2 0 0 0 4 0"/>',
|
|
91
|
+
folder: '<path d="M3 6h6l2 3h10v10H3z"/>',
|
|
92
|
+
code: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>',
|
|
93
|
+
sparkles: '<path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z"/><path d="M19 14l1 2.2 2.2 1-2.2 1L19 20.4l-1-2.2-2.2-1 2.2-1L19 14z"/>',
|
|
94
|
+
zap: '<polygon points="13 2 4 14 12 14 11 22 20 10 12 10 13 2"/>',
|
|
95
|
+
sun: '<circle cx="12" cy="12" r="4"/><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="2" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="22" y2="12"/><line x1="4.6" y1="4.6" x2="6.7" y2="6.7"/><line x1="17.3" y1="17.3" x2="19.4" y2="19.4"/><line x1="4.6" y1="19.4" x2="6.7" y2="17.3"/><line x1="17.3" y1="6.7" x2="19.4" y2="4.6"/>',
|
|
96
|
+
moon: '<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>'
|
|
97
|
+
} as const
|
|
98
|
+
|
|
99
|
+
const iconRegistry = new Map<string, IconDefinition>(
|
|
100
|
+
Object.entries(VUI_ICON_PATHS).map(([name, path]) => [name, { name, path }])
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
export { VUI_ICON_PATHS }
|
|
104
|
+
export { createIcon, registerIcon, resolveIcon }
|
|
105
|
+
|
|
106
|
+
function registerIcon(icon: IconDefinition): () => void {
|
|
107
|
+
const previous = iconRegistry.get(icon.name)
|
|
108
|
+
iconRegistry.set(icon.name, icon)
|
|
109
|
+
return () => {
|
|
110
|
+
if (previous) iconRegistry.set(icon.name, previous)
|
|
111
|
+
else iconRegistry.delete(icon.name)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveIcon(icon: string | IconDefinition | undefined): IconDefinition | undefined {
|
|
116
|
+
return typeof icon === 'string' ? iconRegistry.get(icon) : icon
|
|
117
|
+
}
|