@autumnsgrove/groveengine 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/auth/index.d.ts +1 -2
  2. package/dist/auth/index.js +8 -4
  3. package/dist/auth/session.d.ts +14 -33
  4. package/dist/auth/session.js +5 -103
  5. package/dist/components/admin/FloatingToolbar.svelte +373 -0
  6. package/dist/components/admin/FloatingToolbar.svelte.d.ts +17 -0
  7. package/dist/components/admin/MarkdownEditor.svelte +26 -347
  8. package/dist/components/admin/MarkdownEditor.svelte.d.ts +1 -1
  9. package/dist/components/admin/composables/index.d.ts +0 -2
  10. package/dist/components/admin/composables/index.js +0 -2
  11. package/dist/components/custom/ContentWithGutter.svelte +22 -25
  12. package/dist/components/custom/MobileTOC.svelte +20 -13
  13. package/dist/components/quota/UpgradePrompt.svelte +1 -1
  14. package/dist/server/services/database.d.ts +138 -0
  15. package/dist/server/services/database.js +234 -0
  16. package/dist/server/services/index.d.ts +5 -1
  17. package/dist/server/services/index.js +24 -2
  18. package/dist/server/services/turnstile.d.ts +66 -0
  19. package/dist/server/services/turnstile.js +131 -0
  20. package/dist/server/services/users.d.ts +104 -0
  21. package/dist/server/services/users.js +158 -0
  22. package/dist/styles/README.md +50 -0
  23. package/dist/styles/vine-pattern.css +24 -0
  24. package/dist/types/turnstile.d.ts +42 -0
  25. package/dist/ui/components/forms/TurnstileWidget.svelte +111 -0
  26. package/dist/ui/components/forms/TurnstileWidget.svelte.d.ts +14 -0
  27. package/dist/ui/components/primitives/dialog/dialog-overlay.svelte +1 -1
  28. package/dist/ui/components/primitives/sheet/sheet-overlay.svelte +1 -1
  29. package/dist/ui/components/ui/Glass.svelte +158 -0
  30. package/dist/ui/components/ui/Glass.svelte.d.ts +52 -0
  31. package/dist/ui/components/ui/GlassButton.svelte +157 -0
  32. package/dist/ui/components/ui/GlassButton.svelte.d.ts +39 -0
  33. package/dist/ui/components/ui/GlassCard.svelte +160 -0
  34. package/dist/ui/components/ui/GlassCard.svelte.d.ts +39 -0
  35. package/dist/ui/components/ui/GlassConfirmDialog.svelte +208 -0
  36. package/dist/ui/components/ui/GlassConfirmDialog.svelte.d.ts +52 -0
  37. package/dist/ui/components/ui/GlassOverlay.svelte +93 -0
  38. package/dist/ui/components/ui/GlassOverlay.svelte.d.ts +33 -0
  39. package/dist/ui/components/ui/Logo.svelte +161 -23
  40. package/dist/ui/components/ui/Logo.svelte.d.ts +4 -10
  41. package/dist/ui/components/ui/index.d.ts +5 -0
  42. package/dist/ui/components/ui/index.js +6 -0
  43. package/dist/ui/styles/grove.css +136 -0
  44. package/dist/ui/tokens/fonts.d.ts +69 -0
  45. package/dist/ui/tokens/fonts.js +341 -0
  46. package/dist/ui/tokens/index.d.ts +6 -5
  47. package/dist/ui/tokens/index.js +7 -6
  48. package/dist/utils/gutter.d.ts +2 -8
  49. package/dist/utils/markdown.d.ts +1 -0
  50. package/dist/utils/markdown.js +32 -11
  51. package/package.json +1 -1
  52. package/static/robots.txt +520 -0
  53. package/dist/auth/jwt.d.ts +0 -20
  54. package/dist/auth/jwt.js +0 -123
  55. package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +0 -87
  56. package/dist/components/admin/composables/useCommandPalette.svelte.js +0 -158
  57. package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +0 -104
  58. package/dist/components/admin/composables/useSlashCommands.svelte.js +0 -215
