@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.
- package/assets/mcp/components.json +32 -32
- package/bin/ship-fg-scanner +0 -0
- package/fesm2022/ship-ui-core-ship-blueprint.mjs +1 -1
- package/fesm2022/ship-ui-core-ship-blueprint.mjs.map +1 -1
- package/fesm2022/ship-ui-core-ship-button-group.mjs +2 -2
- package/fesm2022/ship-ui-core-ship-button-group.mjs.map +1 -1
- package/fesm2022/ship-ui-core-ship-button.mjs +6 -4
- package/fesm2022/ship-ui-core-ship-button.mjs.map +1 -1
- package/fesm2022/ship-ui-core-ship-chip.mjs +6 -4
- package/fesm2022/ship-ui-core-ship-chip.mjs.map +1 -1
- package/fesm2022/ship-ui-core-ship-color-picker.mjs +1 -1
- package/fesm2022/ship-ui-core-ship-color-picker.mjs.map +1 -1
- package/fesm2022/ship-ui-core-ship-kbd.mjs +96 -0
- package/fesm2022/ship-ui-core-ship-kbd.mjs.map +1 -0
- package/fesm2022/ship-ui-core-ship-list.mjs +2 -2
- package/fesm2022/ship-ui-core-ship-list.mjs.map +1 -1
- package/fesm2022/ship-ui-core-ship-select.mjs +22 -3
- package/fesm2022/ship-ui-core-ship-select.mjs.map +1 -1
- package/fesm2022/ship-ui-core-ship-spotlight.mjs +590 -0
- package/fesm2022/ship-ui-core-ship-spotlight.mjs.map +1 -0
- package/fesm2022/ship-ui-core-ship-table.mjs +12 -6
- package/fesm2022/ship-ui-core-ship-table.mjs.map +1 -1
- package/fesm2022/ship-ui-core-ship-theme-toggle.mjs +1 -1
- package/fesm2022/ship-ui-core-ship-theme-toggle.mjs.map +1 -1
- package/package.json +9 -1
- package/types/ship-ui-core-ship-button.d.ts +2 -1
- package/types/ship-ui-core-ship-chip.d.ts +2 -1
- package/types/ship-ui-core-ship-kbd.d.ts +19 -0
- package/types/ship-ui-core-ship-select.d.ts +1 -0
- package/types/ship-ui-core-ship-spotlight.d.ts +83 -0
- 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
|