@vobs/ui 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +60 -0
  3. package/package.json +52 -102
  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 -24
  165. package/dist/app-shell-dom.js +0 -573
  166. package/dist/app-shell.d.ts +0 -199
  167. package/dist/app-shell.js +0 -370
  168. package/dist/base.css +0 -52
  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 -862
  175. package/dist/components.css +0 -4568
  176. package/dist/data-display.d.ts +0 -1063
  177. package/dist/data-display.js +0 -2298
  178. package/dist/feedback.d.ts +0 -88
  179. package/dist/feedback.js +0 -281
  180. package/dist/forms.d.ts +0 -378
  181. package/dist/forms.js +0 -1015
  182. package/dist/icons-config.d.ts +0 -25
  183. package/dist/icons-config.js +0 -14
  184. package/dist/icons.d.ts +0 -41
  185. package/dist/icons.js +0 -185
  186. package/dist/index.d.ts +0 -27
  187. package/dist/index.js +0 -34
  188. package/dist/locale/en-US.d.ts +0 -31
  189. package/dist/locale/en-US.js +0 -20
  190. package/dist/locale/zh-CN.d.ts +0 -31
  191. package/dist/locale/zh-CN.js +0 -20
  192. package/dist/navigation.d.ts +0 -609
  193. package/dist/navigation.js +0 -1707
  194. package/dist/overlay.d.ts +0 -304
  195. package/dist/overlay.js +0 -969
  196. package/dist/primitives.d.ts +0 -268
  197. package/dist/primitives.js +0 -764
  198. package/dist/theme-data.d.ts +0 -363
  199. package/dist/theme-data.js +0 -374
  200. package/dist/theme.d.ts +0 -79
  201. package/dist/theme.js +0 -262
  202. package/dist/tokens.css +0 -449
  203. package/dist/tree.d.ts +0 -144
  204. package/dist/tree.js +0 -469
  205. package/dist/virtual-list.d.ts +0 -190
  206. package/dist/virtual-list.js +0 -521
  207. package/dist/workbench.d.ts +0 -136
  208. package/dist/workbench.js +0 -156
