@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/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
|