@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.ts
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @stnd/launcher - The Universal Command Palette Engine
|
|
3
|
+
*
|
|
4
|
+
* Aggregates actions and views from the @stnd module system.
|
|
5
|
+
* Uses virtual:stnd/components to discover extensions.
|
|
6
|
+
* Reads app-specific services (user store, search) from the
|
|
7
|
+
* Standard OS runtime store via provide/inject.
|
|
8
|
+
*/
|
|
9
|
+
import { writable, derived, get } from "svelte/store";
|
|
10
|
+
import { inject, provide } from "@stnd/store";
|
|
11
|
+
import Log from "@stnd/log";
|
|
12
|
+
|
|
13
|
+
// @ts-ignore - virtual module system
|
|
14
|
+
import { modules } from "virtual:stnd/modules-metadata";
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
import { extensions as componentExtensions } from "virtual:stnd/components";
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
import { runHook } from "virtual:stnd/hooks";
|
|
19
|
+
|
|
20
|
+
const log = Log({ scope: "Launcher" }) as any;
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// TYPES
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
export type ViewTrigger = string;
|
|
27
|
+
export type PromptText = string;
|
|
28
|
+
|
|
29
|
+
export interface CommandMeta {
|
|
30
|
+
title: string;
|
|
31
|
+
desc?: string;
|
|
32
|
+
type?: "action" | "view";
|
|
33
|
+
icon?: string;
|
|
34
|
+
context?: string | string[];
|
|
35
|
+
requireRole?: string | string[];
|
|
36
|
+
requireAuth?: boolean;
|
|
37
|
+
requireGuest?: boolean;
|
|
38
|
+
size?: { width: string; height: string };
|
|
39
|
+
hidden?: boolean;
|
|
40
|
+
override?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Command {
|
|
44
|
+
trigger: string;
|
|
45
|
+
title: string;
|
|
46
|
+
desc?: string;
|
|
47
|
+
type: "action" | "view";
|
|
48
|
+
icon?: string;
|
|
49
|
+
context?: string | string[];
|
|
50
|
+
requireRole?: string | string[];
|
|
51
|
+
requireAuth?: boolean;
|
|
52
|
+
requireGuest?: boolean;
|
|
53
|
+
action?: () => void;
|
|
54
|
+
component?: any;
|
|
55
|
+
size?: { width: string; height: string };
|
|
56
|
+
moduleId?: string;
|
|
57
|
+
hidden?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RegistryState {
|
|
61
|
+
commands: Command[];
|
|
62
|
+
views: Record<string, any>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LauncherResult {
|
|
66
|
+
title: string;
|
|
67
|
+
desc?: string;
|
|
68
|
+
icon?: string;
|
|
69
|
+
type: "action" | "view" | "search-result" | "command";
|
|
70
|
+
action?: () => void;
|
|
71
|
+
url?: string;
|
|
72
|
+
trigger?: string;
|
|
73
|
+
id?: string;
|
|
74
|
+
username?: string;
|
|
75
|
+
owner_id?: number | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PaletteState {
|
|
79
|
+
open: boolean;
|
|
80
|
+
view: string;
|
|
81
|
+
returnTo: string | null;
|
|
82
|
+
query: string;
|
|
83
|
+
contexts: string[];
|
|
84
|
+
props?: any;
|
|
85
|
+
dynamicSize?: { width: string; height: string } | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// STORES
|
|
90
|
+
// =============================================================================
|
|
91
|
+
|
|
92
|
+
export const registry = writable<RegistryState>({
|
|
93
|
+
commands: [],
|
|
94
|
+
views: {},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const palette = writable<PaletteState>({
|
|
98
|
+
open: false,
|
|
99
|
+
view: "search",
|
|
100
|
+
returnTo: null,
|
|
101
|
+
query: "",
|
|
102
|
+
contexts: ["global"],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// App-specific filter (still module-scoped — not worth a store key)
|
|
106
|
+
let appFilter: ((trigger: string, meta: CommandMeta) => boolean) | null = null;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Configure app-specific launcher options without booting.
|
|
110
|
+
*
|
|
111
|
+
* Prefer the Standard OS runtime store (provide/inject) for userStore
|
|
112
|
+
* and searchNotes. This function remains for the filter option and
|
|
113
|
+
* backward compatibility.
|
|
114
|
+
*/
|
|
115
|
+
export function configureLauncher(
|
|
116
|
+
options: {
|
|
117
|
+
searchNotes?: (query: string) => Promise<LauncherResult[]>;
|
|
118
|
+
userStore?: any;
|
|
119
|
+
filter?: (trigger: string, meta: CommandMeta) => boolean;
|
|
120
|
+
} = {},
|
|
121
|
+
) {
|
|
122
|
+
// Legacy compat: if callers still pass these, push them into the runtime store
|
|
123
|
+
if (options.searchNotes) provide("search", options.searchNotes);
|
|
124
|
+
if (options.userStore) provide("user", options.userStore);
|
|
125
|
+
if (options.filter) appFilter = options.filter;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Initialize the Launcher with app-specific logic and boot immediately.
|
|
130
|
+
*
|
|
131
|
+
* Shorthand for `configureLauncher(options)` + `bootLauncher()`.
|
|
132
|
+
*/
|
|
133
|
+
export function initLauncher(
|
|
134
|
+
options: {
|
|
135
|
+
searchNotes?: (query: string) => Promise<LauncherResult[]>;
|
|
136
|
+
userStore?: any;
|
|
137
|
+
filter?: (trigger: string, meta: CommandMeta) => boolean;
|
|
138
|
+
} = {},
|
|
139
|
+
) {
|
|
140
|
+
configureLauncher(options);
|
|
141
|
+
|
|
142
|
+
// Initial registry boot
|
|
143
|
+
bootLauncher();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// =============================================================================
|
|
147
|
+
// CORE ENGINE
|
|
148
|
+
// =============================================================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Register a command or view in the launcher
|
|
152
|
+
*/
|
|
153
|
+
export function register(
|
|
154
|
+
trigger: string,
|
|
155
|
+
meta: CommandMeta & { moduleId?: string },
|
|
156
|
+
handler: (() => void) | any,
|
|
157
|
+
) {
|
|
158
|
+
// Apply global filter if set
|
|
159
|
+
if (appFilter && !appFilter(trigger, meta)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
registry.update((r) => {
|
|
164
|
+
// Check for collisions in existing commands
|
|
165
|
+
const existingIndex = r.commands.findIndex((c) => c.trigger === trigger);
|
|
166
|
+
if (existingIndex !== -1 && !meta.override) {
|
|
167
|
+
const errorMsg =
|
|
168
|
+
`[Launcher] Fatal Collision: Trigger "${trigger}" already registered by "${r.commands[existingIndex].moduleId || "core"}". ` +
|
|
169
|
+
`Attempting to register again from "${meta.moduleId || "unknown"}". ` +
|
|
170
|
+
`See: https://stnd.build/manual/5-reference/launcher-trigger-collision`;
|
|
171
|
+
log.error(errorMsg);
|
|
172
|
+
throw new Error(errorMsg);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const m = meta as any;
|
|
176
|
+
let updates: any = { ...r };
|
|
177
|
+
|
|
178
|
+
// Register as view if type is "view"
|
|
179
|
+
if (m.type === "view") {
|
|
180
|
+
updates.views = { ...r.views, [trigger]: handler };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Create a command (either for actions or for views as launcher shortcuts)
|
|
184
|
+
const cmd: Command = {
|
|
185
|
+
trigger,
|
|
186
|
+
title: m.title,
|
|
187
|
+
desc: m.desc,
|
|
188
|
+
type: m.type === "view" ? "view" : "action",
|
|
189
|
+
icon: m.icon,
|
|
190
|
+
context: m.context,
|
|
191
|
+
requireRole: m.requireRole,
|
|
192
|
+
requireAuth: m.requireAuth,
|
|
193
|
+
requireGuest: m.requireGuest,
|
|
194
|
+
size: m.size,
|
|
195
|
+
action: m.type === "view" ? () => launcherView(trigger) : handler,
|
|
196
|
+
component: m.type === "view" ? handler : undefined,
|
|
197
|
+
moduleId: meta.moduleId || "core",
|
|
198
|
+
hidden: m.hidden,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (existingIndex !== -1 && meta.override) {
|
|
202
|
+
updates.commands = [...r.commands];
|
|
203
|
+
updates.commands[existingIndex] = cmd;
|
|
204
|
+
} else {
|
|
205
|
+
updates.commands = [...r.commands, cmd];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return updates;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Boot the Launcher system by collecting extensions from the module system
|
|
214
|
+
*/
|
|
215
|
+
export async function bootLauncher() {
|
|
216
|
+
// 1. Clear registry
|
|
217
|
+
registry.set({ commands: [], views: {} });
|
|
218
|
+
|
|
219
|
+
// 2. Load Actions from extensions
|
|
220
|
+
const actions = componentExtensions?.["launcher:action"] || [];
|
|
221
|
+
actions.forEach((entry: any) => {
|
|
222
|
+
if (!entry) return;
|
|
223
|
+
|
|
224
|
+
// Support both single action and bundled actions
|
|
225
|
+
let actionSource = entry.actions || entry.action?.actions || entry.action;
|
|
226
|
+
|
|
227
|
+
// Handle Module Namespace (default export)
|
|
228
|
+
if (
|
|
229
|
+
actionSource &&
|
|
230
|
+
typeof actionSource === "object" &&
|
|
231
|
+
"default" in actionSource
|
|
232
|
+
) {
|
|
233
|
+
actionSource = actionSource.default;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const bundledActions = Array.isArray(actionSource) ? actionSource : null;
|
|
237
|
+
|
|
238
|
+
if (Array.isArray(bundledActions)) {
|
|
239
|
+
bundledActions.forEach((item: any) => {
|
|
240
|
+
if (!item?.trigger || typeof item.action !== "function") return;
|
|
241
|
+
register(
|
|
242
|
+
item.trigger,
|
|
243
|
+
{
|
|
244
|
+
...(entry.meta || {}),
|
|
245
|
+
...(item.meta || {}),
|
|
246
|
+
type: "action",
|
|
247
|
+
moduleId: entry.moduleId,
|
|
248
|
+
},
|
|
249
|
+
item.action,
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
} else if (entry.trigger && typeof entry.action === "function") {
|
|
253
|
+
register(
|
|
254
|
+
entry.trigger,
|
|
255
|
+
{ ...(entry.meta || {}), type: "action", moduleId: entry.moduleId },
|
|
256
|
+
entry.action,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// 3. Load Views from extensions
|
|
262
|
+
const views = componentExtensions?.["launcher:view"] || [];
|
|
263
|
+
views.forEach((entry: any) => {
|
|
264
|
+
if (entry.trigger && entry.component) {
|
|
265
|
+
register(
|
|
266
|
+
entry.trigger,
|
|
267
|
+
{ ...entry.meta, type: "view", moduleId: entry.moduleId },
|
|
268
|
+
entry.component,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 4. Load Themes from modules metadata
|
|
274
|
+
const themeModules = (modules || []).filter(
|
|
275
|
+
(m: any) => m?.meta?.type === "theme",
|
|
276
|
+
);
|
|
277
|
+
themeModules.forEach((m: any) => {
|
|
278
|
+
const themeId = m.meta.themeId || m.id.replace("theme-", "");
|
|
279
|
+
register(
|
|
280
|
+
`::theme ${themeId}`,
|
|
281
|
+
{
|
|
282
|
+
title: m.meta.label || m.name || themeId,
|
|
283
|
+
type: "action",
|
|
284
|
+
icon: "palette",
|
|
285
|
+
desc: m.description || "A custom theme.",
|
|
286
|
+
moduleId: m.id,
|
|
287
|
+
},
|
|
288
|
+
async () => {
|
|
289
|
+
document.documentElement.setAttribute("data-theme", themeId);
|
|
290
|
+
if (typeof localStorage !== "undefined")
|
|
291
|
+
localStorage.setItem("std_theme", themeId);
|
|
292
|
+
|
|
293
|
+
// Persistence Tier: Save to preferences (Session/DB)
|
|
294
|
+
try {
|
|
295
|
+
const save = inject("savePreference");
|
|
296
|
+
if (typeof save === "function") {
|
|
297
|
+
await save({ theme: themeId });
|
|
298
|
+
}
|
|
299
|
+
} catch (e) {
|
|
300
|
+
log.debug("No preference saver available, theme saved locally only");
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// 5. Load Content entries (via hook)
|
|
307
|
+
try {
|
|
308
|
+
const contentResults = await runHook("stnd:launcher:content:register");
|
|
309
|
+
const allEntries = contentResults?.flat().filter(Boolean) || [];
|
|
310
|
+
// Deduplicate by id (runHook may call the same handler multiple times)
|
|
311
|
+
const seen = new Set();
|
|
312
|
+
const entries = allEntries.filter((entry: any) => {
|
|
313
|
+
if (!entry.id || seen.has(entry.id)) return false;
|
|
314
|
+
seen.add(entry.id);
|
|
315
|
+
return true;
|
|
316
|
+
});
|
|
317
|
+
entries.forEach((entry: any) => {
|
|
318
|
+
if (!entry.url) return;
|
|
319
|
+
const contentType = entry.type || "page";
|
|
320
|
+
register(
|
|
321
|
+
`::go ${contentType} ${entry.id}`,
|
|
322
|
+
{
|
|
323
|
+
title: entry.title,
|
|
324
|
+
type: "action",
|
|
325
|
+
icon: entry.icon || "note",
|
|
326
|
+
desc: entry.desc || "",
|
|
327
|
+
moduleId: entry.moduleId || "core:content",
|
|
328
|
+
},
|
|
329
|
+
() => {
|
|
330
|
+
import("astro:transitions/client").then((m) => m.navigate(entry.url));
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
if (entries.length)
|
|
335
|
+
log.debug(`Registered ${entries.length} content entries`);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
log.debug("No content entries discovered via hook");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
log.debug("Initialized");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// =============================================================================
|
|
344
|
+
// SEARCH & MATCHING
|
|
345
|
+
// =============================================================================
|
|
346
|
+
|
|
347
|
+
export async function search(query: string): Promise<LauncherResult[]> {
|
|
348
|
+
const searchFn = inject("search", async () => []);
|
|
349
|
+
return searchFn(query);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function computeMatches(
|
|
353
|
+
query: string,
|
|
354
|
+
registryCommands: Command[],
|
|
355
|
+
searchResults: LauncherResult[],
|
|
356
|
+
currentUser: any,
|
|
357
|
+
): LauncherResult[] {
|
|
358
|
+
const isCommand = query.startsWith("::");
|
|
359
|
+
const contextualCommands = registryCommands.filter((cmd) => {
|
|
360
|
+
// 0. Filtrage de l'état d'authentification (l'utilisateur anonyme n'est pas authentifié)
|
|
361
|
+
const isAuthenticated =
|
|
362
|
+
currentUser?.isAuthenticated === true ||
|
|
363
|
+
(!!currentUser?.id &&
|
|
364
|
+
currentUser?.id !== "anonymous-system-user" &&
|
|
365
|
+
!currentUser?.isAnonymous);
|
|
366
|
+
if (cmd.requireAuth && !isAuthenticated) return false;
|
|
367
|
+
if (cmd.requireGuest && isAuthenticated) return false;
|
|
368
|
+
|
|
369
|
+
// 1. Role filtering
|
|
370
|
+
if (cmd.requireRole) {
|
|
371
|
+
if (!isAuthenticated) return false;
|
|
372
|
+
const roles = Array.isArray(cmd.requireRole)
|
|
373
|
+
? cmd.requireRole
|
|
374
|
+
: [cmd.requireRole];
|
|
375
|
+
if (!roles.includes(currentUser.role)) return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 2. Context filtering
|
|
379
|
+
// If command defines a context, the palette must have it.
|
|
380
|
+
// context: "note" means palette.contexts must contain "note"
|
|
381
|
+
// context: ["note", "owner"] means palette.contexts must contain BOTH "note" and "owner"
|
|
382
|
+
if (cmd.context) {
|
|
383
|
+
const activeContexts = get(palette).contexts || [];
|
|
384
|
+
const requiredContexts = Array.isArray(cmd.context)
|
|
385
|
+
? cmd.context
|
|
386
|
+
: [cmd.context];
|
|
387
|
+
|
|
388
|
+
// Every required context must be present in active contexts
|
|
389
|
+
const hasRequiredContexts = requiredContexts.every((c) =>
|
|
390
|
+
activeContexts.includes(c),
|
|
391
|
+
);
|
|
392
|
+
if (!hasRequiredContexts) return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return true;
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (isCommand) {
|
|
399
|
+
const searchTerm = query.replace(/^::/, "").toLowerCase();
|
|
400
|
+
return contextualCommands
|
|
401
|
+
.filter((c) => {
|
|
402
|
+
if (c.hidden && !searchTerm) return false;
|
|
403
|
+
return c.trigger.replace(/^::/, "").toLowerCase().includes(searchTerm);
|
|
404
|
+
})
|
|
405
|
+
.map((c) => ({ ...c }));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Regular search
|
|
409
|
+
const matches = contextualCommands
|
|
410
|
+
.filter((c) => {
|
|
411
|
+
if (c.hidden) return false;
|
|
412
|
+
return c.title.toLowerCase().includes(query.toLowerCase());
|
|
413
|
+
})
|
|
414
|
+
.map((c) => ({ ...c }));
|
|
415
|
+
|
|
416
|
+
const finalMatches = [...matches, ...searchResults];
|
|
417
|
+
|
|
418
|
+
// Si une requête de recherche textuelle est saisie, injecter l'option de poser une question à l'IA du jardin
|
|
419
|
+
if (query.trim() && !isCommand) {
|
|
420
|
+
const aiPromptItem: LauncherResult = {
|
|
421
|
+
title: "Demander à l'IA du Jardin",
|
|
422
|
+
desc: `Obtenir une réponse basée sur vos notes pour : "${query}"`,
|
|
423
|
+
icon: "ph:sparkle",
|
|
424
|
+
type: "view",
|
|
425
|
+
trigger: "::ask",
|
|
426
|
+
action: () => launcherView("::ask", { question: query }),
|
|
427
|
+
};
|
|
428
|
+
// Placer l'option IA en premier
|
|
429
|
+
finalMatches.unshift(aiPromptItem);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return finalMatches;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export const activeView = derived(
|
|
436
|
+
[registry, palette],
|
|
437
|
+
([$registry, $palette]) => {
|
|
438
|
+
return $registry.views[$palette.view] || null;
|
|
439
|
+
},
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
export const windowSize = derived(
|
|
443
|
+
[registry, palette, activeView],
|
|
444
|
+
([$registry, $palette, $activeView]) => {
|
|
445
|
+
// 1. Dynamic Override
|
|
446
|
+
if ($palette.dynamicSize) return $palette.dynamicSize;
|
|
447
|
+
|
|
448
|
+
// 2. Registry Size
|
|
449
|
+
if ($activeView) {
|
|
450
|
+
const cmd = $registry.commands.find(
|
|
451
|
+
(c) => c.action === $activeView || c.trigger === $palette.view,
|
|
452
|
+
);
|
|
453
|
+
// Note: The registry commands might store the view component in a different property depending on how it was registered
|
|
454
|
+
// In register(): if type=view, it goes to r.views. We don't necessarily keep a ref in r.commands to the component function easily?
|
|
455
|
+
// Let's check register()...
|
|
456
|
+
// It creates a command with 'action: handler'. handler IS the component for view types.
|
|
457
|
+
// So checking c.action === $activeView should work.
|
|
458
|
+
if (cmd) {
|
|
459
|
+
return (
|
|
460
|
+
cmd.size ||
|
|
461
|
+
(cmd as any).meta?.size || { width: "standard", height: "standard" }
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 3. Default
|
|
467
|
+
return { width: "standard", height: "standard" };
|
|
468
|
+
},
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// =============================================================================
|
|
472
|
+
// API
|
|
473
|
+
// =============================================================================
|
|
474
|
+
|
|
475
|
+
export function launcherToggle(query: string = "") {
|
|
476
|
+
palette.update((p) => {
|
|
477
|
+
const willBeOpen = !p.open;
|
|
478
|
+
return {
|
|
479
|
+
...p,
|
|
480
|
+
open: willBeOpen,
|
|
481
|
+
query,
|
|
482
|
+
view: willBeOpen ? "search" : p.view,
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function launcherView(view: string, props: any = {}) {
|
|
488
|
+
palette.update((p) => ({
|
|
489
|
+
...p,
|
|
490
|
+
open: true,
|
|
491
|
+
view,
|
|
492
|
+
query: "",
|
|
493
|
+
props,
|
|
494
|
+
dynamicSize: null,
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function setSize(size: { width: string; height: string } | null) {
|
|
499
|
+
palette.update((p) => ({ ...p, dynamicSize: size }));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function setContexts(contexts: string[]) {
|
|
503
|
+
palette.update((p) => ({ ...p, contexts }));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function launcherClose() {
|
|
507
|
+
palette.update((p) => ({ ...p, open: false }));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function execute(item: LauncherResult) {
|
|
511
|
+
if (item.action) {
|
|
512
|
+
item.action();
|
|
513
|
+
// Don't close launcher for view commands - they handle their own state
|
|
514
|
+
if (item.type !== "view") {
|
|
515
|
+
launcherClose();
|
|
516
|
+
}
|
|
517
|
+
} else if (item.url) {
|
|
518
|
+
import("astro:transitions/client").then((m) =>
|
|
519
|
+
m.navigate(item.url as string),
|
|
520
|
+
);
|
|
521
|
+
launcherClose();
|
|
522
|
+
} else if (item.trigger) {
|
|
523
|
+
if (item.trigger.startsWith("::")) {
|
|
524
|
+
launcherView(item.trigger);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
export function getPageState(user: any = null): any {
|
|
529
|
+
if (typeof window === "undefined") {
|
|
530
|
+
return { contexts: ["global"] };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const path = window.location.pathname;
|
|
534
|
+
const state: any = {
|
|
535
|
+
contexts: ["global"],
|
|
536
|
+
noteSlug: null,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
if (path.startsWith("/writer") || path.startsWith("/editor")) {
|
|
540
|
+
state.contexts.push("editor");
|
|
541
|
+
return state;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Common pattern for notes
|
|
545
|
+
const noteMatch = path.match(/^\/([^/]+)$/);
|
|
546
|
+
if (noteMatch) {
|
|
547
|
+
state.contexts.push("note");
|
|
548
|
+
state.noteSlug = noteMatch[1];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (path === "/") {
|
|
552
|
+
state.contexts.push("home");
|
|
553
|
+
}
|
|
554
|
+
return state;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Svelte Action: openLauncher
|
|
559
|
+
* usage: <button use:openLauncher={"::trigger"}>Open</button>
|
|
560
|
+
*/
|
|
561
|
+
export function openLauncher(node: HTMLElement, trigger: string = "") {
|
|
562
|
+
function handleClick(e: MouseEvent) {
|
|
563
|
+
e.preventDefault();
|
|
564
|
+
if (trigger.startsWith("::")) {
|
|
565
|
+
launcherView(trigger);
|
|
566
|
+
} else {
|
|
567
|
+
launcherToggle(trigger);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
node.addEventListener("click", handleClick);
|
|
572
|
+
return {
|
|
573
|
+
destroy() {
|
|
574
|
+
node.removeEventListener("click", handleClick);
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stnd/launcher",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"svelte": "./Launcher.svelte",
|
|
6
|
+
"main": "launcher.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"svelte": "./Launcher.svelte",
|
|
10
|
+
"default": "./launcher.ts"
|
|
11
|
+
},
|
|
12
|
+
"./launcher": "./launcher.ts",
|
|
13
|
+
"./Launcher.svelte": "./Launcher.svelte",
|
|
14
|
+
"./transitions": "./transitions.js",
|
|
15
|
+
"./*": "./*"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"svelte": "5.56.4",
|
|
19
|
+
"wrangler": "^4.105.0",
|
|
20
|
+
"@stnd/icon": "0.2.0",
|
|
21
|
+
"@stnd/log": "0.5.0",
|
|
22
|
+
"@stnd/store": "0.5.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"astro": ">=5.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|