@dataengineeringformachinelearning/viking-ui 3.0.0-alpha.2 → 4.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 (198) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -12
  3. package/dist/LICENSE +201 -0
  4. package/dist/README.md +251 -0
  5. package/dist/fesm2022/dataengineeringformachinelearning-viking-ui.mjs +13751 -0
  6. package/dist/fesm2022/dataengineeringformachinelearning-viking-ui.mjs.map +1 -0
  7. package/dist/index.js +1 -4051
  8. package/dist/package.json +87 -0
  9. package/dist/scripts/build-web-components.mjs +37 -0
  10. package/dist/src/tokens/series-presets.ts +15 -0
  11. package/dist/src/tokens/tailwind.preset.js +209 -0
  12. package/dist/src/tokens/viking-tokens.json +318 -0
  13. package/dist/src/web/badge/viking-badge-wc.ts +116 -0
  14. package/dist/src/web/button/viking-button-wc.ts +165 -0
  15. package/dist/src/web/callout/viking-callout-wc.ts +105 -0
  16. package/dist/src/web/card/viking-card-wc.ts +66 -0
  17. package/dist/src/web/core/base.ts +56 -0
  18. package/dist/src/web/core/dom.ts +53 -0
  19. package/dist/src/web/core/icons-inline.ts +44 -0
  20. package/dist/src/web/core/styles.ts +1138 -0
  21. package/dist/src/web/core/types.ts +19 -0
  22. package/dist/src/web/field/viking-field-wc.ts +154 -0
  23. package/dist/src/web/index.ts +73 -0
  24. package/dist/src/web/input/viking-input-wc.ts +211 -0
  25. package/dist/src/web/modal/viking-modal-wc.ts +187 -0
  26. package/dist/src/web/search-palette/viking-search-palette-wc.ts +494 -0
  27. package/dist/src/web/select/viking-select-wc.ts +218 -0
  28. package/dist/src/web/suite-header/viking-suite-header-wc.ts +714 -0
  29. package/dist/src/web/suite-search-palette/viking-suite-search-palette-wc.ts +234 -0
  30. package/dist/src/web/theme-toggle/viking-theme-toggle-wc.ts +139 -0
  31. package/dist/src/web/types.ts +37 -0
  32. package/dist/src/web/web-components.spec.ts +297 -0
  33. package/dist/types/dataengineeringformachinelearning-viking-ui.d.ts +2658 -0
  34. package/dist/viking.manifest.json +271 -0
  35. package/dist/web-components.js +2427 -126
  36. package/package.json +25 -11
  37. package/src/assets/drakkar-favicon.svg +17 -0
  38. package/src/core/brand/deml-lockup.svg +5 -0
  39. package/src/core/brand/deml-mark-compact.svg +4 -0
  40. package/src/core/brand/deml-mark-filled.svg +3 -0
  41. package/src/core/brand/deml-mark-outline.svg +4 -0
  42. package/src/core/brand-icons.ts +62 -0
  43. package/src/core/icons.spec.ts +52 -0
  44. package/src/core/icons.ts +206 -0
  45. package/src/core/integration-brand-icons.ts +77 -0
  46. package/src/core/lucide-paths.generated.ts +130 -0
  47. package/src/core/types.ts +85 -0
  48. package/src/core/uid.ts +7 -0
  49. package/src/lib/accordion/accordion.ts +126 -0
  50. package/src/lib/app-header/app-header.scss +274 -0
  51. package/src/lib/app-header/app-header.ts +43 -0
  52. package/src/lib/app-sidebar/app-sidebar.scss +415 -0
  53. package/src/lib/app-sidebar/app-sidebar.ts +29 -0
  54. package/src/lib/auth-panel/auth-panel.spec.ts +132 -0
  55. package/src/lib/auth-panel/auth-panel.ts +228 -0
  56. package/src/lib/autocomplete/autocomplete.ts +223 -0
  57. package/src/lib/avatar/avatar.ts +126 -0
  58. package/src/lib/badge/badge.ts +182 -0
  59. package/src/lib/bar/bar.ts +31 -0
  60. package/src/lib/brand/brand.ts +60 -0
  61. package/src/lib/breadcrumbs/breadcrumbs.ts +81 -0
  62. package/src/lib/button/button-group.ts +40 -0
  63. package/src/lib/button/button.scss +96 -0
  64. package/src/lib/button/button.ts +98 -0
  65. package/src/lib/calendar/calendar.ts +253 -0
  66. package/src/lib/callout/callout.ts +211 -0
  67. package/src/lib/card/card-title.spec.ts +27 -0
  68. package/src/lib/card/card-title.ts +50 -0
  69. package/src/lib/card/card.ts +170 -0
  70. package/src/lib/carousel/carousel.ts +206 -0
  71. package/src/lib/chart/chart-zoom.spec.ts +65 -0
  72. package/src/lib/chart/chart-zoom.ts +71 -0
  73. package/src/lib/chart/chart.ts +1257 -0
  74. package/src/lib/chart-card-header/chart-card-header.ts +80 -0
  75. package/src/lib/chart-empty-state/chart-empty-state.ts +146 -0
  76. package/src/lib/chart-panel/chart-panel.ts +133 -0
  77. package/src/lib/checkbox/checkbox.ts +139 -0
  78. package/src/lib/color-picker/color-picker.ts +148 -0
  79. package/src/lib/command/command.ts +260 -0
  80. package/src/lib/composer/composer.ts +180 -0
  81. package/src/lib/confirm-dialog/confirm-dialog.ts +163 -0
  82. package/src/lib/confirm-dialog/dialog.service.ts +79 -0
  83. package/src/lib/context/context.ts +75 -0
  84. package/src/lib/core/cva.ts +33 -0
  85. package/src/lib/date-picker/date-picker.ts +139 -0
  86. package/src/lib/dropdown/dropdown.ts +188 -0
  87. package/src/lib/editor/editor.ts +170 -0
  88. package/src/lib/empty-state/empty-state.ts +100 -0
  89. package/src/lib/fab/fab.ts +76 -0
  90. package/src/lib/field/field.ts +111 -0
  91. package/src/lib/file-upload/file-upload.ts +208 -0
  92. package/src/lib/footer/footer.scss +155 -0
  93. package/src/lib/footer/footer.ts +28 -0
  94. package/src/lib/form-section/form-section.ts +200 -0
  95. package/src/lib/forms.spec.ts +92 -0
  96. package/src/lib/gauge-arc/gauge-arc.ts +85 -0
  97. package/src/lib/hud-panel/hud-panel.ts +27 -0
  98. package/src/lib/icon/icon.ts +284 -0
  99. package/src/lib/icon-badge/icon-badge.ts +87 -0
  100. package/src/lib/icon-heading/icon-heading.spec.ts +32 -0
  101. package/src/lib/icon-heading/icon-heading.ts +38 -0
  102. package/src/lib/icon-text/icon-text.ts +120 -0
  103. package/src/lib/input/input.ts +119 -0
  104. package/src/lib/kanban/kanban.ts +211 -0
  105. package/src/lib/kbd/kbd.ts +32 -0
  106. package/src/lib/label/label.ts +39 -0
  107. package/src/lib/loading-overlay/loading-overlay.ts +80 -0
  108. package/src/lib/menubar/menubar.ts +77 -0
  109. package/src/lib/metric-card/metric-card.ts +33 -0
  110. package/src/lib/modal/modal.ts +207 -0
  111. package/src/lib/native-select/native-select.ts +124 -0
  112. package/src/lib/navbar/navbar.ts +124 -0
  113. package/src/lib/navigation-menu/navigation-menu.ts +116 -0
  114. package/src/lib/otp-input/otp-input.ts +164 -0
  115. package/src/lib/page-back-link/page-back-link.ts +74 -0
  116. package/src/lib/page-header/page-header.ts +70 -0
  117. package/src/lib/pagination/pagination.ts +157 -0
  118. package/src/lib/pillbox/pillbox.ts +157 -0
  119. package/src/lib/popover/popover.ts +92 -0
  120. package/src/lib/profile/profile.ts +73 -0
  121. package/src/lib/progress/progress.ts +123 -0
  122. package/src/lib/radio/radio.ts +132 -0
  123. package/src/lib/ring-gauge/ring-gauge.ts +75 -0
  124. package/src/lib/scroll-area/scroll-area.ts +57 -0
  125. package/src/lib/search-palette/search-palette.scss +288 -0
  126. package/src/lib/search-palette/search-palette.ts +114 -0
  127. package/src/lib/select/select.ts +292 -0
  128. package/src/lib/separator/separator.ts +59 -0
  129. package/src/lib/sheet/sheet.ts +192 -0
  130. package/src/lib/sidebar-nav/sidebar-nav.scss +218 -0
  131. package/src/lib/sidebar-nav/sidebar-nav.ts +158 -0
  132. package/src/lib/site-drakkar/site-footer.ts +129 -0
  133. package/src/lib/site-drakkar/site-navbar.scss +2 -0
  134. package/src/lib/site-drakkar/site-navbar.ts +267 -0
  135. package/src/lib/site-drakkar/suite-search-items.spec.ts +61 -0
  136. package/src/lib/skeleton/skeleton.ts +51 -0
  137. package/src/lib/slider/slider.ts +133 -0
  138. package/src/lib/spinner/spinner.ts +75 -0
  139. package/src/lib/status-metric-row/status-metric-row.ts +74 -0
  140. package/src/lib/suite-search-palette/suite-search-palette.ts +95 -0
  141. package/src/lib/switch/switch.ts +131 -0
  142. package/src/lib/table/table.ts +137 -0
  143. package/src/lib/tabs/tab-panel.ts +61 -0
  144. package/src/lib/tabs/tab.ts +107 -0
  145. package/src/lib/tabs/tabs.ts +109 -0
  146. package/src/lib/textarea/textarea.ts +103 -0
  147. package/src/lib/theme-toggle/theme-toggle.spec.ts +40 -0
  148. package/src/lib/theme-toggle/theme-toggle.ts +39 -0
  149. package/src/lib/time-picker/time-picker.ts +197 -0
  150. package/src/lib/timeline/timeline.ts +152 -0
  151. package/src/lib/toast/toast.ts +196 -0
  152. package/src/lib/toggle/toggle-group.ts +60 -0
  153. package/src/lib/toggle/toggle.ts +101 -0
  154. package/src/lib/tooltip/tooltip.ts +119 -0
  155. package/src/lib/typography/heading.ts +52 -0
  156. package/src/lib/typography/text.ts +51 -0
  157. package/src/lib/uptime-bar/uptime-bar.ts +55 -0
  158. package/src/lib/verification-code-field/verification-code-field.ts +149 -0
  159. package/src/lib/whitepaper-cta/whitepaper-cta.scss +130 -0
  160. package/src/lib/whitepaper-cta/whitepaper-cta.ts +78 -0
  161. package/src/lib/wizard/wizard.ts +80 -0
  162. package/src/public-api.ts +172 -0
  163. package/src/tokens/series-presets.ts +15 -0
  164. package/src/tokens/tailwind.preset.js +209 -0
  165. package/src/viking-ui.spec.ts +416 -0
  166. package/src/web/web-components.spec.ts +297 -0
  167. package/dist/elements/button/viking-button.d.ts +0 -14
  168. package/dist/elements/card/viking-card.d.ts +0 -8
  169. package/dist/elements/core/dom.d.ts +0 -4
  170. package/dist/elements/core/styles.d.ts +0 -2
  171. package/dist/elements/modal/viking-modal.d.ts +0 -15
  172. package/dist/index.d.ts +0 -1
  173. package/dist/lib/core/brand-icons.d.ts +0 -29
  174. package/dist/lib/core/icons.d.ts +0 -132
  175. package/dist/lib/core/integration-brand-icons.d.ts +0 -18
  176. package/dist/lib/core/lucide-paths.generated.d.ts +0 -90
  177. package/dist/lib/site-drakkar/site-drakkar.config.d.ts +0 -50
  178. package/dist/lib/site-drakkar/suite-search-items.d.ts +0 -17
  179. package/dist/web/badge/viking-badge-wc.d.ts +0 -28
  180. package/dist/web/button/viking-button-wc.d.ts +0 -28
  181. package/dist/web/callout/viking-callout-wc.d.ts +0 -27
  182. package/dist/web/card/viking-card-wc.d.ts +0 -29
  183. package/dist/web/core/base.d.ts +0 -12
  184. package/dist/web/core/dom.d.ts +0 -11
  185. package/dist/web/core/icons-inline.d.ts +0 -7
  186. package/dist/web/core/styles.d.ts +0 -12
  187. package/dist/web/core/types.d.ts +0 -10
  188. package/dist/web/field/viking-field-wc.d.ts +0 -32
  189. package/dist/web/index.d.ts +0 -15
  190. package/dist/web/input/viking-input-wc.d.ts +0 -32
  191. package/dist/web/modal/viking-modal-wc.d.ts +0 -50
  192. package/dist/web/search-palette/viking-search-palette-wc.d.ts +0 -64
  193. package/dist/web/select/viking-select-wc.d.ts +0 -48
  194. package/dist/web/suite-header/viking-suite-header-wc.d.ts +0 -36
  195. package/dist/web/suite-search-palette/viking-suite-search-palette-wc.d.ts +0 -39
  196. package/dist/web/theme-toggle/viking-theme-toggle-wc.d.ts +0 -19
  197. package/dist/web/types.d.ts +0 -40
  198. package/dist/web-components/index.d.ts +0 -1
