@autumnsgrove/groveengine 0.6.1 → 0.6.3

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 (77) hide show
  1. package/dist/auth/jwt.d.ts +10 -4
  2. package/dist/auth/jwt.js +18 -4
  3. package/dist/auth/session.d.ts +22 -15
  4. package/dist/auth/session.js +35 -16
  5. package/dist/components/admin/GutterManager.svelte +81 -139
  6. package/dist/components/admin/GutterManager.svelte.d.ts +6 -6
  7. package/dist/components/admin/MarkdownEditor.svelte +80 -23
  8. package/dist/components/admin/MarkdownEditor.svelte.d.ts +14 -8
  9. package/dist/components/admin/composables/useAmbientSounds.svelte.d.ts +52 -2
  10. package/dist/components/admin/composables/useAmbientSounds.svelte.js +38 -4
  11. package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +80 -10
  12. package/dist/components/admin/composables/useCommandPalette.svelte.js +45 -5
  13. package/dist/components/admin/composables/useDraftManager.svelte.d.ts +76 -14
  14. package/dist/components/admin/composables/useDraftManager.svelte.js +44 -10
  15. package/dist/components/admin/composables/useEditorTheme.svelte.d.ts +168 -2
  16. package/dist/components/admin/composables/useEditorTheme.svelte.js +40 -7
  17. package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +94 -22
  18. package/dist/components/admin/composables/useSlashCommands.svelte.js +58 -9
  19. package/dist/components/admin/composables/useSnippets.svelte.d.ts +51 -2
  20. package/dist/components/admin/composables/useSnippets.svelte.js +35 -3
  21. package/dist/components/admin/composables/useWritingSession.svelte.d.ts +64 -6
  22. package/dist/components/admin/composables/useWritingSession.svelte.js +42 -5
  23. package/dist/components/custom/ContentWithGutter.svelte +53 -23
  24. package/dist/components/custom/ContentWithGutter.svelte.d.ts +6 -14
  25. package/dist/components/custom/GutterItem.svelte +1 -1
  26. package/dist/components/custom/LeftGutter.svelte +43 -13
  27. package/dist/components/custom/LeftGutter.svelte.d.ts +6 -6
  28. package/dist/config/ai-models.js +1 -1
  29. package/dist/groveauth/client.js +11 -11
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.js +2 -2
  32. package/dist/server/logger.d.ts +74 -26
  33. package/dist/server/logger.js +133 -184
  34. package/dist/server/services/cache.js +1 -10
  35. package/dist/ui/components/charts/ActivityOverview.svelte +14 -3
  36. package/dist/ui/components/charts/ActivityOverview.svelte.d.ts +10 -7
  37. package/dist/ui/components/charts/RepoBreakdown.svelte +9 -3
  38. package/dist/ui/components/charts/RepoBreakdown.svelte.d.ts +12 -11
  39. package/dist/ui/components/charts/Sparkline.svelte +18 -7
  40. package/dist/ui/components/charts/Sparkline.svelte.d.ts +21 -2
  41. package/dist/ui/components/gallery/ImageGallery.svelte +12 -8
  42. package/dist/ui/components/gallery/ImageGallery.svelte.d.ts +2 -2
  43. package/dist/ui/components/gallery/Lightbox.svelte +5 -2
  44. package/dist/ui/components/gallery/ZoomableImage.svelte +8 -5
  45. package/dist/ui/components/primitives/accordion/index.d.ts +1 -1
  46. package/dist/ui/components/primitives/input/input.svelte.d.ts +1 -1
  47. package/dist/ui/components/primitives/tabs/index.d.ts +1 -1
  48. package/dist/ui/components/primitives/textarea/textarea.svelte.d.ts +1 -1
  49. package/dist/ui/components/ui/Button.svelte +5 -0
  50. package/dist/ui/components/ui/Button.svelte.d.ts +4 -1
  51. package/dist/ui/components/ui/Input.svelte +4 -0
  52. package/dist/ui/components/ui/Input.svelte.d.ts +3 -1
  53. package/dist/ui/components/ui/Logo.svelte +86 -0
  54. package/dist/ui/components/ui/Logo.svelte.d.ts +25 -0
  55. package/dist/ui/components/ui/LogoLoader.svelte +71 -0
  56. package/dist/ui/components/ui/LogoLoader.svelte.d.ts +9 -0
  57. package/dist/ui/components/ui/index.d.ts +2 -0
  58. package/dist/ui/components/ui/index.js +2 -0
  59. package/dist/ui/tailwind.preset.js +8 -8
  60. package/dist/utils/api.js +2 -1
  61. package/dist/utils/debounce.d.ts +4 -3
  62. package/dist/utils/debounce.js +10 -6
  63. package/dist/utils/gallery.d.ts +58 -32
  64. package/dist/utils/gallery.js +111 -129
  65. package/dist/utils/gutter.d.ts +47 -26
  66. package/dist/utils/gutter.js +116 -124
  67. package/dist/utils/imageProcessor.d.ts +66 -19
  68. package/dist/utils/imageProcessor.js +31 -10
  69. package/dist/utils/index.d.ts +11 -11
  70. package/dist/utils/index.js +4 -3
  71. package/dist/utils/json.js +1 -1
  72. package/dist/utils/markdown.d.ts +183 -103
  73. package/dist/utils/markdown.js +517 -678
  74. package/dist/utils/sanitize.d.ts +22 -12
  75. package/dist/utils/sanitize.js +268 -282
  76. package/dist/utils/validation.js +4 -3
  77. package/package.json +4 -3
