@vobs/ui 0.3.0 → 1.1.0

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