@vobs/ui 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +117 -0
  136. package/src/icons.js +165 -0
  137. package/src/index.test.ts +213 -0
  138. package/src/index.ts +94 -0
  139. package/src/kbd.ts +72 -0
  140. package/src/menu.ts +119 -0
  141. package/src/nav-list.ts +157 -0
  142. package/src/overlay.test.ts +66 -0
  143. package/src/overlay.ts +205 -0
  144. package/src/page-header.ts +97 -0
  145. package/src/pagination.ts +163 -0
  146. package/src/second-batch.test.ts +205 -0
  147. package/src/stat-card.ts +92 -0
  148. package/src/status-bar.ts +108 -0
  149. package/src/styles/base.css +240 -0
  150. package/src/styles/components.css +1234 -0
  151. package/{dist → src/styles}/styles.css +2 -0
  152. package/src/styles/toast.css +57 -0
  153. package/src/styles/tokens.css +438 -0
  154. package/src/styles/vui.css +47 -0
  155. package/src/table.ts +191 -0
  156. package/src/tabs.ts +100 -0
  157. package/src/tag.ts +33 -0
  158. package/src/third-batch.test.ts +137 -0
  159. package/src/toast.test.ts +55 -0
  160. package/src/toast.ts +79 -0
  161. package/src/types.ts +25 -0
  162. package/src/utils.ts +221 -0
  163. package/src/workbench-titlebar.ts +137 -0
  164. package/dist/app-shell-dom.d.ts +0 -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