@@ -1,573 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/ui
4
- */
5
- import { createIconElement } from '@vobs/icons';
6
- import {} from '@vobs/runtime-dom';
7
- import { resolveUiIcon } from './icons.js';
8
- import { brandPresets, createStorageThemePersistence, createThemeController, resolveBrandColor, } from './theme.js';
9
- const defaultBrandCycle = brandPresets.map((brand) => brand.id);
10
- export function createAppShellFrame(shell, scope, environment) {
11
- const root = document.createElement('div');
12
- root.className = joinClasses('kui-app-shell', shell.className);
13
- root.setAttribute('data-vobs-template', 'k-app-shell');
14
- root.setAttribute('data-vobs-protocol', '1');
15
- root.setAttribute('data-app-shell', shell.id);
16
- root.setAttribute('data-sidebar-mode', shell.sidebarMode ?? 'expanded');
17
- root.setAttribute('data-content-mode', shell.contentMode ?? 'contained');
18
- setOptionalAttribute(root, 'data-theme-mode', shell.theme?.mode);
19
- setOptionalAttribute(root, 'data-accent', shell.theme?.accent);
20
- const topbar = document.createElement('header');
21
- topbar.className = 'kui-app-shell-topbar';
22
- topbar.setAttribute('data-app-shell-region', 'topbar');
23
- topbar.append(createBrand(shell, environment), createTopbarActions(shell, scope, environment));
24
- const sidebar = document.createElement('aside');
25
- sidebar.className = 'kui-app-shell-sidebar';
26
- sidebar.setAttribute('data-app-shell-region', 'sidebar');
27
- sidebar.setAttribute('aria-label', `${shell.brand.label} navigation`);
28
- sidebar.setAttribute('data-sidebar-mode', shell.sidebarMode ?? 'expanded');
29
- const primaryNav = document.createElement('nav');
30
- primaryNav.className = 'kui-app-shell-nav';
31
- primaryNav.setAttribute('aria-label', 'Primary');
32
- const utilityNav = document.createElement('nav');
33
- utilityNav.className = 'kui-app-shell-utility-nav';
34
- utilityNav.setAttribute('aria-label', 'Utility');
35
- sidebar.append(primaryNav, utilityNav);
36
- const main = document.createElement('main');
37
- main.className = 'kui-app-shell-main';
38
- main.setAttribute('data-app-shell-region', 'content');
39
- main.setAttribute('aria-label', `${shell.brand.label} content`);
40
- const pageHeader = document.createElement('header');
41
- pageHeader.className = 'kui-app-shell-page-header';
42
- pageHeader.setAttribute('data-app-shell-region', 'page-header');
43
- const pageHeading = document.createElement('div');
44
- pageHeading.className = 'kui-app-shell-page-heading';
45
- const pageKicker = document.createElement('p');
46
- pageKicker.className = 'kui-app-shell-page-kicker';
47
- const pageTitle = document.createElement('h1');
48
- pageTitle.className = 'kui-app-shell-page-title';
49
- pageHeading.append(pageKicker, pageTitle);
50
- const pageDescription = document.createElement('p');
51
- pageDescription.className = 'kui-app-shell-page-subtitle';
52
- pageHeader.append(pageHeading, pageDescription);
53
- const pageBody = document.createElement('section');
54
- pageBody.className = 'kui-app-shell-page-body';
55
- pageBody.setAttribute('data-mode', shell.contentMode ?? 'contained');
56
- const pageToolbar = document.createElement('div');
57
- pageToolbar.className = 'kui-app-shell-page-toolbar';
58
- pageToolbar.setAttribute('role', 'toolbar');
59
- const pageFrame = document.createElement('section');
60
- pageFrame.className = 'kui-app-shell-page-frame';
61
- pageFrame.append(pageHeader, pageToolbar, pageBody);
62
- main.append(pageFrame);
63
- const footer = document.createElement('footer');
64
- footer.className = 'kui-app-shell-statusbar';
65
- footer.setAttribute('data-app-shell-region', 'statusbar');
66
- footer.setAttribute('aria-live', 'polite');
67
- const status = document.createElement('span');
68
- status.className = 'kui-app-shell-status';
69
- status.setAttribute('data-app-shell-status', 'runtime');
70
- const statusIcon = document.createElement('span');
71
- statusIcon.className = 'kui-app-shell-status-icon';
72
- const statusText = document.createElement('span');
73
- statusText.className = 'kui-app-shell-status-text';
74
- status.append(statusIcon, statusText);
75
- const version = document.createElement('span');
76
- version.className = 'kui-app-shell-status-version';
77
- version.setAttribute('data-app-shell-version', 'true');
78
- version.textContent = shell.statusbar?.version ?? '';
79
- footer.append(status, version);
80
- root.append(topbar, sidebar, main, footer);
81
- return {
82
- root,
83
- primaryNav,
84
- utilityNav,
85
- pageFrame,
86
- pageToolbar,
87
- statusbar: footer,
88
- statusIcon,
89
- statusText,
90
- pageKicker,
91
- pageTitle,
92
- pageDescription,
93
- version,
94
- };
95
- }
96
- export function bindAppShellThemeControls(owner, frame, shell, scope, environment) {
97
- if (shell.theme === undefined)
98
- return;
99
- const controller = createAppShellThemeController(frame, shell);
100
- const themeButton = readActionButton(frame.root, 'theme');
101
- const brandButton = readActionButton(frame.root, 'accent');
102
- const brandCycle = createBrandCycle(shell);
103
- const sync = (state) => {
104
- syncAppShellThemeState(frame, themeButton, brandButton, shell, state, scope, environment);
105
- };
106
- sync(controller.state);
107
- owner.own(controller.subscribe(sync));
108
- owner.own(() => controller.destroy());
109
- if (themeButton !== null) {
110
- const onClick = () => {
111
- controller.toggleTheme();
112
- };
113
- themeButton.addEventListener('click', onClick);
114
- owner.own(() => themeButton.removeEventListener('click', onClick));
115
- }
116
- if (brandButton !== null) {
117
- const onClick = () => {
118
- controller.setBrand(nextBrand(controller.brand, brandCycle));
119
- };
120
- brandButton.addEventListener('click', onClick);
121
- owner.own(() => brandButton.removeEventListener('click', onClick));
122
- }
123
- }
124
- export function bindAppShellAccountMenu(owner, frame, shell) {
125
- const root = frame.root;
126
- const account = shell.topbar?.account;
127
- if (account === undefined || (account.menu?.length ?? 0) === 0)
128
- return;
129
- const accountRoot = root.querySelector('.kui-app-shell-account');
130
- const button = root.querySelector('.kui-app-shell-account-button');
131
- const menu = root.querySelector('.kui-app-shell-account-menu');
132
- if (!(accountRoot instanceof HTMLElement) ||
133
- !(button instanceof HTMLButtonElement) ||
134
- !(menu instanceof HTMLElement)) {
135
- return;
136
- }
137
- const sync = (expanded) => {
138
- const state = expanded ? 'open' : 'closed';
139
- accountRoot.setAttribute('data-state', state);
140
- button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
141
- button.setAttribute('data-state', state);
142
- menu.setAttribute('data-state', state);
143
- menu.hidden = !expanded;
144
- };
145
- const expanded = () => button.getAttribute('aria-expanded') === 'true';
146
- const dispatchToggle = () => {
147
- button.dispatchEvent(new CustomEvent('vobs:app-shell-account-toggle', {
148
- bubbles: true,
149
- detail: { shellId: shell.id, expanded: expanded() },
150
- }));
151
- };
152
- const close = () => {
153
- if (!expanded())
154
- return;
155
- sync(false);
156
- dispatchToggle();
157
- };
158
- const toggle = () => {
159
- sync(!expanded());
160
- dispatchToggle();
161
- };
162
- const onButtonClick = () => {
163
- toggle();
164
- if (expanded())
165
- focusAccountMenuItem(menu, 'first');
166
- };
167
- const onButtonKeyDown = (event) => {
168
- if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
169
- return;
170
- event.preventDefault();
171
- if (!expanded()) {
172
- sync(true);
173
- dispatchToggle();
174
- }
175
- focusAccountMenuItem(menu, event.key === 'ArrowDown' ? 'first' : 'last');
176
- };
177
- const onDocumentPointerDown = (event) => {
178
- if (!expanded())
179
- return;
180
- if (event.target instanceof Node && root.contains(event.target)) {
181
- if (accountRoot?.contains(event.target))
182
- return;
183
- }
184
- close();
185
- };
186
- const onDocumentKeyDown = (event) => {
187
- if (event.key === 'Escape') {
188
- close();
189
- button.focus();
190
- }
191
- };
192
- const onMenuKeyDown = (event) => {
193
- if (event.key === 'Escape') {
194
- event.preventDefault();
195
- event.stopPropagation();
196
- close();
197
- button.focus();
198
- return;
199
- }
200
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
201
- event.preventDefault();
202
- focusAccountMenuItem(menu, event.key === 'ArrowDown' ? 'next' : 'previous');
203
- return;
204
- }
205
- if (event.key === 'Home' || event.key === 'End') {
206
- event.preventDefault();
207
- focusAccountMenuItem(menu, event.key === 'Home' ? 'first' : 'last');
208
- }
209
- };
210
- const onMenuClick = (event) => {
211
- const target = event.target instanceof Element ? event.target : undefined;
212
- const item = target?.closest('[data-account-menu-id]');
213
- if (!(item instanceof HTMLElement))
214
- return;
215
- const itemId = item.getAttribute('data-account-menu-id');
216
- if (itemId === null || item.getAttribute('aria-disabled') === 'true') {
217
- event.preventDefault();
218
- return;
219
- }
220
- item.dispatchEvent(new CustomEvent('vobs:app-shell-account-select', {
221
- bubbles: true,
222
- detail: { shellId: shell.id, itemId },
223
- }));
224
- close();
225
- };
226
- sync(account.menuExpanded === true);
227
- button.addEventListener('click', onButtonClick);
228
- button.addEventListener('keydown', onButtonKeyDown);
229
- menu.addEventListener('click', onMenuClick);
230
- menu.addEventListener('keydown', onMenuKeyDown);
231
- root.ownerDocument.addEventListener('pointerdown', onDocumentPointerDown);
232
- root.ownerDocument.addEventListener('keydown', onDocumentKeyDown);
233
- owner.own(() => {
234
- button.removeEventListener('click', onButtonClick);
235
- button.removeEventListener('keydown', onButtonKeyDown);
236
- menu.removeEventListener('click', onMenuClick);
237
- menu.removeEventListener('keydown', onMenuKeyDown);
238
- root.ownerDocument.removeEventListener('pointerdown', onDocumentPointerDown);
239
- root.ownerDocument.removeEventListener('keydown', onDocumentKeyDown);
240
- });
241
- }
242
- function focusAccountMenuItem(menu, placement) {
243
- const items = Array.from(menu.querySelectorAll('.kui-app-shell-account-menu-item:not([aria-disabled="true"])'));
244
- if (items.length === 0)
245
- return;
246
- const currentIndex = items.findIndex((item) => item === menu.ownerDocument.activeElement);
247
- const nextIndex = placement === 'first'
248
- ? 0
249
- : placement === 'last'
250
- ? items.length - 1
251
- : placement === 'next'
252
- ? (Math.max(0, currentIndex) + 1) % items.length
253
- : currentIndex <= 0
254
- ? items.length - 1
255
- : currentIndex - 1;
256
- items[nextIndex]?.focus();
257
- }
258
- export function syncAppShellFrame(frame, shell, state, status, scope, environment) {
259
- setOptionalAttribute(frame.root, 'data-active-nav', state.activeId);
260
- setOptionalAttribute(frame.root, 'data-route-pending', status.state === 'pending' ? 'true' : undefined);
261
- setOptionalAttribute(frame.statusbar, 'data-state', status.state);
262
- frame.pageKicker.textContent = shell.pageHeader?.kicker ?? shell.title ?? shell.brand.label;
263
- frame.pageTitle.textContent = state.title;
264
- frame.pageTitle.setAttribute('data-title', state.title);
265
- frame.pageDescription.textContent = state.description;
266
- frame.pageDescription.setAttribute('data-subtitle', state.description);
267
- frame.statusText.textContent = status.text;
268
- frame.statusbar.setAttribute('data-status', status.state);
269
- frame.statusIcon.replaceChildren();
270
- appendIcon(frame.statusIcon, status.icon, 'kui-app-shell-status-icon', scope, environment);
271
- frame.version.textContent = shell.statusbar?.version ?? '';
272
- renderNavItems(frame.primaryNav, shell.navigation?.primary ?? [], state.activeId, scope, environment);
273
- renderNavItems(frame.utilityNav, shell.navigation?.utility ?? [], state.activeId, scope, environment);
274
- }
275
- function createAppShellThemeController(frame, shell) {
276
- const persistence = shell.theme?.persist === false
277
- ? undefined
278
- : createBrowserThemePersistence(frame.root.ownerDocument);
279
- const options = {
280
- target: frame.root.ownerDocument.documentElement,
281
- initialBrand: shell.theme?.brand ?? shell.theme?.accent ?? 'green',
282
- ...(shell.theme?.mode === undefined ? {} : { initialTheme: shell.theme.mode }),
283
- ...(persistence === undefined ? {} : { persistence }),
284
- };
285
- return createThemeController(options);
286
- }
287
- function syncAppShellThemeState(frame, themeButton, brandButton, shell, state, scope, environment) {
288
- frame.root.setAttribute('data-theme-mode', state.theme);
289
- frame.root.setAttribute('data-accent', state.brand.id);
290
- frame.root.setAttribute('data-brand', state.brand.id);
291
- frame.statusbar.setAttribute('data-theme-mode', state.theme);
292
- frame.statusbar.setAttribute('data-accent', state.brand.id);
293
- if (themeButton !== null) {
294
- themeButton.setAttribute('aria-pressed', state.theme === 'dark' ? 'true' : 'false');
295
- themeButton.setAttribute('data-state', state.theme);
296
- themeButton.setAttribute('data-theme-mode', state.theme);
297
- themeButton.setAttribute('title', `Theme: ${state.theme}`);
298
- replaceButtonIcon(themeButton, resolveThemeIcon(shell, state), scope, environment);
299
- }
300
- if (brandButton !== null) {
301
- brandButton.setAttribute('aria-pressed', 'false');
302
- brandButton.setAttribute('data-state', state.brand.id);
303
- brandButton.setAttribute('data-brand', state.brand.id);
304
- brandButton.setAttribute('data-color', state.brand.color);
305
- brandButton.setAttribute('title', `Brand: ${state.brand.label}`);
306
- brandButton.style.setProperty('--kui-app-shell-current-brand', state.brand.color);
307
- }
308
- }
309
- function resolveThemeIcon(shell, state) {
310
- return state.theme === 'dark' ? shell.theme?.icons?.dark : shell.theme?.icons?.light;
311
- }
312
- function replaceButtonIcon(button, iconName, scope, environment) {
313
- if (iconName === undefined)
314
- return;
315
- for (const icon of Array.from(button.querySelectorAll(':scope > .kui-app-shell-icon'))) {
316
- icon.remove();
317
- }
318
- appendIcon(button, iconName, 'kui-app-shell-icon', scope, environment);
319
- }
320
- function createBrandCycle(shell) {
321
- const inputs = shell.theme?.brands ?? defaultBrandCycle;
322
- return inputs.map((brand) => resolveBrandColor(brand));
323
- }
324
- function nextBrand(current, cycle) {
325
- if (cycle.length === 0)
326
- return current;
327
- const currentIndex = cycle.findIndex((brand) => brand.id === current.id || brand.color === current.color);
328
- return cycle[(currentIndex + 1) % cycle.length] ?? cycle[0] ?? current;
329
- }
330
- function readActionButton(root, actionId) {
331
- const button = root.querySelector(`[data-action-id="${actionId}"]`);
332
- return button instanceof HTMLButtonElement ? button : null;
333
- }
334
- function createBrowserThemePersistence(document) {
335
- try {
336
- const storage = document.defaultView?.localStorage;
337
- if (storage === undefined)
338
- return undefined;
339
- return createStorageThemePersistence(storage);
340
- }
341
- catch {
342
- return undefined;
343
- }
344
- }
345
- function createBrand(shell, environment) {
346
- const brand = document.createElement('a');
347
- brand.className = 'kui-app-shell-brand';
348
- setHref(brand, shell.brand.href ?? '#', environment);
349
- brand.setAttribute('aria-label', shell.brand.label);
350
- brand.setAttribute('data-label', shell.brand.label);
351
- const mark = document.createElement('span');
352
- mark.className = 'kui-app-shell-brand-mark';
353
- mark.setAttribute('aria-hidden', 'true');
354
- mark.textContent = shell.brand.mark ?? shell.brand.label.slice(0, 1);
355
- const name = document.createElement('span');
356
- name.className = 'kui-app-shell-brand-name';
357
- name.textContent = shell.brand.label;
358
- brand.append(mark, name);
359
- return brand;
360
- }
361
- function createTopbarActions(shell, scope, environment) {
362
- const actions = document.createElement('div');
363
- actions.className = 'kui-app-shell-topbar-actions';
364
- actions.setAttribute('role', 'toolbar');
365
- actions.setAttribute('aria-label', 'Global actions');
366
- for (const action of shell.topbar?.actions ?? []) {
367
- actions.append(createTopbarAction(action, scope, environment));
368
- }
369
- const account = shell.topbar?.account;
370
- if (account !== undefined)
371
- actions.append(createAccountControl(shell, account, scope, environment));
372
- return actions;
373
- }
374
- function createTopbarAction(action, scope, environment) {
375
- const button = document.createElement('button');
376
- button.type = 'button';
377
- button.className = 'kui-app-shell-icon-button';
378
- button.setAttribute('aria-label', action.label);
379
- button.setAttribute('data-action-id', action.id);
380
- setOptionalAttribute(button, 'data-app-shell-intent', action.intent);
381
- button.setAttribute('data-state', action.disabled === true ? 'disabled' : action.pressed === true ? 'pressed' : 'idle');
382
- button.setAttribute('data-has-badge', action.badge === undefined ? 'false' : 'true');
383
- button.setAttribute('data-badge', action.badge === undefined ? '' : String(action.badge));
384
- if (action.pressed !== undefined) {
385
- button.setAttribute('aria-pressed', action.pressed ? 'true' : 'false');
386
- }
387
- if (action.disabled === true)
388
- button.disabled = true;
389
- appendIcon(button, action.icon, 'kui-app-shell-icon', scope, environment);
390
- if (action.badge !== undefined) {
391
- const badge = document.createElement('span');
392
- badge.className = 'kui-app-shell-icon-button-badge';
393
- badge.setAttribute('aria-hidden', 'true');
394
- badge.textContent = String(action.badge);
395
- button.append(badge);
396
- }
397
- return button;
398
- }
399
- function createAccountControl(shell, account, scope, environment) {
400
- const root = document.createElement('div');
401
- root.className = 'kui-app-shell-account';
402
- root.setAttribute('data-state', account.menuExpanded === true ? 'open' : 'closed');
403
- root.append(createAccountButton(shell, account, scope, environment));
404
- if ((account.menu?.length ?? 0) > 0) {
405
- root.append(createAccountMenu(shell, account, scope, environment));
406
- }
407
- return root;
408
- }
409
- function createAccountButton(shell, account, scope, environment) {
410
- const button = document.createElement('button');
411
- const menuId = `${shell.id}-account-menu`;
412
- button.type = 'button';
413
- button.className = 'kui-app-shell-account-button';
414
- button.setAttribute('aria-label', account.label);
415
- button.setAttribute('data-action-id', 'account');
416
- button.setAttribute('data-app-shell-intent', 'open-account-menu');
417
- button.setAttribute('data-state', account.menuExpanded === true ? 'open' : 'closed');
418
- if ((account.menu?.length ?? 0) > 0) {
419
- button.setAttribute('aria-haspopup', 'menu');
420
- button.setAttribute('aria-controls', menuId);
421
- button.setAttribute('aria-expanded', account.menuExpanded === true ? 'true' : 'false');
422
- }
423
- const avatar = document.createElement('span');
424
- avatar.className = 'kui-app-shell-avatar';
425
- avatar.setAttribute('aria-hidden', 'true');
426
- appendIcon(avatar, account.icon, 'kui-app-shell-icon', scope, environment);
427
- if (avatar.childNodes.length === 0)
428
- avatar.textContent = account.initials ?? account.label.slice(0, 2);
429
- const name = document.createElement('span');
430
- name.className = 'kui-app-shell-user-name';
431
- name.textContent = account.label;
432
- button.append(avatar, name);
433
- return button;
434
- }
435
- function createAccountMenu(shell, account, scope, environment) {
436
- const menu = document.createElement('div');
437
- menu.id = `${shell.id}-account-menu`;
438
- menu.className = 'kui-app-shell-account-menu';
439
- menu.setAttribute('role', 'menu');
440
- menu.setAttribute('aria-label', `${account.label} account menu`);
441
- menu.setAttribute('data-state', account.menuExpanded === true ? 'open' : 'closed');
442
- menu.hidden = account.menuExpanded !== true;
443
- const header = document.createElement('div');
444
- header.className = 'kui-app-shell-account-menu-header';
445
- header.setAttribute('aria-hidden', 'true');
446
- const avatar = document.createElement('span');
447
- avatar.className = 'kui-app-shell-avatar';
448
- appendIcon(avatar, account.icon, 'kui-app-shell-icon', scope, environment);
449
- if (avatar.childNodes.length === 0)
450
- avatar.textContent = account.initials ?? account.label.slice(0, 2);
451
- const label = document.createElement('span');
452
- label.className = 'kui-app-shell-account-menu-label';
453
- label.textContent = account.label;
454
- header.append(avatar, label);
455
- menu.append(header);
456
- for (const item of account.menu ?? []) {
457
- menu.append(createAccountMenuItem(item, scope, environment));
458
- }
459
- return menu;
460
- }
461
- function createAccountMenuItem(item, scope, environment) {
462
- if (item.kind === 'separator') {
463
- const separator = document.createElement('div');
464
- separator.className = 'kui-app-shell-account-menu-separator';
465
- separator.setAttribute('role', 'separator');
466
- separator.setAttribute('data-account-menu-id', item.id);
467
- return separator;
468
- }
469
- const href = item.disabled === true ? undefined : (item.href ?? item.route);
470
- const element = href === undefined ? document.createElement('button') : document.createElement('a');
471
- element.className = joinClasses('kui-app-shell-account-menu-item', item.tone === 'danger' ? 'kui-app-shell-account-menu-item--danger' : undefined);
472
- if (href === undefined && element instanceof HTMLButtonElement) {
473
- element.type = 'button';
474
- }
475
- else if (href !== undefined && element instanceof HTMLAnchorElement) {
476
- setHref(element, href, environment);
477
- }
478
- element.setAttribute('role', 'menuitem');
479
- element.setAttribute('data-account-menu-id', item.id);
480
- element.setAttribute('data-label', item.label ?? item.id);
481
- element.setAttribute('data-tone', item.tone ?? 'default');
482
- if (item.disabled === true) {
483
- element.setAttribute('aria-disabled', 'true');
484
- element.setAttribute('tabindex', '-1');
485
- }
486
- appendIcon(element, item.icon, 'kui-app-shell-icon', scope, environment);
487
- const label = document.createElement('span');
488
- label.className = 'kui-app-shell-account-menu-item-label';
489
- label.textContent = item.label ?? item.id;
490
- element.append(label);
491
- return element;
492
- }
493
- function renderNavItems(nav, items, activeId, scope, environment) {
494
- nav.replaceChildren(...items.map((item) => createNavItemElement(item, activeId, scope, environment)));
495
- }
496
- function createNavItemElement(item, activeId, scope, environment) {
497
- const active = item.id === activeId || item.active === true;
498
- const disabled = item.disabled === true;
499
- const anchor = document.createElement('a');
500
- anchor.className = 'kui-app-shell-nav-item';
501
- if (!disabled)
502
- setHref(anchor, routeItemPath(item) ?? '#', environment);
503
- anchor.setAttribute('aria-label', item.label);
504
- anchor.setAttribute('data-nav-id', item.id);
505
- anchor.setAttribute('data-label', item.label);
506
- anchor.setAttribute('data-tooltip', item.label);
507
- anchor.setAttribute('data-state', disabled ? 'disabled' : active ? 'active' : (item.state ?? 'ready'));
508
- anchor.setAttribute('data-tone', item.tone ?? 'default');
509
- setOptionalAttribute(anchor, 'data-route-target', routeItemPath(item));
510
- if (active)
511
- anchor.setAttribute('aria-current', 'page');
512
- if (disabled) {
513
- anchor.setAttribute('aria-disabled', 'true');
514
- anchor.setAttribute('tabindex', '-1');
515
- }
516
- const icon = document.createElement('span');
517
- icon.className = 'kui-app-shell-nav-item-icon';
518
- appendIcon(icon, item.icon, 'kui-app-shell-icon', scope, environment);
519
- const label = document.createElement('span');
520
- label.className = 'kui-app-shell-nav-item-label';
521
- label.textContent = item.label;
522
- anchor.append(icon, label);
523
- if (item.badge !== undefined) {
524
- const badge = document.createElement('span');
525
- badge.className = 'kui-app-shell-nav-badge';
526
- badge.setAttribute('aria-hidden', 'true');
527
- badge.textContent = String(item.badge);
528
- anchor.append(badge);
529
- }
530
- return anchor;
531
- }
532
- function appendIcon(parent, iconName, className, scope, _environment) {
533
- if (iconName === undefined)
534
- return;
535
- const icon = resolveUiIcon(scope.icons, iconName);
536
- if (icon === undefined)
537
- return;
538
- const svg = createIconElement(document, icon, { decorative: true, size: 20 });
539
- svg.classList.add(className);
540
- svg.setAttribute('data-icon-name', icon.name);
541
- parent.append(svg);
542
- }
543
- function routeItemPath(item) {
544
- const path = item.route ?? item.href;
545
- return path === undefined ? undefined : normalizeRoutePath(path);
546
- }
547
- function normalizeRoutePath(path) {
548
- const [withoutHash] = path.split('#', 1);
549
- const [withoutQuery] = (withoutHash ?? path).split('?', 1);
550
- const normalized = withoutQuery === '' ? '/' : (withoutQuery ?? '/');
551
- return normalized.length > 1 ? normalized.replace(/\/+$/u, '') : normalized;
552
- }
553
- function setHref(anchor, href, environment) {
554
- const resolved = environment.security.urlPolicy.resolve(href, {
555
- element: 'a',
556
- attribute: 'href',
557
- });
558
- if (resolved !== null)
559
- anchor.setAttribute('href', resolved);
560
- }
561
- function setOptionalAttribute(element, name, value) {
562
- if (typeof value === 'string' || typeof value === 'number') {
563
- element.setAttribute(name, String(value));
564
- }
565
- else {
566
- element.removeAttribute(name);
567
- }
568
- }
569
- function joinClasses(...classes) {
570
- return classes
571
- .filter((className) => className !== undefined && className.length > 0)
572
- .join(' ');
573
- }