package/dist/auth/jwt.js DELETED
@@ -1,123 +0,0 @@
1
- /**
2
- * JWT utilities using Web Crypto API (Cloudflare Workers compatible)
3
- */
4
-
5
- /**
6
- * @typedef {Object} JwtPayload
7
- * @property {string} [sub]
8
- * @property {string} [email]
9
- * @property {number} [exp]
10
- * @property {number} [iat]
11
- */
12
-
13
- const encoder = new TextEncoder();
14
- const decoder = new TextDecoder();
15
-
16
- /**
17
- * Base64URL encode
18
- * @param {ArrayBuffer} data
19
- * @returns {string}
20
- */
21
- function base64UrlEncode(data) {
22
- const base64 = btoa(String.fromCharCode(...new Uint8Array(data)));
23
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
24
- }
25
-
26
- /**
27
- * Base64URL decode
28
- * @param {string} str
29
- * @returns {Uint8Array}
30
- */
31
- function base64UrlDecode(str) {
32
- const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
33
- const padding = "=".repeat((4 - (base64.length % 4)) % 4);
34
- const binary = atob(base64 + padding);
35
- return Uint8Array.from(binary, (c) => c.charCodeAt(0));
36
- }
37
-
38
- /**
39
- * Create HMAC key from secret
40
- * @param {string} secret
41
- * @returns {Promise<CryptoKey>}
42
- */
43
- async function createKey(secret) {
44
- return await crypto.subtle.importKey(
45
- "raw",
46
- encoder.encode(secret),
47
- { name: "HMAC", hash: "SHA-256" },
48
- false,
49
- ["sign", "verify"],
50
- );
51
- }
52
-
53
- /**
54
- * Sign a JWT payload
55
- * @param {JwtPayload} payload - The payload to sign
56
- * @param {string} secret - The secret key
57
- * @returns {Promise<string>} - The signed JWT token
58
- */
59
- export async function signJwt(payload, secret) {
60
- const header = { alg: "HS256", typ: "JWT" };
61
-
62
- const headerEncoded = base64UrlEncode(encoder.encode(JSON.stringify(header)).buffer);
63
- const payloadEncoded = base64UrlEncode(
64
- encoder.encode(JSON.stringify(payload)).buffer,
65
- );
66
-
67
- const message = `${headerEncoded}.${payloadEncoded}`;
68
- const key = await createKey(secret);
69
-
70
- const signature = await crypto.subtle.sign(
71
- "HMAC",
72
- key,
73
- encoder.encode(message),
74
- );
75
-
76
- const signatureEncoded = base64UrlEncode(signature);
77
-
78
- return `${message}.${signatureEncoded}`;
79
- }
80
-
81
- /**
82
- * Verify and decode a JWT token
83
- * @param {string} token - The JWT token to verify
84
- * @param {string} secret - The secret key
85
- * @returns {Promise<JwtPayload|null>} - The decoded payload or null if invalid
86
- */
87
- export async function verifyJwt(token, secret) {
88
- try {
89
- const parts = token.split(".");
90
- if (parts.length !== 3) {
91
- return null;
92
- }
93
-
94
- const [headerEncoded, payloadEncoded, signatureEncoded] = parts;
95
- const message = `${headerEncoded}.${payloadEncoded}`;
96
-
97
- const key = await createKey(secret);
98
- const signature = base64UrlDecode(signatureEncoded);
99
-
100
- const isValid = await crypto.subtle.verify(
101
- "HMAC",
102
- key,
103
- signature,
104
- encoder.encode(message),
105
- );
106
-
107
- if (!isValid) {
108
- return null;
109
- }
110
-
111
- const payload = JSON.parse(decoder.decode(base64UrlDecode(payloadEncoded)));
112
-
113
- // Check expiration
114
- if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
115
- return null;
116
- }
117
-
118
- return payload;
119
- } catch (error) {
120
- console.error("JWT verification error:", error);
121
- return null;
122
- }
123
- }
@@ -1,87 +0,0 @@
1
- /**
2
- * Command Palette Composable
3
- * Manages the command palette (Cmd+K) functionality
4
- */
5
- /**
6
- * @typedef {Object} PaletteAction
7
- * @property {string} id
8
- * @property {string} label
9
- * @property {string} shortcut
10
- * @property {() => void} action
11
- * @property {string} [themeKey]
12
- * @property {boolean} [isTheme]
13
- */
14
- /**
15
- * @typedef {Object} CommandPaletteState
16
- * @property {boolean} open
17
- * @property {string} query
18
- * @property {number} selectedIndex
19
- */
20
- /**
21
- * @typedef {Object} CommandPaletteOptions
22
- * @property {() => PaletteAction[]} [getActions] - Function to get available actions
23
- * @property {() => Record<string, any>} [getThemes] - Function to get available themes
24
- * @property {() => string} [getCurrentTheme] - Function to get current theme
25
- */
26
- /**
27
- * @typedef {Object} CommandPaletteManager
28
- * @property {CommandPaletteState} state
29
- * @property {boolean} isOpen
30
- * @property {string} query
31
- * @property {number} selectedIndex
32
- * @property {() => PaletteAction[]} getAllCommands
33
- * @property {() => PaletteAction[]} getFilteredCommands
34
- * @property {() => void} open
35
- * @property {() => void} close
36
- * @property {() => void} toggle
37
- * @property {(direction: 'up' | 'down') => void} navigate
38
- * @property {(index: number) => PaletteAction | undefined} execute
39
- * @property {(query: string) => void} setQuery
40
- */
41
- /**
42
- * Creates a command palette manager with Svelte 5 runes
43
- * @param {CommandPaletteOptions} options - Configuration options
44
- * @returns {CommandPaletteManager} Command palette state and controls
45
- */
46
- export function useCommandPalette(options?: CommandPaletteOptions): CommandPaletteManager;
47
- export type PaletteAction = {
48
- id: string;
49
- label: string;
50
- shortcut: string;
51
- action: () => void;
52
- themeKey?: string | undefined;
53
- isTheme?: boolean | undefined;
54
- };
55
- export type CommandPaletteState = {
56
- open: boolean;
57
- query: string;
58
- selectedIndex: number;
59
- };
60
- export type CommandPaletteOptions = {
61
- /**
62
- * - Function to get available actions
63
- */
64
- getActions?: (() => PaletteAction[]) | undefined;
65
- /**
66
- * - Function to get available themes
67
- */
68
- getThemes?: (() => Record<string, any>) | undefined;
69
- /**
70
- * - Function to get current theme
71
- */
72
- getCurrentTheme?: (() => string) | undefined;
73
- };
74
- export type CommandPaletteManager = {
75
- state: CommandPaletteState;
76
- isOpen: boolean;
77
- query: string;
78
- selectedIndex: number;
79
- getAllCommands: () => PaletteAction[];
80
- getFilteredCommands: () => PaletteAction[];
81
- open: () => void;
82
- close: () => void;
83
- toggle: () => void;
84
- navigate: (direction: "up" | "down") => void;
85
- execute: (index: number) => PaletteAction | undefined;
86
- setQuery: (query: string) => void;
87
- };
@@ -1,158 +0,0 @@
1
- /**
2
- * Command Palette Composable
3
- * Manages the command palette (Cmd+K) functionality
4
- */
5
-
6
- /**
7
- * @typedef {Object} PaletteAction
8
- * @property {string} id
9
- * @property {string} label
10
- * @property {string} shortcut
11
- * @property {() => void} action
12
- * @property {string} [themeKey]
13
- * @property {boolean} [isTheme]
14
- */
15
-
16
- /**
17
- * @typedef {Object} CommandPaletteState
18
- * @property {boolean} open
19
- * @property {string} query
20
- * @property {number} selectedIndex
21
- */
22
-
23
- /**
24
- * @typedef {Object} CommandPaletteOptions
25
- * @property {() => PaletteAction[]} [getActions] - Function to get available actions
26
- * @property {() => Record<string, any>} [getThemes] - Function to get available themes
27
- * @property {() => string} [getCurrentTheme] - Function to get current theme
28
- */
29
-
30
- /**
31
- * @typedef {Object} CommandPaletteManager
32
- * @property {CommandPaletteState} state
33
- * @property {boolean} isOpen
34
- * @property {string} query
35
- * @property {number} selectedIndex
36
- * @property {() => PaletteAction[]} getAllCommands
37
- * @property {() => PaletteAction[]} getFilteredCommands
38
- * @property {() => void} open
39
- * @property {() => void} close
40
- * @property {() => void} toggle
41
- * @property {(direction: 'up' | 'down') => void} navigate
42
- * @property {(index: number) => PaletteAction | undefined} execute
43
- * @property {(query: string) => void} setQuery
44
- */
45
-
46
- /**
47
- * Creates a command palette manager with Svelte 5 runes
48
- * @param {CommandPaletteOptions} options - Configuration options
49
- * @returns {CommandPaletteManager} Command palette state and controls
50
- */
51
- export function useCommandPalette(options = {}) {
52
- const { getActions, getThemes, getCurrentTheme } = options;
53
-
54
- let state = $state({
55
- open: false,
56
- query: "",
57
- selectedIndex: 0,
58
- });
59
-
60
- // Get all commands including theme commands
61
- function getAllCommands() {
62
- const actions = getActions ? getActions() : [];
63
- const themes = getThemes ? getThemes() : {};
64
- const currentTheme = getCurrentTheme ? getCurrentTheme() : "";
65
-
66
- const themeCommands = Object.entries(themes).map(([key, theme]) => ({
67
- id: `theme-${key}`,
68
- label: `Theme: ${theme.label} (${theme.desc})`,
69
- shortcut: currentTheme === key ? "●" : "",
70
- action: () => {
71
- // Theme action is handled by the caller
72
- },
73
- themeKey: key,
74
- isTheme: true,
75
- }));
76
-
77
- return [...actions, ...themeCommands];
78
- }
79
-
80
- // Get filtered commands based on query
81
- function getFilteredCommands() {
82
- const allCommands = getAllCommands();
83
- return allCommands.filter((cmd) =>
84
- cmd.label.toLowerCase().includes(state.query.toLowerCase())
85
- );
86
- }
87
-
88
- function open() {
89
- state.open = true;
90
- state.query = "";
91
- state.selectedIndex = 0;
92
- }
93
-
94
- function close() {
95
- state.open = false;
96
- }
97
-
98
- function toggle() {
99
- if (state.open) {
100
- close();
101
- } else {
102
- open();
103
- }
104
- }
105
-
106
- /** @param {'up' | 'down'} direction */
107
- function navigate(direction) {
108
- const filtered = getFilteredCommands();
109
- const count = filtered.length;
110
- if (count === 0) return;
111
-
112
- if (direction === "down") {
113
- state.selectedIndex = (state.selectedIndex + 1) % count;
114
- } else if (direction === "up") {
115
- state.selectedIndex = (state.selectedIndex - 1 + count) % count;
116
- }
117
- }
118
-
119
- /** @param {number} index */
120
- function execute(index) {
121
- const filtered = getFilteredCommands();
122
- const cmd = filtered[index];
123
- if (cmd && cmd.action) {
124
- cmd.action();
125
- close();
126
- }
127
- return cmd;
128
- }
129
-
130
- /** @param {string} query */
131
- function setQuery(query) {
132
- state.query = query;
133
- state.selectedIndex = 0;
134
- }
135
-
136
- return {
137
- get state() {
138
- return state;
139
- },
140
- get isOpen() {
141
- return state.open;
142
- },
143
- get query() {
144
- return state.query;
145
- },
146
- get selectedIndex() {
147
- return state.selectedIndex;
148
- },
149
- getAllCommands,
150
- getFilteredCommands,
151
- open,
152
- close,
153
- toggle,
154
- navigate,
155
- execute,
156
- setQuery,
157
- };
158
- }
@@ -1,104 +0,0 @@
1
- /**
2
- * Creates a slash commands manager with Svelte 5 runes
3
- * @param {SlashCommandsOptions} options - Configuration options
4
- * @returns {SlashCommandsManager} Slash commands state and controls
5
- */
6
- export function useSlashCommands(options?: SlashCommandsOptions): SlashCommandsManager;
7
- /**
8
- * Slash Commands Composable
9
- * Manages the slash command menu and execution
10
- */
11
- /**
12
- * @typedef {Object} SlashCommand
13
- * @property {string} id
14
- * @property {string} label
15
- * @property {string} insert
16
- * @property {number} [cursorOffset]
17
- * @property {boolean} [isSnippet]
18
- * @property {boolean} [isAction]
19
- * @property {(() => void)} [action]
20
- */
21
- /**
22
- * @typedef {Object} SlashMenuState
23
- * @property {boolean} open
24
- * @property {string} query
25
- * @property {{x: number, y: number}} position
26
- * @property {number} selectedIndex
27
- */
28
- /**
29
- * @typedef {Object} SlashCommandsOptions
30
- * @property {() => HTMLTextAreaElement|null} [getTextareaRef] - Function to get textarea reference
31
- * @property {() => string} [getContent] - Function to get content
32
- * @property {(content: string) => void} [setContent] - Function to set content
33
- * @property {() => Array<{id: string, name: string, content: string}>} [getSnippets] - Function to get user snippets
34
- * @property {() => void} [onOpenSnippetsModal] - Callback to open snippets modal
35
- */
36
- /**
37
- * @typedef {Object} SlashCommandsManager
38
- * @property {SlashMenuState} menu
39
- * @property {boolean} isOpen
40
- * @property {() => SlashCommand[]} getAllCommands
41
- * @property {() => SlashCommand[]} getFilteredCommands
42
- * @property {() => void} open
43
- * @property {() => void} close
44
- * @property {(direction: 'up' | 'down') => void} navigate
45
- * @property {(index: number) => void} execute
46
- * @property {(key: string, cursorPos: number, content: string) => boolean} shouldTrigger
47
- */
48
- /** @type {SlashCommand[]} */
49
- export const baseSlashCommands: SlashCommand[];
50
- export type SlashCommand = {
51
- id: string;
52
- label: string;
53
- insert: string;
54
- cursorOffset?: number | undefined;
55
- isSnippet?: boolean | undefined;
56
- isAction?: boolean | undefined;
57
- action?: (() => void) | undefined;
58
- };
59
- export type SlashMenuState = {
60
- open: boolean;
61
- query: string;
62
- position: {
63
- x: number;
64
- y: number;
65
- };
66
- selectedIndex: number;
67
- };
68
- export type SlashCommandsOptions = {
69
- /**
70
- * - Function to get textarea reference
71
- */
72
- getTextareaRef?: (() => HTMLTextAreaElement | null) | undefined;
73
- /**
74
- * - Function to get content
75
- */
76
- getContent?: (() => string) | undefined;
77
- /**
78
- * - Function to set content
79
- */
80
- setContent?: ((content: string) => void) | undefined;
81
- /**
82
- * - Function to get user snippets
83
- */
84
- getSnippets?: (() => Array<{
85
- id: string;
86
- name: string;
87
- content: string;
88
- }>) | undefined;
89
- /**
90
- * - Callback to open snippets modal
91
- */
92
- onOpenSnippetsModal?: (() => void) | undefined;
93
- };
94
- export type SlashCommandsManager = {
95
- menu: SlashMenuState;
96
- isOpen: boolean;
97
- getAllCommands: () => SlashCommand[];
98
- getFilteredCommands: () => SlashCommand[];
99
- open: () => void;
100
- close: () => void;
101
- navigate: (direction: "up" | "down") => void;
102
- execute: (index: number) => void;
103
- shouldTrigger: (key: string, cursorPos: number, content: string) => boolean;
104
- };
@@ -1,215 +0,0 @@
1
- /**
2
- * Slash Commands Composable
3
- * Manages the slash command menu and execution
4
- */
5
-
6
- /**
7
- * @typedef {Object} SlashCommand
8
- * @property {string} id
9
- * @property {string} label
10
- * @property {string} insert
11
- * @property {number} [cursorOffset]
12
- * @property {boolean} [isSnippet]
13
- * @property {boolean} [isAction]
14
- * @property {(() => void)} [action]
15
- */
16
-
17
- /**
18
- * @typedef {Object} SlashMenuState
19
- * @property {boolean} open
20
- * @property {string} query
21
- * @property {{x: number, y: number}} position
22
- * @property {number} selectedIndex
23
- */
24
-
25
- /**
26
- * @typedef {Object} SlashCommandsOptions
27
- * @property {() => HTMLTextAreaElement|null} [getTextareaRef] - Function to get textarea reference
28
- * @property {() => string} [getContent] - Function to get content
29
- * @property {(content: string) => void} [setContent] - Function to set content
30
- * @property {() => Array<{id: string, name: string, content: string}>} [getSnippets] - Function to get user snippets
31
- * @property {() => void} [onOpenSnippetsModal] - Callback to open snippets modal
32
- */
33
-
34
- /**
35
- * @typedef {Object} SlashCommandsManager
36
- * @property {SlashMenuState} menu
37
- * @property {boolean} isOpen
38
- * @property {() => SlashCommand[]} getAllCommands
39
- * @property {() => SlashCommand[]} getFilteredCommands
40
- * @property {() => void} open
41
- * @property {() => void} close
42
- * @property {(direction: 'up' | 'down') => void} navigate
43
- * @property {(index: number) => void} execute
44
- * @property {(key: string, cursorPos: number, content: string) => boolean} shouldTrigger
45
- */
46
-
47
- // Base slash commands definition
48
- /** @type {SlashCommand[]} */
49
- export const baseSlashCommands = [
50
- { id: "heading1", label: "Heading 1", insert: "# " },
51
- { id: "heading2", label: "Heading 2", insert: "## " },
52
- { id: "heading3", label: "Heading 3", insert: "### " },
53
- { id: "code", label: "Code Block", insert: "```\n\n```", cursorOffset: 4 },
54
- { id: "quote", label: "Quote", insert: "> " },
55
- { id: "list", label: "Bullet List", insert: "- " },
56
- { id: "numbered", label: "Numbered List", insert: "1. " },
57
- { id: "link", label: "Link", insert: "[](url)", cursorOffset: 1 },
58
- { id: "image", label: "Image", insert: "![alt](url)", cursorOffset: 2 },
59
- { id: "divider", label: "Divider", insert: "\n---\n" },
60
- {
61
- id: "anchor",
62
- label: "Custom Anchor",
63
- insert: "<!-- anchor:name -->\n",
64
- cursorOffset: 14,
65
- },
66
- ];
67
-
68
- /**
69
- * Creates a slash commands manager with Svelte 5 runes
70
- * @param {SlashCommandsOptions} options - Configuration options
71
- * @returns {SlashCommandsManager} Slash commands state and controls
72
- */
73
- export function useSlashCommands(options = {}) {
74
- const {
75
- getTextareaRef,
76
- getContent,
77
- setContent,
78
- getSnippets,
79
- onOpenSnippetsModal,
80
- } = options;
81
-
82
- let menu = $state({
83
- open: false,
84
- query: "",
85
- position: { x: 0, y: 0 },
86
- selectedIndex: 0,
87
- });
88
-
89
- // Build full command list including snippets
90
- /** @returns {SlashCommand[]} */
91
- function getAllCommands() {
92
- const snippets = getSnippets ? getSnippets() : [];
93
- /** @type {SlashCommand[]} */
94
- const snippetCommands = snippets.map((s) => ({
95
- id: s.id,
96
- label: `> ${s.name}`,
97
- insert: s.content,
98
- isSnippet: true,
99
- }));
100
-
101
- /** @type {SlashCommand} */
102
- const newSnippetCommand = {
103
- id: "newSnippet",
104
- label: "Create New Snippet...",
105
- insert: "",
106
- isAction: true,
107
- action: onOpenSnippetsModal,
108
- };
109
-
110
- return [...baseSlashCommands, newSnippetCommand, ...snippetCommands];
111
- }
112
-
113
- // Get filtered commands based on query
114
- function getFilteredCommands() {
115
- const allCommands = getAllCommands();
116
- return allCommands.filter((cmd) =>
117
- cmd.label.toLowerCase().includes(menu.query.toLowerCase())
118
- );
119
- }
120
-
121
- function open() {
122
- menu.open = true;
123
- menu.query = "";
124
- menu.selectedIndex = 0;
125
- }
126
-
127
- function close() {
128
- menu.open = false;
129
- }
130
-
131
- /** @param {'up' | 'down'} direction */
132
- function navigate(direction) {
133
- const filtered = getFilteredCommands();
134
- const count = filtered.length;
135
- if (count === 0) return;
136
-
137
- if (direction === "down") {
138
- menu.selectedIndex = (menu.selectedIndex + 1) % count;
139
- } else if (direction === "up") {
140
- menu.selectedIndex = (menu.selectedIndex - 1 + count) % count;
141
- }
142
- }
143
-
144
- /** @param {number} index */
145
- function execute(index) {
146
- const filtered = getFilteredCommands();
147
- const cmd = filtered[index];
148
- if (!cmd) return;
149
-
150
- const textareaRef = getTextareaRef ? getTextareaRef() : null;
151
- const content = getContent ? getContent() : '';
152
-
153
- if (!textareaRef || !setContent) return;
154
-
155
- // Handle action commands (like "Create New Snippet...")
156
- if (cmd.isAction && cmd.action) {
157
- // Remove the slash that triggered the menu
158
- const pos = textareaRef.selectionStart;
159
- const textBefore = content.substring(0, pos);
160
- const lastSlashIndex = textBefore.lastIndexOf("/");
161
- if (lastSlashIndex >= 0) {
162
- setContent(content.substring(0, lastSlashIndex) + content.substring(pos));
163
- }
164
- menu.open = false;
165
- cmd.action();
166
- return;
167
- }
168
-
169
- // Remove the slash that triggered the menu and insert command
170
- const pos = textareaRef.selectionStart;
171
- const textBefore = content.substring(0, pos);
172
- const lastSlashIndex = textBefore.lastIndexOf("/");
173
-
174
- if (lastSlashIndex >= 0) {
175
- setContent(
176
- content.substring(0, lastSlashIndex) + cmd.insert + content.substring(pos)
177
- );
178
-
179
- setTimeout(() => {
180
- const newPos = lastSlashIndex + (cmd.cursorOffset || cmd.insert.length);
181
- textareaRef.selectionStart = textareaRef.selectionEnd = newPos;
182
- textareaRef.focus();
183
- }, 0);
184
- }
185
-
186
- menu.open = false;
187
- }
188
-
189
- /**
190
- * @param {string} key
191
- * @param {number} cursorPos
192
- * @param {string} content
193
- */
194
- function shouldTrigger(key, cursorPos, content) {
195
- if (key !== "/" || menu.open) return false;
196
- // Only trigger at start of line or after whitespace
197
- return cursorPos === 0 || /\s$/.test(content.substring(0, cursorPos));
198
- }
199
-
200
- return {
201
- get menu() {
202
- return menu;
203
- },
204
- get isOpen() {
205
- return menu.open;
206
- },
207
- getAllCommands,
208
- getFilteredCommands,
209
- open,
210
- close,
211
- navigate,
212
- execute,
213
- shouldTrigger,
214
- };
215
- }