@ship-ui/core 0.22.3 → 0.22.5

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 (31) hide show
  1. package/assets/mcp/components.json +32 -32
  2. package/bin/ship-fg-scanner +0 -0
  3. package/fesm2022/ship-ui-core-ship-blueprint.mjs +1 -1
  4. package/fesm2022/ship-ui-core-ship-blueprint.mjs.map +1 -1
  5. package/fesm2022/ship-ui-core-ship-button-group.mjs +2 -2
  6. package/fesm2022/ship-ui-core-ship-button-group.mjs.map +1 -1
  7. package/fesm2022/ship-ui-core-ship-button.mjs +6 -4
  8. package/fesm2022/ship-ui-core-ship-button.mjs.map +1 -1
  9. package/fesm2022/ship-ui-core-ship-chip.mjs +6 -4
  10. package/fesm2022/ship-ui-core-ship-chip.mjs.map +1 -1
  11. package/fesm2022/ship-ui-core-ship-color-picker.mjs +1 -1
  12. package/fesm2022/ship-ui-core-ship-color-picker.mjs.map +1 -1
  13. package/fesm2022/ship-ui-core-ship-kbd.mjs +96 -0
  14. package/fesm2022/ship-ui-core-ship-kbd.mjs.map +1 -0
  15. package/fesm2022/ship-ui-core-ship-list.mjs +2 -2
  16. package/fesm2022/ship-ui-core-ship-list.mjs.map +1 -1
  17. package/fesm2022/ship-ui-core-ship-select.mjs +22 -3
  18. package/fesm2022/ship-ui-core-ship-select.mjs.map +1 -1
  19. package/fesm2022/ship-ui-core-ship-spotlight.mjs +590 -0
  20. package/fesm2022/ship-ui-core-ship-spotlight.mjs.map +1 -0
  21. package/fesm2022/ship-ui-core-ship-table.mjs +12 -6
  22. package/fesm2022/ship-ui-core-ship-table.mjs.map +1 -1
  23. package/fesm2022/ship-ui-core-ship-theme-toggle.mjs +1 -1
  24. package/fesm2022/ship-ui-core-ship-theme-toggle.mjs.map +1 -1
  25. package/package.json +9 -1
  26. package/types/ship-ui-core-ship-button.d.ts +2 -1
  27. package/types/ship-ui-core-ship-chip.d.ts +2 -1
  28. package/types/ship-ui-core-ship-kbd.d.ts +19 -0
  29. package/types/ship-ui-core-ship-select.d.ts +1 -0
  30. package/types/ship-ui-core-ship-spotlight.d.ts +83 -0
  31. package/types/ship-ui-core-ship-table.d.ts +1 -1
