@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.
Files changed (208) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +60 -0
  3. package/package.json +52 -102
  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 +117 -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 -24
  165. package/dist/app-shell-dom.js +0 -573
  166. package/dist/app-shell.d.ts +0 -199
  167. package/dist/app-shell.js +0 -370
  168. package/dist/base.css +0 -52
  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 -862
  175. package/dist/components.css +0 -4568
  176. package/dist/data-display.d.ts +0 -1063
  177. package/dist/data-display.js +0 -2298
  178. package/dist/feedback.d.ts +0 -88
  179. package/dist/feedback.js +0 -281
  180. package/dist/forms.d.ts +0 -378
  181. package/dist/forms.js +0 -1015
  182. package/dist/icons-config.d.ts +0 -25
  183. package/dist/icons-config.js +0 -14
  184. package/dist/icons.d.ts +0 -41
  185. package/dist/icons.js +0 -185
  186. package/dist/index.d.ts +0 -27
  187. package/dist/index.js +0 -34
  188. package/dist/locale/en-US.d.ts +0 -31
  189. package/dist/locale/en-US.js +0 -20
  190. package/dist/locale/zh-CN.d.ts +0 -31
  191. package/dist/locale/zh-CN.js +0 -20
  192. package/dist/navigation.d.ts +0 -609
  193. package/dist/navigation.js +0 -1707
  194. package/dist/overlay.d.ts +0 -304
  195. package/dist/overlay.js +0 -969
  196. package/dist/primitives.d.ts +0 -268
  197. package/dist/primitives.js +0 -764
  198. package/dist/theme-data.d.ts +0 -363
  199. package/dist/theme-data.js +0 -374
  200. package/dist/theme.d.ts +0 -79
  201. package/dist/theme.js +0 -262
  202. package/dist/tokens.css +0 -449
  203. package/dist/tree.d.ts +0 -144
  204. package/dist/tree.js +0 -469
  205. package/dist/virtual-list.d.ts +0 -190
  206. package/dist/virtual-list.js +0 -521
  207. package/dist/workbench.d.ts +0 -136
  208. package/dist/workbench.js +0 -156
