@stnd/launcher 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Launcher.svelte +1067 -0
- package/README.md +177 -0
- package/launcher.ts +577 -0
- package/package.json +27 -0
package/Launcher.svelte
ADDED
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* @stnd/launcher - The Universal Command Palette UI
|
|
4
|
+
*
|
|
5
|
+
* Integrated with the Standard Garden OS aesthetic.
|
|
6
|
+
*/
|
|
7
|
+
import Icon from "@stnd/icon/Icon.svelte";
|
|
8
|
+
import Avatar from "@stnd/account/ui/Avatar.svelte";
|
|
9
|
+
import {
|
|
10
|
+
registry,
|
|
11
|
+
palette,
|
|
12
|
+
search,
|
|
13
|
+
computeMatches,
|
|
14
|
+
execute as engineExecute,
|
|
15
|
+
launcherToggle,
|
|
16
|
+
launcherView,
|
|
17
|
+
launcherClose,
|
|
18
|
+
bootLauncher,
|
|
19
|
+
setSize,
|
|
20
|
+
activeView,
|
|
21
|
+
windowSize,
|
|
22
|
+
} from "./launcher.ts";
|
|
23
|
+
|
|
24
|
+
import { onMount, tick } from "svelte";
|
|
25
|
+
import { get } from "svelte/store";
|
|
26
|
+
import { fade, blurFade, scaleBlurFade } from "@stnd/ui/transitions.js";
|
|
27
|
+
import { quintOut } from "svelte/easing";
|
|
28
|
+
import { inject } from "@stnd/store";
|
|
29
|
+
import { registerShortcut } from "@stnd/client/keyboard";
|
|
30
|
+
import Log from "@stnd/log";
|
|
31
|
+
|
|
32
|
+
const log = Log({ scope: "Launcher" });
|
|
33
|
+
|
|
34
|
+
// --- PROPS (all optional — the Launcher is fully self-booting) ---
|
|
35
|
+
// Apps wire services via provide/inject (the Standard OS runtime store).
|
|
36
|
+
// Props exist only for backward compat or one-off overrides.
|
|
37
|
+
let { WelcomeView = null, load = null, hideLogin = false } = $props();
|
|
38
|
+
|
|
39
|
+
// Check global window variable as fallback (set via window.__HIDE_LAUNCHER_LOGIN__)
|
|
40
|
+
let shouldHideLogin = $derived(
|
|
41
|
+
hideLogin ||
|
|
42
|
+
(typeof window !== "undefined" && window.__HIDE_LAUNCHER_LOGIN__),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// --- STATE (Synced with Engine) ---
|
|
46
|
+
let pState = $state({
|
|
47
|
+
open: false,
|
|
48
|
+
view: "search",
|
|
49
|
+
query: "",
|
|
50
|
+
contexts: ["global"],
|
|
51
|
+
props: {},
|
|
52
|
+
});
|
|
53
|
+
let registryState = $state({ commands: [], views: {} });
|
|
54
|
+
let searchResults = $state([]);
|
|
55
|
+
let isSearching = $state(false);
|
|
56
|
+
let activeIndex = $state(0);
|
|
57
|
+
let inputEl = $state();
|
|
58
|
+
let launcherEl = $state();
|
|
59
|
+
let itemsContainer = $state();
|
|
60
|
+
let debounceTimer;
|
|
61
|
+
let currentUser = $state(null);
|
|
62
|
+
let dynamicSize = $state(null);
|
|
63
|
+
// --- UI State for header hover ---
|
|
64
|
+
let isHeaderHovered = $state(false);
|
|
65
|
+
let isSearchHovered = $state(false);
|
|
66
|
+
let isInputFocused = $state(false);
|
|
67
|
+
let contentHeight = $state(0);
|
|
68
|
+
|
|
69
|
+
// --- Derived: Current View Title ---
|
|
70
|
+
let currentViewTitle = $derived.by(() => {
|
|
71
|
+
if (pState.view === "search") return "";
|
|
72
|
+
const view = registryState.views[pState.view];
|
|
73
|
+
if (view && view.meta && view.meta.title) return view.meta.title;
|
|
74
|
+
const cmd = registryState.commands.find((c) => c.trigger === pState.view);
|
|
75
|
+
if (cmd && cmd.title) return cmd.title;
|
|
76
|
+
return pState.view || "";
|
|
77
|
+
});
|
|
78
|
+
let showInput = $derived(
|
|
79
|
+
!currentViewTitle || isSearchHovered || isInputFocused,
|
|
80
|
+
);
|
|
81
|
+
// Remote state sync
|
|
82
|
+
let coreActiveView = $state(null);
|
|
83
|
+
let coreWindowSize = $state({ width: "standard", height: "standard" });
|
|
84
|
+
|
|
85
|
+
// Subscribe to user store from the Standard OS runtime store
|
|
86
|
+
$effect(() => {
|
|
87
|
+
const store = inject("user", false);
|
|
88
|
+
if (store && typeof store.subscribe === "function") {
|
|
89
|
+
const unsub = store.subscribe((u) => {
|
|
90
|
+
currentUser = u;
|
|
91
|
+
});
|
|
92
|
+
return unsub;
|
|
93
|
+
} else if (typeof window !== "undefined" && window.__INITIAL_USER__) {
|
|
94
|
+
// Fallback: mutate plain object before assigning to avoid reading
|
|
95
|
+
// the reactive $state proxy inside the effect (would cause a loop)
|
|
96
|
+
const initialUser = window.__INITIAL_USER__;
|
|
97
|
+
if (initialUser && initialUser.isAuthenticated === undefined) {
|
|
98
|
+
initialUser.isAuthenticated = true;
|
|
99
|
+
}
|
|
100
|
+
currentUser = initialUser;
|
|
101
|
+
} else {
|
|
102
|
+
currentUser = null;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Scroll active item into view
|
|
107
|
+
$effect(() => {
|
|
108
|
+
// Dependency on activeIndex to trigger
|
|
109
|
+
activeIndex;
|
|
110
|
+
if (itemsContainer) {
|
|
111
|
+
const activeEl = itemsContainer.querySelector(
|
|
112
|
+
".std-launcher-item.active",
|
|
113
|
+
);
|
|
114
|
+
if (activeEl) {
|
|
115
|
+
activeEl.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// --- SYNC WITH ENGINE ---
|
|
121
|
+
onMount(() => {
|
|
122
|
+
// 1. Boot the engine — discovers extensions, registers commands/views.
|
|
123
|
+
// App-specific services (user store, search) are already available
|
|
124
|
+
// via inject() from the Standard OS runtime store.
|
|
125
|
+
bootLauncher();
|
|
126
|
+
|
|
127
|
+
// 2. Subscribe to palette state (open, view, query)
|
|
128
|
+
const unsubPalette = palette.subscribe((v) => {
|
|
129
|
+
pState = v;
|
|
130
|
+
if (v.open) {
|
|
131
|
+
// Focus input when opened
|
|
132
|
+
tick().then(() => inputEl?.focus());
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const unsubRegistry = registry.subscribe((v) => {
|
|
137
|
+
registryState = v;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const unsubAV = activeView.subscribe((v) => (coreActiveView = v));
|
|
141
|
+
const unsubWS = windowSize.subscribe((v) => (coreWindowSize = v));
|
|
142
|
+
|
|
143
|
+
// 3. Register Default Shortcuts
|
|
144
|
+
const shortcuts = [
|
|
145
|
+
registerShortcut({
|
|
146
|
+
key: "k",
|
|
147
|
+
meta: true,
|
|
148
|
+
id: "launcher-toggle-cmd-k",
|
|
149
|
+
action: () => launcherToggle(),
|
|
150
|
+
}),
|
|
151
|
+
registerShortcut({
|
|
152
|
+
key: ":",
|
|
153
|
+
shift: true,
|
|
154
|
+
id: "launcher-toggle-command-mode",
|
|
155
|
+
action: () => launcherToggle("::"),
|
|
156
|
+
}),
|
|
157
|
+
registerShortcut({
|
|
158
|
+
key: "Escape",
|
|
159
|
+
id: "launcher-escape",
|
|
160
|
+
action: () => {
|
|
161
|
+
// Only go back if not in a modal/launcher (handled by their own listeners)
|
|
162
|
+
// But for now, simple history back is the standard behavior
|
|
163
|
+
window.history.back();
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
// 4. Keyboard Shortcuts for internal navigation
|
|
169
|
+
const handleInternalKey = (e) => {
|
|
170
|
+
if (!pState.open) return;
|
|
171
|
+
|
|
172
|
+
if (e.key === "Escape") {
|
|
173
|
+
if (ActiveViewComponent && pState.view !== "search") {
|
|
174
|
+
launcherView("search");
|
|
175
|
+
} else {
|
|
176
|
+
launcherClose();
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Only handle arrows/enter if not in a custom view
|
|
182
|
+
if (ActiveViewComponent) return;
|
|
183
|
+
|
|
184
|
+
if (e.key === "ArrowDown") {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
activeIndex = (activeIndex + 1) % matches.length;
|
|
187
|
+
} else if (e.key === "ArrowUp") {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
activeIndex =
|
|
190
|
+
(activeIndex - 1 + matches.length) % (matches.length || 1);
|
|
191
|
+
} else if (e.key === "Enter") {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
if (matches[activeIndex]) {
|
|
194
|
+
execute(matches[activeIndex]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
window.addEventListener("keydown", handleInternalKey);
|
|
200
|
+
|
|
201
|
+
// ===================================
|
|
202
|
+
// GLOBAL EVENTS & DELEGATION
|
|
203
|
+
// (Restored for legacy compatibility)
|
|
204
|
+
// ===================================
|
|
205
|
+
|
|
206
|
+
const toggleLauncherHandler = (e) => {
|
|
207
|
+
const initialQuery = e.detail?.initialQuery || "";
|
|
208
|
+
launcherToggle(initialQuery);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const launcherViewHandler = (e) => {
|
|
212
|
+
const viewTrigger = e.detail?.viewTrigger;
|
|
213
|
+
if (viewTrigger) {
|
|
214
|
+
launcherView(viewTrigger);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const executeLauncherHandler = (e) => {
|
|
219
|
+
const command = e.detail?.command;
|
|
220
|
+
if (command) {
|
|
221
|
+
const cmd = registryState.commands.find((c) => c.trigger === command);
|
|
222
|
+
if (cmd) {
|
|
223
|
+
execute(cmd);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
window.addEventListener("toggle-launcher", toggleLauncherHandler);
|
|
229
|
+
window.addEventListener("open-launcher", launcherViewHandler);
|
|
230
|
+
window.addEventListener("launcher-execute-command", executeLauncherHandler);
|
|
231
|
+
|
|
232
|
+
// Sync user state via events (in case of store collision or lazy init)
|
|
233
|
+
const handleLogin = (e) => {
|
|
234
|
+
currentUser = e.detail;
|
|
235
|
+
};
|
|
236
|
+
const handleLogout = () => {
|
|
237
|
+
currentUser = null;
|
|
238
|
+
};
|
|
239
|
+
window.addEventListener("user-login", handleLogin);
|
|
240
|
+
window.addEventListener("user-logout", handleLogout);
|
|
241
|
+
window.addEventListener("visitor-login", () => {
|
|
242
|
+
if (window.__INITIAL_USER__) {
|
|
243
|
+
currentUser = window.__INITIAL_USER__;
|
|
244
|
+
if (currentUser && currentUser.isAuthenticated === undefined) {
|
|
245
|
+
currentUser.isAuthenticated = true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Global Click Delegation (data-launcher-*)
|
|
251
|
+
const dataAttributeHandler = (e) => {
|
|
252
|
+
// Find closest element with data-launcher attributes
|
|
253
|
+
const viewTrigger = e.target.closest("[data-launcher-view]");
|
|
254
|
+
const navTrigger = e.target.closest("[data-launcher-navigator]");
|
|
255
|
+
const promptTrigger = e.target.closest("[data-launcher-prompt]");
|
|
256
|
+
const toggleTrigger = e.target.closest("[data-launcher-toggle]");
|
|
257
|
+
|
|
258
|
+
if (navTrigger) {
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
const url = navTrigger.getAttribute("data-launcher-navigator");
|
|
261
|
+
launcherView("::navigator", { url });
|
|
262
|
+
} else if (viewTrigger) {
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
const trigger = viewTrigger.getAttribute("data-launcher-view");
|
|
265
|
+
|
|
266
|
+
// 1. Try to find a command first (might be an action)
|
|
267
|
+
const item = registryState.commands.find((c) => c.trigger === trigger);
|
|
268
|
+
if (item) {
|
|
269
|
+
execute(item);
|
|
270
|
+
} else {
|
|
271
|
+
// 2. Fallback to opening view directly
|
|
272
|
+
launcherView(trigger);
|
|
273
|
+
}
|
|
274
|
+
} else if (promptTrigger) {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
const promptText = promptTrigger.getAttribute("data-launcher-prompt");
|
|
277
|
+
if (promptText) launcherToggle(promptText);
|
|
278
|
+
} else if (toggleTrigger) {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
launcherToggle();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
document.addEventListener("click", dataAttributeHandler);
|
|
285
|
+
|
|
286
|
+
// Close on navigation (Astro View Transitions)
|
|
287
|
+
const closeOnNavigationHandler = () => {
|
|
288
|
+
// Always reset on navigation, regardless of open state
|
|
289
|
+
palette.set({
|
|
290
|
+
open: false,
|
|
291
|
+
query: "",
|
|
292
|
+
view: "search",
|
|
293
|
+
returnTo: null,
|
|
294
|
+
contexts: ["global"],
|
|
295
|
+
});
|
|
296
|
+
// Also ensure body scroll is unlocked
|
|
297
|
+
document.body.style.overflow = "";
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
document.addEventListener("astro:before-swap", closeOnNavigationHandler);
|
|
301
|
+
|
|
302
|
+
// Additional cleanup after swap completes
|
|
303
|
+
const afterSwapHandler = () => {
|
|
304
|
+
const current = get(palette);
|
|
305
|
+
if (current.open) {
|
|
306
|
+
palette.set({
|
|
307
|
+
open: false,
|
|
308
|
+
query: "",
|
|
309
|
+
view: "search",
|
|
310
|
+
returnTo: null,
|
|
311
|
+
contexts: ["global"],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
document.body.style.overflow = "";
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
document.addEventListener("astro:after-swap", afterSwapHandler);
|
|
318
|
+
|
|
319
|
+
// Fermeture sur mousedown à l'extérieur — gère les iframes qui avalent les événements click
|
|
320
|
+
const handleOutsideMousedown = (e) => {
|
|
321
|
+
if (!get(palette).open) return;
|
|
322
|
+
|
|
323
|
+
// Si le clic se fait sur un élément déclencheur du launcher, on le laisse gérer
|
|
324
|
+
// pour éviter les conflits (ex: le bouton de bascule ou le bouton de connexion).
|
|
325
|
+
const target = e.target;
|
|
326
|
+
if (
|
|
327
|
+
target &&
|
|
328
|
+
typeof target.closest === "function" &&
|
|
329
|
+
(target.closest("[data-launcher-toggle]") ||
|
|
330
|
+
target.closest("[data-launcher-view]") ||
|
|
331
|
+
target.closest("[data-launcher-navigator]") ||
|
|
332
|
+
target.closest("[data-launcher-prompt]"))
|
|
333
|
+
) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (launcherEl && !launcherEl.contains(e.target)) {
|
|
338
|
+
launcherClose();
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
document.addEventListener("mousedown", handleOutsideMousedown);
|
|
342
|
+
|
|
343
|
+
return () => {
|
|
344
|
+
window.removeEventListener("keydown", handleInternalKey);
|
|
345
|
+
window.removeEventListener("toggle-launcher", toggleLauncherHandler);
|
|
346
|
+
window.removeEventListener("open-launcher", launcherViewHandler);
|
|
347
|
+
window.removeEventListener(
|
|
348
|
+
"launcher-execute-command",
|
|
349
|
+
executeLauncherHandler,
|
|
350
|
+
);
|
|
351
|
+
window.removeEventListener("user-login", handleLogin);
|
|
352
|
+
window.removeEventListener("user-logout", handleLogout);
|
|
353
|
+
document.removeEventListener("click", dataAttributeHandler);
|
|
354
|
+
document.removeEventListener("mousedown", handleOutsideMousedown);
|
|
355
|
+
document.removeEventListener(
|
|
356
|
+
"astro:before-swap",
|
|
357
|
+
closeOnNavigationHandler,
|
|
358
|
+
);
|
|
359
|
+
document.removeEventListener("astro:after-swap", afterSwapHandler);
|
|
360
|
+
unsubPalette();
|
|
361
|
+
unsubRegistry();
|
|
362
|
+
unsubAV();
|
|
363
|
+
unsubWS();
|
|
364
|
+
shortcuts.forEach((unsub) => unsub && unsub());
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// --- DERIVED ---
|
|
369
|
+
let matches = $derived(
|
|
370
|
+
computeMatches(
|
|
371
|
+
pState.query,
|
|
372
|
+
registryState.commands,
|
|
373
|
+
searchResults,
|
|
374
|
+
currentUser,
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
let ActiveViewComponent = $derived.by(() => {
|
|
379
|
+
// 1. Check if it's a folio-provided view
|
|
380
|
+
if (registryState.views[pState.view]) {
|
|
381
|
+
return registryState.views[pState.view];
|
|
382
|
+
}
|
|
383
|
+
// 2. Fallback for search home: prop → ::welcome from registry → null
|
|
384
|
+
if (pState.view === "search" && !pState.query) {
|
|
385
|
+
return WelcomeView || registryState.views["::welcome"] || null;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Determine view size from registry meta
|
|
391
|
+
let activeViewSize = $derived.by(() => {
|
|
392
|
+
// 1. Dynamic Override
|
|
393
|
+
if (dynamicSize) return dynamicSize;
|
|
394
|
+
|
|
395
|
+
// 2. Registry / Default
|
|
396
|
+
if (!ActiveViewComponent) return { width: "standard", height: "standard" };
|
|
397
|
+
const cmd = registryState.commands.find(
|
|
398
|
+
(c) => c.component === ActiveViewComponent,
|
|
399
|
+
);
|
|
400
|
+
return (
|
|
401
|
+
cmd?.meta?.size || cmd?.size || { width: "standard", height: "standard" }
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Reset dynamic size when view changes
|
|
406
|
+
$effect(() => {
|
|
407
|
+
pState.view;
|
|
408
|
+
dynamicSize = null;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// --- ACTIONS ---
|
|
412
|
+
async function performSearch(q) {
|
|
413
|
+
if (!q || q.startsWith("::")) {
|
|
414
|
+
searchResults = [];
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
isSearching = true;
|
|
418
|
+
searchResults = await search(q);
|
|
419
|
+
isSearching = false;
|
|
420
|
+
activeIndex = 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function execute(item) {
|
|
424
|
+
engineExecute(item, {
|
|
425
|
+
close: launcherClose,
|
|
426
|
+
setView: launcherView,
|
|
427
|
+
views: registryState.views,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
$effect(() => {
|
|
432
|
+
clearTimeout(debounceTimer);
|
|
433
|
+
if (pState.query && !pState.query.startsWith("::")) {
|
|
434
|
+
debounceTimer = setTimeout(() => performSearch(pState.query), 200);
|
|
435
|
+
} else {
|
|
436
|
+
searchResults = [];
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Reset index when query changes
|
|
441
|
+
$effect(() => {
|
|
442
|
+
pState.query;
|
|
443
|
+
activeIndex = 0;
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Prevent body scroll when open
|
|
447
|
+
$effect(() => {
|
|
448
|
+
if (pState.open) {
|
|
449
|
+
document.body.style.overflow = "hidden";
|
|
450
|
+
} else {
|
|
451
|
+
document.body.style.overflow = "";
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Helper to resolve flexible filter prop to function
|
|
456
|
+
function resolveFilter(f) {
|
|
457
|
+
if (!f) return null;
|
|
458
|
+
if (typeof f === "function") return f;
|
|
459
|
+
|
|
460
|
+
// Allow-list mode (string or array)
|
|
461
|
+
// "::navigator, ::search" or ["::navigator", "::search"]
|
|
462
|
+
const allowed = Array.isArray(f) ? f : f.split(",").map((s) => s.trim());
|
|
463
|
+
|
|
464
|
+
return (trigger) => allowed.includes(trigger);
|
|
465
|
+
}
|
|
466
|
+
</script>
|
|
467
|
+
|
|
468
|
+
{#if pState.open}
|
|
469
|
+
<div
|
|
470
|
+
class="std-launcher-overlay"
|
|
471
|
+
transition:blurFade={{ duration: 500, maxBlur: 20, easing: quintOut }}
|
|
472
|
+
onoutrostart={(e) => (e.currentTarget.style.pointerEvents = "none")}
|
|
473
|
+
>
|
|
474
|
+
<!-- Backdrop -->
|
|
475
|
+
<div
|
|
476
|
+
class="std-launcher-backdrop"
|
|
477
|
+
role="button"
|
|
478
|
+
tabindex="0"
|
|
479
|
+
aria-label="Close launcher"
|
|
480
|
+
onclick={launcherClose}
|
|
481
|
+
onkeydown={(e) => (e.key === "Enter" || e.key === " ") && launcherClose()}
|
|
482
|
+
></div>
|
|
483
|
+
|
|
484
|
+
<!-- Main Window -->
|
|
485
|
+
<div
|
|
486
|
+
bind:this={launcherEl}
|
|
487
|
+
class="std-launcher {coreWindowSize.width}"
|
|
488
|
+
role="presentation"
|
|
489
|
+
transition:scaleBlurFade={{
|
|
490
|
+
duration: 500,
|
|
491
|
+
maxBlur: 20,
|
|
492
|
+
start: 0.95,
|
|
493
|
+
easing: quintOut,
|
|
494
|
+
}}
|
|
495
|
+
onclick={(e) => e.stopPropagation()}
|
|
496
|
+
>
|
|
497
|
+
<!-- Header -->
|
|
498
|
+
<header class="std-launcher-header">
|
|
499
|
+
<div
|
|
500
|
+
class="std-launcher-header-inner"
|
|
501
|
+
role="presentation"
|
|
502
|
+
data-has-title={currentViewTitle ? "true" : "false"}
|
|
503
|
+
>
|
|
504
|
+
<div
|
|
505
|
+
class="std-launcher-search-area"
|
|
506
|
+
role="search"
|
|
507
|
+
onmouseenter={() => (isSearchHovered = true)}
|
|
508
|
+
onmouseleave={() => (isSearchHovered = false)}
|
|
509
|
+
aria-label="Toggle launcher"
|
|
510
|
+
>
|
|
511
|
+
{#if !showInput}
|
|
512
|
+
<button class="std-launcher-search-icon icon ghost">
|
|
513
|
+
<Icon icon="search" class="text-xs" />
|
|
514
|
+
</button>
|
|
515
|
+
{/if}
|
|
516
|
+
|
|
517
|
+
<input
|
|
518
|
+
bind:this={inputEl}
|
|
519
|
+
value={pState.query}
|
|
520
|
+
oninput={(e) =>
|
|
521
|
+
palette.update((s) => ({
|
|
522
|
+
...s,
|
|
523
|
+
query: e.target.value,
|
|
524
|
+
view: e.target.value ? "search" : s.view,
|
|
525
|
+
}))}
|
|
526
|
+
onfocus={() => (isInputFocused = true)}
|
|
527
|
+
onblur={() => (isInputFocused = false)}
|
|
528
|
+
class="std-launcher-input"
|
|
529
|
+
class:hidden={!showInput}
|
|
530
|
+
placeholder="Search..."
|
|
531
|
+
autocomplete="off"
|
|
532
|
+
spellcheck="false"
|
|
533
|
+
tabindex="0"
|
|
534
|
+
aria-label="Search commands or signals"
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
{#if currentViewTitle}
|
|
538
|
+
<div
|
|
539
|
+
class="std-launcher-title mt-0"
|
|
540
|
+
role="heading"
|
|
541
|
+
aria-level="1"
|
|
542
|
+
class:opacity-0={showInput}
|
|
543
|
+
in:fade={{ duration: 500 }}
|
|
544
|
+
out:fade={{ duration: 500 }}
|
|
545
|
+
>
|
|
546
|
+
{currentViewTitle}
|
|
547
|
+
</div>
|
|
548
|
+
{/if}
|
|
549
|
+
{#if pState.view !== "search"}
|
|
550
|
+
<button
|
|
551
|
+
class="std-launcher-back-btn smaller ghost icon text-subtle m-0"
|
|
552
|
+
onclick={() => launcherView("search")}
|
|
553
|
+
aria-label="Go back"
|
|
554
|
+
transition:fade={{ duration: 500 }}
|
|
555
|
+
>
|
|
556
|
+
<Icon icon="back" />
|
|
557
|
+
</button>
|
|
558
|
+
{/if}
|
|
559
|
+
<button
|
|
560
|
+
class="std-launcher-close-btn smaller ghost icon mt-0 text-subtle"
|
|
561
|
+
style="padding:var(--trim) !important;"
|
|
562
|
+
onclick={launcherClose}
|
|
563
|
+
aria-label="Close launcher"
|
|
564
|
+
title="Close"
|
|
565
|
+
>
|
|
566
|
+
<Icon icon="close" />
|
|
567
|
+
</button>
|
|
568
|
+
</div>
|
|
569
|
+
</header>
|
|
570
|
+
|
|
571
|
+
<!-- Body -->
|
|
572
|
+
<div
|
|
573
|
+
bind:this={itemsContainer}
|
|
574
|
+
class="std-launcher-body rhythm mt-0 {coreWindowSize.height}"
|
|
575
|
+
>
|
|
576
|
+
{#if ActiveViewComponent}
|
|
577
|
+
<div
|
|
578
|
+
class="std-launcher-view"
|
|
579
|
+
in:scaleBlurFade={{ duration: 500, easing: quintOut }}
|
|
580
|
+
>
|
|
581
|
+
<ActiveViewComponent
|
|
582
|
+
close={launcherClose}
|
|
583
|
+
back={() => launcherView("search")}
|
|
584
|
+
onStartTyping={(val) => {
|
|
585
|
+
if (val) {
|
|
586
|
+
palette.update((s) => ({ ...s, query: val }));
|
|
587
|
+
}
|
|
588
|
+
inputEl?.focus();
|
|
589
|
+
}}
|
|
590
|
+
{...pState.props}
|
|
591
|
+
{setSize}
|
|
592
|
+
/>
|
|
593
|
+
</div>
|
|
594
|
+
{:else if isSearching}
|
|
595
|
+
<div class="std-launcher-empty">Searching...</div>
|
|
596
|
+
{:else if matches.length > 0}
|
|
597
|
+
<div class="no-rhythm flex-col gap-0 p-0 my-half mx-trim">
|
|
598
|
+
{#each matches as item, i (item.id || item.trigger || i)}
|
|
599
|
+
<button
|
|
600
|
+
class="std-launcher-item ghost"
|
|
601
|
+
class:active={i === activeIndex}
|
|
602
|
+
onclickcapture={() => execute(item)}
|
|
603
|
+
onmouseenter={() => (activeIndex = i)}
|
|
604
|
+
>
|
|
605
|
+
<span class="std-launcher-item-icon">
|
|
606
|
+
{#if item.icon}
|
|
607
|
+
<Icon icon={item.icon} />
|
|
608
|
+
{:else}
|
|
609
|
+
<Icon icon="note" width="100%" height="100%" />
|
|
610
|
+
{/if}
|
|
611
|
+
</span>
|
|
612
|
+
|
|
613
|
+
<div class="std-launcher-item-content flex items-center gap-xs">
|
|
614
|
+
<div class="std-launcher-item-title">
|
|
615
|
+
{item.title}
|
|
616
|
+
</div>
|
|
617
|
+
{#if item.desc}
|
|
618
|
+
<div class="std-launcher-item-desc sm:hidden">
|
|
619
|
+
{item.desc}
|
|
620
|
+
</div>
|
|
621
|
+
{/if}
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
{#if item.username && item.type === "search-result"}
|
|
625
|
+
<a
|
|
626
|
+
href="/@{item.username}"
|
|
627
|
+
class="std-launcher-item-username"
|
|
628
|
+
aria-label="View {item.username}'s profile"
|
|
629
|
+
onclick={(e) => e.stopPropagation()}
|
|
630
|
+
>
|
|
631
|
+
@{item.username}
|
|
632
|
+
</a>
|
|
633
|
+
{/if}
|
|
634
|
+
</button>
|
|
635
|
+
{/each}
|
|
636
|
+
</div>
|
|
637
|
+
{:else if pState.query}
|
|
638
|
+
<div class="std-launcher-empty">Nothing came up.</div>
|
|
639
|
+
{/if}
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<!-- Footer -->
|
|
643
|
+
<footer class="std-launcher-footer mt-0">
|
|
644
|
+
<div>
|
|
645
|
+
{#if currentUser && currentUser.isAuthenticated}
|
|
646
|
+
<div
|
|
647
|
+
class="std-launcher-user flex items-center gap-xs"
|
|
648
|
+
role="button"
|
|
649
|
+
tabindex="0"
|
|
650
|
+
onclick={() => {
|
|
651
|
+
if (registryState.views["::account-settings"]) {
|
|
652
|
+
launcherView("::account-settings");
|
|
653
|
+
} else if (registryState.views["::settings"]) {
|
|
654
|
+
launcherView("::settings");
|
|
655
|
+
} else {
|
|
656
|
+
launcherView("::my-space");
|
|
657
|
+
}
|
|
658
|
+
}}
|
|
659
|
+
onkeydown={(e) => {
|
|
660
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
661
|
+
if (registryState.views["::account-settings"]) {
|
|
662
|
+
launcherView("::account-settings");
|
|
663
|
+
} else if (registryState.views["::settings"]) {
|
|
664
|
+
launcherView("::settings");
|
|
665
|
+
} else {
|
|
666
|
+
launcherView("::my-space");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}}
|
|
670
|
+
style="cursor: pointer;"
|
|
671
|
+
>
|
|
672
|
+
<Avatar
|
|
673
|
+
src={currentUser.image}
|
|
674
|
+
name={currentUser.username || currentUser.name}
|
|
675
|
+
email={currentUser.email}
|
|
676
|
+
size="18px"
|
|
677
|
+
border={false}
|
|
678
|
+
/>
|
|
679
|
+
<span class="smaller muted">
|
|
680
|
+
{currentUser.name || currentUser.username || currentUser.email}
|
|
681
|
+
</span>
|
|
682
|
+
</div>
|
|
683
|
+
{:else if !shouldHideLogin}
|
|
684
|
+
<button
|
|
685
|
+
class="no-decoration smaller ghost text-subtle"
|
|
686
|
+
onclick={() => launcherView("::login")}
|
|
687
|
+
>
|
|
688
|
+
Login
|
|
689
|
+
</button>
|
|
690
|
+
{/if}
|
|
691
|
+
<div class="std-launcher-mode smaller inline hidden">
|
|
692
|
+
{ActiveViewComponent ? "SYSTEM VIEW" : "COMMAND MODE"}
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
<div class="std-launcher-hints sm:hidden hidden">
|
|
697
|
+
<span><kbd>↑</kbd> <kbd>↓</kbd> navigate</span>
|
|
698
|
+
<span><kbd>esc</kbd> close</span>
|
|
699
|
+
</div>
|
|
700
|
+
</footer>
|
|
701
|
+
</div>
|
|
702
|
+
</div>
|
|
703
|
+
{/if}
|
|
704
|
+
|
|
705
|
+
<style>
|
|
706
|
+
/* Overlay */
|
|
707
|
+
.std-launcher-overlay {
|
|
708
|
+
position: fixed;
|
|
709
|
+
inset: 0;
|
|
710
|
+
display: flex;
|
|
711
|
+
align-items: flex-start;
|
|
712
|
+
justify-content: center;
|
|
713
|
+
z-index: 5000;
|
|
714
|
+
margin: 0;
|
|
715
|
+
padding-top: var(--launcher-top, 15vh);
|
|
716
|
+
font-family: var(--font-interface, sans-serif);
|
|
717
|
+
&:has(.std-launcher-body.fill) {
|
|
718
|
+
padding-top: var(--gap-body);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.std-launcher-backdrop {
|
|
723
|
+
position: absolute;
|
|
724
|
+
inset: 0;
|
|
725
|
+
background: var(--color-glass);
|
|
726
|
+
backdrop-filter: var(--filter-glass);
|
|
727
|
+
overflow: hidden;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.std-launcher-backdrop::after {
|
|
731
|
+
content: "";
|
|
732
|
+
position: absolute;
|
|
733
|
+
inset: 0;
|
|
734
|
+
pointer-events: none;
|
|
735
|
+
transform: translateZ(0);
|
|
736
|
+
will-change: opacity;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/* Main Container */
|
|
740
|
+
.std-launcher {
|
|
741
|
+
position: relative;
|
|
742
|
+
width: 100%;
|
|
743
|
+
max-width: 400px;
|
|
744
|
+
font-family: var(--font-interface);
|
|
745
|
+
background: var(--color-surface-high);
|
|
746
|
+
border-radius: var(--radius);
|
|
747
|
+
border: 0;
|
|
748
|
+
box-shadow: var(--shadow-raised), var(--shadow-xl);
|
|
749
|
+
transition: all var(--transition-fast);
|
|
750
|
+
overflow: hidden;
|
|
751
|
+
|
|
752
|
+
* {
|
|
753
|
+
text-shadow: none !important;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
&.compact {
|
|
757
|
+
max-width: 50vw;
|
|
758
|
+
}
|
|
759
|
+
&.standard {
|
|
760
|
+
max-width: 60vw;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
&.large {
|
|
764
|
+
max-width: 70vw;
|
|
765
|
+
}
|
|
766
|
+
&.fill {
|
|
767
|
+
max-width: calc(100% - (var(--gap-body) * 2));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/* Header */
|
|
772
|
+
.std-launcher-header {
|
|
773
|
+
padding: 0 var(--space-d2);
|
|
774
|
+
box-shadow: var(--shadow);
|
|
775
|
+
background-color: var(--color-surface-high);
|
|
776
|
+
text-shadow: none !important;
|
|
777
|
+
box-shadow: var(--shadow-raised), var(--shadow-glow);
|
|
778
|
+
position: relative;
|
|
779
|
+
z-index: 1;
|
|
780
|
+
transition: all var(--transition-fast);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.std-launcher-header-inner {
|
|
784
|
+
display: flex;
|
|
785
|
+
align-items: center;
|
|
786
|
+
justify-content: flex-start;
|
|
787
|
+
position: relative;
|
|
788
|
+
width: 100%;
|
|
789
|
+
min-height: 2.5em;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.std-launcher-back-btn {
|
|
793
|
+
position: absolute;
|
|
794
|
+
right: calc(var(--space) * 1.25);
|
|
795
|
+
top: 50%;
|
|
796
|
+
transform: translateY(-50%);
|
|
797
|
+
background: transparent;
|
|
798
|
+
border: none;
|
|
799
|
+
color: var(--color-muted);
|
|
800
|
+
cursor: pointer;
|
|
801
|
+
padding: var(--space-d4, 0.25rem);
|
|
802
|
+
border-radius: var(--radius);
|
|
803
|
+
display: flex;
|
|
804
|
+
align-items: center;
|
|
805
|
+
justify-content: center;
|
|
806
|
+
transition: all var(--transition-fast, 0.15s ease);
|
|
807
|
+
z-index: 4;
|
|
808
|
+
}
|
|
809
|
+
.std-launcher-back-btn:hover {
|
|
810
|
+
color: var(--color-foreground);
|
|
811
|
+
background: var(--color-surface-highest);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.std-launcher-search-area {
|
|
815
|
+
display: flex;
|
|
816
|
+
flex: 1;
|
|
817
|
+
align-items: center;
|
|
818
|
+
position: relative;
|
|
819
|
+
z-index: 3;
|
|
820
|
+
/* Ensure it takes full height to capture hover */
|
|
821
|
+
align-self: stretch;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.std-launcher-input.hidden {
|
|
825
|
+
display: none;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.opacity-0 {
|
|
829
|
+
opacity: 0;
|
|
830
|
+
pointer-events: none;
|
|
831
|
+
transition: opacity var(--transition-fast);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.std-launcher-title {
|
|
835
|
+
position: absolute;
|
|
836
|
+
left: 50%;
|
|
837
|
+
transform: translateX(-50%);
|
|
838
|
+
font-size: var(--size-sm);
|
|
839
|
+
color: var(--color-muted);
|
|
840
|
+
font-weight: 600;
|
|
841
|
+
font-family: var(--font-interface, sans-serif);
|
|
842
|
+
pointer-events: none;
|
|
843
|
+
white-space: nowrap;
|
|
844
|
+
z-index: 2;
|
|
845
|
+
transition: opacity var(--transition-fast);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.std-launcher-search-area .std-launcher-input {
|
|
849
|
+
width: 100%;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.std-launcher-input {
|
|
853
|
+
flex: 1;
|
|
854
|
+
background: transparent;
|
|
855
|
+
border: none;
|
|
856
|
+
outline: none !important;
|
|
857
|
+
font-weight: 600;
|
|
858
|
+
font-family: var(--font-interface, sans-serif);
|
|
859
|
+
font-size: var(--size-sm);
|
|
860
|
+
color: var(--color-muted);
|
|
861
|
+
caret-color: var(--color-accent);
|
|
862
|
+
margin: 0;
|
|
863
|
+
padding: 0 var(--space) 0 0;
|
|
864
|
+
box-shadow: none;
|
|
865
|
+
border-radius: 0 !important;
|
|
866
|
+
text-shadow: none !important;
|
|
867
|
+
z-index: 3;
|
|
868
|
+
min-width: 0;
|
|
869
|
+
opacity: 1;
|
|
870
|
+
transition: opacity var(--transition-fast);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.std-launcher-close-btn {
|
|
874
|
+
position: absolute;
|
|
875
|
+
right: var(--space-d3, 0.5rem);
|
|
876
|
+
background: transparent;
|
|
877
|
+
border: none;
|
|
878
|
+
color: var(--color-muted);
|
|
879
|
+
cursor: pointer;
|
|
880
|
+
padding: var(--space-d4, 0.25rem);
|
|
881
|
+
border-radius: var(--radius);
|
|
882
|
+
display: flex;
|
|
883
|
+
align-items: center;
|
|
884
|
+
justify-content: center;
|
|
885
|
+
transition: all var(--transition-fast, 0.15s ease);
|
|
886
|
+
z-index: 4;
|
|
887
|
+
}
|
|
888
|
+
.std-launcher-close-btn:hover {
|
|
889
|
+
color: var(--color-foreground);
|
|
890
|
+
background: var(--color-surface-highest);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/* Body */
|
|
894
|
+
.std-launcher-body {
|
|
895
|
+
overflow-y: auto;
|
|
896
|
+
transition: all var(--transition-fast);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.std-launcher-view > :first-child {
|
|
900
|
+
padding: var(--space);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.std-launcher-body.standard {
|
|
904
|
+
max-height: 30vh;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.std-launcher-body.large {
|
|
908
|
+
max-height: 70vh;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.std-launcher-body.fill {
|
|
912
|
+
max-height: calc(100% - (var(--gap-body) * 2)) !important;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/* List Items */
|
|
916
|
+
.std-launcher-item {
|
|
917
|
+
display: flex;
|
|
918
|
+
min-height: var(--space);
|
|
919
|
+
align-items: center;
|
|
920
|
+
gap: var(--leading);
|
|
921
|
+
width: 100%;
|
|
922
|
+
padding: var(--trim) var(--leading) !important;
|
|
923
|
+
background: transparent;
|
|
924
|
+
margin: 0 !important;
|
|
925
|
+
border: none;
|
|
926
|
+
border-radius: var(--radius);
|
|
927
|
+
cursor: pointer;
|
|
928
|
+
|
|
929
|
+
text-align: left;
|
|
930
|
+
transition: all var(--transition-fast);
|
|
931
|
+
box-shadow: none;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.std-launcher-item-title {
|
|
935
|
+
color: var(--color-muted);
|
|
936
|
+
font-size: var(--size-sm);
|
|
937
|
+
text-overflow: ellipsis;
|
|
938
|
+
white-space: nowrap;
|
|
939
|
+
overflow: hidden;
|
|
940
|
+
text-box-trim: none;
|
|
941
|
+
text-shadow: none !important;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.std-launcher-item-desc {
|
|
945
|
+
font-size: var(--size-2xs);
|
|
946
|
+
color: var(--color-subtle);
|
|
947
|
+
font-weight: 400;
|
|
948
|
+
text-align: left;
|
|
949
|
+
text-shadow: none !important;
|
|
950
|
+
white-space: nowrap;
|
|
951
|
+
overflow: hidden;
|
|
952
|
+
text-overflow: ellipsis;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
@media (max-width: 640px) {
|
|
956
|
+
.std-launcher-item-desc {
|
|
957
|
+
display: none;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.std-launcher-item:hover,
|
|
962
|
+
.std-launcher-item.active {
|
|
963
|
+
background: var(--color-surface-highest);
|
|
964
|
+
box-shadow: var(--shadow);
|
|
965
|
+
.std-launcher-item-icon {
|
|
966
|
+
color: var(--color-accent);
|
|
967
|
+
}
|
|
968
|
+
.std-launcher-item-title {
|
|
969
|
+
color: var(--color-accent);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.std-launcher-item-desc {
|
|
973
|
+
color: var(--color-foreground);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.std-launcher-item:active {
|
|
978
|
+
box-shadow: var(--shadow-inset);
|
|
979
|
+
background: var(--color-surface-low);
|
|
980
|
+
}
|
|
981
|
+
.std-launcher-item-content {
|
|
982
|
+
flex: 1;
|
|
983
|
+
min-width: 0;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.std-launcher-item-username {
|
|
987
|
+
color: var(--color-accent);
|
|
988
|
+
font-size: var(--scale-xs);
|
|
989
|
+
text-decoration: none;
|
|
990
|
+
white-space: nowrap;
|
|
991
|
+
flex-shrink: 0;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.std-launcher-item-icon {
|
|
995
|
+
display: flex;
|
|
996
|
+
align-items: center;
|
|
997
|
+
justify-content: center;
|
|
998
|
+
width: 1rem;
|
|
999
|
+
height: 1rem;
|
|
1000
|
+
flex-shrink: 0;
|
|
1001
|
+
opacity: 0.5;
|
|
1002
|
+
|
|
1003
|
+
filter: blur(0.2px);
|
|
1004
|
+
transition: all var(--duration-fast) var(--ease-snappy);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.std-launcher-empty {
|
|
1008
|
+
padding: var(--space-2) 0;
|
|
1009
|
+
text-align: center;
|
|
1010
|
+
color: var(--color-subtle);
|
|
1011
|
+
font-style: italic;
|
|
1012
|
+
font-size: var(--scale-d2);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/* Footer */
|
|
1016
|
+
.std-launcher-footer {
|
|
1017
|
+
display: flex;
|
|
1018
|
+
justify-content: space-between;
|
|
1019
|
+
align-items: center;
|
|
1020
|
+
padding: var(--space-d2) var(--space-d2);
|
|
1021
|
+
font-size: var(--size-xs);
|
|
1022
|
+
color: var(--color-muted);
|
|
1023
|
+
border-top: var(--border);
|
|
1024
|
+
background-color: var(--color-surface-high);
|
|
1025
|
+
|
|
1026
|
+
box-shadow: var(--shadow-raised);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.std-launcher-user {
|
|
1030
|
+
display: flex;
|
|
1031
|
+
align-items: center;
|
|
1032
|
+
gap: var(--space-d4);
|
|
1033
|
+
color: var(--color-foreground-muted);
|
|
1034
|
+
padding: var(--space-d4);
|
|
1035
|
+
border-radius: var(--radius);
|
|
1036
|
+
transition: all var(--transition-fast);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
.std-launcher-user:hover {
|
|
1040
|
+
background: var(--color-surface-highest);
|
|
1041
|
+
color: var(--color-foreground);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
.std-launcher-user span {
|
|
1045
|
+
max-width: 150px;
|
|
1046
|
+
white-space: nowrap;
|
|
1047
|
+
overflow: hidden;
|
|
1048
|
+
text-overflow: ellipsis;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
.std-launcher-hints {
|
|
1052
|
+
display: flex;
|
|
1053
|
+
gap: 1rem;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/* Mobile: Bottom Sheet */
|
|
1057
|
+
@media (max-width: 640px) {
|
|
1058
|
+
.std-launcher-overlay {
|
|
1059
|
+
align-items: flex-start;
|
|
1060
|
+
padding-top: var(--launcher-mobile-top, 1rem);
|
|
1061
|
+
}
|
|
1062
|
+
.std-launcher {
|
|
1063
|
+
margin-inline: 1rem;
|
|
1064
|
+
max-width: 100% !important;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
</style>
|