@@ -0,0 +1,494 @@
1
+ import {
2
+ attachShadowStyles,
3
+ readBoolAttr,
4
+ closeModalDialog,
5
+ showModalDialog,
6
+ } from "../core/base";
7
+ import {
8
+ defineCustomElement,
9
+ defineCustomElementAlias,
10
+ escapeHtml,
11
+ HTMLElementBase,
12
+ modKeyLabel,
13
+ vikingWcUid,
14
+ } from "../core/dom";
15
+ import { renderInlineIcon } from "../core/icons-inline";
16
+ import { VIKING_SEARCH_PALETTE_STYLES } from "../core/styles";
17
+ import type { VikingSearchPaletteItem } from "../core/types";
18
+
19
+ export type { VikingSearchPaletteItem };
20
+
21
+ const parseItems = (el: HTMLElement): VikingSearchPaletteItem[] => {
22
+ const raw = el.getAttribute("items");
23
+ if (!raw) {
24
+ return [];
25
+ }
26
+ try {
27
+ const parsed = JSON.parse(raw) as VikingSearchPaletteItem[];
28
+ return Array.isArray(parsed) ? parsed : [];
29
+ } catch {
30
+ return [];
31
+ }
32
+ };
33
+
34
+ const matchesQuery = (
35
+ item: VikingSearchPaletteItem,
36
+ query: string,
37
+ ): boolean => {
38
+ const haystack = [
39
+ item.title,
40
+ item.snippet ?? "",
41
+ item.group ?? "",
42
+ item.href,
43
+ ...(item.keywords ?? []),
44
+ ]
45
+ .join(" ")
46
+ .toLowerCase();
47
+ return haystack.includes(query);
48
+ };
49
+
50
+ const filterItems = (
51
+ items: VikingSearchPaletteItem[],
52
+ query: string,
53
+ ): VikingSearchPaletteItem[] => {
54
+ const q = query.trim().toLowerCase();
55
+ if (!q) {
56
+ return items;
57
+ }
58
+ return items.filter((item) => matchesQuery(item, q));
59
+ };
60
+
61
+ const groupItems = (
62
+ items: VikingSearchPaletteItem[],
63
+ ): { group: string | null; items: VikingSearchPaletteItem[] }[] => {
64
+ const groups = new Map<string | null, VikingSearchPaletteItem[]>();
65
+ items.forEach((item) => {
66
+ const key = item.group ?? null;
67
+ const bucket = groups.get(key) ?? [];
68
+ bucket.push(item);
69
+ groups.set(key, bucket);
70
+ });
71
+ return Array.from(groups.entries()).map(([group, grouped]) => ({
72
+ group,
73
+ items: grouped,
74
+ }));
75
+ };
76
+
77
+ /**
78
+ * Framework-agnostic command palette / search overlay Web Component.
79
+ * Tag: `viking-command-palette`
80
+ * Aliases: `viking-search-palette`, `viking-search-palette-wc`
81
+ *
82
+ * @attr open - When present, shows the palette
83
+ * @attr global-shortcut - Bind ⌘K / Ctrl+K to open
84
+ * @attr placeholder - Search input placeholder
85
+ * @attr items - JSON array of `{ title, href, snippet?, group? }`
86
+ *
87
+ * @method openPalette() - Programmatically open
88
+ * @method closePalette() - Programmatically close
89
+ *
90
+ * @event viking-close - Palette closed
91
+ * @event viking-query - `{ detail: { query: string } }` on input
92
+ * @event viking-select - `{ detail: { item } }` when a result is activated
93
+ *
94
+ * @example
95
+ * <viking-command-palette
96
+ * global-shortcut
97
+ * items='[{"title":"Components","href":"/components","group":"Docs"}]'
98
+ * ></viking-command-palette>
99
+ */
100
+ export class VikingSearchPaletteWc extends HTMLElementBase {
101
+ static readonly tag = "viking-command-palette";
102
+ static readonly searchTag = "viking-search-palette";
103
+ static readonly legacyTag = "viking-search-palette-wc";
104
+
105
+ static get observedAttributes(): string[] {
106
+ return ["open", "placeholder", "items", "global-shortcut"];
107
+ }
108
+
109
+ private readonly shadow: ShadowRoot;
110
+ private dialogEl: HTMLDialogElement | null = null;
111
+ private inputEl: HTMLInputElement | null = null;
112
+ private resultsEl: HTMLDivElement | null = null;
113
+ private globalKeyHandler: ((event: KeyboardEvent) => void) | null = null;
114
+ private readonly resultsId = vikingWcUid("viking-search-results");
115
+ private readonly inputId = vikingWcUid("viking-search-input");
116
+ private query = "";
117
+ private activeIndex = 0;
118
+ private flatResults: VikingSearchPaletteItem[] = [];
119
+
120
+ constructor() {
121
+ super();
122
+ this.shadow = this.attachShadow({ mode: "open" });
123
+ attachShadowStyles(this.shadow, VIKING_SEARCH_PALETTE_STYLES);
124
+ }
125
+
126
+ connectedCallback(): void {
127
+ this.render();
128
+ this.syncOpen();
129
+ this.bindGlobalShortcut();
130
+ this.dialogEl?.addEventListener("close", this.onClose);
131
+ this.dialogEl?.addEventListener("click", this.onBackdropClick);
132
+ this.inputEl?.addEventListener("input", this.onInput);
133
+ this.inputEl?.addEventListener("keydown", this.onInputKeydown);
134
+ }
135
+
136
+ disconnectedCallback(): void {
137
+ this.dialogEl?.removeEventListener("close", this.onClose);
138
+ this.dialogEl?.removeEventListener("click", this.onBackdropClick);
139
+ this.inputEl?.removeEventListener("input", this.onInput);
140
+ this.inputEl?.removeEventListener("keydown", this.onInputKeydown);
141
+ this.unbindGlobalShortcut();
142
+ }
143
+
144
+ attributeChangedCallback(name: string): void {
145
+ if (!this.isConnected) {
146
+ return;
147
+ }
148
+ if (name === "open") {
149
+ this.syncOpen();
150
+ }
151
+ if (name === "global-shortcut") {
152
+ this.unbindGlobalShortcut();
153
+ this.bindGlobalShortcut();
154
+ }
155
+ if (name === "items" || name === "placeholder") {
156
+ if (name === "placeholder" && this.inputEl) {
157
+ const placeholder =
158
+ this.getAttribute("placeholder") ??
159
+ "Search documentation, dashboard, API…";
160
+ this.inputEl.placeholder = placeholder;
161
+ this.inputEl.setAttribute("aria-label", placeholder);
162
+ } else {
163
+ this.renderResults();
164
+ }
165
+ }
166
+ }
167
+
168
+ /** Opens the command palette. */
169
+ openPalette(): void {
170
+ this.setAttribute("open", "");
171
+ this.syncOpen();
172
+ }
173
+
174
+ /** Closes the command palette. */
175
+ closePalette(): void {
176
+ this.removeAttribute("open");
177
+ closeModalDialog(this.dialogEl);
178
+ }
179
+
180
+ /** Sets the query string and re-renders filtered results. */
181
+ search(query: string): void {
182
+ this.query = query;
183
+ if (this.inputEl) {
184
+ this.inputEl.value = query;
185
+ }
186
+ this.activeIndex = 0;
187
+ this.renderResults();
188
+ this.dispatchEvent(
189
+ new CustomEvent("viking-query", {
190
+ bubbles: true,
191
+ composed: true,
192
+ detail: { query: this.query },
193
+ }),
194
+ );
195
+ }
196
+
197
+ private readonly onClose = (): void => {
198
+ this.removeAttribute("open");
199
+ this.query = "";
200
+ this.activeIndex = 0;
201
+ if (this.inputEl) {
202
+ this.inputEl.value = "";
203
+ }
204
+ this.dispatchEvent(
205
+ new CustomEvent("viking-close", { bubbles: true, composed: true }),
206
+ );
207
+ };
208
+
209
+ private readonly onBackdropClick = (event: MouseEvent): void => {
210
+ if (event.target === this.dialogEl) {
211
+ this.closePalette();
212
+ }
213
+ };
214
+
215
+ private readonly onInput = (event: Event): void => {
216
+ this.query =
217
+ (event.target as HTMLInputElement).value ?? this.inputEl?.value ?? "";
218
+ this.activeIndex = 0;
219
+ this.renderResults();
220
+ this.dispatchEvent(
221
+ new CustomEvent("viking-query", {
222
+ bubbles: true,
223
+ composed: true,
224
+ detail: { query: this.query },
225
+ }),
226
+ );
227
+ };
228
+
229
+ private readonly onInputKeydown = (event: KeyboardEvent): void => {
230
+ if (this.flatResults.length === 0) {
231
+ if (event.key === "Escape") {
232
+ event.preventDefault();
233
+ this.closePalette();
234
+ }
235
+ return;
236
+ }
237
+
238
+ if (event.key === "ArrowDown") {
239
+ event.preventDefault();
240
+ this.activeIndex = Math.min(
241
+ this.flatResults.length - 1,
242
+ this.activeIndex + 1,
243
+ );
244
+ this.renderResults();
245
+ this.scrollActiveIntoView();
246
+ return;
247
+ }
248
+
249
+ if (event.key === "ArrowUp") {
250
+ event.preventDefault();
251
+ this.activeIndex = Math.max(0, this.activeIndex - 1);
252
+ this.renderResults();
253
+ this.scrollActiveIntoView();
254
+ return;
255
+ }
256
+
257
+ if (event.key === "Enter") {
258
+ event.preventDefault();
259
+ const item = this.flatResults[this.activeIndex];
260
+ if (item) {
261
+ this.activateItem(item);
262
+ }
263
+ }
264
+ };
265
+
266
+ private bindGlobalShortcut(): void {
267
+ if (!readBoolAttr(this, "global-shortcut")) {
268
+ return;
269
+ }
270
+ this.globalKeyHandler = (event: KeyboardEvent) => {
271
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
272
+ event.preventDefault();
273
+ if (this.hasAttribute("open")) {
274
+ this.closePalette();
275
+ } else {
276
+ this.openPalette();
277
+ }
278
+ }
279
+ };
280
+ document.addEventListener("keydown", this.globalKeyHandler);
281
+ }
282
+
283
+ private unbindGlobalShortcut(): void {
284
+ if (this.globalKeyHandler) {
285
+ document.removeEventListener("keydown", this.globalKeyHandler);
286
+ this.globalKeyHandler = null;
287
+ }
288
+ }
289
+
290
+ private syncOpen(): void {
291
+ if (!this.dialogEl) {
292
+ return;
293
+ }
294
+ const shouldOpen = this.hasAttribute("open");
295
+ if (shouldOpen && !this.dialogEl.open) {
296
+ this.dialogEl.removeAttribute("aria-hidden");
297
+ showModalDialog(this.dialogEl);
298
+ this.activeIndex = 0;
299
+ this.renderResults();
300
+ queueMicrotask(() => this.inputEl?.focus());
301
+ } else if (!shouldOpen && this.dialogEl.open) {
302
+ closeModalDialog(this.dialogEl);
303
+ this.dialogEl.setAttribute("aria-hidden", "true");
304
+ } else if (!shouldOpen) {
305
+ this.dialogEl.setAttribute("aria-hidden", "true");
306
+ }
307
+ }
308
+
309
+ private scrollActiveIntoView(): void {
310
+ const active = this.resultsEl?.querySelector(
311
+ ".viking-search-result.is-selected",
312
+ );
313
+ active?.scrollIntoView({ block: "nearest" });
314
+ }
315
+
316
+ private activateItem(item: VikingSearchPaletteItem): void {
317
+ this.dispatchEvent(
318
+ new CustomEvent("viking-select", {
319
+ bubbles: true,
320
+ composed: true,
321
+ detail: { item },
322
+ }),
323
+ );
324
+ this.closePalette();
325
+
326
+ if (item.action === "cookie-settings") {
327
+ const widgets = (
328
+ globalThis as { DemlWidgets?: { openCookieSettings?: () => void } }
329
+ ).DemlWidgets;
330
+ widgets?.openCookieSettings?.();
331
+ return;
332
+ }
333
+
334
+ if (item.action === "bug-report") {
335
+ const widgets = (
336
+ globalThis as { DemlWidgets?: { openBugReport?: () => void } }
337
+ ).DemlWidgets;
338
+ if (widgets?.openBugReport) {
339
+ widgets.openBugReport();
340
+ return;
341
+ }
342
+ }
343
+
344
+ if (item.href && item.href !== "#") {
345
+ window.location.assign(item.href);
346
+ }
347
+ }
348
+
349
+ private renderResults(): void {
350
+ if (!this.resultsEl) {
351
+ return;
352
+ }
353
+
354
+ const items = parseItems(this);
355
+ const q = this.query.trim().toLowerCase();
356
+ const filtered = filterItems(items, q);
357
+ this.flatResults = filtered;
358
+
359
+ if (filtered.length === 0) {
360
+ const q = this.query.trim();
361
+ this.resultsEl.innerHTML = `<p class="viking-search-empty" role="status">${q ? "No results found" : "Start typing to search…"}</p>`;
362
+ this.inputEl?.removeAttribute("aria-activedescendant");
363
+ return;
364
+ }
365
+
366
+ if (this.activeIndex >= filtered.length) {
367
+ this.activeIndex = filtered.length - 1;
368
+ }
369
+
370
+ let resultIndex = 0;
371
+ const grouped = groupItems(filtered);
372
+
373
+ const markup = grouped
374
+ .map(({ group, items: groupItemsList }) => {
375
+ const groupLabel = group
376
+ ? `<p class="viking-search-group-label" role="presentation">${escapeHtml(group)}</p>`
377
+ : "";
378
+ const rows = groupItemsList
379
+ .map((item) => {
380
+ const id = `${this.resultsId}-result-${resultIndex}`;
381
+ const selected = resultIndex === this.activeIndex;
382
+ resultIndex += 1;
383
+ return `
384
+ <a
385
+ id="${id}"
386
+ class="viking-search-result${selected ? " is-selected" : ""}"
387
+ role="option"
388
+ aria-selected="${selected}"
389
+ href="${escapeHtml(item.href)}"
390
+ part="result"
391
+ data-index="${resultIndex - 1}"
392
+ >
393
+ <div>
394
+ <div class="viking-search-result-title">${escapeHtml(item.title)}</div>
395
+ ${item.snippet ? `<div class="viking-search-result-snippet">${escapeHtml(item.snippet)}</div>` : ""}
396
+ </div>
397
+ </a>`;
398
+ })
399
+ .join("");
400
+ return `${groupLabel}${rows}`;
401
+ })
402
+ .join("");
403
+
404
+ this.resultsEl.innerHTML = `<div class="viking-search-results" id="${this.resultsId}" role="listbox" aria-label="Search results">${markup}</div>`;
405
+
406
+ const activeId = `${this.resultsId}-result-${this.activeIndex}`;
407
+ this.inputEl?.setAttribute("aria-activedescendant", activeId);
408
+ this.inputEl?.setAttribute("role", "combobox");
409
+ this.inputEl?.setAttribute("aria-expanded", "true");
410
+ this.inputEl?.setAttribute("aria-controls", this.resultsId);
411
+
412
+ this.resultsEl.querySelectorAll(".viking-search-result").forEach((node) => {
413
+ node.addEventListener("click", (event) => {
414
+ event.preventDefault();
415
+ const index = Number((node as HTMLElement).dataset["index"] ?? 0);
416
+ const item = this.flatResults[index];
417
+ if (item) {
418
+ this.activateItem(item);
419
+ }
420
+ });
421
+ node.addEventListener("mouseenter", () => {
422
+ const index = Number((node as HTMLElement).dataset["index"] ?? 0);
423
+ this.activeIndex = index;
424
+ this.renderResults();
425
+ });
426
+ });
427
+ }
428
+
429
+ private render(): void {
430
+ const placeholder =
431
+ this.getAttribute("placeholder") ??
432
+ "Search documentation, dashboard, API…";
433
+ const mod = modKeyLabel();
434
+
435
+ this.shadow.innerHTML = `
436
+ <dialog class="viking-search-palette-backdrop" aria-label="Search" aria-hidden="true">
437
+ <div class="viking-search-palette" part="panel" role="document">
438
+ <div class="viking-search-palette-header" part="header">
439
+ <span class="viking-search-palette-icon" aria-hidden="true">${renderInlineIcon("search", 24)}</span>
440
+ <input
441
+ id="${this.inputId}"
442
+ type="search"
443
+ class="viking-search-palette-input"
444
+ part="input"
445
+ placeholder="${escapeHtml(placeholder)}"
446
+ aria-label="${escapeHtml(placeholder)}"
447
+ aria-autocomplete="list"
448
+ autocomplete="off"
449
+ spellcheck="false"
450
+ />
451
+ <button type="button" class="viking-search-palette-close" part="close" aria-label="Close search">${renderInlineIcon("x", 20)}</button>
452
+ </div>
453
+ <div class="viking-search-palette-body" part="body">
454
+ <slot></slot>
455
+ <div class="viking-search-results-host"></div>
456
+ </div>
457
+ <footer class="viking-search-palette-footer" part="footer">
458
+ <span class="viking-kbd">${mod}</span><span class="viking-kbd">K</span> toggle ·
459
+ <span class="viking-kbd">↑</span><span class="viking-kbd">↓</span> navigate ·
460
+ <span class="viking-kbd">Enter</span> open ·
461
+ <span class="viking-kbd">Esc</span> close
462
+ </footer>
463
+ </div>
464
+ </dialog>
465
+ `;
466
+
467
+ this.dialogEl = this.shadow.querySelector("dialog");
468
+ this.inputEl = this.shadow.querySelector("input");
469
+ this.resultsEl = this.shadow.querySelector(".viking-search-results-host");
470
+
471
+ this.shadow
472
+ .querySelector(".viking-search-palette-close")
473
+ ?.addEventListener("click", () => this.closePalette());
474
+
475
+ this.dialogEl?.addEventListener("keydown", (event) => {
476
+ if (event.key === "Escape") {
477
+ event.preventDefault();
478
+ this.closePalette();
479
+ }
480
+ });
481
+ }
482
+ }
483
+
484
+ export const registerVikingSearchPaletteWc = (): void => {
485
+ defineCustomElement(VikingSearchPaletteWc.tag, VikingSearchPaletteWc);
486
+ defineCustomElementAlias(
487
+ VikingSearchPaletteWc.searchTag,
488
+ VikingSearchPaletteWc,
489
+ );
490
+ defineCustomElementAlias(
491
+ VikingSearchPaletteWc.legacyTag,
492
+ VikingSearchPaletteWc,
493
+ );
494
+ };
@@ -0,0 +1,218 @@
1
+ import {
2
+ attachElementInternals,
3
+ attachShadowStyles,
4
+ setFormValue,
5
+ } from "../core/base";
6
+ import {
7
+ defineCustomElement,
8
+ defineCustomElementAlias,
9
+ escapeHtml,
10
+ HTMLElementBase,
11
+ vikingWcUid,
12
+ } from "../core/dom";
13
+ import { VIKING_SELECT_STYLES } from "../core/styles";
14
+
15
+ /**
16
+ * Framework-agnostic native select Web Component with form association.
17
+ * Tag: `viking-select` (legacy alias: `viking-select-wc`)
18
+ *
19
+ * Place `<option>` elements as light DOM children; they are mirrored into the shadow select.
20
+ *
21
+ * @attr label - Visible field label
22
+ * @attr name - Form field name (participates in form submission via ElementInternals)
23
+ * @attr value - Selected option value
24
+ * @attr placeholder - Placeholder option text when no value is selected
25
+ * @attr description - Helper text below the control
26
+ * @attr error - Validation message; sets aria-invalid
27
+ * @attr width - full | half (default: half)
28
+ * @attr disabled - Disables the control
29
+ * @attr required - Marks field as required
30
+ *
31
+ * @event viking-change - `{ detail: { value: string } }`
32
+ *
33
+ * @example
34
+ * <viking-select-wc label="Retention" name="retention" value="30d">
35
+ * <option value="7d">7 days</option>
36
+ * <option value="30d">30 days</option>
37
+ * </viking-select-wc>
38
+ */
39
+ export class VikingSelectWc extends HTMLElementBase {
40
+ static readonly formAssociated = true;
41
+ static readonly tag = "viking-select";
42
+ static readonly legacyTag = "viking-select-wc";
43
+
44
+ static get observedAttributes(): string[] {
45
+ return [
46
+ "label",
47
+ "name",
48
+ "value",
49
+ "placeholder",
50
+ "description",
51
+ "error",
52
+ "width",
53
+ "disabled",
54
+ "required",
55
+ ];
56
+ }
57
+
58
+ private readonly shadow: ShadowRoot;
59
+ private readonly internals: ElementInternals | null;
60
+ private selectEl: HTMLSelectElement | null = null;
61
+ private optionObserver: MutationObserver | null = null;
62
+ private readonly controlId = vikingWcUid("viking-select");
63
+
64
+ constructor() {
65
+ super();
66
+ this.shadow = this.attachShadow({ mode: "open" });
67
+ this.internals = attachElementInternals(this);
68
+ attachShadowStyles(this.shadow, VIKING_SELECT_STYLES);
69
+ }
70
+
71
+ connectedCallback(): void {
72
+ this.render();
73
+ this.syncOptions();
74
+ this.observeOptions();
75
+ this.selectEl?.addEventListener("change", this.onChange);
76
+ }
77
+
78
+ disconnectedCallback(): void {
79
+ this.selectEl?.removeEventListener("change", this.onChange);
80
+ this.optionObserver?.disconnect();
81
+ this.optionObserver = null;
82
+ }
83
+
84
+ attributeChangedCallback(name: string): void {
85
+ if (!this.isConnected) {
86
+ return;
87
+ }
88
+ if (name === "value" && this.selectEl) {
89
+ this.selectEl.value = this.getAttribute("value") ?? "";
90
+ this.syncFormValue();
91
+ return;
92
+ }
93
+ if (name === "error" || name === "description" || name === "label") {
94
+ this.render();
95
+ this.syncOptions();
96
+ return;
97
+ }
98
+ this.render();
99
+ this.syncOptions();
100
+ }
101
+
102
+ get value(): string {
103
+ return this.selectEl?.value ?? this.getAttribute("value") ?? "";
104
+ }
105
+
106
+ set value(next: string) {
107
+ this.setAttribute("value", next);
108
+ if (this.selectEl) {
109
+ this.selectEl.value = next;
110
+ }
111
+ this.syncFormValue();
112
+ }
113
+
114
+ private readonly onChange = (): void => {
115
+ const value = this.selectEl?.value ?? "";
116
+ this.setAttribute("value", value);
117
+ this.syncFormValue();
118
+ this.dispatchEvent(
119
+ new CustomEvent("viking-change", {
120
+ bubbles: true,
121
+ composed: true,
122
+ detail: { value },
123
+ }),
124
+ );
125
+ };
126
+
127
+ private syncFormValue(): void {
128
+ setFormValue(this.internals, this.value);
129
+ }
130
+
131
+ private observeOptions(): void {
132
+ this.optionObserver?.disconnect();
133
+ this.optionObserver = new MutationObserver(() => this.syncOptions());
134
+ this.optionObserver.observe(this, {
135
+ childList: true,
136
+ subtree: true,
137
+ characterData: true,
138
+ });
139
+ }
140
+
141
+ private render(): void {
142
+ const label = this.getAttribute("label") ?? "";
143
+ const name = this.getAttribute("name") ?? "";
144
+ const disabled = this.hasAttribute("disabled");
145
+ const required = this.hasAttribute("required");
146
+ const error = this.getAttribute("error") ?? "";
147
+ const description = this.getAttribute("description") ?? "";
148
+ const describedBy = [
149
+ description && `${this.controlId}-desc`,
150
+ error && `${this.controlId}-error`,
151
+ ]
152
+ .filter(Boolean)
153
+ .join(" ");
154
+
155
+ this.shadow.innerHTML = `
156
+ <div class="viking-field" part="field">
157
+ ${label ? `<label class="viking-field-label" part="label" for="${this.controlId}">${escapeHtml(label)}</label>` : ""}
158
+ <select
159
+ id="${this.controlId}"
160
+ class="viking-select-native"
161
+ part="control"
162
+ ${name ? `name="${escapeHtml(name)}"` : ""}
163
+ ${disabled ? "disabled" : ""}
164
+ ${required ? "required" : ""}
165
+ ${error ? 'aria-invalid="true"' : ""}
166
+ ${describedBy ? `aria-describedby="${describedBy}"` : ""}
167
+ ></select>
168
+ ${description ? `<p id="${this.controlId}-desc" class="viking-field-description" part="description">${escapeHtml(description)}</p>` : ""}
169
+ ${error ? `<p id="${this.controlId}-error" class="viking-field-error" part="error" role="alert">${escapeHtml(error)}</p>` : ""}
170
+ </div>
171
+ `;
172
+
173
+ this.selectEl = this.shadow.querySelector("select");
174
+ const value = this.getAttribute("value");
175
+ if (this.selectEl && value) {
176
+ this.selectEl.value = value;
177
+ }
178
+ this.syncFormValue();
179
+ }
180
+
181
+ private syncOptions(): void {
182
+ if (!this.selectEl) {
183
+ return;
184
+ }
185
+
186
+ const currentValue = this.selectEl.value;
187
+ this.selectEl.innerHTML = "";
188
+ const options = this.querySelectorAll("option");
189
+
190
+ if (options.length === 0) {
191
+ const placeholder = this.getAttribute("placeholder") ?? "Select…";
192
+ const opt = document.createElement("option");
193
+ opt.value = "";
194
+ opt.textContent = placeholder;
195
+ opt.disabled = true;
196
+ opt.selected = !this.getAttribute("value");
197
+ this.selectEl.append(opt);
198
+ return;
199
+ }
200
+
201
+ options.forEach((opt) => {
202
+ this.selectEl?.append(opt.cloneNode(true));
203
+ });
204
+
205
+ const value = this.getAttribute("value");
206
+ if (value) {
207
+ this.selectEl.value = value;
208
+ } else if (currentValue) {
209
+ this.selectEl.value = currentValue;
210
+ }
211
+ this.syncFormValue();
212
+ }
213
+ }
214
+
215
+ export const registerVikingSelectWc = (): void => {
216
+ defineCustomElement(VikingSelectWc.tag, VikingSelectWc);
217
+ defineCustomElementAlias(VikingSelectWc.legacyTag, VikingSelectWc);
218
+ };