@autumnsgrove/groveengine 0.6.2 → 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.
- package/dist/auth/jwt.d.ts +10 -4
- package/dist/auth/jwt.js +18 -4
- package/dist/auth/session.d.ts +22 -15
- package/dist/auth/session.js +35 -16
- package/dist/components/admin/GutterManager.svelte +81 -139
- package/dist/components/admin/GutterManager.svelte.d.ts +6 -6
- package/dist/components/admin/MarkdownEditor.svelte +80 -23
- package/dist/components/admin/MarkdownEditor.svelte.d.ts +14 -8
- package/dist/components/admin/composables/useAmbientSounds.svelte.d.ts +52 -2
- package/dist/components/admin/composables/useAmbientSounds.svelte.js +38 -4
- package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +80 -10
- package/dist/components/admin/composables/useCommandPalette.svelte.js +45 -5
- package/dist/components/admin/composables/useDraftManager.svelte.d.ts +76 -14
- package/dist/components/admin/composables/useDraftManager.svelte.js +44 -10
- package/dist/components/admin/composables/useEditorTheme.svelte.d.ts +168 -2
- package/dist/components/admin/composables/useEditorTheme.svelte.js +40 -7
- package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +94 -22
- package/dist/components/admin/composables/useSlashCommands.svelte.js +58 -9
- package/dist/components/admin/composables/useSnippets.svelte.d.ts +51 -2
- package/dist/components/admin/composables/useSnippets.svelte.js +35 -3
- package/dist/components/admin/composables/useWritingSession.svelte.d.ts +64 -6
- package/dist/components/admin/composables/useWritingSession.svelte.js +42 -5
- package/dist/components/custom/ContentWithGutter.svelte +53 -23
- package/dist/components/custom/ContentWithGutter.svelte.d.ts +6 -14
- package/dist/components/custom/GutterItem.svelte +1 -1
- package/dist/components/custom/LeftGutter.svelte +43 -13
- package/dist/components/custom/LeftGutter.svelte.d.ts +6 -6
- package/dist/config/ai-models.js +1 -1
- package/dist/groveauth/client.js +11 -11
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -2
- package/dist/server/logger.d.ts +74 -26
- package/dist/server/logger.js +133 -184
- package/dist/server/services/cache.js +1 -10
- package/dist/ui/components/charts/ActivityOverview.svelte +14 -3
- package/dist/ui/components/charts/ActivityOverview.svelte.d.ts +10 -7
- package/dist/ui/components/charts/RepoBreakdown.svelte +9 -3
- package/dist/ui/components/charts/RepoBreakdown.svelte.d.ts +12 -11
- package/dist/ui/components/charts/Sparkline.svelte +18 -7
- package/dist/ui/components/charts/Sparkline.svelte.d.ts +21 -2
- package/dist/ui/components/gallery/ImageGallery.svelte +12 -8
- package/dist/ui/components/gallery/ImageGallery.svelte.d.ts +2 -2
- package/dist/ui/components/gallery/Lightbox.svelte +5 -2
- package/dist/ui/components/gallery/ZoomableImage.svelte +8 -5
- package/dist/ui/components/primitives/accordion/index.d.ts +1 -1
- package/dist/ui/components/primitives/input/input.svelte.d.ts +1 -1
- package/dist/ui/components/primitives/tabs/index.d.ts +1 -1
- package/dist/ui/components/primitives/textarea/textarea.svelte.d.ts +1 -1
- package/dist/ui/components/ui/Button.svelte +5 -0
- package/dist/ui/components/ui/Button.svelte.d.ts +4 -1
- package/dist/ui/components/ui/Input.svelte +4 -0
- package/dist/ui/components/ui/Input.svelte.d.ts +3 -1
- package/dist/ui/components/ui/Logo.svelte +86 -0
- package/dist/ui/components/ui/Logo.svelte.d.ts +25 -0
- package/dist/ui/components/ui/LogoLoader.svelte +71 -0
- package/dist/ui/components/ui/LogoLoader.svelte.d.ts +9 -0
- package/dist/ui/components/ui/index.d.ts +2 -0
- package/dist/ui/components/ui/index.js +2 -0
- package/dist/ui/tailwind.preset.js +8 -8
- package/dist/utils/api.js +2 -1
- package/dist/utils/debounce.d.ts +4 -3
- package/dist/utils/debounce.js +10 -6
- package/dist/utils/gallery.d.ts +58 -32
- package/dist/utils/gallery.js +111 -129
- package/dist/utils/gutter.d.ts +47 -26
- package/dist/utils/gutter.js +116 -124
- package/dist/utils/imageProcessor.d.ts +66 -19
- package/dist/utils/imageProcessor.js +31 -10
- package/dist/utils/index.d.ts +11 -11
- package/dist/utils/index.js +4 -3
- package/dist/utils/json.js +1 -1
- package/dist/utils/markdown.d.ts +183 -103
- package/dist/utils/markdown.js +517 -678
- package/dist/utils/sanitize.d.ts +22 -12
- package/dist/utils/sanitize.js +268 -282
- package/dist/utils/validation.js +4 -3
- package/package.json +3 -2
package/dist/auth/jwt.d.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sign a JWT payload
|
|
3
|
-
* @param {
|
|
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:
|
|
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<
|
|
12
|
+
* @returns {Promise<JwtPayload|null>} - The decoded payload or null if invalid
|
|
13
13
|
*/
|
|
14
|
-
export function verifyJwt(token: string, secret: string): Promise<
|
|
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 {
|
|
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<
|
|
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 {
|
package/dist/auth/session.d.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create a session token for a user
|
|
3
|
-
* @param {
|
|
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<
|
|
12
|
+
* @returns {Promise<User|null>} - User data or null if invalid
|
|
16
13
|
*/
|
|
17
|
-
export function verifySession(token: string, secret: string): Promise<
|
|
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 {
|
|
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:
|
|
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 {
|
|
55
|
-
* @param {string} tenantId - Tenant ID from request
|
|
56
|
-
* @param {
|
|
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 {
|
|
55
|
+
* @throws {SessionError} - If unauthorized
|
|
59
56
|
*/
|
|
60
|
-
export function getVerifiedTenantId(db:
|
|
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
|
+
};
|
package/dist/auth/session.js
CHANGED
|
@@ -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 {
|
|
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<
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
141
|
-
* @param {string} tenantId - Tenant ID from request
|
|
142
|
-
* @param {
|
|
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 {
|
|
160
|
+
* @throws {SessionError} - If unauthorized
|
|
145
161
|
*/
|
|
146
162
|
export async function getVerifiedTenantId(db, tenantId, user) {
|
|
147
163
|
if (!tenantId) {
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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</
|
|
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
|
-
|
|
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
|
-
|
|
461
|
+
{/snippet}
|
|
413
462
|
</Dialog>
|
|
414
463
|
|
|
415
464
|
<!-- Image Picker Modal -->
|
|
416
|
-
<Dialog bind:open={showImagePicker}>
|
|
417
|
-
|
|
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
|
-
|
|
497
|
+
{#snippet footer()}
|
|
449
498
|
<Button variant="outline" onclick={closeImagePicker}>Cancel</Button>
|
|
450
|
-
|
|
499
|
+
{/snippet}
|
|
451
500
|
</Dialog>
|
|
452
501
|
|
|
453
502
|
<style>
|
|
@@ -577,43 +626,13 @@
|
|
|
577
626
|
text-overflow: ellipsis;
|
|
578
627
|
}
|
|
579
628
|
|
|
580
|
-
/*
|
|
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
|
-
|
|
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?:
|
|
9
|
-
availableAnchors?: any
|
|
7
|
+
gutterItems?: any;
|
|
8
|
+
onInsertAnchor?: any;
|
|
9
|
+
availableAnchors?: any;
|
|
10
10
|
}, {}, "gutterItems">;
|
|
11
11
|
type $$ComponentProps = {
|
|
12
|
-
gutterItems?: any
|
|
13
|
-
onInsertAnchor?:
|
|
14
|
-
availableAnchors?: any
|
|
12
|
+
gutterItems?: any;
|
|
13
|
+
onInsertAnchor?: any;
|
|
14
|
+
availableAnchors?: any;
|
|
15
15
|
};
|