@@ -1,14 +1,20 @@
1
1
  /**
2
2
  * Sign a JWT payload
3
- * @param {Object} payload - The payload to sign
3
+ * @param {JwtPayload} payload - The payload to sign
4
4
  * @param {string} secret - The secret key
5
5
  * @returns {Promise<string>} - The signed JWT token
6
6
  */
7
- export function signJwt(payload: Object, secret: string): Promise<string>;
7
+ export function signJwt(payload: JwtPayload, secret: string): Promise<string>;
8
8
  /**
9
9
  * Verify and decode a JWT token
10
10
  * @param {string} token - The JWT token to verify
11
11
  * @param {string} secret - The secret key
12
- * @returns {Promise<Object|null>} - The decoded payload or null if invalid
12
+ * @returns {Promise<JwtPayload|null>} - The decoded payload or null if invalid
13
13
  */
14
- export function verifyJwt(token: string, secret: string): Promise<Object | null>;
14
+ export function verifyJwt(token: string, secret: string): Promise<JwtPayload | null>;
15
+ export type JwtPayload = {
16
+ sub?: string | undefined;
17
+ email?: string | undefined;
18
+ exp?: number | undefined;
19
+ iat?: number | undefined;
20
+ };
package/dist/auth/jwt.js CHANGED
@@ -2,11 +2,21 @@
2
2
  * JWT utilities using Web Crypto API (Cloudflare Workers compatible)
3
3
  */
4
4
 
5
+ /**
6
+ * @typedef {Object} JwtPayload
7
+ * @property {string} [sub]
8
+ * @property {string} [email]
9
+ * @property {number} [exp]
10
+ * @property {number} [iat]
11
+ */
12
+
5
13
  const encoder = new TextEncoder();
6
14
  const decoder = new TextDecoder();
7
15
 
8
16
  /**
9
17
  * Base64URL encode
18
+ * @param {ArrayBuffer} data
19
+ * @returns {string}
10
20
  */