package/src/table.ts ADDED
@@ -0,0 +1,191 @@
1
+ import {
2
+ addEventListener,
3
+ createElement,
4
+ createFragment,
5
+ createText,
6
+ insertBefore,
7
+ insertDynamic,
8
+ setAttribute,
9
+ setProperty,
10
+ type VobsNode
11
+ } from '@vobs/vobs'
12
+ import {
13
+ bindClassList,
14
+ bindCommonAttributes,
15
+ bindTextContent,
16
+ bindUserStyle,
17
+ hasProp,
18
+ mountSlot,
19
+ readProp,
20
+ resolveSlot,
21
+ setOptionalAttribute
22
+ } from './utils'
23
+ import type { VuiChildren, VuiCommonProps } from './types'
24
+
25
+ export type TableAlign = 'start' | 'center' | 'end'
26
+
27
+ export interface TableColumn<Row = Record<string, unknown>> {
28
+ readonly id: string
29
+ readonly label: string
30
+ readonly key?: keyof Row
31
+ readonly render?: (row: Row, index: number) => VuiChildren
32
+ readonly align?: TableAlign
33
+ }
34
+
35
+ export interface TableProps<Row = Record<string, unknown>> extends VuiCommonProps {
36
+ readonly columns?: readonly TableColumn<Row>[]
37
+ readonly rows?: readonly Row[]
38
+ readonly caption?: string
39
+ readonly empty?: VuiChildren
40
+ readonly rowKey?: (row: Row, index: number) => unknown
41
+ readonly onRowClick?: (row: Row, index: number) => void
42
+ }
43
+
44
+ export function Table<Row = Record<string, unknown>>(props?: TableProps<Row>): VobsNode
45
+ export function Table(props?: TableProps<any>): VobsNode
46
+ export function Table<Row = Record<string, unknown>>(props: TableProps<Row> = {}): VobsNode {
47
+ const root = createElement('table')
48
+ const head = createElement('thead')
49
+ const body = createElement('tbody')
50
+
51
+ bindClassList(root, props, () => ['vui-table'])
52
+ bindCommonAttributes(root, props, ['columns', 'rows', 'caption', 'empty', 'rowKey', 'onRowClick'])
53
+ bindUserStyle(root, props)
54
+
55
+ if (hasProp(props, 'caption')) {
56
+ const caption = createElement('caption')
57
+ const text = createText('')
58
+ bindTextContent(text, () => readProp(props, 'caption', ''))
59
+ insertBefore(caption, text, null)
60
+ insertBefore(root, caption, null)
61
+ }
62
+ insertDynamic(head, null, () => createTableHead(props))
63
+ insertDynamic(body, null, () => createTableBody(props))
64
+ insertBefore(root, head, null)
65
+ insertBefore(root, body, null)
66
+ return root
67
+ }
68
+
69
+ export interface TablePanelProps<Row = Record<string, unknown>> extends VuiCommonProps {
70
+ readonly columns?: readonly TableColumn<Row>[]
71
+ readonly rows?: readonly Row[]
72
+ readonly empty?: VuiChildren
73
+ readonly footer?: VuiChildren
74
+ readonly rowKey?: (row: Row, index: number) => unknown
75
+ readonly onRowClick?: (row: Row, index: number) => void
76
+ }
77
+
78
+ export function TablePanel<Row = Record<string, unknown>>(props?: TablePanelProps<Row>): VobsNode
79
+ export function TablePanel(props?: TablePanelProps<any>): VobsNode
80
+ export function TablePanel<Row = Record<string, unknown>>(props: TablePanelProps<Row> = {}): VobsNode {
81
+ const root = createElement('div')
82
+ bindClassList(root, props, () => ['vui-tablepanel'])
83
+ bindCommonAttributes(root, props, ['columns', 'rows', 'empty', 'footer', 'rowKey', 'onRowClick'])
84
+ bindUserStyle(root, props)
85
+
86
+ const table = Table<Row>({
87
+ get columns() { return readProp<readonly TableColumn<Row>[]>(props, 'columns', []) },
88
+ get rows() { return readProp<readonly Row[]>(props, 'rows', []) },
89
+ get empty() { return readProp<VuiChildren | undefined>(props, 'empty', undefined) },
90
+ get rowKey() { return readProp<TablePanelProps<Row>['rowKey']>(props, 'rowKey', undefined) },
91
+ get onRowClick() { return readProp<TablePanelProps<Row>['onRowClick']>(props, 'onRowClick', undefined) }
92
+ })
93
+ insertBefore(root, table, null)
94
+
95
+ if (hasProp(props, 'footer')) {
96
+ const footer = createElement('div')
97
+ setAttribute(footer, 'class', 'vui-tablepanel__foot')
98
+ mountSlot(footer, props, 'footer')
99
+ insertBefore(root, footer, null)
100
+ }
101
+ return root
102
+ }
103
+
104
+ function createTableHead<Row>(props: TableProps<Row>): VobsNode {
105
+ const columns = readProp<readonly TableColumn<Row>[]>(props, 'columns', [])
106
+ return createFragment((parent, anchor) => {
107
+ const row = createElement('tr')
108
+ for (const column of columns) {
109
+ const cell = createElement('th')
110
+ setAttribute(cell, 'scope', 'col')
111
+ setAttribute(cell, 'data-column-id', column.id)
112
+ applyAlignment(cell, column.align)
113
+ insertBefore(cell, createText(column.label), null)
114
+ insertBefore(row, cell, null)
115
+ }
116
+ insertBefore(parent, row, anchor)
117
+ })
118
+ }
119
+
120
+ function createTableBody<Row>(props: TableProps<Row>): VobsNode {
121
+ const columns = readProp<readonly TableColumn<Row>[]>(props, 'columns', [])
122
+ const rows = readProp<readonly Row[]>(props, 'rows', [])
123
+ if (rows.length === 0) return createEmptyRow(props, columns.length)
124
+
125
+ return createFragment((parent, anchor) => {
126
+ for (let index = 0; index < rows.length; index++) {
127
+ insertBefore(parent, createTableRow(rows[index], index, columns, props), anchor)
128
+ }
129
+ })
130
+ }
131
+
132
+ function createTableRow<Row>(
133
+ row: Row,
134
+ index: number,
135
+ columns: readonly TableColumn<Row>[],
136
+ props: TableProps<Row>
137
+ ): VobsNode {
138
+ const tableRow = createElement('tr')
139
+ const rowKey = readProp<TableProps<Row>['rowKey']>(props, 'rowKey', undefined)
140
+ if (rowKey) setOptionalAttribute(tableRow, 'data-row-key', rowKey(row, index))
141
+
142
+ for (const column of columns) {
143
+ const cell = createElement('td')
144
+ setAttribute(cell, 'data-column-id', column.id)
145
+ applyAlignment(cell, column.align)
146
+ insertDynamic(cell, null, () => {
147
+ const value = column.render
148
+ ? column.render(row, index)
149
+ : column.key === undefined
150
+ ? undefined
151
+ : readRowValue(row, column.key)
152
+ return resolveSlot(value)
153
+ })
154
+ insertBefore(tableRow, cell, null)
155
+ }
156
+
157
+ const onRowClick = readProp<TableProps<Row>['onRowClick']>(props, 'onRowClick', undefined)
158
+ if (onRowClick) {
159
+ setProperty(tableRow, 'tabIndex', 0)
160
+ setAttribute(tableRow, 'data-clickable', 'true')
161
+ addEventListener(tableRow, 'click', () => onRowClick(row, index))
162
+ }
163
+ return tableRow
164
+ }
165
+
166
+ function createEmptyRow<Row>(props: TableProps<Row>, columnCount: number): VobsNode {
167
+ const row = createElement('tr')
168
+ const cell = createElement('td')
169
+ setAttribute(cell, 'colspan', String(Math.max(1, columnCount)))
170
+ if (hasProp(props, 'empty')) {
171
+ insertDynamic(cell, null, () => resolveSlot(Reflect.get(props, 'empty')))
172
+ } else {
173
+ insertBefore(cell, createText('No data'), null)
174
+ }
175
+ insertBefore(row, cell, null)
176
+ return row
177
+ }
178
+
179
+ function applyAlignment(cell: Element, align: TableAlign | undefined): void {
180
+ if (align === 'end') {
181
+ setAttribute(cell, 'class', 'num')
182
+ setAttribute(cell, 'data-align', 'end')
183
+ } else if (align === 'center') {
184
+ setAttribute(cell, 'data-align', 'center')
185
+ }
186
+ }
187
+
188
+ function readRowValue<Row>(row: Row, key: keyof Row): unknown {
189
+ if (row === null || row === undefined) return undefined
190
+ return Reflect.get(row as object, key)
191
+ }
package/src/tabs.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { effect, 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
+ hasProp,
17
+ mountSlot,
18
+ readProp,
19
+ resolveSlot
20
+ } from './utils'
21
+ import type { VuiChildren, VuiCommonProps } from './types'
22
+
23
+ export interface TabItem {
24
+ readonly id: string
25
+ readonly label: string
26
+ readonly disabled?: boolean
27
+ readonly content?: VuiChildren
28
+ }
29
+
30
+ export interface TabsProps extends VuiCommonProps {
31
+ readonly items: readonly TabItem[]
32
+ readonly value?: string
33
+ readonly variant?: 'default' | 'filled'
34
+ readonly onChange?: (id: string) => void
35
+ }
36
+
37
+ export function Tabs(props: TabsProps): VobsNode {
38
+ const root = createElement('div')
39
+ const tablist = createElement('div')
40
+ const panel = createElement('div')
41
+ const internalValue = state<string | undefined>(undefined)
42
+
43
+ bindClassList(root, props, () => ['vui-tabs-root'])
44
+ bindCommonAttributes(root, props, ['items', 'value', 'variant', 'onChange'])
45
+ setAttribute(tablist, 'role', 'tablist')
46
+ effect(() => {
47
+ const variant = readProp<'default' | 'filled'>(props, 'variant', 'default')
48
+ setAttribute(tablist, 'class', `vui-tabs${variant === 'filled' ? ' vui-tabs--filled' : ''}`)
49
+ })
50
+ setAttribute(panel, 'class', 'vui-tabs__panel')
51
+
52
+ insertDynamic(tablist, null, () => createTabButtons(props, internalValue))
53
+ insertDynamic(panel, null, () => {
54
+ const item = activeItem(readProp<readonly TabItem[]>(props, 'items', []), activeId(props, internalValue))
55
+ return item?.content === undefined ? null : resolveSlot(item.content)
56
+ })
57
+ insertBefore(root, tablist, null)
58
+ insertBefore(root, panel, null)
59
+ if (hasProp(props, 'children')) mountSlot(root, props, 'children')
60
+ return root
61
+ }
62
+
63
+ function createTabButtons(props: TabsProps, internalValue: { value: string | undefined }): VobsNode {
64
+ const items = readProp<readonly TabItem[]>(props, 'items', [])
65
+ const selected = activeId(props, internalValue)
66
+ return createFragment((parent, anchor) => {
67
+ for (const item of items) {
68
+ const button = createElement('button')
69
+ const isActive = item.id === selected
70
+ setAttribute(button, 'class', `vui-tab${isActive ? ' is-active' : ''}`)
71
+ setAttribute(button, 'role', 'tab')
72
+ setAttribute(button, 'type', 'button')
73
+ setAttribute(button, 'aria-selected', isActive ? 'true' : 'false')
74
+ setAttribute(button, 'data-tab-id', item.id)
75
+ if (item.disabled === true) {
76
+ setAttribute(button, 'aria-disabled', 'true')
77
+ setProperty(button, 'disabled', true)
78
+ }
79
+ insertBefore(button, createText(item.label), null)
80
+ addEventListener(button, 'click', () => {
81
+ if (item.disabled === true) return
82
+ if (readProp<string | undefined>(props, 'value', undefined) === undefined) internalValue.value = item.id
83
+ const onChange = readProp<unknown>(props, 'onChange', undefined)
84
+ if (typeof onChange === 'function') onChange(item.id)
85
+ })
86
+ insertBefore(parent, button, anchor)
87
+ }
88
+ })
89
+ }
90
+
91
+ function activeId(props: TabsProps, internalValue: { value: string | undefined }): string | undefined {
92
+ const controlled = readProp<string | undefined>(props, 'value', undefined)
93
+ if (controlled !== undefined) return controlled
94
+ if (internalValue.value !== undefined) return internalValue.value
95
+ return readProp<readonly TabItem[]>(props, 'items', []).find(item => item.disabled !== true)?.id
96
+ }
97
+
98
+ function activeItem(items: readonly TabItem[], id: string | undefined): TabItem | undefined {
99
+ return items.find(item => item.id === id)
100
+ }
package/src/tag.ts ADDED
@@ -0,0 +1,33 @@
1
+ import {
2
+ createElement,
3
+ type VobsNode
4
+ } from '@vobs/vobs'
5
+ import {
6
+ bindClassList,
7
+ bindCommonAttributes,
8
+ bindUserStyle,
9
+ hasProp,
10
+ mountSlot,
11
+ readProp
12
+ } from './utils'
13
+ import type { VuiCommonProps } from './types'
14
+
15
+ export type TagTone = 'default' | 'brand' | 'success' | 'warning' | 'danger' | 'count' | 'neutral-strong'
16
+
17
+ export interface TagProps extends VuiCommonProps {
18
+ readonly tone?: TagTone
19
+ }
20
+
21
+ export function Tag(props: TagProps = {}): VobsNode {
22
+ const root = createElement('span')
23
+ bindClassList(root, props, () => [
24
+ 'vui-tag',
25
+ readProp<TagTone>(props, 'tone', 'default') === 'default'
26
+ ? undefined
27
+ : `vui-tag--${readProp<TagTone>(props, 'tone', 'default')}`
28
+ ])
29
+ bindCommonAttributes(root, props, ['tone'])
30
+ bindUserStyle(root, props)
31
+ if (hasProp(props, 'children')) mountSlot(root, props, 'children')
32
+ return root
33
+ }
@@ -0,0 +1,137 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+ import { state } from '@vobs/reactivity'
3
+ import {
4
+ createComponent,
5
+ createDOMRenderer,
6
+ createElement,
7
+ createVobs,
8
+ insertBefore,
9
+ setRenderer
10
+ } from '@vobs/vobs'
11
+ import { ActivityRail } from './activity-rail'
12
+ import { ChatComposer } from './chat-composer'
13
+ import { EditorTabs } from './editor-tabs'
14
+ import { FileTree } from './file-tree'
15
+ import { StatusBar } from './status-bar'
16
+ import { WorkbenchTitlebar } from './workbench-titlebar'
17
+
18
+ describe('@vobs/ui third batch', () => {
19
+ beforeEach(() => {
20
+ setRenderer(createDOMRenderer())
21
+ })
22
+
23
+ it('ActivityRail keeps selection controlled and preserves spacer/divider structure', () => {
24
+ const active = state('files')
25
+ const container = document.createElement('main')
26
+ const app = createVobs({
27
+ render: () => createComponent(ActivityRail, {
28
+ items: [
29
+ { id: 'files', label: 'Files' },
30
+ { id: 'search', label: 'Search' },
31
+ { divider: true },
32
+ { spacer: true },
33
+ { id: 'settings', label: 'Settings' }
34
+ ],
35
+ get value() { return active.value },
36
+ onChange: id => { active.value = id }
37
+ })
38
+ })
39
+ app.mount(container)
40
+
41
+ expect(container.querySelectorAll('.vui-activityrail__btn').length).toBe(3)
42
+ expect(container.querySelector('.vui-activityrail__divider')).toBeTruthy()
43
+ expect(container.querySelector('.vui-activityrail__spacer')).toBeTruthy()
44
+ ;(container.querySelector('[aria-label="Settings"]') as HTMLElement).click()
45
+ app.update()
46
+ expect(active.value).toBe('settings')
47
+ expect(container.querySelector('[aria-label="Settings"]')?.classList.contains('is-active')).toBe(true)
48
+ app.destroy()
49
+ })
50
+
51
+ it('FileTree expands folders, selects rows and emits toggle state', () => {
52
+ const toggles: Array<[string, boolean]> = []
53
+ const selected: string[] = []
54
+ const container = document.createElement('main')
55
+ const app = createVobs({
56
+ render: () => createComponent(FileTree, {
57
+ items: [{
58
+ id: 'src',
59
+ label: 'src',
60
+ kind: 'folder',
61
+ children: [{ id: 'main', label: 'main.ts', kind: 'file' }]
62
+ }],
63
+ onToggle: (id, expanded) => toggles.push([id, expanded]),
64
+ onSelect: id => selected.push(id)
65
+ })
66
+ })
67
+ app.mount(container)
68
+
69
+ expect(container.querySelector('[data-file-id="main"]')).toBeNull()
70
+ ;(container.querySelector('[data-file-id="src"]') as HTMLElement).click()
71
+ app.update()
72
+ expect(container.querySelector('[data-file-id="main"]')).toBeTruthy()
73
+ expect(toggles).toEqual([['src', true]])
74
+ expect(selected).toEqual(['src'])
75
+ app.destroy()
76
+ })
77
+
78
+ it('EditorTabs supports active selection, close callbacks and arrow navigation', () => {
79
+ const active = state('one')
80
+ const closed: string[] = []
81
+ const container = document.createElement('main')
82
+ const app = createVobs({
83
+ render: () => createComponent(EditorTabs, {
84
+ tabs: [
85
+ { id: 'one', label: 'one.ts', closeable: true },
86
+ { id: 'two', label: 'two.ts' }
87
+ ],
88
+ get value() { return active.value },
89
+ onChange: id => { active.value = id },
90
+ onClose: id => closed.push(id)
91
+ })
92
+ })
93
+ app.mount(container)
94
+
95
+ const first = container.querySelector('[data-editor-tab-id="one"]') as HTMLElement
96
+ first.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
97
+ app.update()
98
+ expect(active.value).toBe('two')
99
+ ;(container.querySelector('.close') as HTMLElement).click()
100
+ expect(closed).toEqual(['one'])
101
+ app.destroy()
102
+ })
103
+
104
+ it('WorkbenchTitlebar, StatusBar and ChatComposer expose named native controls', () => {
105
+ const sent: string[] = []
106
+ const container = document.createElement('main')
107
+ const app = createVobs({
108
+ render: () => {
109
+ const root = createElement('div')
110
+ insertBefore(root, createComponent(WorkbenchTitlebar, {
111
+ mode: 'Agent',
112
+ project: 'vobs',
113
+ onProjectClick: () => undefined
114
+ }), null)
115
+ insertBefore(root, createComponent(StatusBar, {
116
+ left: [{ id: 'sync', label: 'Synced', dot: 'success' }],
117
+ onItemClick: () => undefined
118
+ }), null)
119
+ insertBefore(root, createComponent(ChatComposer, {
120
+ value: 'hello',
121
+ onSend: value => sent.push(value)
122
+ }), null)
123
+ return root
124
+ }
125
+ })
126
+ app.mount(container)
127
+
128
+ expect(container.querySelector('.vui-wbtitlebar__mode-chip')?.textContent).toBe('Agent')
129
+ expect(container.querySelector('.vui-wbtitlebar__project-selector')?.textContent).toBe('vobs')
130
+ expect(container.querySelector('.vui-wbtitlebar__light')?.hasAttribute('style')).toBe(false)
131
+ expect(container.querySelector('.vui-statusbar__dot--success')).toBeTruthy()
132
+ expect((container.querySelector('.vui-composer__input') as HTMLTextAreaElement).value).toBe('hello')
133
+ ;(container.querySelector('.vui-composer__send') as HTMLButtonElement).click()
134
+ expect(sent).toEqual(['hello'])
135
+ app.destroy()
136
+ })
137
+ })
@@ -0,0 +1,55 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+ import { createComponent, createDOMRenderer, createVobs, setRenderer } from '@vobs/vobs'
3
+ import { createNotification, notificationPlugin } from '@vobs/notification'
4
+ import { createDOMPortalAdapter } from './overlay'
5
+ import { ToastHost } from './toast'
6
+
7
+ describe('@vobs/ui ToastHost', () => {
8
+ beforeEach(() => {
9
+ setRenderer(createDOMRenderer())
10
+ })
11
+
12
+ it('消费 NotificationContext,按类型呈现可访问 toast 并允许手动关闭', () => {
13
+ const notification = createNotification({ defaultDuration: 0 })
14
+ const container = document.createElement('main')
15
+ const app = createVobs({
16
+ render: () => createComponent(ToastHost, { notification, position: 'bottom-left' })
17
+ })
18
+ app.mount(container)
19
+ notification.success('Saved', { title: 'Document' })
20
+ app.update()
21
+
22
+ const toast = container.querySelector('[data-notification-id="notification-1"]') as HTMLElement
23
+ expect(toast.className).toContain('vui-toast--success')
24
+ expect(toast.getAttribute('role')).toBe('status')
25
+ expect(toast.textContent).toContain('Document')
26
+ expect(container.querySelector('.vui-toast-host')?.className).toContain('bottom-left')
27
+ ;(toast.querySelector('button') as HTMLButtonElement).click()
28
+ app.update()
29
+ expect(container.querySelector('.vui-toast')).toBeNull()
30
+ app.destroy()
31
+ notification.dispose()
32
+ })
33
+
34
+ it('可通过 notificationPlugin 读取上下文,并支持 portal 与 error alert', () => {
35
+ const notification = createNotification({ defaultDuration: 0 })
36
+ const container = document.createElement('main')
37
+ const target = document.createElement('aside')
38
+ const app = createVobs({
39
+ render: () => createComponent(ToastHost, {
40
+ portal: createDOMPortalAdapter(),
41
+ portalTarget: target
42
+ }),
43
+ plugins: [notificationPlugin({ notification })]
44
+ })
45
+ app.mount(container)
46
+ notification.error('Cannot save')
47
+ app.update()
48
+
49
+ expect(container.querySelector('.vui-toast-host')).toBeNull()
50
+ expect(target.querySelector('.vui-toast')?.getAttribute('role')).toBe('alert')
51
+ app.destroy()
52
+ expect(target.querySelector('.vui-toast-host')).toBeNull()
53
+ notification.dispose()
54
+ })
55
+ })
package/src/toast.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { insertList } from '@vobs/vobs'
2
+ import {
3
+ addEventListener,
4
+ createElement,
5
+ createText,
6
+ insertBefore,
7
+ setAttribute,
8
+ type VobsNode
9
+ } from '@vobs/vobs'
10
+ import { type Notification, type NotificationContext, useNotification } from '@vobs/notification'
11
+ import { Icon } from './icon'
12
+ import { createPortal, type VuiPortalAdapter } from './overlay'
13
+ import { bindClassList, bindCommonAttributes, bindUserStyle, readProp } from './utils'
14
+ import type { VuiCommonProps } from './types'
15
+
16
+ export type ToastPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
17
+
18
+ export interface ToastHostProps extends VuiCommonProps {
19
+ readonly notification?: NotificationContext
20
+ readonly position?: ToastPosition
21
+ readonly closeLabel?: string
22
+ readonly portal?: VuiPortalAdapter<unknown>
23
+ readonly portalTarget?: unknown
24
+ }
25
+
26
+ export function ToastHost(props: ToastHostProps = {}): VobsNode {
27
+ const notification = props.notification ?? useNotification()
28
+ const root = createElement('ol')
29
+ bindClassList(root, props, () => [
30
+ 'vui-toast-host',
31
+ `vui-toast-host--${readProp<ToastPosition>(props, 'position', 'top-right')}`
32
+ ])
33
+ bindCommonAttributes(root, props, ['notification', 'position', 'closeLabel', 'portal', 'portalTarget'])
34
+ bindUserStyle(root, props)
35
+ setAttribute(root, 'aria-label', 'Notifications')
36
+ setAttribute(root, 'role', 'region')
37
+
38
+ insertList(root, null, () => notification.notifications.value, entry => (
39
+ createToast(entry, notification, readProp(props, 'closeLabel', 'Dismiss notification'))
40
+ ), entry => entry.id)
41
+
42
+ const portal = readProp<VuiPortalAdapter<unknown> | undefined>(props, 'portal', undefined)
43
+ return portal
44
+ ? createPortal(root, portal, readProp(props, 'portalTarget', undefined))
45
+ : root
46
+ }
47
+
48
+ function createToast(
49
+ notification: Notification,
50
+ context: NotificationContext,
51
+ closeLabel: string
52
+ ): VobsNode {
53
+ const item = createElement('li')
54
+ const content = createElement('div')
55
+ const close = createElement('button')
56
+ setAttribute(item, 'class', `vui-toast vui-toast--${notification.type}`)
57
+ setAttribute(item, 'data-notification-id', notification.id)
58
+ setAttribute(item, 'role', notification.type === 'error' ? 'alert' : 'status')
59
+ setAttribute(content, 'class', 'vui-toast__content')
60
+ setAttribute(close, 'class', 'vui-toast__close')
61
+ setAttribute(close, 'type', 'button')
62
+ setAttribute(close, 'aria-label', closeLabel)
63
+ addEventListener(close, 'click', () => context.dismiss(notification.id))
64
+
65
+ if (notification.title) {
66
+ const title = createElement('div')
67
+ setAttribute(title, 'class', 'vui-toast__title')
68
+ insertBefore(title, createText(notification.title), null)
69
+ insertBefore(content, title, null)
70
+ }
71
+ const message = createElement('div')
72
+ setAttribute(message, 'class', 'vui-toast__message')
73
+ insertBefore(message, createText(notification.content), null)
74
+ insertBefore(content, message, null)
75
+ insertBefore(close, Icon({ name: 'x', size: 16, decorative: true }), null)
76
+ insertBefore(item, content, null)
77
+ insertBefore(item, close, null)
78
+ return item
79
+ }
package/src/types.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { VobsNode } from '@vobs/vobs'
2
+
3
+ export type VuiAttributeValue = string | number | boolean | undefined
4
+
5
+ export type VuiChildren =
6
+ | VobsNode
7
+ | string
8
+ | number
9
+ | readonly VuiChildren[]
10
+ | (() => VuiChildren | null | undefined)
11
+
12
+ export interface VuiCommonProps {
13
+ readonly class?: string
14
+ readonly className?: string
15
+ readonly style?: string
16
+ readonly id?: string
17
+ readonly title?: string
18
+ readonly role?: string
19
+ readonly tabIndex?: number
20
+ readonly children?: VuiChildren
21
+ readonly [name: `aria-${string}`]: VuiAttributeValue
22
+ readonly [name: `data-${string}`]: VuiAttributeValue
23
+ }
24
+
25
+ export type VuiEventHandler<EventType extends Event = Event> = (event: EventType) => void