@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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { effect } from '@vobs/reactivity'
|
|
2
|
+
import {
|
|
3
|
+
addEventListener,
|
|
4
|
+
createElement,
|
|
5
|
+
createText,
|
|
6
|
+
insertBefore,
|
|
7
|
+
setAttribute,
|
|
8
|
+
setProperty,
|
|
9
|
+
type VobsNode
|
|
10
|
+
} from '@vobs/vobs'
|
|
11
|
+
import {
|
|
12
|
+
bindClassList,
|
|
13
|
+
bindCommonAttributes,
|
|
14
|
+
bindTextContent,
|
|
15
|
+
bindPropertyValue,
|
|
16
|
+
bindUserStyle,
|
|
17
|
+
hasProp,
|
|
18
|
+
listen,
|
|
19
|
+
mountSlot,
|
|
20
|
+
readProp,
|
|
21
|
+
setOptionalAttribute,
|
|
22
|
+
setOptionalProperty
|
|
23
|
+
} from './utils'
|
|
24
|
+
import type { VuiChildren, VuiCommonProps, VuiEventHandler } from './types'
|
|
25
|
+
|
|
26
|
+
export interface ChatComposerProps extends VuiCommonProps {
|
|
27
|
+
readonly value?: string
|
|
28
|
+
readonly placeholder?: string
|
|
29
|
+
readonly disabled?: boolean
|
|
30
|
+
readonly model?: string
|
|
31
|
+
readonly modelIcon?: VuiChildren
|
|
32
|
+
readonly modelChevron?: VuiChildren
|
|
33
|
+
readonly tools?: VuiChildren
|
|
34
|
+
readonly actions?: VuiChildren
|
|
35
|
+
readonly sendIcon?: VuiChildren
|
|
36
|
+
readonly sendLabel?: string
|
|
37
|
+
readonly onInput?: VuiEventHandler<InputEvent>
|
|
38
|
+
readonly onSubmit?: (value: string, event: KeyboardEvent) => void
|
|
39
|
+
readonly onSend?: (value: string, event: MouseEvent) => void
|
|
40
|
+
readonly onModelClick?: (event: MouseEvent) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ChatComposer(props: ChatComposerProps = {}): VobsNode {
|
|
44
|
+
const root = createElement('div')
|
|
45
|
+
const input = createElement('textarea')
|
|
46
|
+
const toolbar = createElement('div')
|
|
47
|
+
const tools = createElement('div')
|
|
48
|
+
const actions = createElement('div')
|
|
49
|
+
|
|
50
|
+
bindClassList(root, props, () => ['vui-composer'])
|
|
51
|
+
bindUserStyle(root, props)
|
|
52
|
+
bindCommonAttributes(input, props, [
|
|
53
|
+
'value',
|
|
54
|
+
'placeholder',
|
|
55
|
+
'disabled',
|
|
56
|
+
'model',
|
|
57
|
+
'modelIcon',
|
|
58
|
+
'modelChevron',
|
|
59
|
+
'tools',
|
|
60
|
+
'actions',
|
|
61
|
+
'sendIcon',
|
|
62
|
+
'sendLabel',
|
|
63
|
+
'onInput',
|
|
64
|
+
'onSubmit',
|
|
65
|
+
'onSend',
|
|
66
|
+
'onModelClick'
|
|
67
|
+
])
|
|
68
|
+
setAttribute(input, 'class', 'vui-composer__input')
|
|
69
|
+
setAttribute(toolbar, 'class', 'vui-composer__toolbar')
|
|
70
|
+
setAttribute(tools, 'class', 'vui-composer__tools')
|
|
71
|
+
setAttribute(actions, 'class', 'vui-composer__actions')
|
|
72
|
+
|
|
73
|
+
effect(() => {
|
|
74
|
+
setOptionalAttribute(input, 'placeholder', readProp<string | undefined>(props, 'placeholder', undefined))
|
|
75
|
+
setOptionalProperty(input, 'disabled', readProp(props, 'disabled', false))
|
|
76
|
+
setOptionalAttribute(input, 'aria-disabled', readProp(props, 'disabled', false) ? 'true' : undefined)
|
|
77
|
+
})
|
|
78
|
+
if (hasProp(props, 'value')) {
|
|
79
|
+
bindPropertyValue(input, 'value', () => readProp(props, 'value', '') ?? '')
|
|
80
|
+
}
|
|
81
|
+
listen(input, 'input', props, 'onInput', () => readProp(props, 'disabled', false))
|
|
82
|
+
addEventListener(input, 'keydown', event => {
|
|
83
|
+
const keyboardEvent = event as KeyboardEvent
|
|
84
|
+
if (keyboardEvent.key !== 'Enter' || (!keyboardEvent.metaKey && !keyboardEvent.ctrlKey)) return
|
|
85
|
+
keyboardEvent.preventDefault?.()
|
|
86
|
+
const handler = readProp<unknown>(props, 'onSubmit', undefined)
|
|
87
|
+
if (typeof handler === 'function') (handler as ChatComposerProps['onSubmit'])!(currentValue(input, props), keyboardEvent)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if (hasProp(props, 'tools')) mountSlot(tools, props, 'tools')
|
|
91
|
+
else if (hasProp(props, 'children')) mountSlot(tools, props, 'children')
|
|
92
|
+
if (hasProp(props, 'model')) insertBefore(tools, createModelSelector(props), null)
|
|
93
|
+
|
|
94
|
+
if (hasProp(props, 'actions')) mountSlot(actions, props, 'actions')
|
|
95
|
+
else insertBefore(actions, createSendButton(input, props), null)
|
|
96
|
+
|
|
97
|
+
insertBefore(root, input, null)
|
|
98
|
+
insertBefore(toolbar, tools, null)
|
|
99
|
+
insertBefore(toolbar, actions, null)
|
|
100
|
+
insertBefore(root, toolbar, null)
|
|
101
|
+
return root
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createModelSelector(props: ChatComposerProps): VobsNode {
|
|
105
|
+
const button = createElement('button')
|
|
106
|
+
setAttribute(button, 'class', 'vui-composer__model')
|
|
107
|
+
setAttribute(button, 'type', 'button')
|
|
108
|
+
if (hasProp(props, 'modelIcon')) {
|
|
109
|
+
const icon = createElement('span')
|
|
110
|
+
setAttribute(icon, 'class', 'vui-composer__model-icon')
|
|
111
|
+
mountSlot(icon, props, 'modelIcon')
|
|
112
|
+
insertBefore(button, icon, null)
|
|
113
|
+
}
|
|
114
|
+
const text = createText('')
|
|
115
|
+
bindTextContent(text, () => readProp(props, 'model', ''))
|
|
116
|
+
insertBefore(button, text, null)
|
|
117
|
+
if (hasProp(props, 'modelChevron')) {
|
|
118
|
+
const chevron = createElement('span')
|
|
119
|
+
setAttribute(chevron, 'class', 'vui-composer__model-chevron')
|
|
120
|
+
mountSlot(chevron, props, 'modelChevron')
|
|
121
|
+
insertBefore(button, chevron, null)
|
|
122
|
+
}
|
|
123
|
+
addEventListener(button, 'click', event => {
|
|
124
|
+
const handler = readProp<unknown>(props, 'onModelClick', undefined)
|
|
125
|
+
if (typeof handler === 'function') (handler as ChatComposerProps['onModelClick'])!(event as MouseEvent)
|
|
126
|
+
})
|
|
127
|
+
return button
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function createSendButton(input: Element, props: ChatComposerProps): VobsNode {
|
|
131
|
+
const button = createElement('button')
|
|
132
|
+
setAttribute(button, 'class', 'vui-composer__send')
|
|
133
|
+
setAttribute(button, 'type', 'button')
|
|
134
|
+
const label = readProp(props, 'sendLabel', 'Send')
|
|
135
|
+
setOptionalAttribute(button, 'aria-label', label)
|
|
136
|
+
if (hasProp(props, 'sendIcon')) {
|
|
137
|
+
const icon = createElement('span')
|
|
138
|
+
setAttribute(icon, 'class', 'vui-composer__send-icon')
|
|
139
|
+
mountSlot(icon, props, 'sendIcon')
|
|
140
|
+
insertBefore(button, icon, null)
|
|
141
|
+
} else {
|
|
142
|
+
insertBefore(button, createText(label), null)
|
|
143
|
+
}
|
|
144
|
+
effect(() => setProperty(button, 'disabled', readProp(props, 'disabled', false)))
|
|
145
|
+
addEventListener(button, 'click', event => {
|
|
146
|
+
if (readProp(props, 'disabled', false)) return
|
|
147
|
+
const handler = readProp<unknown>(props, 'onSend', undefined)
|
|
148
|
+
if (typeof handler === 'function') (handler as ChatComposerProps['onSend'])!(currentValue(input, props), event as MouseEvent)
|
|
149
|
+
})
|
|
150
|
+
return button
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function currentValue(input: Element, props: ChatComposerProps): string {
|
|
154
|
+
const value = Reflect.get(input, 'value')
|
|
155
|
+
return typeof value === 'string' ? value : readProp(props, 'value', '') ?? ''
|
|
156
|
+
}
|
package/src/dialog.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { effect } from '@vobs/reactivity'
|
|
2
|
+
import {
|
|
3
|
+
addEventListener,
|
|
4
|
+
createElement,
|
|
5
|
+
createText,
|
|
6
|
+
insertBefore,
|
|
7
|
+
setAttribute,
|
|
8
|
+
type VobsNode
|
|
9
|
+
} from '@vobs/vobs'
|
|
10
|
+
import {
|
|
11
|
+
bindClassList,
|
|
12
|
+
bindCommonAttributes,
|
|
13
|
+
bindTextContent,
|
|
14
|
+
bindUserStyle,
|
|
15
|
+
hasProp,
|
|
16
|
+
mountSlot,
|
|
17
|
+
readProp,
|
|
18
|
+
setOptionalAttribute,
|
|
19
|
+
setOptionalProperty
|
|
20
|
+
} from './utils'
|
|
21
|
+
import type { VuiCommonProps } from './types'
|
|
22
|
+
import type { VuiPortalAdapter } from './overlay'
|
|
23
|
+
import { createPortal } from './overlay'
|
|
24
|
+
|
|
25
|
+
export type DialogCloseReason = 'close-button' | 'backdrop' | 'escape'
|
|
26
|
+
|
|
27
|
+
export interface DialogProps extends VuiCommonProps {
|
|
28
|
+
readonly open?: boolean
|
|
29
|
+
readonly title?: string
|
|
30
|
+
readonly headerActions?: VobsNode | (() => VobsNode | null | undefined)
|
|
31
|
+
readonly size?: 'sm' | 'md' | 'lg'
|
|
32
|
+
readonly modal?: boolean
|
|
33
|
+
readonly closeOnBackdrop?: boolean
|
|
34
|
+
readonly closeOnEscape?: boolean
|
|
35
|
+
readonly titleId?: string
|
|
36
|
+
readonly descriptionId?: string
|
|
37
|
+
readonly closeLabel?: string
|
|
38
|
+
readonly onClose?: (reason: DialogCloseReason) => void
|
|
39
|
+
readonly portal?: VuiPortalAdapter<unknown>
|
|
40
|
+
readonly portalTarget?: unknown
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function Dialog(props: DialogProps = {}): VobsNode {
|
|
44
|
+
const root = createElement('div')
|
|
45
|
+
const surface = createElement('section')
|
|
46
|
+
const body = createElement('div')
|
|
47
|
+
|
|
48
|
+
bindClassList(root, props, () => ['vui-backdrop'])
|
|
49
|
+
bindUserStyle(root, props)
|
|
50
|
+
bindCommonAttributes(surface, props, [
|
|
51
|
+
'open',
|
|
52
|
+
'title',
|
|
53
|
+
'headerActions',
|
|
54
|
+
'size',
|
|
55
|
+
'modal',
|
|
56
|
+
'closeOnBackdrop',
|
|
57
|
+
'closeOnEscape',
|
|
58
|
+
'titleId',
|
|
59
|
+
'descriptionId',
|
|
60
|
+
'closeLabel',
|
|
61
|
+
'onClose',
|
|
62
|
+
'portal',
|
|
63
|
+
'portalTarget'
|
|
64
|
+
])
|
|
65
|
+
setAttribute(root, 'role', 'presentation')
|
|
66
|
+
setAttribute(surface, 'class', 'vui-dialog')
|
|
67
|
+
setAttribute(surface, 'role', 'dialog')
|
|
68
|
+
setAttribute(body, 'class', 'vui-dialog__body')
|
|
69
|
+
|
|
70
|
+
effect(() => {
|
|
71
|
+
const open = readProp(props, 'open', false)
|
|
72
|
+
const modal = readProp(props, 'modal', true)
|
|
73
|
+
setOptionalProperty(root, 'hidden', !open)
|
|
74
|
+
setOptionalAttribute(root, 'data-state', open ? 'open' : 'closed')
|
|
75
|
+
setOptionalAttribute(surface, 'aria-modal', modal ? 'true' : 'false')
|
|
76
|
+
setOptionalAttribute(surface, 'aria-labelledby', readProp<string | undefined>(props, 'titleId', undefined))
|
|
77
|
+
setOptionalAttribute(surface, 'aria-describedby', readProp<string | undefined>(props, 'descriptionId', undefined))
|
|
78
|
+
setOptionalAttribute(surface, 'data-size', readProp(props, 'size', 'md'))
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (hasProp(props, 'title') || hasProp(props, 'headerActions') || hasProp(props, 'onClose')) {
|
|
82
|
+
const head = createElement('header')
|
|
83
|
+
setAttribute(head, 'class', 'vui-dialog__head')
|
|
84
|
+
if (hasProp(props, 'title')) {
|
|
85
|
+
const title = createElement('div')
|
|
86
|
+
const text = createText('')
|
|
87
|
+
setAttribute(title, 'class', 'vui-dialog__title')
|
|
88
|
+
bindTextContent(text, () => readProp(props, 'title', ''))
|
|
89
|
+
insertBefore(title, text, null)
|
|
90
|
+
insertBefore(head, title, null)
|
|
91
|
+
}
|
|
92
|
+
if (hasProp(props, 'headerActions')) mountSlot(head, props, 'headerActions')
|
|
93
|
+
if (hasProp(props, 'onClose')) {
|
|
94
|
+
const close = createElement('button')
|
|
95
|
+
setAttribute(close, 'class', 'vui-dialog__close')
|
|
96
|
+
setAttribute(close, 'type', 'button')
|
|
97
|
+
setAttribute(close, 'aria-label', readProp(props, 'closeLabel', 'Close'))
|
|
98
|
+
insertBefore(close, createText('x'), null)
|
|
99
|
+
addEventListener(close, 'click', () => emitClose(props, 'close-button'))
|
|
100
|
+
insertBefore(head, close, null)
|
|
101
|
+
}
|
|
102
|
+
insertBefore(surface, head, null)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (hasProp(props, 'children')) mountSlot(body, props, 'children')
|
|
106
|
+
insertBefore(surface, body, null)
|
|
107
|
+
insertBefore(root, surface, null)
|
|
108
|
+
|
|
109
|
+
addEventListener(root, 'click', event => {
|
|
110
|
+
if (event.target !== root) return
|
|
111
|
+
if (readProp(props, 'closeOnBackdrop', true)) emitClose(props, 'backdrop')
|
|
112
|
+
})
|
|
113
|
+
addEventListener(root, 'keydown', event => {
|
|
114
|
+
if ((event as KeyboardEvent).key !== 'Escape') return
|
|
115
|
+
if (readProp(props, 'closeOnEscape', true)) emitClose(props, 'escape')
|
|
116
|
+
})
|
|
117
|
+
const portal = readProp<VuiPortalAdapter<unknown> | undefined>(props, 'portal', undefined)
|
|
118
|
+
return portal
|
|
119
|
+
? createPortal(root, portal, readProp(props, 'portalTarget', undefined))
|
|
120
|
+
: root
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function emitClose(props: DialogProps, reason: DialogCloseReason): void {
|
|
124
|
+
if (!readProp(props, 'open', false)) return
|
|
125
|
+
const handler = readProp<unknown>(props, 'onClose', undefined)
|
|
126
|
+
if (typeof handler === 'function') handler(reason)
|
|
127
|
+
}
|
package/src/drawer.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { effect } from '@vobs/reactivity'
|
|
2
|
+
import {
|
|
3
|
+
addEventListener,
|
|
4
|
+
createElement,
|
|
5
|
+
createText,
|
|
6
|
+
insertBefore,
|
|
7
|
+
setAttribute,
|
|
8
|
+
type VobsNode
|
|
9
|
+
} from '@vobs/vobs'
|
|
10
|
+
import {
|
|
11
|
+
bindClassList,
|
|
12
|
+
bindCommonAttributes,
|
|
13
|
+
bindTextContent,
|
|
14
|
+
bindUserStyle,
|
|
15
|
+
hasProp,
|
|
16
|
+
mountSlot,
|
|
17
|
+
readProp,
|
|
18
|
+
setOptionalAttribute,
|
|
19
|
+
setOptionalProperty
|
|
20
|
+
} from './utils'
|
|
21
|
+
import { createPortal, type VuiPortalAdapter } from './overlay'
|
|
22
|
+
import type { VuiCommonProps } from './types'
|
|
23
|
+
|
|
24
|
+
export type DrawerCloseReason = 'close-button' | 'backdrop' | 'escape'
|
|
25
|
+
|
|
26
|
+
export interface DrawerProps extends VuiCommonProps {
|
|
27
|
+
readonly open?: boolean
|
|
28
|
+
readonly title?: string
|
|
29
|
+
readonly side?: 'left' | 'right'
|
|
30
|
+
readonly closeOnBackdrop?: boolean
|
|
31
|
+
readonly closeOnEscape?: boolean
|
|
32
|
+
readonly modal?: boolean
|
|
33
|
+
readonly titleId?: string
|
|
34
|
+
readonly descriptionId?: string
|
|
35
|
+
readonly closeLabel?: string
|
|
36
|
+
readonly footer?: VuiCommonProps['children']
|
|
37
|
+
readonly onClose?: (reason: DrawerCloseReason) => void
|
|
38
|
+
readonly portal?: VuiPortalAdapter<unknown>
|
|
39
|
+
readonly portalTarget?: unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Drawer(props: DrawerProps = {}): VobsNode {
|
|
43
|
+
const root = createElement('div')
|
|
44
|
+
const surface = createElement('aside')
|
|
45
|
+
const body = createElement('div')
|
|
46
|
+
bindClassList(root, props, () => ['vui-backdrop'])
|
|
47
|
+
bindUserStyle(root, props)
|
|
48
|
+
bindCommonAttributes(surface, props, [
|
|
49
|
+
'open',
|
|
50
|
+
'title',
|
|
51
|
+
'side',
|
|
52
|
+
'closeOnBackdrop',
|
|
53
|
+
'closeOnEscape',
|
|
54
|
+
'modal',
|
|
55
|
+
'titleId',
|
|
56
|
+
'descriptionId',
|
|
57
|
+
'closeLabel',
|
|
58
|
+
'footer',
|
|
59
|
+
'onClose',
|
|
60
|
+
'portal',
|
|
61
|
+
'portalTarget'
|
|
62
|
+
])
|
|
63
|
+
setAttribute(root, 'role', 'presentation')
|
|
64
|
+
setAttribute(surface, 'class', 'vui-drawer')
|
|
65
|
+
setAttribute(surface, 'role', 'dialog')
|
|
66
|
+
setAttribute(body, 'class', 'vui-drawer__body')
|
|
67
|
+
|
|
68
|
+
effect(() => {
|
|
69
|
+
const open = readProp(props, 'open', false)
|
|
70
|
+
const modal = readProp(props, 'modal', true)
|
|
71
|
+
setOptionalProperty(root, 'hidden', !open)
|
|
72
|
+
setOptionalAttribute(root, 'data-state', open ? 'open' : 'closed')
|
|
73
|
+
setOptionalAttribute(surface, 'aria-modal', modal ? 'true' : 'false')
|
|
74
|
+
setOptionalAttribute(surface, 'aria-labelledby', readProp<string | undefined>(props, 'titleId', undefined))
|
|
75
|
+
setOptionalAttribute(surface, 'aria-describedby', readProp<string | undefined>(props, 'descriptionId', undefined))
|
|
76
|
+
setOptionalAttribute(surface, 'data-side', readProp(props, 'side', 'right'))
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if (hasProp(props, 'title') || hasProp(props, 'onClose')) {
|
|
80
|
+
const head = createElement('header')
|
|
81
|
+
setAttribute(head, 'class', 'vui-drawer__head')
|
|
82
|
+
if (hasProp(props, 'title')) {
|
|
83
|
+
const title = createElement('div')
|
|
84
|
+
const text = createText('')
|
|
85
|
+
setAttribute(title, 'class', 'vui-dialog__title')
|
|
86
|
+
bindTextContent(text, () => readProp(props, 'title', ''))
|
|
87
|
+
insertBefore(title, text, null)
|
|
88
|
+
insertBefore(head, title, null)
|
|
89
|
+
}
|
|
90
|
+
if (hasProp(props, 'onClose')) {
|
|
91
|
+
const close = createElement('button')
|
|
92
|
+
setAttribute(close, 'class', 'vui-dialog__close')
|
|
93
|
+
setAttribute(close, 'type', 'button')
|
|
94
|
+
setAttribute(close, 'aria-label', readProp(props, 'closeLabel', 'Close'))
|
|
95
|
+
insertBefore(close, createText('x'), null)
|
|
96
|
+
addEventListener(close, 'click', () => emitClose(props, 'close-button'))
|
|
97
|
+
insertBefore(head, close, null)
|
|
98
|
+
}
|
|
99
|
+
insertBefore(surface, head, null)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (hasProp(props, 'children')) mountSlot(body, props, 'children')
|
|
103
|
+
insertBefore(surface, body, null)
|
|
104
|
+
if (hasProp(props, 'footer')) {
|
|
105
|
+
const footer = createElement('footer')
|
|
106
|
+
setAttribute(footer, 'class', 'vui-dialog__foot')
|
|
107
|
+
mountSlot(footer, props, 'footer')
|
|
108
|
+
insertBefore(surface, footer, null)
|
|
109
|
+
}
|
|
110
|
+
insertBefore(root, surface, null)
|
|
111
|
+
|
|
112
|
+
addEventListener(root, 'click', event => {
|
|
113
|
+
if (event.target !== root) return
|
|
114
|
+
if (readProp(props, 'closeOnBackdrop', true)) emitClose(props, 'backdrop')
|
|
115
|
+
})
|
|
116
|
+
addEventListener(root, 'keydown', event => {
|
|
117
|
+
if ((event as KeyboardEvent).key !== 'Escape') return
|
|
118
|
+
if (readProp(props, 'closeOnEscape', true)) emitClose(props, 'escape')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const portal = readProp<VuiPortalAdapter<unknown> | undefined>(props, 'portal', undefined)
|
|
122
|
+
return portal
|
|
123
|
+
? createPortal(root, portal, readProp(props, 'portalTarget', undefined))
|
|
124
|
+
: root
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function emitClose(props: DrawerProps, reason: DrawerCloseReason): void {
|
|
128
|
+
if (!readProp(props, 'open', false)) return
|
|
129
|
+
const handler = readProp<unknown>(props, 'onClose', undefined)
|
|
130
|
+
if (typeof handler === 'function') handler(reason)
|
|
131
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { state } from '@vobs/reactivity'
|
|
2
|
+
import {
|
|
3
|
+
addEventListener,
|
|
4
|
+
createElement,
|
|
5
|
+
createFragment,
|
|
6
|
+
createText,
|
|
7
|
+
insertBefore,
|
|
8
|
+
insertDynamic,
|
|
9
|
+
setAttribute,
|
|
10
|
+
type VobsNode
|
|
11
|
+
} from '@vobs/vobs'
|
|
12
|
+
import {
|
|
13
|
+
bindClassList,
|
|
14
|
+
bindCommonAttributes,
|
|
15
|
+
bindUserStyle,
|
|
16
|
+
hasProp,
|
|
17
|
+
mountSlot,
|
|
18
|
+
readProp,
|
|
19
|
+
resolveSlot,
|
|
20
|
+
setOptionalAttribute
|
|
21
|
+
} from './utils'
|
|
22
|
+
import type { VuiChildren, VuiCommonProps } from './types'
|
|
23
|
+
|
|
24
|
+
export interface EditorTabItem {
|
|
25
|
+
readonly id: string
|
|
26
|
+
readonly label: string
|
|
27
|
+
readonly icon?: VuiChildren
|
|
28
|
+
readonly closeIcon?: VuiChildren
|
|
29
|
+
readonly closeable?: boolean
|
|
30
|
+
readonly disabled?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EditorTabsProps extends VuiCommonProps {
|
|
34
|
+
readonly tabs?: readonly EditorTabItem[]
|
|
35
|
+
readonly value?: string
|
|
36
|
+
readonly defaultValue?: string
|
|
37
|
+
readonly closeIcon?: VuiChildren
|
|
38
|
+
readonly actions?: VuiChildren
|
|
39
|
+
readonly onChange?: (id: string, event: MouseEvent | KeyboardEvent) => void
|
|
40
|
+
readonly onClose?: (id: string, event: MouseEvent) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function EditorTabs(props: EditorTabsProps = {}): VobsNode {
|
|
44
|
+
const root = createElement('div')
|
|
45
|
+
const internalValue = state<string | undefined>(undefined)
|
|
46
|
+
bindClassList(root, props, () => ['vui-editortabs'])
|
|
47
|
+
bindCommonAttributes(root, props, [
|
|
48
|
+
'tabs',
|
|
49
|
+
'value',
|
|
50
|
+
'defaultValue',
|
|
51
|
+
'closeIcon',
|
|
52
|
+
'actions',
|
|
53
|
+
'onChange',
|
|
54
|
+
'onClose'
|
|
55
|
+
])
|
|
56
|
+
bindUserStyle(root, props)
|
|
57
|
+
insertDynamic(root, null, () => createEditorTabs(props, internalValue))
|
|
58
|
+
if (hasProp(props, 'children')) mountSlot(root, props, 'children')
|
|
59
|
+
return root
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createEditorTabs(
|
|
63
|
+
props: EditorTabsProps,
|
|
64
|
+
internalValue: { value: string | undefined }
|
|
65
|
+
): VobsNode {
|
|
66
|
+
const tabs = readProp<readonly EditorTabItem[]>(props, 'tabs', [])
|
|
67
|
+
const selected = activeId(props, tabs, internalValue)
|
|
68
|
+
return createFragment((parent, anchor) => {
|
|
69
|
+
for (const tab of tabs) {
|
|
70
|
+
insertBefore(parent, createEditorTab(tab, tabs, selected, props, internalValue), anchor)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const spacer = createElement('span')
|
|
74
|
+
setAttribute(spacer, 'class', 'vui-editortabs__spacer')
|
|
75
|
+
insertBefore(parent, spacer, anchor)
|
|
76
|
+
|
|
77
|
+
if (hasProp(props, 'actions')) {
|
|
78
|
+
const actions = createElement('span')
|
|
79
|
+
setAttribute(actions, 'class', 'vui-editortabs__actions')
|
|
80
|
+
mountSlot(actions, props, 'actions')
|
|
81
|
+
insertBefore(parent, actions, anchor)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createEditorTab(
|
|
87
|
+
tab: EditorTabItem,
|
|
88
|
+
tabs: readonly EditorTabItem[],
|
|
89
|
+
selected: string | undefined,
|
|
90
|
+
props: EditorTabsProps,
|
|
91
|
+
internalValue: { value: string | undefined }
|
|
92
|
+
): VobsNode {
|
|
93
|
+
const active = tab.id === selected
|
|
94
|
+
const root = createElement('span')
|
|
95
|
+
setAttribute(root, 'class', `vui-editortab${active ? ' is-active' : ''}`)
|
|
96
|
+
setAttribute(root, 'role', 'tab')
|
|
97
|
+
setAttribute(root, 'aria-selected', active ? 'true' : 'false')
|
|
98
|
+
setAttribute(root, 'tabindex', active ? '0' : '-1')
|
|
99
|
+
setOptionalAttribute(root, 'data-editor-tab-id', tab.id)
|
|
100
|
+
if (tab.disabled === true) setAttribute(root, 'aria-disabled', 'true')
|
|
101
|
+
|
|
102
|
+
if (tab.icon !== undefined) {
|
|
103
|
+
const icon = createElement('span')
|
|
104
|
+
setAttribute(icon, 'class', 'ic')
|
|
105
|
+
mountSlot(icon, tab, 'icon')
|
|
106
|
+
insertBefore(root, icon, null)
|
|
107
|
+
}
|
|
108
|
+
const label = createElement('span')
|
|
109
|
+
setAttribute(label, 'class', 'vui-editortab__label')
|
|
110
|
+
insertBefore(label, createText(tab.label), null)
|
|
111
|
+
insertBefore(root, label, null)
|
|
112
|
+
|
|
113
|
+
if (tab.closeable === true) {
|
|
114
|
+
const close = createElement('button')
|
|
115
|
+
setAttribute(close, 'class', 'close')
|
|
116
|
+
setAttribute(close, 'type', 'button')
|
|
117
|
+
setAttribute(close, 'aria-label', `Close ${tab.label}`)
|
|
118
|
+
const closeIcon = tab.closeIcon ?? readProp<VuiChildren | undefined>(props, 'closeIcon', undefined)
|
|
119
|
+
const icon = closeIcon === undefined ? createText('x') : resolveSlot(closeIcon)
|
|
120
|
+
if (icon) insertBefore(close, icon, null)
|
|
121
|
+
addEventListener(close, 'click', event => {
|
|
122
|
+
event.stopPropagation?.()
|
|
123
|
+
const handler = readProp<unknown>(props, 'onClose', undefined)
|
|
124
|
+
if (typeof handler === 'function') (handler as EditorTabsProps['onClose'])!(tab.id, event as MouseEvent)
|
|
125
|
+
})
|
|
126
|
+
insertBefore(root, close, null)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
addEventListener(root, 'click', event => {
|
|
130
|
+
if (tab.disabled === true) return
|
|
131
|
+
selectTab(tab.id, props, internalValue, event as MouseEvent)
|
|
132
|
+
})
|
|
133
|
+
addEventListener(root, 'keydown', event => {
|
|
134
|
+
const keyboardEvent = event as KeyboardEvent
|
|
135
|
+
if (keyboardEvent.key === 'Enter' || keyboardEvent.key === ' ') {
|
|
136
|
+
keyboardEvent.preventDefault?.()
|
|
137
|
+
if (tab.disabled !== true) selectTab(tab.id, props, internalValue, keyboardEvent)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (keyboardEvent.key !== 'ArrowLeft' && keyboardEvent.key !== 'ArrowRight') return
|
|
141
|
+
keyboardEvent.preventDefault?.()
|
|
142
|
+
const direction = keyboardEvent.key === 'ArrowLeft' ? -1 : 1
|
|
143
|
+
const next = adjacentTab(tabs, tab.id, direction)
|
|
144
|
+
if (next) selectTab(next.id, props, internalValue, keyboardEvent)
|
|
145
|
+
})
|
|
146
|
+
return root
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function selectTab(
|
|
150
|
+
id: string,
|
|
151
|
+
props: EditorTabsProps,
|
|
152
|
+
internalValue: { value: string | undefined },
|
|
153
|
+
event: MouseEvent | KeyboardEvent
|
|
154
|
+
): void {
|
|
155
|
+
if (readProp<string | undefined>(props, 'value', undefined) === undefined) internalValue.value = id
|
|
156
|
+
const handler = readProp<unknown>(props, 'onChange', undefined)
|
|
157
|
+
if (typeof handler === 'function') (handler as EditorTabsProps['onChange'])!(id, event)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function adjacentTab(
|
|
161
|
+
tabs: readonly EditorTabItem[],
|
|
162
|
+
id: string,
|
|
163
|
+
direction: -1 | 1
|
|
164
|
+
): EditorTabItem | undefined {
|
|
165
|
+
const start = tabs.findIndex(tab => tab.id === id)
|
|
166
|
+
if (start < 0) return undefined
|
|
167
|
+
for (let index = start + direction; index >= 0 && index < tabs.length; index += direction) {
|
|
168
|
+
if (tabs[index].disabled !== true) return tabs[index]
|
|
169
|
+
}
|
|
170
|
+
return undefined
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function activeId(
|
|
174
|
+
props: EditorTabsProps,
|
|
175
|
+
tabs: readonly EditorTabItem[],
|
|
176
|
+
internalValue: { value: string | undefined }
|
|
177
|
+
): string | undefined {
|
|
178
|
+
const controlled = readProp<string | undefined>(props, 'value', undefined)
|
|
179
|
+
if (controlled !== undefined) return controlled
|
|
180
|
+
if (internalValue.value !== undefined) return internalValue.value
|
|
181
|
+
const defaultValue = readProp<string | undefined>(props, 'defaultValue', undefined)
|
|
182
|
+
if (defaultValue !== undefined) return defaultValue
|
|
183
|
+
return tabs.find(tab => tab.disabled !== true)?.id
|
|
184
|
+
}
|