@@ -0,0 +1,590 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, viewChild, input, model, output, computed, signal, effect, ChangeDetectionStrategy, ViewEncapsulation, Component, inject, DOCUMENT, DestroyRef, Injectable } from '@angular/core';
3
+ import { ShipIcon } from '@ship-ui/core/ship-icon';
4
+ import { ShipList } from '@ship-ui/core/ship-list';
5
+ import { ShipKbd } from '@ship-ui/core/ship-kbd';
6
+ import { ShipDialogService } from '@ship-ui/core/ship-dialog';
7
+
8
+ const SHIP_SPOTLIGHT_CONFIG = new InjectionToken('SHIP_SPOTLIGHT_CONFIG');
9
+ function provideShipSpotlight(config) {
10
+ return {
11
+ provide: SHIP_SPOTLIGHT_CONFIG,
12
+ useValue: config,
13
+ };
14
+ }
15
+ class ShipSpotlight {
16
+ constructor() {
17
+ this.inputRef = viewChild('inputRef', /* @ts-ignore */
18
+ ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
19
+ this.resultsRef = viewChild('resultsRef', /* @ts-ignore */
20
+ ...(ngDevMode ? [{ debugName: "resultsRef" }] : /* istanbul ignore next */ []));
21
+ // Input config passed from ShipDialogService
22
+ this.data = input(/* @ts-ignore */
23
+ ...(ngDevMode ? [undefined, { debugName: "data" }] : /* istanbul ignore next */ []));
24
+ // Standard inputs (fallback)
25
+ this.items = input([], /* @ts-ignore */
26
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
27
+ this.placeholder = input('Search actions, settings, or pages...', /* @ts-ignore */
28
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
29
+ this.customFilter = input(false, /* @ts-ignore */
30
+ ...(ngDevMode ? [{ debugName: "customFilter" }] : /* istanbul ignore next */ []));
31
+ this.searchQuery = model('', /* @ts-ignore */
32
+ ...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
33
+ this.itemSelected = output();
34
+ this.closed = output();
35
+ // Merged config properties
36
+ this.mergedItems = computed(() => this.data()?.items ?? this.items(), /* @ts-ignore */
37
+ ...(ngDevMode ? [{ debugName: "mergedItems" }] : /* istanbul ignore next */ []));
38
+ this.mergedPlaceholder = computed(() => this.data()?.placeholder ?? this.placeholder(), /* @ts-ignore */
39
+ ...(ngDevMode ? [{ debugName: "mergedPlaceholder" }] : /* istanbul ignore next */ []));
40
+ this.mergedCustomFilter = computed(() => this.data()?.customFilter ?? this.customFilter(), /* @ts-ignore */
41
+ ...(ngDevMode ? [{ debugName: "mergedCustomFilter" }] : /* istanbul ignore next */ []));
42
+ this.activeOptionIndex = signal(0, /* @ts-ignore */
43
+ ...(ngDevMode ? [{ debugName: "activeOptionIndex" }] : /* istanbul ignore next */ []));
44
+ // Computes the scored and filtered flat list of items
45
+ this.flatFilteredItems = computed(() => {
46
+ const query = this.searchQuery().toLowerCase().trim();
47
+ const allItems = this.mergedItems();
48
+ if (this.mergedCustomFilter() || !query) {
49
+ return allItems;
50
+ }
51
+ const scored = allItems
52
+ .map((item) => {
53
+ const labelScore = this.#calculateMatchScore(item.label.toLowerCase(), query);
54
+ const descScore = item.description ? this.#calculateMatchScore(item.description.toLowerCase(), query) : 0;
55
+ return { item, score: Math.max(labelScore, descScore) };
56
+ })
57
+ .filter((x) => x.score > 0)
58
+ .sort((a, b) => b.score - a.score);
59
+ return scored.map((x) => x.item);
60
+ }, /* @ts-ignore */
61
+ ...(ngDevMode ? [{ debugName: "flatFilteredItems" }] : /* istanbul ignore next */ []));
62
+ // Groups flat list of items by category and precomputes flat indices
63
+ this.groupedFilteredItems = computed(() => {
64
+ const flat = this.flatFilteredItems();
65
+ const groups = [];
66
+ let currentFlatIndex = 0;
67
+ flat.forEach((item) => {
68
+ const cat = item.category || 'Actions';
69
+ let group = groups.find((g) => g.category === cat);
70
+ if (!group) {
71
+ group = { category: cat, items: [] };
72
+ groups.push(group);
73
+ }
74
+ group.items.push({ item, flatIndex: currentFlatIndex++ });
75
+ });
76
+ return groups;
77
+ }, /* @ts-ignore */
78
+ ...(ngDevMode ? [{ debugName: "groupedFilteredItems" }] : /* istanbul ignore next */ []));
79
+ this.validateItemsEffect = effect(() => {
80
+ const items = this.mergedItems();
81
+ for (const item of items) {
82
+ if (item.shortcut) {
83
+ this.#validateShortcut(item.shortcut);
84
+ }
85
+ }
86
+ }, /* @ts-ignore */
87
+ ...(ngDevMode ? [{ debugName: "validateItemsEffect" }] : /* istanbul ignore next */ []));
88
+ // Sync initial search query if provided in dialog data
89
+ effect(() => {
90
+ const initialQuery = this.data()?.searchQuery;
91
+ if (initialQuery !== undefined) {
92
+ this.searchQuery.set(initialQuery);
93
+ }
94
+ });
95
+ // Auto-focus input when the view is initialized
96
+ effect(() => {
97
+ const inputEl = this.inputRef()?.nativeElement;
98
+ if (inputEl && typeof inputEl.focus === 'function') {
99
+ setTimeout(() => inputEl.focus(), 50);
100
+ }
101
+ });
102
+ // Scroll active item into view
103
+ effect(() => {
104
+ const index = this.activeOptionIndex();
105
+ if (index > -1) {
106
+ queueMicrotask(() => this.scrollToActiveItem());
107
+ }
108
+ });
109
+ }
110
+ onSearchInput(event) {
111
+ const val = event.target.value;
112
+ this.searchQuery.set(val);
113
+ this.activeOptionIndex.set(0);
114
+ }
115
+ clearSearch() {
116
+ this.searchQuery.set('');
117
+ const inputEl = this.inputRef()?.nativeElement;
118
+ if (inputEl) {
119
+ inputEl.value = '';
120
+ if (typeof inputEl.focus === 'function') {
121
+ inputEl.focus();
122
+ }
123
+ }
124
+ this.activeOptionIndex.set(0);
125
+ }
126
+ onKeyDown(event) {
127
+ // Check if the event matches any item shortcut
128
+ const allItems = this.mergedItems();
129
+ const shortcutMatch = allItems.find((item) => item.shortcut && this.#checkShortcutMatch(event, item.shortcut));
130
+ if (shortcutMatch) {
131
+ event.preventDefault();
132
+ event.stopPropagation();
133
+ // Delay selection to ensure the browser honors preventDefault before the DOM node is potentially destroyed
134
+ setTimeout(() => this.selectItem(shortcutMatch), 10);
135
+ return;
136
+ }
137
+ const flat = this.flatFilteredItems();
138
+ if (flat.length === 0)
139
+ return;
140
+ if (event.key === 'ArrowDown') {
141
+ event.preventDefault();
142
+ const nextIdx = (this.activeOptionIndex() + 1) % flat.length;
143
+ this.activeOptionIndex.set(nextIdx);
144
+ }
145
+ else if (event.key === 'ArrowUp') {
146
+ event.preventDefault();
147
+ const prevIdx = (this.activeOptionIndex() - 1 + flat.length) % flat.length;
148
+ this.activeOptionIndex.set(prevIdx);
149
+ }
150
+ else if (event.key === 'Tab') {
151
+ event.preventDefault();
152
+ if (event.shiftKey) {
153
+ const prevIdx = (this.activeOptionIndex() - 1 + flat.length) % flat.length;
154
+ this.activeOptionIndex.set(prevIdx);
155
+ }
156
+ else {
157
+ const nextIdx = (this.activeOptionIndex() + 1) % flat.length;
158
+ this.activeOptionIndex.set(nextIdx);
159
+ }
160
+ }
161
+ else if (event.key === 'Enter') {
162
+ event.preventDefault();
163
+ const selected = flat[this.activeOptionIndex()];
164
+ if (selected) {
165
+ this.selectItem(selected);
166
+ }
167
+ }
168
+ }
169
+ selectItem(item) {
170
+ this.itemSelected.emit(item);
171
+ this.closed.emit();
172
+ }
173
+ scrollToActiveItem() {
174
+ const resultsEl = this.resultsRef()?.nativeElement;
175
+ if (!resultsEl)
176
+ return;
177
+ const activeEl = resultsEl.querySelector('[action].active');
178
+ if (activeEl && typeof activeEl.scrollIntoView === 'function') {
179
+ activeEl.scrollIntoView({ block: 'nearest' });
180
+ }
181
+ }
182
+ parseShortcutKeys(shortcut) {
183
+ if (!shortcut)
184
+ return [];
185
+ return shortcut.split('+').map((s) => s.trim().toLowerCase());
186
+ }
187
+ #validateShortcut(shortcut) {
188
+ const normalized = shortcut
189
+ .toLowerCase()
190
+ .split('+')
191
+ .map((k) => k.trim())
192
+ .join('+');
193
+ const reserved = [
194
+ 'meta+n', 'ctrl+n',
195
+ 'meta+t', 'ctrl+t',
196
+ 'meta+w', 'ctrl+w',
197
+ 'meta+q', 'ctrl+q',
198
+ 'meta+r', 'ctrl+r',
199
+ 'meta+l', 'ctrl+l',
200
+ 'meta+shift+n', 'ctrl+shift+n',
201
+ 'meta+shift+t', 'ctrl+shift+t',
202
+ 'meta+shift+w', 'ctrl+shift+w',
203
+ 'cmd+n', 'cmd+t', 'cmd+w', 'cmd+q', 'cmd+r', 'cmd+l',
204
+ 'cmd+shift+n', 'cmd+shift+t', 'cmd+shift+w',
205
+ ];
206
+ if (reserved.includes(normalized)) {
207
+ throw new Error(`[ShipUI Error] The shortcut "${shortcut}" uses a reserved browser hotkey that cannot be reliably prevented (e.g. New Window, New Tab). Please choose a different shortcut.`);
208
+ }
209
+ }
210
+ #checkShortcutMatch(event, shortcut) {
211
+ if (!shortcut)
212
+ return false;
213
+ const keys = this.parseShortcutKeys(shortcut);
214
+ const needsMeta = keys.includes('meta') || keys.includes('cmd') || keys.includes('command');
215
+ const needsCtrl = keys.includes('ctrl') || keys.includes('control');
216
+ const needsAlt = keys.includes('alt') || keys.includes('option');
217
+ const needsShift = keys.includes('shift');
218
+ if (event.metaKey !== needsMeta)
219
+ return false;
220
+ if (event.ctrlKey !== needsCtrl)
221
+ return false;
222
+ if (event.altKey !== needsAlt)
223
+ return false;
224
+ if (event.shiftKey !== needsShift)
225
+ return false;
226
+ const mainKeys = keys.filter((k) => !['meta', 'cmd', 'command', 'ctrl', 'control', 'alt', 'option', 'shift'].includes(k));
227
+ if (mainKeys.length === 1) {
228
+ return event.key.toLowerCase() === mainKeys[0].toLowerCase();
229
+ }
230
+ return false;
231
+ }
232
+ #calculateMatchScore(option, input) {
233
+ if (!input)
234
+ return 0;
235
+ let score = 0;
236
+ let lastIndex = -1;
237
+ let matchCount = 0;
238
+ let inSequence = true;
239
+ for (let i = 0; i < input.length; i++) {
240
+ const char = input[i];
241
+ if (option.length > lastIndex + 1 && option[lastIndex + 1] === char) {
242
+ score += i === 0 ? 100 : 150;
243
+ lastIndex++;
244
+ matchCount++;
245
+ }
246
+ else {
247
+ const charIndex = option.indexOf(char, lastIndex + 1);
248
+ if (i > 0) {
249
+ inSequence = false;
250
+ }
251
+ if (charIndex === -1) {
252
+ return 0;
253
+ }
254
+ score += 100;
255
+ lastIndex = charIndex;
256
+ matchCount++;
257
+ }
258
+ }
259
+ if (inSequence && input.length === matchCount) {
260
+ score += 1000;
261
+ }
262
+ score += matchCount * 20;
263
+ return score;
264
+ }
265
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ShipSpotlight, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
266
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: ShipSpotlight, isStandalone: true, selector: "sh-spotlight", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, customFilter: { classPropertyName: "customFilter", publicName: "customFilter", isSignal: true, isRequired: false, transformFunction: null }, searchQuery: { classPropertyName: "searchQuery", publicName: "searchQuery", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { searchQuery: "searchQueryChange", itemSelected: "itemSelected", closed: "closed" }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputRef"], descendants: true, isSignal: true }, { propertyName: "resultsRef", first: true, predicate: ["resultsRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
267
+ <div class="sh-spotlight-container">
268
+ <div class="sh-spotlight-search-bar">
269
+ <sh-icon class="sh-spotlight-search-icon">magnifying-glass</sh-icon>
270
+ <input
271
+ #inputRef
272
+ type="text"
273
+ class="sh-spotlight-input"
274
+ [placeholder]="mergedPlaceholder()"
275
+ [value]="searchQuery()"
276
+ (input)="onSearchInput($event)"
277
+ (keydown)="onKeyDown($event)" />
278
+ @if (searchQuery()) {
279
+ <button class="sh-spotlight-clear-btn" (click)="clearSearch()">
280
+ <sh-icon>x-bold</sh-icon>
281
+ </button>
282
+ }
283
+ </div>
284
+
285
+ <div #resultsRef class="sh-spotlight-results">
286
+ @if (groupedFilteredItems().length > 0) {
287
+ @for (group of groupedFilteredItems(); track group.category) {
288
+ <div class="sh-spotlight-category">
289
+ <div class="sh-spotlight-category-title">{{ group.category }}</div>
290
+ <sh-list class="type-b">
291
+ @for (itemWithIdx of group.items; track itemWithIdx.item.id) {
292
+ <button
293
+ action
294
+ type="button"
295
+ [class.active]="itemWithIdx.flatIndex === activeOptionIndex()"
296
+ (mouseenter)="activeOptionIndex.set(itemWithIdx.flatIndex)"
297
+ (click)="selectItem(itemWithIdx.item)">
298
+ @if (itemWithIdx.item.icon) {
299
+ <sh-icon>{{ itemWithIdx.item.icon }}</sh-icon>
300
+ }
301
+ <div class="text-group">
302
+ <span class="label">{{ itemWithIdx.item.label }}</span>
303
+ @if (itemWithIdx.item.description) {
304
+ <span class="description">{{ itemWithIdx.item.description }}</span>
305
+ }
306
+ </div>
307
+ @if (itemWithIdx.item.shortcut) {
308
+ <span class="shortcut">
309
+ @for (key of parseShortcutKeys(itemWithIdx.item.shortcut); track key) {
310
+ <sh-kbd
311
+ [meta]="key === 'meta' || key === 'cmd' || key === 'command'"
312
+ [shift]="key === 'shift'"
313
+ [alt]="key === 'alt' || key === 'option'"
314
+ [ctrl]="key === 'ctrl' || key === 'control'"
315
+ [enter]="key === 'enter' || key === 'return'"
316
+ [escape]="key === 'escape' || key === 'esc'"
317
+ [backspace]="key === 'backspace'"
318
+ >
319
+ @if (!['meta', 'cmd', 'command', 'shift', 'alt', 'option', 'ctrl', 'control', 'enter', 'return', 'escape', 'esc', 'backspace'].includes(key)) {
320
+ {{ key }}
321
+ }
322
+ </sh-kbd>
323
+ }
324
+ </span>
325
+ }
326
+ </button>
327
+ }
328
+ </sh-list>
329
+ </div>
330
+ }
331
+ } @else {
332
+ <div class="sh-spotlight-no-results">
333
+ <sh-icon class="sh-spotlight-no-results-icon">warning-octagon</sh-icon>
334
+ <span class="sh-spotlight-no-results-text">No results found for "{{ searchQuery() }}"</span>
335
+ </div>
336
+ }
337
+ </div>
338
+
339
+ <div class="sh-spotlight-footer">
340
+ <span class="sh-spotlight-footer-tip">
341
+ <sh-kbd>↓</sh-kbd> <sh-kbd>↑</sh-kbd>
342
+ to navigate
343
+ </span>
344
+ <span class="sh-spotlight-footer-tip">
345
+ <sh-kbd enter></sh-kbd>
346
+ to select
347
+ </span>
348
+ <span class="sh-spotlight-footer-tip">
349
+ <sh-kbd escape></sh-kbd>
350
+ to close
351
+ </span>
352
+ </div>
353
+ </div>
354
+ `, isInline: true, styles: ["sh-spotlight{display:block;width:100%}sh-spotlight .sh-spotlight-container{display:flex;flex-direction:column;width:100%;overflow:hidden}sh-spotlight .sh-spotlight-search-bar{display:flex;align-items:center;padding:1rem;border-bottom:1px solid rgb(from var(--base-4) r g b/30%);gap:.75rem}sh-spotlight .sh-spotlight-search-icon{font-size:1.25rem;color:var(--base-8)}sh-spotlight .sh-spotlight-input{flex:1;background:transparent;border:none;outline:none;font:var(--paragraph-20);color:var(--base-12)}sh-spotlight .sh-spotlight-input::placeholder{color:var(--base-7)}sh-spotlight .sh-spotlight-clear-btn{background:transparent;border:none;padding:.25rem;cursor:pointer;color:var(--base-7);display:flex;align-items:center;justify-content:center;border-radius:var(--shape-1)}sh-spotlight .sh-spotlight-clear-btn:hover{background:var(--base-3);color:var(--base-10)}sh-spotlight .sh-spotlight-clear-btn sh-icon{font-size:.875rem}sh-spotlight .sh-spotlight-results{max-height:21.875rem;padding:.5rem;display:flex;flex-direction:column;gap:.5rem;outline:none;overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:auto}sh-spotlight .sh-spotlight-category-title{font:var(--paragraph-40B);color:var(--base-7);padding:.5rem .75rem .25rem;text-transform:uppercase;letter-spacing:.0625rem}sh-spotlight .sh-spotlight-no-results{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2.5rem 1.25rem;color:var(--base-7);gap:.75rem}sh-spotlight .sh-spotlight-no-results-icon{font-size:2rem;color:var(--base-6)}sh-spotlight .sh-spotlight-no-results-text{font:var(--paragraph-30)}sh-spotlight .sh-spotlight-footer{display:flex;align-items:center;padding:.75rem 1rem;background:rgb(from var(--base-1) r g b/90%);border-top:1px solid rgb(from var(--base-4) r g b/30%);gap:1rem;font-size:.6875rem;color:var(--base-7)}sh-spotlight .sh-spotlight-footer .sh-spotlight-footer-tip{display:flex;align-items:center;gap:.25rem}\n"], dependencies: [{ kind: "component", type: ShipIcon, selector: "sh-icon", inputs: ["color", "size"] }, { kind: "component", type: ShipList, selector: "sh-list" }, { kind: "component", type: ShipKbd, selector: "sh-kbd, [sh-kbd]", inputs: ["meta", "shift", "alt", "ctrl", "enter", "escape", "backspace"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
355
+ }
356
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ShipSpotlight, decorators: [{
357
+ type: Component,
358
+ args: [{ selector: 'sh-spotlight', encapsulation: ViewEncapsulation.None, imports: [ShipIcon, ShipList, ShipKbd], template: `
359
+ <div class="sh-spotlight-container">
360
+ <div class="sh-spotlight-search-bar">
361
+ <sh-icon class="sh-spotlight-search-icon">magnifying-glass</sh-icon>
362
+ <input
363
+ #inputRef
364
+ type="text"
365
+ class="sh-spotlight-input"
366
+ [placeholder]="mergedPlaceholder()"
367
+ [value]="searchQuery()"
368
+ (input)="onSearchInput($event)"
369
+ (keydown)="onKeyDown($event)" />
370
+ @if (searchQuery()) {
371
+ <button class="sh-spotlight-clear-btn" (click)="clearSearch()">
372
+ <sh-icon>x-bold</sh-icon>
373
+ </button>
374
+ }
375
+ </div>
376
+
377
+ <div #resultsRef class="sh-spotlight-results">
378
+ @if (groupedFilteredItems().length > 0) {
379
+ @for (group of groupedFilteredItems(); track group.category) {
380
+ <div class="sh-spotlight-category">
381
+ <div class="sh-spotlight-category-title">{{ group.category }}</div>
382
+ <sh-list class="type-b">
383
+ @for (itemWithIdx of group.items; track itemWithIdx.item.id) {
384
+ <button
385
+ action
386
+ type="button"
387
+ [class.active]="itemWithIdx.flatIndex === activeOptionIndex()"
388
+ (mouseenter)="activeOptionIndex.set(itemWithIdx.flatIndex)"
389
+ (click)="selectItem(itemWithIdx.item)">
390
+ @if (itemWithIdx.item.icon) {
391
+ <sh-icon>{{ itemWithIdx.item.icon }}</sh-icon>
392
+ }
393
+ <div class="text-group">
394
+ <span class="label">{{ itemWithIdx.item.label }}</span>
395
+ @if (itemWithIdx.item.description) {
396
+ <span class="description">{{ itemWithIdx.item.description }}</span>
397
+ }
398
+ </div>
399
+ @if (itemWithIdx.item.shortcut) {
400
+ <span class="shortcut">
401
+ @for (key of parseShortcutKeys(itemWithIdx.item.shortcut); track key) {
402
+ <sh-kbd
403
+ [meta]="key === 'meta' || key === 'cmd' || key === 'command'"
404
+ [shift]="key === 'shift'"
405
+ [alt]="key === 'alt' || key === 'option'"
406
+ [ctrl]="key === 'ctrl' || key === 'control'"
407
+ [enter]="key === 'enter' || key === 'return'"
408
+ [escape]="key === 'escape' || key === 'esc'"
409
+ [backspace]="key === 'backspace'"
410
+ >
411
+ @if (!['meta', 'cmd', 'command', 'shift', 'alt', 'option', 'ctrl', 'control', 'enter', 'return', 'escape', 'esc', 'backspace'].includes(key)) {
412
+ {{ key }}
413
+ }
414
+ </sh-kbd>
415
+ }
416
+ </span>
417
+ }
418
+ </button>
419
+ }
420
+ </sh-list>
421
+ </div>
422
+ }
423
+ } @else {
424
+ <div class="sh-spotlight-no-results">
425
+ <sh-icon class="sh-spotlight-no-results-icon">warning-octagon</sh-icon>
426
+ <span class="sh-spotlight-no-results-text">No results found for "{{ searchQuery() }}"</span>
427
+ </div>
428
+ }
429
+ </div>
430
+
431
+ <div class="sh-spotlight-footer">
432
+ <span class="sh-spotlight-footer-tip">
433
+ <sh-kbd>↓</sh-kbd> <sh-kbd>↑</sh-kbd>
434
+ to navigate
435
+ </span>
436
+ <span class="sh-spotlight-footer-tip">
437
+ <sh-kbd enter></sh-kbd>
438
+ to select
439
+ </span>
440
+ <span class="sh-spotlight-footer-tip">
441
+ <sh-kbd escape></sh-kbd>
442
+ to close
443
+ </span>
444
+ </div>
445
+ </div>
446
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["sh-spotlight{display:block;width:100%}sh-spotlight .sh-spotlight-container{display:flex;flex-direction:column;width:100%;overflow:hidden}sh-spotlight .sh-spotlight-search-bar{display:flex;align-items:center;padding:1rem;border-bottom:1px solid rgb(from var(--base-4) r g b/30%);gap:.75rem}sh-spotlight .sh-spotlight-search-icon{font-size:1.25rem;color:var(--base-8)}sh-spotlight .sh-spotlight-input{flex:1;background:transparent;border:none;outline:none;font:var(--paragraph-20);color:var(--base-12)}sh-spotlight .sh-spotlight-input::placeholder{color:var(--base-7)}sh-spotlight .sh-spotlight-clear-btn{background:transparent;border:none;padding:.25rem;cursor:pointer;color:var(--base-7);display:flex;align-items:center;justify-content:center;border-radius:var(--shape-1)}sh-spotlight .sh-spotlight-clear-btn:hover{background:var(--base-3);color:var(--base-10)}sh-spotlight .sh-spotlight-clear-btn sh-icon{font-size:.875rem}sh-spotlight .sh-spotlight-results{max-height:21.875rem;padding:.5rem;display:flex;flex-direction:column;gap:.5rem;outline:none;overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:auto}sh-spotlight .sh-spotlight-category-title{font:var(--paragraph-40B);color:var(--base-7);padding:.5rem .75rem .25rem;text-transform:uppercase;letter-spacing:.0625rem}sh-spotlight .sh-spotlight-no-results{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2.5rem 1.25rem;color:var(--base-7);gap:.75rem}sh-spotlight .sh-spotlight-no-results-icon{font-size:2rem;color:var(--base-6)}sh-spotlight .sh-spotlight-no-results-text{font:var(--paragraph-30)}sh-spotlight .sh-spotlight-footer{display:flex;align-items:center;padding:.75rem 1rem;background:rgb(from var(--base-1) r g b/90%);border-top:1px solid rgb(from var(--base-4) r g b/30%);gap:1rem;font-size:.6875rem;color:var(--base-7)}sh-spotlight .sh-spotlight-footer .sh-spotlight-footer-tip{display:flex;align-items:center;gap:.25rem}\n"] }]
447
+ }], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['inputRef', { isSignal: true }] }], resultsRef: [{ type: i0.ViewChild, args: ['resultsRef', { isSignal: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], customFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "customFilter", required: false }] }], searchQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchQuery", required: false }] }, { type: i0.Output, args: ["searchQueryChange"] }], itemSelected: [{ type: i0.Output, args: ["itemSelected"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
448
+
449
+ class ShipSpotlightService {
450
+ #document;
451
+ #dialogService;
452
+ #config;
453
+ #nextId;
454
+ #registries;
455
+ #globalItemSelected;
456
+ #isShortcutsEnabled;
457
+ #aggregatedItems;
458
+ constructor() {
459
+ this.#document = inject(DOCUMENT);
460
+ this.#dialogService = inject(ShipDialogService);
461
+ this.#config = inject(SHIP_SPOTLIGHT_CONFIG, { optional: true });
462
+ this.#nextId = 0;
463
+ this.#registries = signal([], /* @ts-ignore */
464
+ ...(ngDevMode ? [{ debugName: "#registries" }] : /* istanbul ignore next */ []));
465
+ this.#globalItemSelected = signal(null, /* @ts-ignore */
466
+ ...(ngDevMode ? [{ debugName: "#globalItemSelected" }] : /* istanbul ignore next */ []));
467
+ this.globalItemSelected = this.#globalItemSelected.asReadonly();
468
+ this.#isShortcutsEnabled = signal(false, /* @ts-ignore */
469
+ ...(ngDevMode ? [{ debugName: "#isShortcutsEnabled" }] : /* istanbul ignore next */ []));
470
+ this.isShortcutsEnabled = this.#isShortcutsEnabled.asReadonly();
471
+ this.#aggregatedItems = computed(() => {
472
+ const registries = this.#registries();
473
+ const defaults = this.#config?.defaultItems ?? [];
474
+ let aggregated = [...defaults];
475
+ for (const entry of registries) {
476
+ if (entry.overwrite) {
477
+ aggregated = [...entry.items];
478
+ }
479
+ else {
480
+ aggregated.push(...entry.items);
481
+ }
482
+ }
483
+ return aggregated;
484
+ }, /* @ts-ignore */
485
+ ...(ngDevMode ? [{ debugName: "#aggregatedItems" }] : /* istanbul ignore next */ []));
486
+ this.hasOverwriteItems = computed(() => this.#registries().some((r) => r.overwrite), /* @ts-ignore */
487
+ ...(ngDevMode ? [{ debugName: "hasOverwriteItems" }] : /* istanbul ignore next */ []));
488
+ this.#contextualRegistryId = null;
489
+ this.#globalShortcutListener = null;
490
+ this.#globalShortcutOptions = null;
491
+ if (this.#config?.enableShortcuts !== false) {
492
+ this.enableGlobalShortcuts();
493
+ }
494
+ }
495
+ registerItems(items, overwrite = false) {
496
+ const id = this.#nextId++;
497
+ this.#registries.update((regs) => [...regs, { id, items, overwrite }]);
498
+ const cleanup = () => {
499
+ this.#registries.update((regs) => regs.filter((r) => r.id !== id));
500
+ };
501
+ try {
502
+ const destroyRef = inject(DestroyRef);
503
+ destroyRef.onDestroy(cleanup);
504
+ }
505
+ catch {
506
+ // Ignore if not called in injection context
507
+ }
508
+ return cleanup;
509
+ }
510
+ #contextualRegistryId;
511
+ setContextualItems(items, overwrite = false) {
512
+ if (this.#contextualRegistryId !== null) {
513
+ const oldId = this.#contextualRegistryId;
514
+ this.#registries.update((regs) => regs.filter((r) => r.id !== oldId));
515
+ }
516
+ const id = this.#nextId++;
517
+ this.#contextualRegistryId = id;
518
+ this.#registries.update((regs) => [...regs, { id, items, overwrite }]);
519
+ }
520
+ clearContextualItems() {
521
+ if (this.#contextualRegistryId !== null) {
522
+ const oldId = this.#contextualRegistryId;
523
+ this.#registries.update((regs) => regs.filter((r) => r.id !== oldId));
524
+ this.#contextualRegistryId = null;
525
+ }
526
+ }
527
+ #globalShortcutListener;
528
+ #globalShortcutOptions;
529
+ enableGlobalShortcuts(options) {
530
+ if (this.#globalShortcutListener) {
531
+ this.disableGlobalShortcuts();
532
+ }
533
+ if (options) {
534
+ this.#globalShortcutOptions = options;
535
+ }
536
+ this.#isShortcutsEnabled.set(true);
537
+ this.#globalShortcutListener = (event) => {
538
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
539
+ event.preventDefault();
540
+ const instance = this.open(this.#globalShortcutOptions || undefined);
541
+ const sub = instance.itemSelected.subscribe((item) => {
542
+ this.#globalItemSelected.set(item);
543
+ });
544
+ instance.closed.subscribe(() => sub.unsubscribe());
545
+ }
546
+ };
547
+ this.#document.addEventListener('keydown', this.#globalShortcutListener);
548
+ }
549
+ disableGlobalShortcuts() {
550
+ if (this.#globalShortcutListener) {
551
+ this.#document.removeEventListener('keydown', this.#globalShortcutListener);
552
+ this.#globalShortcutListener = null;
553
+ }
554
+ this.#isShortcutsEnabled.set(false);
555
+ }
556
+ open(options) {
557
+ const finalOptions = {
558
+ ...options,
559
+ items: options?.items ?? this.#aggregatedItems(),
560
+ };
561
+ const dialogRef = this.#dialogService.open(ShipSpotlight, {
562
+ data: finalOptions,
563
+ class: 'spotlight-dialog',
564
+ closeOnOutsideClick: true,
565
+ closeOnEsc: true,
566
+ width: '600px',
567
+ maxWidth: '90vw',
568
+ });
569
+ return {
570
+ close: () => dialogRef.close(),
571
+ itemSelected: dialogRef.component.itemSelected,
572
+ closed: dialogRef.closed,
573
+ };
574
+ }
575
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ShipSpotlightService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
576
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ShipSpotlightService, providedIn: 'root' }); }
577
+ }
578
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: ShipSpotlightService, decorators: [{
579
+ type: Injectable,
580
+ args: [{
581
+ providedIn: 'root',
582
+ }]
583
+ }], ctorParameters: () => [] });
584
+
585
+ /**
586
+ * Generated bundle index. Do not edit.
587
+ */
588
+
589
+ export { SHIP_SPOTLIGHT_CONFIG, ShipSpotlight, ShipSpotlightService, provideShipSpotlight };
590
+ //# sourceMappingURL=ship-ui-core-ship-spotlight.mjs.map