@vobs/ui 0.3.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +60 -0
  3. package/package.json +52 -110
  4. package/src/activity-rail.ts +125 -0
  5. package/src/alert.ts +65 -0
  6. package/src/assets/icons/alert-circle.svg +1 -0
  7. package/src/assets/icons/alert-triangle.svg +1 -0
  8. package/src/assets/icons/alert.d27e24.svg +1 -0
  9. package/src/assets/icons/alert.svg +1 -0
  10. package/src/assets/icons/apple.svg +1 -0
  11. package/src/assets/icons/arrow-down.svg +1 -0
  12. package/src/assets/icons/arrow-expand.svg +1 -0
  13. package/src/assets/icons/arrow-left.svg +1 -0
  14. package/src/assets/icons/arrow-minimize.svg +1 -0
  15. package/src/assets/icons/arrow-right-to-line.svg +1 -0
  16. package/src/assets/icons/arrow-right.svg +1 -0
  17. package/src/assets/icons/arrow-up-right.svg +1 -0
  18. package/src/assets/icons/arrow-up.0c0c0d.svg +1 -0
  19. package/src/assets/icons/arrow-up.svg +1 -0
  20. package/src/assets/icons/at.svg +1 -0
  21. package/src/assets/icons/atom.0c0c0d.svg +1 -0
  22. package/src/assets/icons/atom.svg +1 -0
  23. package/src/assets/icons/bar.svg +1 -0
  24. package/src/assets/icons/bell.svg +1 -0
  25. package/src/assets/icons/box.svg +1 -0
  26. package/src/assets/icons/bug.svg +1 -0
  27. package/src/assets/icons/calendar.svg +1 -0
  28. package/src/assets/icons/check-circle.33c192.svg +1 -0
  29. package/src/assets/icons/check-circle.svg +1 -0
  30. package/src/assets/icons/check.d1d3db.svg +1 -0
  31. package/src/assets/icons/check.svg +1 -0
  32. package/src/assets/icons/chevron-down.svg +1 -0
  33. package/src/assets/icons/chevron-right.svg +1 -0
  34. package/src/assets/icons/chevron-up.svg +1 -0
  35. package/src/assets/icons/circle.svg +1 -0
  36. package/src/assets/icons/clock.svg +1 -0
  37. package/src/assets/icons/cmd.svg +1 -0
  38. package/src/assets/icons/code.svg +1 -0
  39. package/src/assets/icons/columns.svg +1 -0
  40. package/src/assets/icons/copy.svg +1 -0
  41. package/src/assets/icons/cpu.svg +1 -0
  42. package/src/assets/icons/dashboard.0c0c0d.svg +1 -0
  43. package/src/assets/icons/dashboard.svg +1 -0
  44. package/src/assets/icons/dollar.svg +1 -0
  45. package/src/assets/icons/download.svg +1 -0
  46. package/src/assets/icons/edit.svg +1 -0
  47. package/src/assets/icons/extensions.svg +1 -0
  48. package/src/assets/icons/external.svg +1 -0
  49. package/src/assets/icons/eye.svg +1 -0
  50. package/src/assets/icons/file-text.svg +1 -0
  51. package/src/assets/icons/file.svg +1 -0
  52. package/src/assets/icons/files.svg +1 -0
  53. package/src/assets/icons/filter.svg +1 -0
  54. package/src/assets/icons/folder.svg +1 -0
  55. package/src/assets/icons/gear.svg +1 -0
  56. package/src/assets/icons/git.svg +1 -0
  57. package/src/assets/icons/github.svg +1 -0
  58. package/src/assets/icons/globe.svg +1 -0
  59. package/src/assets/icons/grid-2x2.svg +1 -0
  60. package/src/assets/icons/hash.svg +1 -0
  61. package/src/assets/icons/heart.svg +1 -0
  62. package/src/assets/icons/help.svg +1 -0
  63. package/src/assets/icons/home.svg +1 -0
  64. package/src/assets/icons/image.svg +1 -0
  65. package/src/assets/icons/info-circle.svg +1 -0
  66. package/src/assets/icons/info-square.svg +1 -0
  67. package/src/assets/icons/info.387bff.svg +1 -0
  68. package/src/assets/icons/info.svg +1 -0
  69. package/src/assets/icons/key.svg +1 -0
  70. package/src/assets/icons/layers.svg +1 -0
  71. package/src/assets/icons/layout.svg +1 -0
  72. package/src/assets/icons/link.svg +1 -0
  73. package/src/assets/icons/list.svg +1 -0
  74. package/src/assets/icons/lock.svg +1 -0
  75. package/src/assets/icons/log-out.svg +1 -0
  76. package/src/assets/icons/logo.svg +1 -0
  77. package/src/assets/icons/mail.svg +1 -0
  78. package/src/assets/icons/menu.svg +1 -0
  79. package/src/assets/icons/message-circle.svg +1 -0
  80. package/src/assets/icons/message-plus.svg +1 -0
  81. package/src/assets/icons/message-square.svg +1 -0
  82. package/src/assets/icons/mic.svg +1 -0
  83. package/src/assets/icons/minus.svg +1 -0
  84. package/src/assets/icons/moon.svg +1 -0
  85. package/src/assets/icons/more-h.svg +1 -0
  86. package/src/assets/icons/mouse.svg +1 -0
  87. package/src/assets/icons/palette.svg +1 -0
  88. package/src/assets/icons/panel-bottom.svg +1 -0
  89. package/src/assets/icons/pause.svg +1 -0
  90. package/src/assets/icons/play.svg +1 -0
  91. package/src/assets/icons/plug.svg +1 -0
  92. package/src/assets/icons/plus.0c0c0d.svg +1 -0
  93. package/src/assets/icons/plus.svg +1 -0
  94. package/src/assets/icons/refresh.svg +1 -0
  95. package/src/assets/icons/ruler.svg +1 -0
  96. package/src/assets/icons/scroll-text.svg +1 -0
  97. package/src/assets/icons/search-menu.svg +1 -0
  98. package/src/assets/icons/search.svg +1 -0
  99. package/src/assets/icons/send.0c0c0d.svg +1 -0
  100. package/src/assets/icons/send.svg +1 -0
  101. package/src/assets/icons/settings.0c0c0d.svg +1 -0
  102. package/src/assets/icons/settings.svg +1 -0
  103. package/src/assets/icons/shield.svg +1 -0
  104. package/src/assets/icons/sidebar-left.svg +1 -0
  105. package/src/assets/icons/sidebar-right.svg +1 -0
  106. package/src/assets/icons/sliders.svg +1 -0
  107. package/src/assets/icons/sparkles.32f08c.svg +1 -0
  108. package/src/assets/icons/sparkles.svg +1 -0
  109. package/src/assets/icons/square.svg +1 -0
  110. package/src/assets/icons/star.svg +1 -0
  111. package/src/assets/icons/sun.svg +1 -0
  112. package/src/assets/icons/terminal.svg +1 -0
  113. package/src/assets/icons/trash.svg +1 -0
  114. package/src/assets/icons/trending-down.svg +1 -0
  115. package/src/assets/icons/trending-up.svg +1 -0
  116. package/src/assets/icons/type.svg +1 -0
  117. package/src/assets/icons/upload.svg +1 -0
  118. package/src/assets/icons/user-circle.svg +1 -0
  119. package/src/assets/icons/user.svg +1 -0
  120. package/src/assets/icons/users.svg +1 -0
  121. package/src/assets/icons/wrench.svg +1 -0
  122. package/src/assets/icons/x-circle.svg +1 -0
  123. package/src/assets/icons/x.svg +1 -0
  124. package/src/assets/icons/zap.0c0c0d.svg +1 -0
  125. package/src/assets/icons/zap.svg +1 -0
  126. package/src/avatar.ts +64 -0
  127. package/src/button.ts +88 -0
  128. package/src/card.ts +59 -0
  129. package/src/chat-composer.ts +156 -0
  130. package/src/dialog.ts +127 -0
  131. package/src/drawer.ts +131 -0
  132. package/src/editor-tabs.ts +184 -0
  133. package/src/file-tree.ts +214 -0
  134. package/src/forms.ts +270 -0
  135. package/src/icon.ts +118 -0
  136. package/src/icons.js +165 -0
  137. package/src/index.test.ts +213 -0
  138. package/src/index.ts +94 -0
  139. package/src/kbd.ts +72 -0
  140. package/src/menu.ts +119 -0
  141. package/src/nav-list.ts +157 -0
  142. package/src/overlay.test.ts +66 -0
  143. package/src/overlay.ts +205 -0
  144. package/src/page-header.ts +97 -0
  145. package/src/pagination.ts +163 -0
  146. package/src/second-batch.test.ts +205 -0
  147. package/src/stat-card.ts +92 -0
  148. package/src/status-bar.ts +108 -0
  149. package/src/styles/base.css +240 -0
  150. package/src/styles/components.css +1234 -0
  151. package/{dist → src/styles}/styles.css +2 -0
  152. package/src/styles/toast.css +57 -0
  153. package/src/styles/tokens.css +438 -0
  154. package/src/styles/vui.css +47 -0
  155. package/src/table.ts +191 -0
  156. package/src/tabs.ts +100 -0
  157. package/src/tag.ts +33 -0
  158. package/src/third-batch.test.ts +137 -0
  159. package/src/toast.test.ts +55 -0
  160. package/src/toast.ts +79 -0
  161. package/src/types.ts +25 -0
  162. package/src/utils.ts +221 -0
  163. package/src/workbench-titlebar.ts +137 -0
  164. package/dist/app-shell-dom.d.ts +0 -30
  165. package/dist/app-shell-dom.js +0 -913
  166. package/dist/app-shell.d.ts +0 -235
  167. package/dist/app-shell.js +0 -415
  168. package/dist/base.css +0 -51
  169. package/dist/calendar-picker.d.ts +0 -45
  170. package/dist/calendar-picker.js +0 -217
  171. package/dist/calendar.d.ts +0 -95
  172. package/dist/calendar.js +0 -194
  173. package/dist/complex-inputs.d.ts +0 -300
  174. package/dist/complex-inputs.js +0 -866
  175. package/dist/components.css +0 -4756
  176. package/dist/data-display.d.ts +0 -1094
  177. package/dist/data-display.js +0 -2341
  178. package/dist/dialog.d.ts +0 -26
  179. package/dist/dialog.js +0 -94
  180. package/dist/feedback.d.ts +0 -88
  181. package/dist/feedback.js +0 -281
  182. package/dist/forms.d.ts +0 -378
  183. package/dist/forms.js +0 -1019
  184. package/dist/icons-config.d.ts +0 -25
  185. package/dist/icons-config.js +0 -14
  186. package/dist/icons.d.ts +0 -41
  187. package/dist/icons.js +0 -185
  188. package/dist/index.d.ts +0 -31
  189. package/dist/index.js +0 -40
  190. package/dist/locale/en-US.d.ts +0 -31
  191. package/dist/locale/en-US.js +0 -20
  192. package/dist/locale/zh-CN.d.ts +0 -31
  193. package/dist/locale/zh-CN.js +0 -20
  194. package/dist/navigation.d.ts +0 -609
  195. package/dist/navigation.js +0 -1707
  196. package/dist/overlay.d.ts +0 -308
  197. package/dist/overlay.js +0 -974
  198. package/dist/page-state.d.ts +0 -23
  199. package/dist/page-state.js +0 -96
  200. package/dist/primitives.d.ts +0 -268
  201. package/dist/primitives.js +0 -764
  202. package/dist/theme-data.d.ts +0 -394
  203. package/dist/theme-data.js +0 -417
  204. package/dist/theme.d.ts +0 -97
  205. package/dist/theme.js +0 -301
  206. package/dist/tokens.css +0 -570
  207. package/dist/tree.d.ts +0 -144
  208. package/dist/tree.js +0 -475
  209. package/dist/virtual-list.d.ts +0 -190
  210. package/dist/virtual-list.js +0 -538
  211. package/dist/workbench.d.ts +0 -136
  212. package/dist/workbench.js +0 -156
@@ -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,118 @@
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
+ terminal: '<polyline points="4 7 9 12 4 17"/><line x1="12" y1="17" x2="20" y2="17"/>'
98
+ } as const
99
+
100
+ const iconRegistry = new Map<string, IconDefinition>(
101
+ Object.entries(VUI_ICON_PATHS).map(([name, path]) => [name, { name, path }])
102
+ )
103
+
104
+ export { VUI_ICON_PATHS }
105
+ export { createIcon, registerIcon, resolveIcon }
106
+
107
+ function registerIcon(icon: IconDefinition): () => void {
108
+ const previous = iconRegistry.get(icon.name)
109
+ iconRegistry.set(icon.name, icon)
110
+ return () => {
111
+ if (previous) iconRegistry.set(icon.name, previous)
112
+ else iconRegistry.delete(icon.name)
113
+ }
114
+ }
115
+
116
+ function resolveIcon(icon: string | IconDefinition | undefined): IconDefinition | undefined {
117
+ return typeof icon === 'string' ? iconRegistry.get(icon) : icon
118
+ }