@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
@@ -1,913 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/ui
4
- */
5
- import { createIconElement } from '@vobs/icons';
6
- import { effect } from '@vobs/reactivity';
7
- import {} from '@vobs/runtime-dom';
8
- import { resolveUiIcon } from './icons.js';
9
- import { brandPresets, createStorageThemePersistence, createMediaThemePreference, createThemeController, resolveBrandColor, THEME_PREFERENCE_CHANGE_EVENT, THEME_STATE_CHANGE_EVENT, } from './theme.js';
10
- import { resolveAppShellText } from './app-shell.js';
11
- function appShellTranslate(scope) {
12
- const translation = scope.translation;
13
- return translation === undefined ? undefined : (key) => translation.t(key);
14
- }
15
- const defaultBrandCycle = brandPresets.map((brand) => brand.id);
16
- export function createAppShellFrame(shell, scope, environment) {
17
- const root = document.createElement('div');
18
- root.className = joinClasses('kui-app-shell', shell.className);
19
- root.setAttribute('data-vobs-template', 'k-app-shell');
20
- root.setAttribute('data-vobs-protocol', '1');
21
- root.setAttribute('data-app-shell', shell.id);
22
- root.setAttribute('data-sidebar-mode', shell.sidebarMode ?? 'expanded');
23
- root.setAttribute('data-content-mode', shell.contentMode ?? 'contained');
24
- setOptionalAttribute(root, 'data-theme-mode', shell.theme?.mode);
25
- setOptionalAttribute(root, 'data-accent', shell.theme?.accent);
26
- const brand = createBrand(shell, scope, environment);
27
- const topbar = document.createElement('header');
28
- topbar.className = 'kui-app-shell-topbar';
29
- topbar.setAttribute('data-app-shell-region', 'topbar');
30
- topbar.append(brand, createTopbarActions(shell, scope, environment));
31
- const sidebar = document.createElement('aside');
32
- sidebar.className = 'kui-app-shell-sidebar';
33
- sidebar.setAttribute('data-app-shell-region', 'sidebar');
34
- const brandLabel = resolveAppShellText(shell.brand.label, appShellTranslate(scope)) ?? '';
35
- sidebar.setAttribute('aria-label', `${brandLabel} navigation`);
36
- sidebar.setAttribute('data-sidebar-mode', shell.sidebarMode ?? 'expanded');
37
- const primaryNav = document.createElement('nav');
38
- primaryNav.className = 'kui-app-shell-nav';
39
- primaryNav.setAttribute('aria-label', 'Primary');
40
- const utilityNav = document.createElement('nav');
41
- utilityNav.className = 'kui-app-shell-utility-nav';
42
- utilityNav.setAttribute('aria-label', 'Utility');
43
- sidebar.append(primaryNav, utilityNav);
44
- const main = document.createElement('main');
45
- main.className = 'kui-app-shell-main';
46
- main.setAttribute('data-app-shell-region', 'content');
47
- main.setAttribute('aria-label', `${brandLabel} content`);
48
- const pageHeader = document.createElement('header');
49
- pageHeader.className = 'kui-app-shell-page-header';
50
- pageHeader.setAttribute('data-app-shell-region', 'page-header');
51
- const pageHeading = document.createElement('div');
52
- pageHeading.className = 'kui-app-shell-page-heading';
53
- const pageKicker = document.createElement('p');
54
- pageKicker.className = 'kui-app-shell-page-kicker';
55
- const pageTitle = document.createElement('h1');
56
- pageTitle.className = 'kui-app-shell-page-title';
57
- pageHeading.append(pageKicker, pageTitle);
58
- const pageDescription = document.createElement('p');
59
- pageDescription.className = 'kui-app-shell-page-subtitle';
60
- pageHeader.append(pageHeading, pageDescription);
61
- const pageBody = document.createElement('section');
62
- pageBody.className = 'kui-app-shell-page-body';
63
- pageBody.setAttribute('data-mode', shell.contentMode ?? 'contained');
64
- const pageToolbar = document.createElement('div');
65
- pageToolbar.className = 'kui-app-shell-page-toolbar';
66
- pageToolbar.setAttribute('role', 'toolbar');
67
- const pageFrame = document.createElement('section');
68
- pageFrame.className = 'kui-app-shell-page-frame';
69
- pageFrame.append(pageHeader, pageToolbar, pageBody);
70
- main.append(pageFrame);
71
- const footer = document.createElement('footer');
72
- footer.className = 'kui-app-shell-statusbar';
73
- footer.setAttribute('data-app-shell-region', 'statusbar');
74
- footer.setAttribute('aria-live', 'polite');
75
- const status = document.createElement('span');
76
- status.className = 'kui-app-shell-status';
77
- status.setAttribute('data-app-shell-status', 'runtime');
78
- const statusIcon = document.createElement('span');
79
- statusIcon.className = 'kui-app-shell-status-icon';
80
- const statusText = document.createElement('span');
81
- statusText.className = 'kui-app-shell-status-text';
82
- status.append(statusIcon, statusText);
83
- const version = document.createElement('span');
84
- version.className = 'kui-app-shell-status-version';
85
- version.setAttribute('data-app-shell-version', 'true');
86
- version.textContent =
87
- resolveAppShellText(shell.statusbar?.version, appShellTranslate(scope)) ?? '';
88
- footer.append(status, version);
89
- root.append(topbar, sidebar, main, footer);
90
- return {
91
- root,
92
- brand,
93
- sidebar,
94
- main,
95
- primaryNav,
96
- utilityNav,
97
- pageFrame,
98
- pageHeader,
99
- pageToolbar,
100
- statusbar: footer,
101
- statusIcon,
102
- statusText,
103
- pageKicker,
104
- pageTitle,
105
- pageDescription,
106
- version,
107
- };
108
- }
109
- export function bindAppShellThemeControls(owner, frame, shell, scope, environment) {
110
- if (shell.theme === undefined)
111
- return;
112
- const controller = createAppShellThemeController(frame, shell);
113
- const themeButton = readActionButton(frame.root, 'theme');
114
- const brandButton = readActionButton(frame.root, 'accent');
115
- const brandCycle = createBrandCycle(shell);
116
- const sync = (state) => {
117
- syncAppShellThemeState(frame, themeButton, brandButton, shell, state, scope, environment);
118
- frame.root.ownerDocument.dispatchEvent(new CustomEvent(THEME_STATE_CHANGE_EVENT, {
119
- detail: {
120
- shellId: shell.id,
121
- theme: state.theme,
122
- preference: controller.preference,
123
- brand: state.brand,
124
- },
125
- }));
126
- };
127
- sync(controller.state);
128
- owner.own(controller.subscribe(sync));
129
- owner.own(() => controller.destroy());
130
- if (themeButton !== null) {
131
- const onClick = () => {
132
- controller.toggleTheme();
133
- };
134
- themeButton.addEventListener('click', onClick);
135
- owner.own(() => themeButton.removeEventListener('click', onClick));
136
- }
137
- if (brandButton !== null) {
138
- const onClick = () => {
139
- controller.setBrand(nextBrand(controller.brand, brandCycle));
140
- };
141
- brandButton.addEventListener('click', onClick);
142
- owner.own(() => brandButton.removeEventListener('click', onClick));
143
- }
144
- const onPreferenceChange = (event) => {
145
- if (!(event instanceof CustomEvent))
146
- return;
147
- const detail = event.detail;
148
- if (detail?.shellId !== undefined && detail.shellId !== shell.id)
149
- return;
150
- if (detail?.theme !== undefined && isThemePreference(detail.theme)) {
151
- controller.setPreference(detail.theme);
152
- }
153
- if (detail?.brand !== undefined && isBrandInput(detail.brand)) {
154
- controller.setBrand(detail.brand);
155
- }
156
- };
157
- frame.root.ownerDocument.addEventListener(THEME_PREFERENCE_CHANGE_EVENT, onPreferenceChange);
158
- owner.own(() => frame.root.ownerDocument.removeEventListener(THEME_PREFERENCE_CHANGE_EVENT, onPreferenceChange));
159
- }
160
- export function bindAppShellAccountMenu(owner, frame, shell) {
161
- const root = frame.root;
162
- const account = shell.topbar?.account;
163
- if (account === undefined || (account.menu?.length ?? 0) === 0)
164
- return;
165
- const accountRoot = root.querySelector('.kui-app-shell-account');
166
- const button = root.querySelector('.kui-app-shell-account-button');
167
- const menu = root.querySelector('.kui-app-shell-account-menu');
168
- if (!(accountRoot instanceof HTMLElement) ||
169
- !(button instanceof HTMLButtonElement) ||
170
- !(menu instanceof HTMLElement)) {
171
- return;
172
- }
173
- const sync = (expanded) => {
174
- const state = expanded ? 'open' : 'closed';
175
- accountRoot.setAttribute('data-state', state);
176
- button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
177
- button.setAttribute('data-state', state);
178
- menu.setAttribute('data-state', state);
179
- menu.hidden = !expanded;
180
- };
181
- const expanded = () => button.getAttribute('aria-expanded') === 'true';
182
- const dispatchToggle = () => {
183
- button.dispatchEvent(new CustomEvent('vobs:app-shell-account-toggle', {
184
- bubbles: true,
185
- detail: { shellId: shell.id, expanded: expanded() },
186
- }));
187
- };
188
- const close = () => {
189
- if (!expanded())
190
- return;
191
- sync(false);
192
- dispatchToggle();
193
- };
194
- const toggle = () => {
195
- sync(!expanded());
196
- dispatchToggle();
197
- };
198
- const onButtonClick = () => {
199
- toggle();
200
- if (expanded())
201
- focusAccountMenuItem(menu, 'first');
202
- };
203
- const onButtonKeyDown = (event) => {
204
- if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
205
- return;
206
- event.preventDefault();
207
- if (!expanded()) {
208
- sync(true);
209
- dispatchToggle();
210
- }
211
- focusAccountMenuItem(menu, event.key === 'ArrowDown' ? 'first' : 'last');
212
- };
213
- const onDocumentPointerDown = (event) => {
214
- if (!expanded())
215
- return;
216
- if (event.target instanceof Node && root.contains(event.target)) {
217
- if (accountRoot?.contains(event.target))
218
- return;
219
- }
220
- close();
221
- };
222
- const onDocumentKeyDown = (event) => {
223
- if (event.key === 'Escape') {
224
- close();
225
- button.focus();
226
- }
227
- };
228
- const onMenuKeyDown = (event) => {
229
- if (event.key === 'Escape') {
230
- event.preventDefault();
231
- event.stopPropagation();
232
- close();
233
- button.focus();
234
- return;
235
- }
236
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
237
- event.preventDefault();
238
- focusAccountMenuItem(menu, event.key === 'ArrowDown' ? 'next' : 'previous');
239
- return;
240
- }
241
- if (event.key === 'Home' || event.key === 'End') {
242
- event.preventDefault();
243
- focusAccountMenuItem(menu, event.key === 'Home' ? 'first' : 'last');
244
- }
245
- };
246
- const onMenuClick = (event) => {
247
- const target = event.target instanceof Element ? event.target : undefined;
248
- const item = target?.closest('[data-account-menu-id]');
249
- if (!(item instanceof HTMLElement))
250
- return;
251
- const itemId = item.getAttribute('data-account-menu-id');
252
- if (itemId === null || item.getAttribute('aria-disabled') === 'true') {
253
- event.preventDefault();
254
- return;
255
- }
256
- item.dispatchEvent(new CustomEvent('vobs:app-shell-account-select', {
257
- bubbles: true,
258
- detail: { shellId: shell.id, itemId },
259
- }));
260
- close();
261
- };
262
- sync(account.menuExpanded === true);
263
- button.addEventListener('click', onButtonClick);
264
- button.addEventListener('keydown', onButtonKeyDown);
265
- menu.addEventListener('click', onMenuClick);
266
- menu.addEventListener('keydown', onMenuKeyDown);
267
- root.ownerDocument.addEventListener('pointerdown', onDocumentPointerDown);
268
- root.ownerDocument.addEventListener('keydown', onDocumentKeyDown);
269
- owner.own(() => {
270
- button.removeEventListener('click', onButtonClick);
271
- button.removeEventListener('keydown', onButtonKeyDown);
272
- menu.removeEventListener('click', onMenuClick);
273
- menu.removeEventListener('keydown', onMenuKeyDown);
274
- root.ownerDocument.removeEventListener('pointerdown', onDocumentPointerDown);
275
- root.ownerDocument.removeEventListener('keydown', onDocumentKeyDown);
276
- });
277
- }
278
- export function bindAppShellActions(owner, frame, shell) {
279
- for (const action of shell.topbar?.actions ?? []) {
280
- const button = readActionButton(frame.root, action.id);
281
- if (button === null)
282
- continue;
283
- const onClick = () => {
284
- button.dispatchEvent(new CustomEvent('vobs:app-shell-action', {
285
- bubbles: true,
286
- detail: { shellId: shell.id, actionId: action.id },
287
- }));
288
- };
289
- button.addEventListener('click', onClick);
290
- owner.own(() => button.removeEventListener('click', onClick));
291
- }
292
- }
293
- function focusAccountMenuItem(menu, placement) {
294
- const items = Array.from(menu.querySelectorAll('.kui-app-shell-account-menu-item:not([aria-disabled="true"])'));
295
- if (items.length === 0)
296
- return;
297
- const currentIndex = items.findIndex((item) => item === menu.ownerDocument.activeElement);
298
- const nextIndex = placement === 'first'
299
- ? 0
300
- : placement === 'last'
301
- ? items.length - 1
302
- : placement === 'next'
303
- ? (Math.max(0, currentIndex) + 1) % items.length
304
- : currentIndex <= 0
305
- ? items.length - 1
306
- : currentIndex - 1;
307
- items[nextIndex]?.focus();
308
- }
309
- export function syncAppShellFrame(frame, shell, state, status, scope, environment) {
310
- setOptionalAttribute(frame.root, 'data-active-nav', state.activeId);
311
- setOptionalAttribute(frame.root, 'data-route-pending', status.state === 'pending' ? 'true' : undefined);
312
- setOptionalAttribute(frame.statusbar, 'data-state', status.state);
313
- frame.pageHeader.hidden = !state.pageHeader.visible;
314
- frame.pageHeader.setAttribute('data-state', state.pageHeader.visible ? 'visible' : 'hidden');
315
- setOptionalAttribute(frame.pageHeader, 'aria-hidden', state.pageHeader.visible ? undefined : 'true');
316
- frame.pageKicker.textContent = state.pageHeader.kicker;
317
- frame.pageTitle.textContent = state.pageHeader.title;
318
- frame.pageTitle.setAttribute('data-title', state.pageHeader.title);
319
- frame.pageDescription.textContent = state.pageHeader.description;
320
- frame.pageDescription.setAttribute('data-subtitle', state.pageHeader.description);
321
- frame.statusText.textContent = status.text;
322
- frame.statusbar.setAttribute('data-status', status.state);
323
- frame.statusIcon.replaceChildren();
324
- appendIcon(frame.statusIcon, status.icon, 'kui-app-shell-status-icon', scope, environment);
325
- const brandLabel = resolveAppShellText(shell.brand.label, appShellTranslate(scope)) ?? '';
326
- frame.brand.setAttribute('aria-label', brandLabel);
327
- frame.brand.setAttribute('data-label', brandLabel);
328
- const brandName = frame.brand.querySelector('.kui-app-shell-brand-name');
329
- if (brandName !== null)
330
- brandName.textContent = brandLabel;
331
- frame.sidebar.setAttribute('aria-label', `${brandLabel} navigation`);
332
- frame.main.setAttribute('aria-label', `${brandLabel} content`);
333
- frame.version.textContent =
334
- resolveAppShellText(shell.statusbar?.version, appShellTranslate(scope)) ?? '';
335
- syncTopbarText(frame.root, shell, scope);
336
- renderNavItems(frame.primaryNav, shell.navigation?.primary ?? [], state.activeId, scope, environment);
337
- renderNavItems(frame.utilityNav, shell.navigation?.utility ?? [], state.activeId, scope, environment);
338
- }
339
- function createAppShellThemeController(frame, shell) {
340
- const persistence = shell.theme?.persist === false
341
- ? undefined
342
- : createBrowserThemePersistence(frame.root.ownerDocument);
343
- const systemPreference = createBrowserSystemThemePreference(frame.root.ownerDocument);
344
- const options = {
345
- target: frame.root.ownerDocument.documentElement,
346
- initialBrand: shell.theme?.brand ?? shell.theme?.accent ?? 'green',
347
- ...(shell.theme?.mode === undefined ? {} : { initialPreference: shell.theme.mode }),
348
- ...(systemPreference === undefined ? {} : { systemPreference }),
349
- ...(persistence === undefined ? {} : { persistence }),
350
- };
351
- return createThemeController(options);
352
- }
353
- function syncAppShellThemeState(frame, themeButton, brandButton, shell, state, scope, environment) {
354
- frame.root.setAttribute('data-theme-mode', state.theme);
355
- frame.root.setAttribute('data-accent', state.brand.id);
356
- frame.root.setAttribute('data-brand', state.brand.id);
357
- frame.statusbar.setAttribute('data-theme-mode', state.theme);
358
- frame.statusbar.setAttribute('data-accent', state.brand.id);
359
- if (themeButton !== null) {
360
- themeButton.setAttribute('aria-pressed', state.theme === 'dark' ? 'true' : 'false');
361
- themeButton.setAttribute('data-state', state.theme);
362
- themeButton.setAttribute('data-theme-mode', state.theme);
363
- themeButton.setAttribute('title', `Theme: ${state.theme}`);
364
- replaceButtonIcon(themeButton, resolveThemeIcon(shell, state), scope, environment);
365
- }
366
- if (brandButton !== null) {
367
- brandButton.setAttribute('aria-pressed', 'false');
368
- brandButton.setAttribute('data-state', state.brand.id);
369
- brandButton.setAttribute('data-brand', state.brand.id);
370
- brandButton.setAttribute('data-color', state.brand.color);
371
- brandButton.setAttribute('title', `Brand: ${state.brand.label}`);
372
- brandButton.style.setProperty('--kui-app-shell-current-brand', state.brand.color);
373
- }
374
- }
375
- function resolveThemeIcon(shell, state) {
376
- return state.theme === 'dark' ? shell.theme?.icons?.dark : shell.theme?.icons?.light;
377
- }
378
- function replaceButtonIcon(button, iconName, scope, environment) {
379
- if (iconName === undefined)
380
- return;
381
- for (const icon of Array.from(button.querySelectorAll(':scope > .kui-app-shell-icon'))) {
382
- icon.remove();
383
- }
384
- appendIcon(button, iconName, 'kui-app-shell-icon', scope, environment);
385
- }
386
- function createBrandCycle(shell) {
387
- const inputs = shell.theme?.brands ?? defaultBrandCycle;
388
- return inputs.map((brand) => resolveBrandColor(brand));
389
- }
390
- function nextBrand(current, cycle) {
391
- if (cycle.length === 0)
392
- return current;
393
- const currentIndex = cycle.findIndex((brand) => brand.id === current.id || brand.color === current.color);
394
- return cycle[(currentIndex + 1) % cycle.length] ?? cycle[0] ?? current;
395
- }
396
- function readActionButton(root, actionId) {
397
- const button = root.querySelector(`[data-action-id="${actionId}"]`);
398
- return button instanceof HTMLButtonElement ? button : null;
399
- }
400
- function isThemePreference(value) {
401
- return value === 'dark' || value === 'light' || value === 'system';
402
- }
403
- function isBrandInput(value) {
404
- if (typeof value === 'string')
405
- return true;
406
- return (typeof value === 'object' &&
407
- value !== null &&
408
- 'color' in value &&
409
- typeof value.color === 'string');
410
- }
411
- function createBrowserThemePersistence(document) {
412
- try {
413
- const storage = document.defaultView?.localStorage;
414
- if (storage === undefined)
415
- return undefined;
416
- return createStorageThemePersistence(storage);
417
- }
418
- catch {
419
- return undefined;
420
- }
421
- }
422
- function createBrowserSystemThemePreference(document) {
423
- try {
424
- const query = document.defaultView?.matchMedia?.('(prefers-color-scheme: light)');
425
- return query === undefined ? undefined : createMediaThemePreference(query);
426
- }
427
- catch {
428
- return undefined;
429
- }
430
- }
431
- function createBrand(shell, scope, environment) {
432
- const label = resolveAppShellText(shell.brand.label, appShellTranslate(scope)) ?? '';
433
- const brand = document.createElement('a');
434
- brand.className = 'kui-app-shell-brand';
435
- setHref(brand, shell.brand.href ?? '#', environment);
436
- brand.setAttribute('aria-label', label);
437
- brand.setAttribute('data-label', label);
438
- const mark = document.createElement('span');
439
- mark.className = 'kui-app-shell-brand-mark';
440
- mark.setAttribute('aria-hidden', 'true');
441
- mark.textContent = shell.brand.mark ?? label.slice(0, 1);
442
- const name = document.createElement('span');
443
- name.className = 'kui-app-shell-brand-name';
444
- name.textContent = label;
445
- brand.append(mark, name);
446
- return brand;
447
- }
448
- function createTopbarActions(shell, scope, environment) {
449
- const actions = document.createElement('div');
450
- actions.className = 'kui-app-shell-topbar-actions';
451
- actions.setAttribute('role', 'toolbar');
452
- actions.setAttribute('aria-label', 'Global actions');
453
- for (const action of shell.topbar?.actions ?? []) {
454
- actions.append(createTopbarAction(action, scope, environment));
455
- }
456
- if (shell.topbar?.language !== undefined) {
457
- actions.append(createLanguageControl(shell, shell.topbar.language, scope, environment));
458
- }
459
- const account = shell.topbar?.account;
460
- if (account !== undefined)
461
- actions.append(createAccountControl(shell, account, scope, environment));
462
- return actions;
463
- }
464
- function createTopbarAction(action, scope, environment) {
465
- const label = resolveAppShellText(action.label, appShellTranslate(scope)) ?? '';
466
- const button = document.createElement('button');
467
- button.type = 'button';
468
- button.className = 'kui-app-shell-icon-button';
469
- button.setAttribute('aria-label', label);
470
- button.setAttribute('data-action-id', action.id);
471
- setOptionalAttribute(button, 'data-app-shell-intent', action.intent);
472
- button.setAttribute('data-state', action.disabled === true ? 'disabled' : action.pressed === true ? 'pressed' : 'idle');
473
- button.setAttribute('data-has-badge', action.badge === undefined ? 'false' : 'true');
474
- button.setAttribute('data-badge', action.badge === undefined ? '' : String(action.badge));
475
- if (action.pressed !== undefined) {
476
- button.setAttribute('aria-pressed', action.pressed ? 'true' : 'false');
477
- }
478
- if (action.disabled === true)
479
- button.disabled = true;
480
- appendIcon(button, action.icon, 'kui-app-shell-icon', scope, environment);
481
- if (action.badge !== undefined) {
482
- const badge = document.createElement('span');
483
- badge.className = 'kui-app-shell-icon-button-badge';
484
- badge.setAttribute('aria-hidden', 'true');
485
- badge.textContent = String(action.badge);
486
- button.append(badge);
487
- }
488
- return button;
489
- }
490
- function createLanguageControl(shell, config, scope, environment) {
491
- const label = resolveAppShellText(config.label, appShellTranslate(scope)) ?? 'Language';
492
- const menuId = `${shell.id}-language-menu`;
493
- const root = document.createElement('div');
494
- root.className = 'kui-app-shell-language';
495
- root.setAttribute('data-state', 'closed');
496
- const button = document.createElement('button');
497
- button.type = 'button';
498
- button.className = 'kui-app-shell-icon-button';
499
- button.setAttribute('aria-label', label);
500
- button.setAttribute('title', label);
501
- button.setAttribute('data-action-id', 'language');
502
- button.setAttribute('data-app-shell-intent', 'open-language-menu');
503
- button.setAttribute('data-state', 'idle');
504
- button.setAttribute('aria-haspopup', 'menu');
505
- button.setAttribute('aria-controls', menuId);
506
- button.setAttribute('aria-expanded', 'false');
507
- appendIcon(button, config.icon, 'kui-app-shell-icon', scope, environment);
508
- const menu = document.createElement('div');
509
- menu.id = menuId;
510
- menu.className = 'kui-app-shell-language-menu';
511
- menu.setAttribute('role', 'menu');
512
- menu.setAttribute('aria-label', label);
513
- menu.setAttribute('data-state', 'closed');
514
- menu.hidden = true;
515
- for (const option of config.options) {
516
- const optionLabel = resolveAppShellText(option.label, appShellTranslate(scope)) ?? option.locale;
517
- const item = document.createElement('button');
518
- item.type = 'button';
519
- item.className = 'kui-app-shell-language-option';
520
- item.setAttribute('role', 'menuitemradio');
521
- item.setAttribute('data-language-locale', option.locale);
522
- item.setAttribute('data-label', optionLabel);
523
- item.setAttribute('aria-checked', 'false');
524
- item.setAttribute('data-state', 'idle');
525
- item.textContent = optionLabel;
526
- menu.append(item);
527
- }
528
- root.append(button, menu);
529
- return root;
530
- }
531
- export function bindAppShellLanguageMenu(owner, frame, shell, scope, environment) {
532
- const config = shell.topbar?.language;
533
- if (config === undefined || config.options.length === 0)
534
- return;
535
- const languageRoot = frame.root.querySelector('.kui-app-shell-language');
536
- const button = frame.root.querySelector('[data-action-id="language"]');
537
- const menu = frame.root.querySelector('.kui-app-shell-language-menu');
538
- if (!(languageRoot instanceof HTMLElement) ||
539
- !(button instanceof HTMLButtonElement) ||
540
- !(menu instanceof HTMLElement)) {
541
- return;
542
- }
543
- const readLocale = () => scope.i18n?.locale.value ?? config.options[0]?.locale ?? '';
544
- const expanded = () => button.getAttribute('aria-expanded') === 'true';
545
- const sync = () => {
546
- const locale = readLocale();
547
- const activeOption = config.options.find((option) => option.locale === locale);
548
- const label = resolveAppShellText(config.label, appShellTranslate(scope)) ?? 'Language';
549
- const activeLabel = resolveAppShellText(activeOption?.label, appShellTranslate(scope)) ??
550
- activeOption?.locale ??
551
- locale;
552
- const state = expanded() ? 'open' : 'closed';
553
- languageRoot.setAttribute('data-state', state);
554
- button.setAttribute('aria-label', label);
555
- button.setAttribute('title', `${label}: ${activeLabel}`);
556
- button.setAttribute('data-locale', locale);
557
- button.setAttribute('aria-expanded', expanded() ? 'true' : 'false');
558
- menu.setAttribute('aria-label', label);
559
- menu.setAttribute('data-state', state);
560
- menu.hidden = !expanded();
561
- for (const option of config.options) {
562
- const item = findElementByDataAttribute(menu, 'data-language-locale', option.locale);
563
- if (!(item instanceof HTMLButtonElement))
564
- continue;
565
- const optionLabel = resolveAppShellText(option.label, appShellTranslate(scope)) ?? option.locale;
566
- const selected = option.locale === locale;
567
- item.textContent = optionLabel;
568
- item.setAttribute('data-label', optionLabel);
569
- item.setAttribute('aria-checked', selected ? 'true' : 'false');
570
- item.setAttribute('data-state', selected ? 'selected' : 'idle');
571
- }
572
- };
573
- const setExpanded = (nextExpanded) => {
574
- languageRoot.setAttribute('data-state', nextExpanded ? 'open' : 'closed');
575
- button.setAttribute('data-state', nextExpanded ? 'open' : 'idle');
576
- button.setAttribute('aria-expanded', nextExpanded ? 'true' : 'false');
577
- menu.setAttribute('data-state', nextExpanded ? 'open' : 'closed');
578
- menu.hidden = !nextExpanded;
579
- };
580
- const close = () => {
581
- if (!expanded())
582
- return;
583
- setExpanded(false);
584
- sync();
585
- };
586
- const focusOption = (placement) => {
587
- const items = Array.from(menu.querySelectorAll('[data-language-locale]'));
588
- if (items.length === 0)
589
- return;
590
- const currentIndex = items.findIndex((item) => item === menu.ownerDocument.activeElement);
591
- const selectedIndex = items.findIndex((item) => item.getAttribute('aria-checked') === 'true');
592
- const baseIndex = currentIndex < 0 ? selectedIndex : currentIndex;
593
- const index = placement === 'first'
594
- ? 0
595
- : placement === 'last'
596
- ? items.length - 1
597
- : placement === 'next'
598
- ? (Math.max(0, baseIndex) + 1) % items.length
599
- : baseIndex <= 0
600
- ? items.length - 1
601
- : baseIndex - 1;
602
- items[index]?.focus();
603
- };
604
- const onButtonClick = () => {
605
- const nextExpanded = !expanded();
606
- setExpanded(nextExpanded);
607
- sync();
608
- if (nextExpanded)
609
- focusOption('first');
610
- };
611
- const onButtonKeyDown = (event) => {
612
- if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
613
- return;
614
- event.preventDefault();
615
- if (!expanded()) {
616
- setExpanded(true);
617
- sync();
618
- }
619
- focusOption(event.key === 'ArrowDown' ? 'first' : 'last');
620
- };
621
- const onMenuKeyDown = (event) => {
622
- if (event.key === 'Escape') {
623
- event.preventDefault();
624
- event.stopPropagation();
625
- close();
626
- button.focus();
627
- return;
628
- }
629
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
630
- event.preventDefault();
631
- focusOption(event.key === 'ArrowDown' ? 'next' : 'previous');
632
- return;
633
- }
634
- if (event.key === 'Home' || event.key === 'End') {
635
- event.preventDefault();
636
- focusOption(event.key === 'Home' ? 'first' : 'last');
637
- }
638
- };
639
- const onMenuClick = (event) => {
640
- const target = event.target instanceof Element ? event.target : undefined;
641
- const item = target?.closest('[data-language-locale]');
642
- if (!(item instanceof HTMLButtonElement))
643
- return;
644
- const locale = item.getAttribute('data-language-locale');
645
- if (locale === null || !config.options.some((option) => option.locale === locale))
646
- return;
647
- scope.i18n?.setLocale(locale);
648
- item.dispatchEvent(new CustomEvent('vobs:app-shell-language-select', {
649
- bubbles: true,
650
- detail: { shellId: shell.id, locale },
651
- }));
652
- close();
653
- button.focus();
654
- };
655
- const onDocumentPointerDown = (event) => {
656
- if (!expanded())
657
- return;
658
- if (event.target instanceof Node && languageRoot.contains(event.target))
659
- return;
660
- close();
661
- };
662
- const onDocumentKeyDown = (event) => {
663
- if (event.key !== 'Escape')
664
- return;
665
- close();
666
- button.focus();
667
- };
668
- owner.run(() => {
669
- const stop = effect(() => {
670
- void scope.i18n?.locale.value;
671
- void scope.translation?.locale?.value;
672
- sync();
673
- }, { scheduler: environment.scheduler });
674
- owner.own(stop);
675
- });
676
- button.addEventListener('click', onButtonClick);
677
- button.addEventListener('keydown', onButtonKeyDown);
678
- menu.addEventListener('click', onMenuClick);
679
- menu.addEventListener('keydown', onMenuKeyDown);
680
- frame.root.ownerDocument.addEventListener('pointerdown', onDocumentPointerDown);
681
- frame.root.ownerDocument.addEventListener('keydown', onDocumentKeyDown);
682
- owner.own(() => {
683
- button.removeEventListener('click', onButtonClick);
684
- button.removeEventListener('keydown', onButtonKeyDown);
685
- menu.removeEventListener('click', onMenuClick);
686
- menu.removeEventListener('keydown', onMenuKeyDown);
687
- frame.root.ownerDocument.removeEventListener('pointerdown', onDocumentPointerDown);
688
- frame.root.ownerDocument.removeEventListener('keydown', onDocumentKeyDown);
689
- });
690
- }
691
- function syncTopbarText(root, shell, scope) {
692
- const translate = appShellTranslate(scope);
693
- for (const action of shell.topbar?.actions ?? []) {
694
- const button = findElementByDataAttribute(root, 'data-action-id', action.id);
695
- if (!(button instanceof HTMLButtonElement))
696
- continue;
697
- const label = resolveAppShellText(action.label, translate) ?? '';
698
- button.setAttribute('aria-label', label);
699
- button.setAttribute('title', label);
700
- }
701
- const account = shell.topbar?.account;
702
- if (account !== undefined) {
703
- const label = resolveAppShellText(account.label, translate) ?? '';
704
- const button = root.querySelector('.kui-app-shell-account-button');
705
- if (button instanceof HTMLButtonElement) {
706
- button.setAttribute('aria-label', label);
707
- const userName = button.querySelector('.kui-app-shell-user-name');
708
- if (userName !== null)
709
- userName.textContent = label;
710
- }
711
- const menu = root.querySelector('.kui-app-shell-account-menu');
712
- if (menu instanceof HTMLElement) {
713
- menu.setAttribute('aria-label', `${label} account menu`);
714
- const menuLabel = menu.querySelector('.kui-app-shell-account-menu-label');
715
- if (menuLabel !== null)
716
- menuLabel.textContent = label;
717
- }
718
- for (const item of account.menu ?? []) {
719
- if (item.kind === 'separator')
720
- continue;
721
- const element = findElementByDataAttribute(root, 'data-account-menu-id', item.id);
722
- if (!(element instanceof HTMLElement))
723
- continue;
724
- const itemLabel = resolveAppShellText(item.label, translate) ?? item.id;
725
- element.setAttribute('data-label', itemLabel);
726
- const itemLabelElement = element.querySelector('.kui-app-shell-account-menu-item-label');
727
- if (itemLabelElement !== null)
728
- itemLabelElement.textContent = itemLabel;
729
- }
730
- }
731
- }
732
- function findElementByDataAttribute(root, attribute, value) {
733
- return Array.from(root.querySelectorAll(`[${attribute}]`)).find((element) => element.getAttribute(attribute) === value);
734
- }
735
- function createAccountControl(shell, account, scope, environment) {
736
- const root = document.createElement('div');
737
- root.className = 'kui-app-shell-account';
738
- root.setAttribute('data-state', account.menuExpanded === true ? 'open' : 'closed');
739
- root.append(createAccountButton(shell, account, scope, environment));
740
- if ((account.menu?.length ?? 0) > 0) {
741
- root.append(createAccountMenu(shell, account, scope, environment));
742
- }
743
- return root;
744
- }
745
- function createAccountButton(shell, account, scope, environment) {
746
- const label = resolveAppShellText(account.label, appShellTranslate(scope)) ?? '';
747
- const button = document.createElement('button');
748
- const menuId = `${shell.id}-account-menu`;
749
- button.type = 'button';
750
- button.className = 'kui-app-shell-account-button';
751
- button.setAttribute('aria-label', label);
752
- button.setAttribute('data-action-id', 'account');
753
- button.setAttribute('data-app-shell-intent', 'open-account-menu');
754
- button.setAttribute('data-state', account.menuExpanded === true ? 'open' : 'closed');
755
- if ((account.menu?.length ?? 0) > 0) {
756
- button.setAttribute('aria-haspopup', 'menu');
757
- button.setAttribute('aria-controls', menuId);
758
- button.setAttribute('aria-expanded', account.menuExpanded === true ? 'true' : 'false');
759
- }
760
- const avatar = document.createElement('span');
761
- avatar.className = 'kui-app-shell-avatar';
762
- avatar.setAttribute('aria-hidden', 'true');
763
- appendIcon(avatar, account.icon, 'kui-app-shell-icon', scope, environment);
764
- if (avatar.childNodes.length === 0)
765
- avatar.textContent = account.initials ?? label.slice(0, 2);
766
- const name = document.createElement('span');
767
- name.className = 'kui-app-shell-user-name';
768
- name.textContent = label;
769
- button.append(avatar, name);
770
- return button;
771
- }
772
- function createAccountMenu(shell, account, scope, environment) {
773
- const accountLabel = resolveAppShellText(account.label, appShellTranslate(scope)) ?? '';
774
- const menu = document.createElement('div');
775
- menu.id = `${shell.id}-account-menu`;
776
- menu.className = 'kui-app-shell-account-menu';
777
- menu.setAttribute('role', 'menu');
778
- menu.setAttribute('aria-label', `${accountLabel} account menu`);
779
- menu.setAttribute('data-state', account.menuExpanded === true ? 'open' : 'closed');
780
- menu.hidden = account.menuExpanded !== true;
781
- const header = document.createElement('div');
782
- header.className = 'kui-app-shell-account-menu-header';
783
- header.setAttribute('aria-hidden', 'true');
784
- const avatar = document.createElement('span');
785
- avatar.className = 'kui-app-shell-avatar';
786
- appendIcon(avatar, account.icon, 'kui-app-shell-icon', scope, environment);
787
- if (avatar.childNodes.length === 0)
788
- avatar.textContent = account.initials ?? accountLabel.slice(0, 2);
789
- const label = document.createElement('span');
790
- label.className = 'kui-app-shell-account-menu-label';
791
- label.textContent = accountLabel;
792
- header.append(avatar, label);
793
- menu.append(header);
794
- for (const item of account.menu ?? []) {
795
- menu.append(createAccountMenuItem(item, scope, environment));
796
- }
797
- return menu;
798
- }
799
- function createAccountMenuItem(item, scope, environment) {
800
- if (item.kind === 'separator') {
801
- const separator = document.createElement('div');
802
- separator.className = 'kui-app-shell-account-menu-separator';
803
- separator.setAttribute('role', 'separator');
804
- separator.setAttribute('data-account-menu-id', item.id);
805
- return separator;
806
- }
807
- const itemLabel = resolveAppShellText(item.label, appShellTranslate(scope)) ?? item.id;
808
- const href = item.disabled === true ? undefined : (item.href ?? item.route);
809
- const element = href === undefined ? document.createElement('button') : document.createElement('a');
810
- element.className = joinClasses('kui-app-shell-account-menu-item', item.tone === 'danger' ? 'kui-app-shell-account-menu-item--danger' : undefined);
811
- if (href === undefined && element instanceof HTMLButtonElement) {
812
- element.type = 'button';
813
- }
814
- else if (href !== undefined && element instanceof HTMLAnchorElement) {
815
- setHref(element, href, environment);
816
- }
817
- element.setAttribute('role', 'menuitem');
818
- element.setAttribute('data-account-menu-id', item.id);
819
- element.setAttribute('data-label', itemLabel);
820
- element.setAttribute('data-tone', item.tone ?? 'default');
821
- if (item.disabled === true) {
822
- element.setAttribute('aria-disabled', 'true');
823
- element.setAttribute('tabindex', '-1');
824
- }
825
- appendIcon(element, item.icon, 'kui-app-shell-icon', scope, environment);
826
- const label = document.createElement('span');
827
- label.className = 'kui-app-shell-account-menu-item-label';
828
- label.textContent = itemLabel;
829
- element.append(label);
830
- return element;
831
- }
832
- function renderNavItems(nav, items, activeId, scope, environment) {
833
- nav.replaceChildren(...items.map((item) => createNavItemElement(item, activeId, scope, environment)));
834
- }
835
- function createNavItemElement(item, activeId, scope, environment) {
836
- const active = item.id === activeId || item.active === true;
837
- const disabled = item.disabled === true;
838
- const label = resolveAppShellText(item.label, appShellTranslate(scope)) ?? '';
839
- const anchor = document.createElement('a');
840
- anchor.className = 'kui-app-shell-nav-item';
841
- if (!disabled)
842
- setHref(anchor, routeItemPath(item) ?? '#', environment);
843
- anchor.setAttribute('aria-label', label);
844
- anchor.setAttribute('data-nav-id', item.id);
845
- anchor.setAttribute('data-label', label);
846
- anchor.setAttribute('data-tooltip', label);
847
- anchor.setAttribute('data-state', disabled ? 'disabled' : active ? 'active' : (item.state ?? 'ready'));
848
- anchor.setAttribute('data-tone', item.tone ?? 'default');
849
- setOptionalAttribute(anchor, 'data-route-target', routeItemPath(item));
850
- if (active)
851
- anchor.setAttribute('aria-current', 'page');
852
- if (disabled) {
853
- anchor.setAttribute('aria-disabled', 'true');
854
- anchor.setAttribute('tabindex', '-1');
855
- }
856
- const icon = document.createElement('span');
857
- icon.className = 'kui-app-shell-nav-item-icon';
858
- appendIcon(icon, item.icon, 'kui-app-shell-icon', scope, environment);
859
- const labelElement = document.createElement('span');
860
- labelElement.className = 'kui-app-shell-nav-item-label';
861
- labelElement.textContent = label;
862
- anchor.append(icon, labelElement);
863
- if (item.badge !== undefined) {
864
- const badge = document.createElement('span');
865
- badge.className = 'kui-app-shell-nav-badge';
866
- badge.setAttribute('aria-hidden', 'true');
867
- badge.textContent = String(item.badge);
868
- anchor.append(badge);
869
- }
870
- return anchor;
871
- }
872
- function appendIcon(parent, iconName, className, scope, _environment) {
873
- if (iconName === undefined)
874
- return;
875
- const icon = resolveUiIcon(scope.icons, iconName);
876
- if (icon === undefined)
877
- return;
878
- const svg = createIconElement(document, icon, { decorative: true, size: 20 });
879
- svg.classList.add(className);
880
- svg.setAttribute('data-icon-name', icon.name);
881
- parent.append(svg);
882
- }
883
- function routeItemPath(item) {
884
- const path = item.route ?? item.href;
885
- return path === undefined ? undefined : normalizeRoutePath(path);
886
- }
887
- function normalizeRoutePath(path) {
888
- const [withoutHash] = path.split('#', 1);
889
- const [withoutQuery] = (withoutHash ?? path).split('?', 1);
890
- const normalized = withoutQuery === '' ? '/' : (withoutQuery ?? '/');
891
- return normalized.length > 1 ? normalized.replace(/\/+$/u, '') : normalized;
892
- }
893
- function setHref(anchor, href, environment) {
894
- const resolved = environment.security.urlPolicy.resolve(href, {
895
- element: 'a',
896
- attribute: 'href',
897
- });
898
- if (resolved !== null)
899
- anchor.setAttribute('href', resolved);
900
- }
901
- function setOptionalAttribute(element, name, value) {
902
- if (typeof value === 'string' || typeof value === 'number') {
903
- element.setAttribute(name, String(value));
904
- }
905
- else {
906
- element.removeAttribute(name);
907
- }
908
- }
909
- function joinClasses(...classes) {
910
- return classes
911
- .filter((className) => className !== undefined && className.length > 0)
912
- .join(' ');
913
- }