11
21
  function base64UrlEncode(data) {
12
22
  const base64 = btoa(String.fromCharCode(...new Uint8Array(data)));
@@ -15,6 +25,8 @@ function base64UrlEncode(data) {
15
25
 
16
26
  /**
17
27
  * Base64URL decode
28
+ * @param {string} str
29
+ * @returns {Uint8Array}
18
30
  */
19
31
  function base64UrlDecode(str) {
20
32
  const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
@@ -25,6 +37,8 @@ function base64UrlDecode(str) {
25
37
 
26
38
  /**
27
39
  * Create HMAC key from secret
40
+ * @param {string} secret
41
+ * @returns {Promise<CryptoKey>}
28
42
  */
29
43
  async function createKey(secret) {
30
44
  return await crypto.subtle.importKey(
@@ -38,16 +52,16 @@ async function createKey(secret) {
38
52
 
39
53
  /**
40
54
  * Sign a JWT payload
41
- * @param {Object} payload - The payload to sign
55
+ * @param {JwtPayload} payload - The payload to sign
42
56
  * @param {string} secret - The secret key
43
57
  * @returns {Promise<string>} - The signed JWT token
44
58
  */
45
59
  export async function signJwt(payload, secret) {
46
60
  const header = { alg: "HS256", typ: "JWT" };
47
61
 
48
- const headerEncoded = base64UrlEncode(encoder.encode(JSON.stringify(header)));
62
+ const headerEncoded = base64UrlEncode(encoder.encode(JSON.stringify(header)).buffer);
49
63
  const payloadEncoded = base64UrlEncode(
50
- encoder.encode(JSON.stringify(payload)),
64
+ encoder.encode(JSON.stringify(payload)).buffer,
51
65
  );
52
66
 
53
67
  const message = `${headerEncoded}.${payloadEncoded}`;
@@ -68,7 +82,7 @@ export async function signJwt(payload, secret) {
68
82
  * Verify and decode a JWT token
69
83
  * @param {string} token - The JWT token to verify
70
84
  * @param {string} secret - The secret key
71
- * @returns {Promise<Object|null>} - The decoded payload or null if invalid
85
+ * @returns {Promise<JwtPayload|null>} - The decoded payload or null if invalid
72
86
  */
73
87
  export async function verifyJwt(token, secret) {
74
88
  try {
@@ -1,20 +1,17 @@
1
1
  /**
2
2
  * Create a session token for a user
3
- * @param {Object} user - User data
4
- * @param {string} user.email - User email address
3
+ * @param {User} user - User data
5
4
  * @param {string} secret - Session secret
6
5
  * @returns {Promise<string>} - Signed JWT token
7
6
  */
8
- export function createSession(user: {
9
- email: string;
10
- }, secret: string): Promise<string>;
7
+ export function createSession(user: User, secret: string): Promise<string>;
11
8
  /**
12
9
  * Verify a session token and return user data
13
10
  * @param {string} token - Session token
14
11
  * @param {string} secret - Session secret
15
- * @returns {Promise<Object|null>} - User data or null if invalid
12
+ * @returns {Promise<User|null>} - User data or null if invalid
16
13
  */
17
- export function verifySession(token: string, secret: string): Promise<Object | null>;
14
+ export function verifySession(token: string, secret: string): Promise<User | null>;
18
15
  /**
19
16
  * Create Set-Cookie header value for session
20
17
  * @param {string} token - Session token
@@ -42,19 +39,29 @@ export function parseSessionCookie(cookieHeader: string): string | null;
42
39
  export function isAllowedAdmin(email: string, allowedList: string): boolean;
43
40
  /**
44
41
  * Verify that a user owns/has access to a tenant
45
- * @param {Object} db - D1 database instance
46
- * @param {string} tenantId - Tenant ID to check
42
+ * @param {import('@cloudflare/workers-types').D1Database} db - D1 database instance
43
+ * @param {string | undefined | null} tenantId - Tenant ID to check
47
44
  * @param {string} userEmail - User's email address
48
45
  * @returns {Promise<boolean>} - Whether the user owns the tenant
49
46
  */
50
- export function verifyTenantOwnership(db: Object, tenantId: string, userEmail: string): Promise<boolean>;
47
+ export function verifyTenantOwnership(db: import("@cloudflare/workers-types").D1Database, tenantId: string | undefined | null, userEmail: string): Promise<boolean>;
51
48
  /**
52
49
  * Get tenant ID with ownership verification
53
50
  * Throws 403 if user doesn't own the tenant
54
- * @param {Object} db - D1 database instance
55
- * @param {string} tenantId - Tenant ID from request
56
- * @param {Object} user - User object with email
51
+ * @param {import('@cloudflare/workers-types').D1Database} db - D1 database instance
52
+ * @param {string | undefined | null} tenantId - Tenant ID from request
53
+ * @param {User | null | undefined} user - User object with email
57
54
  * @returns {Promise<string>} - Verified tenant ID
58
- * @throws {Error} - If unauthorized
55
+ * @throws {SessionError} - If unauthorized
59
56
  */
60
- export function getVerifiedTenantId(db: Object, tenantId: string, user: Object): Promise<string>;
57
+ export function getVerifiedTenantId(db: import("@cloudflare/workers-types").D1Database, tenantId: string | undefined | null, user: User | null | undefined): Promise<string>;
58
+ export type User = {
59
+ email: string;
60
+ };
61
+ export type SessionError = {
62
+ message: string;
63
+ status: number;
64
+ };
65
+ export type TenantRow = {
66
+ email: string;
67
+ };
@@ -4,13 +4,28 @@
4
4
 
5
5
  import { signJwt, verifyJwt } from "./jwt.js";
6
6
 
7
+ /**
8
+ * @typedef {Object} User
9
+ * @property {string} email
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} SessionError
14
+ * @property {string} message
15
+ * @property {number} status
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} TenantRow
20
+ * @property {string} email
21
+ */
22
+
7
23
  const SESSION_COOKIE_NAME = "session";
8
24
  const SESSION_DURATION_SECONDS = 60 * 60 * 24 * 7; // 7 days
9
25
 
10
26
  /**
11
27
  * Create a session token for a user
12
- * @param {Object} user - User data
13
- * @param {string} user.email - User email address
28
+ * @param {User} user - User data
14
29
  * @param {string} secret - Session secret
15
30
  * @returns {Promise<string>} - Signed JWT token
16
31
  */
@@ -28,12 +43,12 @@ export async function createSession(user, secret) {
28
43
  * Verify a session token and return user data
29
44
  * @param {string} token - Session token
30
45
  * @param {string} secret - Session secret
31
- * @returns {Promise<Object|null>} - User data or null if invalid
46
+ * @returns {Promise<User|null>} - User data or null if invalid
32
47
  */
33
48
  export async function verifySession(token, secret) {
34
49
  const payload = await verifyJwt(token, secret);
35
50
 
36
- if (!payload) {
51
+ if (!payload || !payload.email) {
37
52
  return null;
38
53
  }
39
54
 
@@ -82,7 +97,8 @@ export function parseSessionCookie(cookieHeader) {
82
97
  return null;
83
98
  }
84
99
 
85
- const cookies = cookieHeader.split(";").reduce((acc, cookie) => {
100
+ /** @type {Record<string, string>} */
101
+ const cookies = cookieHeader.split(";").reduce((/** @type {Record<string, string>} */ acc, /** @type {string} */ cookie) => {
86
102
  const [key, value] = cookie.trim().split("=");
87
103
  if (key && value) {
88
104
  acc[key] = value;
@@ -106,8 +122,8 @@ export function isAllowedAdmin(email, allowedList) {
106
122
 
107
123
  /**
108
124
  * Verify that a user owns/has access to a tenant
109
- * @param {Object} db - D1 database instance
110
- * @param {string} tenantId - Tenant ID to check
125
+ * @param {import('@cloudflare/workers-types').D1Database} db - D1 database instance
126
+ * @param {string | undefined | null} tenantId - Tenant ID to check
111
127
  * @param {string} userEmail - User's email address
112
128
  * @returns {Promise<boolean>} - Whether the user owns the tenant
113
129
  */
@@ -117,10 +133,10 @@ export async function verifyTenantOwnership(db, tenantId, userEmail) {
117
133
  }
118
134
 
119
135
  try {
120
- const tenant = await db
136
+ const tenant = /** @type {TenantRow | null} */ (await db
121
137
  .prepare("SELECT email FROM tenants WHERE id = ?")
122
138
  .bind(tenantId)
123
- .first();
139
+ .first());
124
140
 
125
141
  if (!tenant) {
126
142
  return false;
@@ -137,28 +153,31 @@ export async function verifyTenantOwnership(db, tenantId, userEmail) {
137
153
  /**
138
154
  * Get tenant ID with ownership verification
139
155
  * Throws 403 if user doesn't own the tenant
140
- * @param {Object} db - D1 database instance
141
- * @param {string} tenantId - Tenant ID from request
142
- * @param {Object} user - User object with email
156
+ * @param {import('@cloudflare/workers-types').D1Database} db - D1 database instance
157
+ * @param {string | undefined | null} tenantId - Tenant ID from request
158
+ * @param {User | null | undefined} user - User object with email
143
159
  * @returns {Promise<string>} - Verified tenant ID
144
- * @throws {Error} - If unauthorized
160
+ * @throws {SessionError} - If unauthorized
145
161
  */
146
162
  export async function getVerifiedTenantId(db, tenantId, user) {
147
163
  if (!tenantId) {
148
- const err = new Error("Tenant ID required");
164
+ /** @type {SessionError & Error} */
165
+ const err = /** @type {SessionError & Error} */ (new Error("Tenant ID required"));
149
166
  err.status = 400;
150
167
  throw err;
151
168
  }
152
169
 
153
170
  if (!user?.email) {
154
- const err = new Error("Unauthorized");
171
+ /** @type {SessionError & Error} */
172
+ const err = /** @type {SessionError & Error} */ (new Error("Unauthorized"));
155
173
  err.status = 401;
156
174
  throw err;
157
175
  }
158
176
 
159
177
  const isOwner = await verifyTenantOwnership(db, tenantId, user.email);
160
178
  if (!isOwner) {
161
- const err = new Error("Access denied - you do not own this tenant");
179
+ /** @type {SessionError & Error} */
180
+ const err = /** @type {SessionError & Error} */ (new Error("Access denied - you do not own this tenant"));
162
181
  err.status = 403;
163
182
  throw err;
164
183
  }
@@ -5,17 +5,43 @@
5
5
  import Select from "../../ui/components/ui/Select.svelte";
6
6
  import { toast } from "../../ui/components/ui/toast";
7
7
 
8
+ /**
9
+ * @typedef {Object} GutterItem
10
+ * @property {string} type
11
+ * @property {string} [anchor]
12
+ * @property {string} [content]
13
+ * @property {string} [url]
14
+ * @property {string} [file]
15
+ * @property {string} [caption]
16
+ * @property {GalleryImage[]} [images]
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} GalleryImage
21
+ * @property {string} url
22
+ * @property {string} [alt]
23
+ * @property {string} [caption]
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} CdnImage
28
+ * @property {string} key
29
+ * @property {string} url
30
+ */
31
+
8
32
  // Props
9
33
  let {
10
- gutterItems = $bindable([]),
11
- onInsertAnchor = (anchorName) => {},
12
- availableAnchors = [],
34
+ gutterItems = $bindable(/** @type {GutterItem[]} */ ([])),
35
+ onInsertAnchor = /** @type {(anchorName: string) => void} */ ((anchorName) => {}),
36
+ availableAnchors = /** @type {string[]} */ ([]),
13
37
  } = $props();
14
38
 
15
39
  // State
16
40
  let showAddModal = $state(false);
41
+ /** @type {number | null} */
17
42
  let editingIndex = $state(null);
18
43
  let showImagePicker = $state(false);
44
+ /** @type {((url: string) => void) | null} */
19
45
  let imagePickerCallback = $state(null);
20
46
 
21
47
  // Form state for add/edit
@@ -24,9 +50,11 @@
24
50
  let itemContent = $state("");
25
51
  let itemCaption = $state("");
26
52
  let itemUrl = $state("");
53
+ /** @type {GalleryImage[]} */
27
54
  let galleryImages = $state([]);
28
55
 
29
56
  // Image picker state
57
+ /** @type {CdnImage[]} */
30
58
  let cdnImages = $state([]);
31
59
  let cdnLoading = $state(false);
32
60
  let cdnFilter = $state("");
@@ -46,6 +74,7 @@
46
74
  showAddModal = true;
47
75
  }
48
76
 
77
+ /** @param {number} index */
49
78
  function openEditModal(index) {
50
79
  const item = gutterItems[index];
51
80
  itemType = item.type;
@@ -65,6 +94,7 @@
65
94
  }
66
95
 
67
96
  function saveItem() {
97
+ /** @type {GutterItem} */
68
98
  const newItem = {
69
99
  type: itemType,
70
100
  anchor: itemAnchor,
@@ -89,11 +119,16 @@
89
119
  closeModal();
90
120
  }
91
121
 
122
+ /** @param {number} index */
92
123
  function deleteItem(index) {
93
- gutterItems = gutterItems.filter((_, i) => i !== index);
124
+ gutterItems = gutterItems.filter((/** @type {GutterItem} */ _, /** @type {number} */ i) => i !== index);
94
125
  toast.success("Gutter item deleted");
95
126
  }
96
127
 
128
+ /**
129
+ * @param {number} index
130
+ * @param {number} direction
131
+ */
97
132
  function moveItem(index, direction) {
98
133
  const newIndex = index + direction;
99
134
  if (newIndex < 0 || newIndex >= gutterItems.length) return;
@@ -106,6 +141,7 @@
106
141
  }
107
142
 
108
143
  // Generate anchor name from text
144
+ /** @param {string} text */
109
145
  function generateAnchorName(text) {
110
146
  return text
111
147
  .toLowerCase()
@@ -138,7 +174,7 @@
138
174
 
139
175
  if (response.ok) {
140
176
  const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"];
141
- cdnImages = data.images.filter((img) => {
177
+ cdnImages = data.images.filter((/** @type {CdnImage} */ img) => {
142
178
  const key = img.key.toLowerCase();
143
179
  return imageExtensions.some((ext) => key.endsWith(ext));
144
180
  });
@@ -152,12 +188,14 @@
152
188
  }
153
189
  }
154
190
 
191
+ /** @param {(url: string) => void} callback */
155
192
  function openImagePicker(callback) {
156
193
  imagePickerCallback = callback;
157
194
  showImagePicker = true;
158
195
  loadCdnImages();
159
196
  }
160
197
 
198
+ /** @param {CdnImage} image */
161
199
  function selectImage(image) {
162
200
  if (imagePickerCallback) {
163
201
  imagePickerCallback(image.url);
@@ -181,16 +219,23 @@
181
219
  });
182
220
  }
183
221
 
222
+ /** @param {number} index */
184
223
  function removeGalleryImage(index) {
185
- galleryImages = galleryImages.filter((_, i) => i !== index);
224
+ galleryImages = galleryImages.filter((/** @type {GalleryImage} */ _, /** @type {number} */ i) => i !== index);
186
225
  }
187
226
 
227
+ /**
228
+ * @param {number} index
229
+ * @param {keyof GalleryImage} field
230
+ * @param {string} value
231
+ */
188
232
  function updateGalleryImage(index, field, value) {
189
233
  galleryImages[index][field] = value;
190
234
  galleryImages = [...galleryImages];
191
235
  }
192
236
 
193
237
  // Get preview of item content
238
+ /** @param {GutterItem} item */
194
239
  function getItemPreview(item) {
195
240
  if (item.type === "comment" && item.content) {
196
241
  return item.content.substring(0, 50) + (item.content.length > 50 ? "..." : "");
@@ -204,6 +249,7 @@
204
249
  return "";
205
250
  }
206
251
 
252
+ /** @param {string} type */
207
253
  function getTypeIcon(type) {
208
254
  switch (type) {
209
255
  case "comment":
@@ -269,17 +315,19 @@
269
315
  </div>
270
316
 
271
317
  <!-- Add/Edit Modal -->
272
- <Dialog bind:open={showAddModal}>
273
- <h3 slot="title">{editingIndex !== null ? "Edit" : "Add"} Gutter Item</h3>
274
-
275
- <div class="form-group">
276
- <label for="item-type">Type</label>
277
- <Select id="item-type" bind:value={itemType}>
278
- <option value="comment">Comment (Markdown)</option>
279
- <option value="photo">Photo</option>
280
- <option value="gallery">Image Gallery</option>
281
- </Select>
282
- </div>
318
+ <Dialog bind:open={showAddModal} title={editingIndex !== null ? "Edit Gutter Item" : "Add Gutter Item"}>
319
+ {#snippet children()}
320
+ <div class="form-group">
321
+ <label for="item-type">Type</label>
322
+ <Select
323
+ bind:value={itemType}
324
+ options={[
325
+ { value: "comment", label: "Comment (Markdown)" },
326
+ { value: "photo", label: "Photo" },
327
+ { value: "gallery", label: "Image Gallery" }
328
+ ]}
329
+ />
330
+ </div>
283
331
 
284
332
  <div class="form-group">
285
333
  <label for="item-anchor">Anchor</label>
@@ -369,7 +417,7 @@
369
417
 
370
418
  {#if itemType === "gallery"}
371
419
  <div class="form-group">
372
- <label>Gallery Images</label>
420
+ <div class="gallery-label">Gallery Images</div>
373
421
  <div class="gallery-list">
374
422
  {#each galleryImages as image, i (i)}
375
423
  <div class="gallery-image-item">
@@ -378,14 +426,14 @@
378
426
  <Input
379
427
  type="text"
380
428
  value={image.alt}
381
- oninput={(e) => updateGalleryImage(i, "alt", e.target.value)}
429
+ oninput={(/** @type {Event} */ e) => updateGalleryImage(i, "alt", /** @type {HTMLInputElement} */ (e.target).value)}
382
430
  placeholder="Alt text"
383
431
  class="small"
384
432
  />
385
433
  <Input
386
434
  type="text"
387
435
  value={image.caption}
388
- oninput={(e) => updateGalleryImage(i, "caption", e.target.value)}
436
+ oninput={(/** @type {Event} */ e) => updateGalleryImage(i, "caption", /** @type {HTMLInputElement} */ (e.target).value)}
389
437
  placeholder="Caption"
390
438
  class="small"
391
439
  />
@@ -403,20 +451,20 @@
403
451
  </button>
404
452
  </div>
405
453
  {/if}
454
+ {/snippet}
406
455
 
407
- <div slot="footer" style="display: flex; gap: 0.75rem; justify-content: flex-end;">
456
+ {#snippet footer()}
408
457
  <Button variant="outline" onclick={closeModal}>Cancel</Button>
409
458
  <Button onclick={saveItem}>
410
459
  {editingIndex !== null ? "Update" : "Add"} Item
411
460
  </Button>
412
- </div>
461
+ {/snippet}
413
462
  </Dialog>
414
463
 
415
464
  <!-- Image Picker Modal -->
416
- <Dialog bind:open={showImagePicker}>
417
- <h3 slot="title">Select Image from CDN</h3>
418
-
419
- <div class="picker-controls">
465
+ <Dialog bind:open={showImagePicker} title="Select Image from CDN">
466
+ {#snippet children()}
467
+ <div class="picker-controls">
420
468
  <Input
421
469
  type="text"
422
470
  bind:value={cdnFilter}
@@ -444,10 +492,11 @@
444
492
  {/each}
445
493
  {/if}
446
494
  </div>
495
+ {/snippet}
447
496
 
448
- <div slot="footer" style="display: flex; gap: 0.75rem; justify-content: flex-end;">
497
+ {#snippet footer()}
449
498
  <Button variant="outline" onclick={closeImagePicker}>Cancel</Button>
450
- </div>
499
+ {/snippet}
451
500
  </Dialog>
452
501
 
453
502
  <style>
@@ -577,43 +626,13 @@
577
626
  text-overflow: ellipsis;
578
627
  }
579
628
 
580
- /* Modal Styles */
581
- .modal-overlay {
582
- position: fixed;
583
- top: 0;
584
- left: 0;
585
- right: 0;
586
- bottom: 0;
587
- background: rgba(0, 0, 0, 0.7);
588
- display: flex;
589
- align-items: center;
590
- justify-content: center;
591
- z-index: 1000;
592
- padding: 1rem;
593
- }
594
-
595
- .modal-content {
596
- background: #1e1e1e;
597
- border: 1px solid #3a3a3a;
598
- border-radius: 8px;
599
- padding: 1.5rem;
600
- max-width: 500px;
601
- width: 100%;
602
- max-height: 80vh;
603
- overflow-y: auto;
604
- }
605
-
606
- .modal-content h3 {
607
- margin: 0 0 1.25rem 0;
608
- color: #d4d4d4;
609
- font-size: 1.1rem;
610
- }
611
-
629
+ /* Form Styles */
612
630
  .form-group {
613
631
  margin-bottom: 1rem;
614
632
  }
615
633
 
616
- .form-group label {
634
+ .form-group label,
635
+ .gallery-label {
617
636
  display: block;
618
637
  margin-bottom: 0.4rem;
619
638
  font-size: 0.85rem;
@@ -636,11 +655,6 @@
636
655
  border-color: #4a7c4a;
637
656
  }
638
657
 
639
- .form-input.small {
640
- padding: 0.35rem 0.5rem;
641
- font-size: 0.8rem;
642
- }
643
-
644
658
  .form-textarea {
645
659
  resize: vertical;
646
660
  min-height: 100px;
@@ -672,23 +686,6 @@
672
686
  flex: 1;
673
687
  }
674
688
 
675
- .insert-anchor-btn,
676
- .browse-btn {
677
- padding: 0.5rem 0.75rem;
678
- background: #2d4a2d;
679
- color: #a8dca8;
680
- border: 1px solid #3d5a3d;
681
- border-radius: 4px;
682
- font-size: 0.8rem;
683
- white-space: nowrap;
684
- cursor: pointer;
685
- }
686
-
687
- .insert-anchor-btn:hover,
688
- .browse-btn:hover {
689
- background: #3d5a3d;
690
- }
691
-
692
689
  .available-anchors {
693
690
  display: flex;
694
691
  flex-wrap: wrap;
@@ -790,49 +787,7 @@
790
787
  color: #8bc48b;
791
788
  }
792
789
 
793
- .modal-actions {
794
- display: flex;
795
- justify-content: flex-end;
796
- gap: 0.75rem;
797
- margin-top: 1.5rem;
798
- padding-top: 1rem;
799
- border-top: 1px solid #3a3a3a;
800
- }
801
-
802
- .cancel-btn,
803
- .save-btn {
804
- padding: 0.5rem 1rem;
805
- border-radius: 4px;
806
- font-size: 0.9rem;
807
- cursor: pointer;
808
- transition: all 0.15s ease;
809
- }
810
-
811
- .cancel-btn {
812
- background: transparent;
813
- border: 1px solid #3a3a3a;
814
- color: #9d9d9d;
815
- }
816
-
817
- .cancel-btn:hover {
818
- background: #3a3a3a;
819
- }
820
-
821
- .save-btn {
822
- background: #4a7c4a;
823
- border: none;
824
- color: #c8f0c8;
825
- }
826
-
827
- .save-btn:hover {
828
- background: #5a9c5a;
829
- }
830
-
831
- /* Image Picker Modal */
832
- .image-picker-modal {
833
- max-width: 700px;
834
- }
835
-
790
+ /* Image Picker */
836
791
  .picker-controls {
837
792
  display: flex;
838
793
  gap: 0.5rem;
@@ -843,19 +798,6 @@
843
798
  flex: 1;
844
799
  }
845
800
 
846
- .filter-btn {
847
- padding: 0.5rem 1rem;
848
- background: #3a3a3a;
849
- border: none;
850
- border-radius: 4px;
851
- color: #d4d4d4;
852
- cursor: pointer;
853
- }
854
-
855
- .filter-btn:hover {
856
- background: #4a4a4a;
857
- }
858
-
859
801
  .image-grid {
860
802
  display: grid;
861
803
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
@@ -4,12 +4,12 @@ type GutterManager = {
4
4
  $set?(props: Partial<$$ComponentProps>): void;
5
5
  };
6
6
  declare const GutterManager: import("svelte").Component<{
7
- gutterItems?: any[];
8
- onInsertAnchor?: Function;
9
- availableAnchors?: any[];
7
+ gutterItems?: any;
8
+ onInsertAnchor?: any;
9
+ availableAnchors?: any;
10
10
  }, {}, "gutterItems">;
11
11
  type $$ComponentProps = {
12
- gutterItems?: any[];
13
- onInsertAnchor?: Function;
14
- availableAnchors?: any[];
12
+ gutterItems?: any;
13
+ onInsertAnchor?: any;
14
+ availableAnchors?: any;
15
15
  };