package/dist/overlay.js DELETED
@@ -1,974 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/ui
4
- */
5
- import { classNames, createButtonPrimitive, createUiDomPlan, } from './primitives.js';
6
- export function createOverlayStack(options) {
7
- const entries = [];
8
- const portalTarget = options.portalTarget;
9
- const scrollTarget = options.scrollLockTarget ?? options.document.body;
10
- let scrollLockCount = 0;
11
- let previousOverflow = '';
12
- const onKeyDown = (event) => {
13
- const top = entries.at(-1);
14
- if (top === undefined)
15
- return;
16
- if (event.key === 'Escape' && top.closeOnEscape) {
17
- event.preventDefault();
18
- top.handle.close('escape');
19
- return;
20
- }
21
- if (event.key === 'Tab' && top.modal && top.element !== undefined) {
22
- trapFocus(event, top.element);
23
- }
24
- };
25
- const onPointerDown = (event) => {
26
- const top = entries.at(-1);
27
- const target = event.target;
28
- if (top === undefined ||
29
- !top.closeOnOutside ||
30
- top.element === undefined ||
31
- !isNode(target) ||
32
- top.element.contains(target)) {
33
- return;
34
- }
35
- event.preventDefault();
36
- top.handle.close('outside');
37
- };
38
- options.document.addEventListener('keydown', onKeyDown);
39
- options.document.addEventListener('pointerdown', onPointerDown);
40
- const stack = {
41
- get size() {
42
- return entries.length;
43
- },
44
- get top() {
45
- return entries.at(-1)?.handle;
46
- },
47
- open(openOptions) {
48
- const previouslyFocused = activeHtmlElement(options.document);
49
- const modal = openOptions.modal ?? (openOptions.kind === undefined || openOptions.kind === 'dialog');
50
- const entry = createEntry(openOptions, modal, entries.length + 1, openOptions.restoreFocusTarget ?? previouslyFocused);
51
- entries.push(entry);
52
- if (portalTarget !== undefined &&
53
- entry.element !== undefined &&
54
- openOptions.managedElement !== false)
55
- portalTarget.append(entry.element);
56
- if (modal)
57
- lockScroll();
58
- focusInitialElement(openOptions.initialFocus, entry.element, openOptions.autoFocus === true);
59
- return entry.handle;
60
- },
61
- destroy() {
62
- for (const entry of [...entries].reverse())
63
- entry.handle.destroy();
64
- options.document.removeEventListener('keydown', onKeyDown);
65
- options.document.removeEventListener('pointerdown', onPointerDown);
66
- },
67
- };
68
- function createEntry(openOptions, modal, layer, previouslyFocused) {
69
- const plans = openOptions.kind === undefined || openOptions.kind === 'dialog'
70
- ? createDialogOverlayPlans({
71
- id: openOptions.id,
72
- titleId: `${openOptions.id}-title`,
73
- descriptionId: `${openOptions.id}-description`,
74
- modal,
75
- })
76
- : createPositionedOverlayPlans({
77
- id: openOptions.id,
78
- kind: openOptions.kind,
79
- });
80
- let active = true;
81
- const handle = {
82
- id: openOptions.id,
83
- kind: openOptions.kind ?? 'dialog',
84
- modal,
85
- layer,
86
- plans,
87
- ...(openOptions.element === undefined ? {} : { element: openOptions.element }),
88
- close(reason = 'programmatic') {
89
- if (!active)
90
- return;
91
- openOptions.onClose?.(reason);
92
- handle.destroy();
93
- },
94
- destroy() {
95
- if (!active)
96
- return;
97
- active = false;
98
- const index = entries.indexOf(entry);
99
- if (index >= 0)
100
- entries.splice(index, 1);
101
- if (modal)
102
- unlockScroll();
103
- if (openOptions.managedElement !== false)
104
- openOptions.element?.remove();
105
- if (openOptions.restoreFocus !== false)
106
- previouslyFocused?.focus();
107
- },
108
- };
109
- const entry = {
110
- handle,
111
- ...(openOptions.element === undefined ? {} : { element: openOptions.element }),
112
- closeOnEscape: openOptions.closeOnEscape ?? true,
113
- closeOnOutside: openOptions.closeOnOutside ?? true,
114
- modal,
115
- };
116
- return entry;
117
- }
118
- function lockScroll() {
119
- scrollLockCount += 1;
120
- if (scrollLockCount !== 1)
121
- return;
122
- previousOverflow = scrollTarget.style.overflow;
123
- scrollTarget.style.overflow = 'hidden';
124
- }
125
- function unlockScroll() {
126
- if (scrollLockCount === 0)
127
- return;
128
- scrollLockCount -= 1;
129
- if (scrollLockCount === 0)
130
- scrollTarget.style.overflow = previousOverflow;
131
- }
132
- return stack;
133
- }
134
- export function createDialogOverlayPlans(options) {
135
- const modal = options.modal ?? true;
136
- return {
137
- root: createUiDomPlan('div', 'overlay-root', {
138
- state: 'open',
139
- attrs: { 'data-overlay-root': options.id },
140
- }),
141
- ...(modal
142
- ? {
143
- backdrop: createUiDomPlan('div', 'overlay-backdrop', {
144
- state: 'open',
145
- attrs: { 'data-overlay-backdrop': options.id },
146
- }),
147
- }
148
- : {}),
149
- surface: createUiDomPlan('div', 'dialog', {
150
- state: 'open',
151
- attrs: {
152
- id: options.id,
153
- role: 'dialog',
154
- 'aria-modal': modal ? 'true' : undefined,
155
- 'aria-labelledby': options.titleId,
156
- 'aria-describedby': options.descriptionId,
157
- tabindex: -1,
158
- },
159
- }),
160
- };
161
- }
162
- export function createModalOverlayPlans(options) {
163
- return createDialogOverlayPlans({ ...options, modal: true });
164
- }
165
- export function createPositionedOverlayPlans(options) {
166
- const attrs = {
167
- id: options.id,
168
- role: overlayRole(options.kind),
169
- 'data-placement': options.placement ?? 'bottom-start',
170
- tabindex: -1,
171
- };
172
- return {
173
- root: createUiDomPlan('div', 'overlay-root', {
174
- state: 'open',
175
- attrs: { 'data-overlay-root': options.id },
176
- }),
177
- surface: createUiDomPlan('div', options.kind, {
178
- state: 'open',
179
- attrs,
180
- }),
181
- };
182
- }
183
- export function createDialogPlans(options) {
184
- const overlay = createDialogOverlayPlans(options);
185
- const actions = (options.actions ?? []).map((action) => createButtonPrimitive({
186
- label: action.label,
187
- variant: action.tone === 'primary' ? 'solid' : 'soft',
188
- disabled: action.disabled === true,
189
- className: classNames('kui-dialog-action', action.tone === undefined ? undefined : `kui-dialog-action--${action.tone}`),
190
- attrs: {
191
- id: `${options.id}-${action.id}-action`,
192
- 'data-action-id': action.id,
193
- 'data-tone': action.tone ?? 'default',
194
- },
195
- }));
196
- return {
197
- ...overlay,
198
- surface: {
199
- ...overlay.surface,
200
- attrs: {
201
- ...overlay.surface.attrs,
202
- 'data-size': options.size ?? 'md',
203
- 'data-tone': options.tone ?? 'default',
204
- },
205
- },
206
- header: createUiDomPlan('header', 'dialog-header', {
207
- attrs: { 'data-dialog-section': 'header' },
208
- }),
209
- title: createUiDomPlan('h2', 'dialog-title', {
210
- attrs: { id: options.titleId },
211
- }),
212
- ...(options.descriptionId === undefined
213
- ? {}
214
- : {
215
- description: createUiDomPlan('p', 'dialog-description', {
216
- attrs: { id: options.descriptionId },
217
- }),
218
- }),
219
- ...(options.closeLabel === undefined
220
- ? {}
221
- : {
222
- closeButton: createButtonPrimitive({
223
- label: options.closeLabel,
224
- variant: 'ghost',
225
- iconOnly: true,
226
- className: 'kui-dialog-close',
227
- attrs: { id: `${options.id}-close`, 'data-dialog-close': '' },
228
- }),
229
- }),
230
- body: createUiDomPlan('div', 'dialog-body', {
231
- attrs: { 'data-dialog-section': 'body' },
232
- }),
233
- ...(actions.length === 0
234
- ? {}
235
- : {
236
- footer: createUiDomPlan('footer', 'dialog-footer', {
237
- attrs: { 'data-dialog-section': 'footer' },
238
- }),
239
- }),
240
- actions,
241
- };
242
- }
243
- export function createOverlayTriggerPlan(options) {
244
- const open = options.open === true;
245
- return createButtonPrimitive({
246
- label: options.label,
247
- disabled: options.disabled === true,
248
- variant: 'soft',
249
- className: classNames('kui-overlay-trigger', options.kind === undefined ? undefined : `kui-${options.kind}-trigger`),
250
- attrs: {
251
- id: options.id,
252
- 'aria-haspopup': triggerPopupKind(options.kind),
253
- 'aria-expanded': options.kind === 'tooltip' ? undefined : open ? 'true' : 'false',
254
- 'aria-controls': open && options.kind !== 'tooltip' ? options.controlsId : undefined,
255
- 'aria-describedby': options.kind === 'tooltip' && open ? options.controlsId : undefined,
256
- 'data-overlay-trigger': options.controlsId,
257
- },
258
- });
259
- }
260
- export function createTooltipPlans(options) {
261
- const overlay = createPositionedOverlayPlans({
262
- id: options.id,
263
- kind: 'tooltip',
264
- placement: options.placement ?? 'bottom-start',
265
- });
266
- return {
267
- ...overlay,
268
- root: rootWithOpenState(overlay.root, options.open),
269
- surface: {
270
- ...surfaceWithOpenState(overlay.surface, options.open),
271
- attrs: {
272
- ...overlay.surface.attrs,
273
- 'data-state': options.open === true ? 'open' : 'closed',
274
- 'data-strategy': options.strategy ?? 'hover',
275
- 'data-delay-ms': options.delayMs ?? 400,
276
- hidden: options.open === true ? undefined : true,
277
- },
278
- },
279
- trigger: createOverlayTriggerPlan({
280
- id: options.triggerId,
281
- controlsId: options.id,
282
- label: options.label,
283
- open: options.open === true,
284
- disabled: options.disabled === true,
285
- kind: 'tooltip',
286
- }),
287
- arrow: createOverlayArrowPlan(options.placement),
288
- };
289
- }
290
- export function createPopoverPlans(options) {
291
- const overlay = createPositionedOverlayPlans({
292
- id: options.id,
293
- kind: 'popover',
294
- placement: options.placement ?? 'bottom-start',
295
- });
296
- return {
297
- ...overlay,
298
- root: rootWithOpenState(overlay.root, options.open),
299
- surface: {
300
- ...surfaceWithOpenState(overlay.surface, options.open),
301
- attrs: {
302
- ...overlay.surface.attrs,
303
- 'data-state': options.open === true ? 'open' : 'closed',
304
- 'aria-labelledby': options.titleId,
305
- 'aria-describedby': options.descriptionId,
306
- hidden: options.open === true ? undefined : true,
307
- },
308
- },
309
- trigger: createOverlayTriggerPlan({
310
- id: options.triggerId,
311
- controlsId: options.id,
312
- label: options.label,
313
- open: options.open === true,
314
- kind: 'popover',
315
- }),
316
- arrow: createOverlayArrowPlan(options.placement),
317
- ...(options.titleId === undefined
318
- ? {}
319
- : {
320
- header: createUiDomPlan('header', 'popover-header'),
321
- title: createUiDomPlan('h3', 'popover-title', { attrs: { id: options.titleId } }),
322
- }),
323
- body: createUiDomPlan('div', 'popover-body'),
324
- ...(options.descriptionId === undefined
325
- ? {}
326
- : {
327
- footer: createUiDomPlan('footer', 'popover-footer', {
328
- attrs: { id: options.descriptionId },
329
- }),
330
- }),
331
- };
332
- }
333
- export function createDropdownMenuPlans(options) {
334
- const overlay = createPositionedOverlayPlans({
335
- id: options.id,
336
- kind: 'dropdown',
337
- placement: options.placement ?? 'bottom-start',
338
- });
339
- const activeId = activeDropdownItemId(options.items, options.activeId);
340
- const menu = dropdownMenuPlan(overlay.surface, options.open, 'dropdown');
341
- return {
342
- ...overlay,
343
- root: rootWithOpenState(overlay.root, options.open),
344
- surface: menu,
345
- trigger: createOverlayTriggerPlan({
346
- id: options.triggerId,
347
- controlsId: options.id,
348
- label: options.label,
349
- open: options.open === true,
350
- kind: 'dropdown',
351
- }),
352
- menu,
353
- items: options.items.map((item) => dropdownItemPlan(options.id, item, activeId)),
354
- shortcuts: options.items.map((item) => dropdownShortcutPlan(options.id, item)),
355
- };
356
- }
357
- export function createContextMenuPlans(options) {
358
- const overlay = createPositionedOverlayPlans({
359
- id: options.id,
360
- kind: 'context-menu',
361
- placement: options.placement ?? 'bottom-start',
362
- });
363
- const activeId = activeDropdownItemId(options.items, options.activeId);
364
- const style = options.x === undefined || options.y === undefined
365
- ? undefined
366
- : `--kui-context-menu-x: ${options.x}px; --kui-context-menu-y: ${options.y}px;`;
367
- const menu = dropdownMenuPlan({
368
- ...overlay.surface,
369
- className: `${overlay.surface.className} kui-context-menu`,
370
- attrs: { ...overlay.surface.attrs, style },
371
- }, options.open, 'context-menu');
372
- return {
373
- ...overlay,
374
- root: rootWithOpenState(overlay.root, options.open),
375
- surface: menu,
376
- menu,
377
- items: options.items.map((item) => dropdownItemPlan(options.id, item, activeId)),
378
- shortcuts: options.items.map((item) => dropdownShortcutPlan(options.id, item)),
379
- };
380
- }
381
- export function createDropdownMenuPermissionRemoteActionContract(options) {
382
- const disabled = options.disabled === true;
383
- const knownIds = new Set(options.items.map((item) => item.id));
384
- const allowedInputIds = normalizeOverlayMenuIds(options.allowedIds, knownIds);
385
- const deniedInputIds = normalizeOverlayMenuIds(options.deniedIds, knownIds);
386
- const remoteActionInputIds = normalizeOverlayMenuIds(options.remoteActionIds, knownIds);
387
- const remoteActionDisabledInputIds = normalizeOverlayMenuIds(options.remoteActionDisabledIds, knownIds);
388
- const remoteActionLoadingInputIds = normalizeOverlayMenuIds(options.remoteActionLoadingIds, knownIds);
389
- const submenuLoadingInputIds = normalizeOverlayMenuIds(options.submenuLoadingIds, knownIds);
390
- const allowedSet = new Set(allowedInputIds);
391
- const deniedSet = new Set(deniedInputIds);
392
- const remoteActionSet = new Set(remoteActionInputIds);
393
- const remoteActionDisabledSet = new Set(remoteActionDisabledInputIds);
394
- const remoteActionLoadingSet = new Set(remoteActionLoadingInputIds);
395
- const submenuLoadingSet = new Set(submenuLoadingInputIds);
396
- const hasAllowedIds = options.allowedIds !== undefined;
397
- const items = options.items.map((item) => dropdownPermissionRemoteActionItemContract(item, {
398
- allowed: allowedSet.has(item.id),
399
- denied: deniedSet.has(item.id),
400
- disabled,
401
- disabledReason: normalizedOverlayMenuReason(options.disabledReasons?.[item.id]),
402
- hasAllowedIds,
403
- remoteAction: remoteActionSet.has(item.id),
404
- remoteActionDisabled: remoteActionDisabledSet.has(item.id),
405
- remoteActionDisabledReason: normalizedOverlayMenuReason(options.remoteActionDisabledReasons?.[item.id]),
406
- remoteActionError: normalizedOverlayMenuReason(options.remoteActionErrors?.[item.id]),
407
- remoteActionLoading: remoteActionLoadingSet.has(item.id),
408
- submenuError: normalizedOverlayMenuReason(options.submenuErrors?.[item.id]),
409
- submenuItemCount: normalizedOverlayMenuItemCount(options.submenuItemCounts?.[item.id]),
410
- submenuLoading: submenuLoadingSet.has(item.id),
411
- }));
412
- const allowedIds = items.filter((item) => item.permission === 'allowed').map((item) => item.id);
413
- const deniedIds = items.filter((item) => item.permission === 'denied').map((item) => item.id);
414
- const remoteActionIds = items.filter((item) => item.remoteAction).map((item) => item.id);
415
- const remoteActionDisabledIds = items
416
- .filter((item) => item.remoteAction && item.remoteActionState === 'disabled')
417
- .map((item) => item.id);
418
- const remoteActionLoadingIds = items
419
- .filter((item) => item.remoteAction && item.remoteActionState === 'pending')
420
- .map((item) => item.id);
421
- const remoteActionErrorIds = items
422
- .filter((item) => item.remoteActionError !== '')
423
- .map((item) => item.id);
424
- const submenuLoadingIds = items
425
- .filter((item) => item.submenuState === 'loading')
426
- .map((item) => item.id);
427
- const submenuErrorIds = items
428
- .filter((item) => item.submenuState === 'error')
429
- .map((item) => item.id);
430
- const disabledReasonCount = items.filter((item) => item.disabledReason !== '').length;
431
- const state = dropdownMenuPermissionRemoteActionState({
432
- deniedCount: deniedIds.length,
433
- itemCount: options.items.length,
434
- remoteActionLoadingCount: remoteActionLoadingIds.length,
435
- submenuErrorCount: submenuErrorIds.length,
436
- });
437
- return {
438
- state,
439
- disabled,
440
- itemCount: options.items.length,
441
- allowedCount: allowedIds.length,
442
- deniedCount: deniedIds.length,
443
- disabledReasonCount,
444
- remoteActionCount: remoteActionIds.length,
445
- remoteActionDisabledCount: remoteActionDisabledIds.length,
446
- remoteActionLoadingCount: remoteActionLoadingIds.length,
447
- remoteActionErrorCount: remoteActionErrorIds.length,
448
- submenuLoadingCount: submenuLoadingIds.length,
449
- submenuErrorCount: submenuErrorIds.length,
450
- allowedIds,
451
- deniedIds,
452
- remoteActionIds,
453
- remoteActionDisabledIds,
454
- remoteActionLoadingIds,
455
- remoteActionErrorIds,
456
- submenuLoadingIds,
457
- submenuErrorIds,
458
- items,
459
- attrs: {
460
- 'data-dropdown-permission-remote-contract': 'true',
461
- 'data-dropdown-permission-remote-state': state,
462
- 'data-state': state,
463
- 'data-disabled': disabled ? 'true' : undefined,
464
- 'data-item-count': options.items.length,
465
- 'data-allowed-count': allowedIds.length,
466
- 'data-denied-count': deniedIds.length,
467
- 'data-disabled-reason-count': disabledReasonCount,
468
- 'data-remote-action-count': remoteActionIds.length,
469
- 'data-remote-action-disabled-count': remoteActionDisabledIds.length,
470
- 'data-remote-action-loading-count': remoteActionLoadingIds.length,
471
- 'data-remote-action-error-count': remoteActionErrorIds.length,
472
- 'data-submenu-loading-count': submenuLoadingIds.length,
473
- 'data-submenu-error-count': submenuErrorIds.length,
474
- 'data-allowed-ids': allowedIds.join(','),
475
- 'data-denied-ids': deniedIds.join(','),
476
- 'data-remote-action-ids': remoteActionIds.join(','),
477
- 'data-remote-action-disabled-ids': remoteActionDisabledIds.join(','),
478
- 'data-remote-action-loading-ids': remoteActionLoadingIds.join(','),
479
- 'data-remote-action-error-ids': remoteActionErrorIds.join(','),
480
- 'data-submenu-loading-ids': submenuLoadingIds.join(','),
481
- 'data-submenu-error-ids': submenuErrorIds.join(','),
482
- 'aria-busy': remoteActionLoadingIds.length > 0 || submenuLoadingIds.length > 0 ? 'true' : undefined,
483
- },
484
- };
485
- }
486
- export function createDropdownMenuSubmenuPortalStateContract(options) {
487
- const submenuIds = new Set(options.items.filter((item) => item.kind === 'submenu').map((item) => item.id));
488
- const openPathIds = normalizeOverlayMenuIds(options.openPathIds, submenuIds);
489
- const mountedInputIds = normalizeOverlayMenuIds(options.mountedIds, submenuIds);
490
- const openSet = new Set(openPathIds);
491
- const mountedSet = new Set(mountedInputIds);
492
- const items = options.items
493
- .filter((item) => item.kind === 'submenu')
494
- .map((item) => dropdownMenuSubmenuPortalItemContract(item, {
495
- focusReturnId: normalizedOverlayMenuReason(options.focusReturnIds?.[item.id]),
496
- mounted: mountedSet.has(item.id),
497
- openPathIndex: openPathIds.indexOf(item.id),
498
- parentOverlayId: normalizedOverlayMenuReason(options.parentOverlayIds?.[item.id]),
499
- placement: options.placements?.[item.id],
500
- portalTargetId: normalizedOverlayMenuReason(options.portalTargetIds?.[item.id]),
501
- open: openSet.has(item.id),
502
- }));
503
- const mountedIds = items.filter((item) => item.mounted).map((item) => item.id);
504
- const portalMissingIds = items
505
- .filter((item) => item.state === 'portal-missing')
506
- .map((item) => item.id);
507
- const portalTargetIds = items
508
- .map((item) => item.portalTargetId)
509
- .filter((id, index, ids) => id !== '' && ids.indexOf(id) === index);
510
- const state = dropdownMenuSubmenuPortalContractState({
511
- mountedCount: mountedIds.length,
512
- openCount: openPathIds.length,
513
- portalMissingCount: portalMissingIds.length,
514
- submenuCount: items.length,
515
- });
516
- return {
517
- state,
518
- submenuCount: items.length,
519
- openCount: openPathIds.length,
520
- mountedCount: mountedIds.length,
521
- portalMissingCount: portalMissingIds.length,
522
- openPathIds,
523
- mountedIds,
524
- portalMissingIds,
525
- portalTargetIds,
526
- items,
527
- attrs: {
528
- 'data-dropdown-submenu-portal-contract': 'true',
529
- 'data-dropdown-submenu-portal-state': state,
530
- 'data-submenu-count': items.length,
531
- 'data-open-submenu-count': openPathIds.length,
532
- 'data-mounted-submenu-count': mountedIds.length,
533
- 'data-portal-missing-count': portalMissingIds.length,
534
- 'data-open-path-ids': openPathIds.join(','),
535
- 'data-mounted-submenu-ids': mountedIds.join(','),
536
- 'data-portal-missing-ids': portalMissingIds.join(','),
537
- 'data-portal-target-ids': portalTargetIds.join(','),
538
- },
539
- };
540
- }
541
- export function createPopconfirmPlans(options) {
542
- const overlay = createPositionedOverlayPlans({
543
- id: options.id,
544
- kind: 'popover',
545
- placement: options.placement ?? 'bottom-start',
546
- });
547
- return {
548
- ...overlay,
549
- root: rootWithOpenState(overlay.root, options.open),
550
- surface: {
551
- ...surfaceWithOpenState(overlay.surface, options.open),
552
- className: `${overlay.surface.className} kui-popconfirm`,
553
- attrs: {
554
- ...overlay.surface.attrs,
555
- 'data-state': options.open === true ? 'open' : 'closed',
556
- role: 'dialog',
557
- 'aria-labelledby': options.titleId,
558
- 'aria-describedby': options.descriptionId,
559
- 'data-tone': options.tone ?? 'danger',
560
- hidden: options.open === true ? undefined : true,
561
- },
562
- },
563
- trigger: createOverlayTriggerPlan({
564
- id: options.triggerId,
565
- controlsId: options.id,
566
- label: options.label,
567
- open: options.open === true,
568
- kind: 'popover',
569
- }),
570
- arrow: createOverlayArrowPlan(options.placement),
571
- title: createUiDomPlan('h3', 'popconfirm-title', { attrs: { id: options.titleId } }),
572
- ...(options.descriptionId === undefined
573
- ? {}
574
- : {
575
- description: createUiDomPlan('p', 'popconfirm-description', {
576
- attrs: { id: options.descriptionId },
577
- }),
578
- }),
579
- footer: createUiDomPlan('footer', 'popconfirm-footer'),
580
- cancelButton: createButtonPrimitive({
581
- label: options.cancelLabel ?? 'Cancel',
582
- variant: 'ghost',
583
- size: 'sm',
584
- attrs: { 'data-popconfirm-action': 'cancel' },
585
- }),
586
- confirmButton: createButtonPrimitive({
587
- label: options.confirmLabel ?? 'Confirm',
588
- variant: options.tone === 'primary' ? 'solid' : 'soft',
589
- size: 'sm',
590
- className: classNames('kui-popconfirm-confirm', `kui-popconfirm-confirm--${options.tone ?? 'danger'}`),
591
- attrs: { 'data-popconfirm-action': 'confirm', 'data-tone': options.tone ?? 'danger' },
592
- }),
593
- };
594
- }
595
- export function getNextDropdownItemId(items, activeId, key) {
596
- const enabled = dropdownFocusableItems(items);
597
- if (enabled.length === 0)
598
- return activeId;
599
- const currentIndex = Math.max(-1, enabled.findIndex((item) => item.id === activeId));
600
- if (key === 'Home')
601
- return enabled[0]?.id;
602
- if (key === 'End')
603
- return enabled.at(-1)?.id;
604
- if (key === 'ArrowUp')
605
- return enabled.at(currentIndex - 1)?.id ?? enabled.at(-1)?.id;
606
- if (key === 'ArrowDown')
607
- return enabled[(currentIndex + 1) % enabled.length]?.id;
608
- return activeId;
609
- }
610
- export function shouldOpenOverlayForTriggerKey(key) {
611
- return key === 'Enter' || key === ' ' || key === 'ArrowDown' || key === 'ArrowUp';
612
- }
613
- export function shouldCloseOverlayForKey(key) {
614
- return key === 'Escape';
615
- }
616
- export function shouldSelectDropdownItem(key) {
617
- return key === 'Enter' || key === ' ';
618
- }
619
- function activeHtmlElement(document) {
620
- const active = document.activeElement;
621
- return isHTMLElement(active) ? active : undefined;
622
- }
623
- function focusInitialElement(initialFocus, element, autoFocus) {
624
- if (initialFocus !== undefined) {
625
- initialFocus.focus();
626
- return;
627
- }
628
- if (!autoFocus)
629
- return;
630
- const target = element === undefined ? undefined : (firstFocusable(element) ?? element);
631
- target?.focus();
632
- }
633
- function trapFocus(event, root) {
634
- const focusable = findFocusable(root);
635
- if (focusable.length === 0) {
636
- event.preventDefault();
637
- root.focus();
638
- return;
639
- }
640
- const current = root.ownerDocument.activeElement;
641
- const currentIndex = focusable.findIndex((item) => item === current);
642
- const nextIndex = event.shiftKey
643
- ? currentIndex <= 0
644
- ? focusable.length - 1
645
- : currentIndex - 1
646
- : currentIndex === focusable.length - 1
647
- ? 0
648
- : currentIndex + 1;
649
- if (currentIndex < 0 || nextIndex !== currentIndex + (event.shiftKey ? -1 : 1)) {
650
- event.preventDefault();
651
- focusable[nextIndex]?.focus();
652
- }
653
- }
654
- function firstFocusable(root) {
655
- return findFocusable(root)[0];
656
- }
657
- function findFocusable(root) {
658
- return Array.from(root.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((element) => !element.hasAttribute('disabled') && element.tabIndex >= 0);
659
- }
660
- function createOverlayArrowPlan(placement = 'bottom-start') {
661
- return createUiDomPlan('span', 'overlay-arrow', {
662
- attrs: { 'aria-hidden': 'true', 'data-placement': placement },
663
- });
664
- }
665
- function rootWithOpenState(plan, open = false) {
666
- return {
667
- ...plan,
668
- attrs: { ...plan.attrs, 'data-state': open ? 'open' : 'closed' },
669
- };
670
- }
671
- function surfaceWithOpenState(plan, open = false) {
672
- return {
673
- ...plan,
674
- attrs: { ...plan.attrs, 'data-state': open ? 'open' : 'closed' },
675
- };
676
- }
677
- function triggerPopupKind(kind) {
678
- if (kind === 'tooltip')
679
- return undefined;
680
- if (kind === 'dropdown' || kind === 'context-menu')
681
- return 'menu';
682
- return 'dialog';
683
- }
684
- function dropdownMenuPlan(surface, open, kind) {
685
- return {
686
- ...surfaceWithOpenState(surface, open),
687
- attrs: {
688
- ...surface.attrs,
689
- 'data-state': open === true ? 'open' : 'closed',
690
- role: 'menu',
691
- 'data-menu-kind': kind,
692
- hidden: open === true ? undefined : true,
693
- },
694
- };
695
- }
696
- function dropdownItemPlan(rootId, item, activeId) {
697
- const kind = item.kind ?? 'item';
698
- const active = item.id === activeId;
699
- const separator = kind === 'separator';
700
- const remoteActionState = dropdownItemRemoteActionState(item);
701
- const submenuState = dropdownItemSubmenuState(item);
702
- return createUiDomPlan('div', 'dropdown-item', {
703
- state: separator || item.disabled === true
704
- ? 'disabled'
705
- : active
706
- ? 'active'
707
- : item.checked === true
708
- ? 'checked'
709
- : 'idle',
710
- ...(item.disabled === true || separator ? { disabled: true } : {}),
711
- className: classNames(item.danger === true ? 'kui-dropdown-item--danger' : undefined),
712
- attrs: {
713
- id: `${rootId}-${item.id}-item`,
714
- role: separator ? 'separator' : menuItemRole(kind),
715
- tabindex: active && !separator && item.disabled !== true ? 0 : -1,
716
- 'aria-disabled': item.disabled === true ? 'true' : undefined,
717
- 'aria-haspopup': kind === 'submenu' ? 'menu' : undefined,
718
- 'aria-expanded': kind === 'submenu' ? (item.expanded === true ? 'true' : 'false') : undefined,
719
- 'aria-busy': item.remoteActionLoading === true || item.submenuLoading === true ? 'true' : undefined,
720
- 'aria-checked': kind === 'checkbox' || kind === 'radio' ? String(item.checked === true) : undefined,
721
- 'data-item-id': item.id,
722
- 'data-kind': kind,
723
- 'data-label': item.label,
724
- 'data-group-id': item.groupId,
725
- 'data-danger': item.danger === true ? '' : undefined,
726
- 'data-shortcut': item.shortcut,
727
- 'data-permission': item.permission,
728
- 'data-disabled-reason': item.disabledReason,
729
- 'data-remote-action': item.remoteAction === true ? 'true' : undefined,
730
- 'data-remote-action-state': remoteActionState,
731
- 'data-remote-action-disabled-reason': item.remoteActionDisabledReason,
732
- 'data-remote-action-error': item.remoteActionError,
733
- 'data-can-invoke-remote-action': remoteActionState === 'available' && item.disabled !== true ? 'true' : undefined,
734
- 'data-submenu-state': submenuState,
735
- 'data-submenu-error': item.submenuError,
736
- 'data-submenu-item-count': item.submenuItemCount,
737
- 'data-submenu-portal-state': item.submenuPortalState,
738
- 'data-submenu-portal-target': item.submenuPortalTargetId,
739
- 'data-submenu-open-path-index': item.submenuOpenPathIndex,
740
- 'data-submenu-focus-return': item.submenuFocusReturnId,
741
- 'data-submenu-parent-overlay': item.submenuParentOverlayId,
742
- 'data-submenu-placement': item.submenuPlacement,
743
- 'aria-controls': item.submenuPortalTargetId,
744
- },
745
- });
746
- }
747
- function dropdownShortcutPlan(rootId, item) {
748
- return createUiDomPlan('kbd', 'dropdown-shortcut', {
749
- attrs: {
750
- id: `${rootId}-${item.id}-shortcut`,
751
- 'aria-hidden': 'true',
752
- 'data-shortcut': item.shortcut,
753
- },
754
- });
755
- }
756
- function activeDropdownItemId(items, activeId) {
757
- const enabled = dropdownFocusableItems(items);
758
- return enabled.some((item) => item.id === activeId) ? activeId : enabled[0]?.id;
759
- }
760
- function dropdownFocusableItems(items) {
761
- return items.filter((item) => item.disabled !== true && item.kind !== 'separator');
762
- }
763
- function menuItemRole(kind) {
764
- if (kind === 'checkbox')
765
- return 'menuitemcheckbox';
766
- if (kind === 'radio')
767
- return 'menuitemradio';
768
- return 'menuitem';
769
- }
770
- function dropdownMenuSubmenuPortalItemContract(item, options) {
771
- const mounted = options.mounted || options.open;
772
- const state = options.open && options.portalTargetId === ''
773
- ? 'portal-missing'
774
- : options.open
775
- ? 'open'
776
- : mounted
777
- ? 'mounted'
778
- : 'closed';
779
- const openPathIndex = options.openPathIndex < 0 ? undefined : options.openPathIndex;
780
- const contractItem = {
781
- ...item,
782
- ...(options.open ? { expanded: true } : {}),
783
- submenuPortalState: state,
784
- ...(options.portalTargetId === '' ? {} : { submenuPortalTargetId: options.portalTargetId }),
785
- ...(openPathIndex === undefined ? {} : { submenuOpenPathIndex: openPathIndex }),
786
- ...(options.focusReturnId === '' ? {} : { submenuFocusReturnId: options.focusReturnId }),
787
- ...(options.parentOverlayId === '' ? {} : { submenuParentOverlayId: options.parentOverlayId }),
788
- ...(options.placement === undefined ? {} : { submenuPlacement: options.placement }),
789
- };
790
- return {
791
- id: item.id,
792
- item: contractItem,
793
- state,
794
- open: options.open,
795
- mounted,
796
- portalTargetId: options.portalTargetId,
797
- focusReturnId: options.focusReturnId,
798
- parentOverlayId: options.parentOverlayId,
799
- placement: options.placement,
800
- openPathIndex,
801
- attrs: {
802
- 'data-item-id': item.id,
803
- 'data-submenu-portal-state': state,
804
- 'data-submenu-portal-target': options.portalTargetId,
805
- 'data-submenu-open-path-index': openPathIndex,
806
- 'data-submenu-focus-return': options.focusReturnId,
807
- 'data-submenu-parent-overlay': options.parentOverlayId,
808
- 'data-submenu-placement': options.placement,
809
- 'aria-controls': options.portalTargetId === '' ? undefined : options.portalTargetId,
810
- 'aria-expanded': options.open ? 'true' : item.expanded === true ? 'true' : 'false',
811
- },
812
- };
813
- }
814
- function dropdownPermissionRemoteActionItemContract(item, options) {
815
- const permission = options.denied || (options.hasAllowedIds && !options.allowed) ? 'denied' : 'allowed';
816
- const baseDisabledReason = options.disabledReason || normalizedOverlayMenuReason(item.disabledReason);
817
- const disabledReason = permission === 'denied' && baseDisabledReason === ''
818
- ? 'Permission required'
819
- : baseDisabledReason;
820
- const disabled = options.disabled || item.disabled === true || permission === 'denied';
821
- const remoteAction = options.remoteAction ||
822
- options.remoteActionDisabled ||
823
- options.remoteActionLoading ||
824
- options.remoteActionError !== '' ||
825
- item.remoteAction === true ||
826
- item.remoteActionDisabled === true ||
827
- item.remoteActionLoading === true ||
828
- normalizedOverlayMenuReason(item.remoteActionError) !== '';
829
- const remoteActionError = options.remoteActionError || normalizedOverlayMenuReason(item.remoteActionError);
830
- const remoteActionDisabled = options.remoteActionDisabled || item.remoteActionDisabled === true;
831
- const remoteActionLoading = options.remoteActionLoading || item.remoteActionLoading === true;
832
- const remoteActionDisabledReason = options.remoteActionDisabledReason ||
833
- normalizedOverlayMenuReason(item.remoteActionDisabledReason);
834
- const remoteActionState = remoteActionLoading && remoteActionError === ''
835
- ? 'pending'
836
- : remoteActionDisabled || disabled || remoteActionError !== ''
837
- ? 'disabled'
838
- : 'available';
839
- const submenuError = options.submenuError || normalizedOverlayMenuReason(item.submenuError);
840
- const submenuLoading = options.submenuLoading || item.submenuLoading === true;
841
- const submenuItemCount = options.submenuItemCount ?? item.submenuItemCount;
842
- const contractItem = {
843
- ...item,
844
- disabled,
845
- ...(disabledReason === '' ? {} : { disabledReason }),
846
- permission,
847
- ...(remoteAction ? { remoteAction: true } : {}),
848
- ...(remoteActionState === 'disabled' ? { remoteActionDisabled: true } : {}),
849
- ...(remoteActionState === 'pending' ? { remoteActionLoading: true } : {}),
850
- ...(remoteActionDisabledReason === '' ? {} : { remoteActionDisabledReason }),
851
- ...(remoteActionError === '' ? {} : { remoteActionError }),
852
- ...(submenuError === '' ? {} : { submenuError }),
853
- ...(submenuItemCount === undefined ? {} : { submenuItemCount }),
854
- ...(submenuLoading ? { submenuLoading: true } : {}),
855
- };
856
- const submenuState = dropdownItemSubmenuState(contractItem);
857
- return {
858
- id: item.id,
859
- item: contractItem,
860
- permission,
861
- disabled,
862
- disabledReason,
863
- remoteAction,
864
- remoteActionState,
865
- remoteActionDisabledReason,
866
- remoteActionError,
867
- submenuState,
868
- submenuError,
869
- submenuItemCount,
870
- attrs: {
871
- 'data-item-id': item.id,
872
- 'data-permission': permission,
873
- 'data-disabled-reason': disabledReason,
874
- 'data-remote-action': remoteAction ? 'true' : undefined,
875
- 'data-remote-action-state': remoteAction ? remoteActionState : undefined,
876
- 'data-remote-action-disabled-reason': remoteActionDisabledReason,
877
- 'data-remote-action-error': remoteActionError,
878
- 'data-can-invoke-remote-action': remoteAction && remoteActionState === 'available' && !disabled ? 'true' : undefined,
879
- 'data-submenu-state': submenuState,
880
- 'data-submenu-error': submenuError,
881
- 'data-submenu-item-count': submenuItemCount,
882
- 'aria-disabled': disabled ? 'true' : undefined,
883
- 'aria-busy': remoteActionState === 'pending' || submenuState === 'loading' ? 'true' : undefined,
884
- },
885
- };
886
- }
887
- function dropdownMenuPermissionRemoteActionState(options) {
888
- if (options.itemCount === 0)
889
- return 'empty';
890
- if (options.remoteActionLoadingCount > 0)
891
- return 'remote-pending';
892
- if (options.submenuErrorCount > 0)
893
- return 'submenu-error';
894
- if (options.deniedCount > 0)
895
- return 'permission-limited';
896
- return 'ready';
897
- }
898
- function dropdownMenuSubmenuPortalContractState(options) {
899
- if (options.submenuCount === 0)
900
- return 'empty';
901
- if (options.portalMissingCount > 0)
902
- return 'portal-missing';
903
- if (options.openCount > 0)
904
- return 'open';
905
- if (options.mountedCount > 0)
906
- return 'mounted';
907
- return 'closed';
908
- }
909
- function dropdownItemRemoteActionState(item) {
910
- if (item.remoteAction !== true)
911
- return undefined;
912
- if (item.remoteActionLoading === true &&
913
- normalizedOverlayMenuReason(item.remoteActionError) === '') {
914
- return 'pending';
915
- }
916
- if (item.remoteActionDisabled === true ||
917
- item.disabled === true ||
918
- normalizedOverlayMenuReason(item.remoteActionError) !== '') {
919
- return 'disabled';
920
- }
921
- return 'available';
922
- }
923
- function dropdownItemSubmenuState(item) {
924
- if (item.kind !== 'submenu')
925
- return undefined;
926
- if (item.submenuLoading === true)
927
- return 'loading';
928
- if (normalizedOverlayMenuReason(item.submenuError) !== '')
929
- return 'error';
930
- if (item.expanded === true)
931
- return 'open';
932
- if ((item.submenuItemCount ?? 0) > 0)
933
- return 'ready';
934
- return 'closed';
935
- }
936
- function normalizeOverlayMenuIds(value, knownIds) {
937
- if (value === undefined)
938
- return [];
939
- const values = Array.isArray(value)
940
- ? value
941
- : [...value];
942
- const result = [];
943
- for (const id of values) {
944
- if (!knownIds.has(id) || result.includes(id))
945
- continue;
946
- result.push(id);
947
- }
948
- return result;
949
- }
950
- function normalizedOverlayMenuReason(value) {
951
- return value?.trim() ?? '';
952
- }
953
- function normalizedOverlayMenuItemCount(value) {
954
- if (value === undefined || !Number.isFinite(value))
955
- return undefined;
956
- return Math.max(0, Math.trunc(value));
957
- }
958
- function overlayRole(kind) {
959
- switch (kind) {
960
- case 'tooltip':
961
- return 'tooltip';
962
- case 'dropdown':
963
- case 'context-menu':
964
- return 'menu';
965
- default:
966
- return 'dialog';
967
- }
968
- }
969
- function isNode(value) {
970
- return typeof Node !== 'undefined' && value instanceof Node;
971
- }
972
- function isHTMLElement(value) {
973
- return typeof HTMLElement !== 'undefined' && value instanceof HTMLElement;
974
- }