@autumnsgrove/groveengine 0.6.2 → 0.6.4
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/LICENSE +378 -0
- 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 +23 -23
- package/static/fonts/alagard.ttf +0 -0
|
@@ -3,7 +3,49 @@
|
|
|
3
3
|
* Manages the slash command menu and execution
|
|
4
4
|
*/
|
|
5
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
|
+
|
|
6
47
|
// Base slash commands definition
|
|
48
|
+
/** @type {SlashCommand[]} */
|
|
7
49
|
export const baseSlashCommands = [
|
|
8
50
|
{ id: "heading1", label: "Heading 1", insert: "# " },
|
|
9
51
|
{ id: "heading2", label: "Heading 2", insert: "## " },
|
|
@@ -25,13 +67,8 @@ export const baseSlashCommands = [
|
|
|
25
67
|
|
|
26
68
|
/**
|
|
27
69
|
* Creates a slash commands manager with Svelte 5 runes
|
|
28
|
-
* @param {
|
|
29
|
-
* @
|
|
30
|
-
* @param {Function} options.getContent - Function to get content
|
|
31
|
-
* @param {Function} options.setContent - Function to set content
|
|
32
|
-
* @param {Function} options.getSnippets - Function to get user snippets
|
|
33
|
-
* @param {Function} options.onOpenSnippetsModal - Callback to open snippets modal
|
|
34
|
-
* @returns {object} Slash commands state and controls
|
|
70
|
+
* @param {SlashCommandsOptions} options - Configuration options
|
|
71
|
+
* @returns {SlashCommandsManager} Slash commands state and controls
|
|
35
72
|
*/
|
|
36
73
|
export function useSlashCommands(options = {}) {
|
|
37
74
|
const {
|
|
@@ -50,8 +87,10 @@ export function useSlashCommands(options = {}) {
|
|
|
50
87
|
});
|
|
51
88
|
|
|
52
89
|
// Build full command list including snippets
|
|
90
|
+
/** @returns {SlashCommand[]} */
|
|
53
91
|
function getAllCommands() {
|
|
54
92
|
const snippets = getSnippets ? getSnippets() : [];
|
|
93
|
+
/** @type {SlashCommand[]} */
|
|
55
94
|
const snippetCommands = snippets.map((s) => ({
|
|
56
95
|
id: s.id,
|
|
57
96
|
label: `> ${s.name}`,
|
|
@@ -59,6 +98,7 @@ export function useSlashCommands(options = {}) {
|
|
|
59
98
|
isSnippet: true,
|
|
60
99
|
}));
|
|
61
100
|
|
|
101
|
+
/** @type {SlashCommand} */
|
|
62
102
|
const newSnippetCommand = {
|
|
63
103
|
id: "newSnippet",
|
|
64
104
|
label: "Create New Snippet...",
|
|
@@ -88,6 +128,7 @@ export function useSlashCommands(options = {}) {
|
|
|
88
128
|
menu.open = false;
|
|
89
129
|
}
|
|
90
130
|
|
|
131
|
+
/** @param {'up' | 'down'} direction */
|
|
91
132
|
function navigate(direction) {
|
|
92
133
|
const filtered = getFilteredCommands();
|
|
93
134
|
const count = filtered.length;
|
|
@@ -100,13 +141,16 @@ export function useSlashCommands(options = {}) {
|
|
|
100
141
|
}
|
|
101
142
|
}
|
|
102
143
|
|
|
144
|
+
/** @param {number} index */
|
|
103
145
|
function execute(index) {
|
|
104
146
|
const filtered = getFilteredCommands();
|
|
105
147
|
const cmd = filtered[index];
|
|
106
148
|
if (!cmd) return;
|
|
107
149
|
|
|
108
|
-
const textareaRef = getTextareaRef();
|
|
109
|
-
const content = getContent();
|
|
150
|
+
const textareaRef = getTextareaRef ? getTextareaRef() : null;
|
|
151
|
+
const content = getContent ? getContent() : '';
|
|
152
|
+
|
|
153
|
+
if (!textareaRef || !setContent) return;
|
|
110
154
|
|
|
111
155
|
// Handle action commands (like "Create New Snippet...")
|
|
112
156
|
if (cmd.isAction && cmd.action) {
|
|
@@ -142,6 +186,11 @@ export function useSlashCommands(options = {}) {
|
|
|
142
186
|
menu.open = false;
|
|
143
187
|
}
|
|
144
188
|
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} key
|
|
191
|
+
* @param {number} cursorPos
|
|
192
|
+
* @param {string} content
|
|
193
|
+
*/
|
|
145
194
|
function shouldTrigger(key, cursorPos, content) {
|
|
146
195
|
if (key !== "/" || menu.open) return false;
|
|
147
196
|
// Only trigger at start of line or after whitespace
|
|
@@ -1,5 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} Snippet
|
|
3
|
+
* @property {string} id
|
|
4
|
+
* @property {string} name
|
|
5
|
+
* @property {string} content
|
|
6
|
+
* @property {string|null} trigger
|
|
7
|
+
* @property {string} [createdAt]
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} SnippetModal
|
|
11
|
+
* @property {boolean} open
|
|
12
|
+
* @property {string|null} editingId
|
|
13
|
+
* @property {string} name
|
|
14
|
+
* @property {string} content
|
|
15
|
+
* @property {string} trigger
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} SnippetsManager
|
|
19
|
+
* @property {Snippet[]} snippets
|
|
20
|
+
* @property {SnippetModal} modal
|
|
21
|
+
* @property {() => void} load
|
|
22
|
+
* @property {(editId?: string|null) => void} openModal
|
|
23
|
+
* @property {() => void} closeModal
|
|
24
|
+
* @property {() => void} saveSnippet
|
|
25
|
+
* @property {(id: string) => void} deleteSnippet
|
|
26
|
+
*/
|
|
1
27
|
/**
|
|
2
28
|
* Creates a snippets manager with Svelte 5 runes
|
|
3
|
-
* @returns {
|
|
29
|
+
* @returns {SnippetsManager} Snippets state and controls
|
|
4
30
|
*/
|
|
5
|
-
export function useSnippets():
|
|
31
|
+
export function useSnippets(): SnippetsManager;
|
|
32
|
+
export type Snippet = {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
content: string;
|
|
36
|
+
trigger: string | null;
|
|
37
|
+
createdAt?: string | undefined;
|
|
38
|
+
};
|
|
39
|
+
export type SnippetModal = {
|
|
40
|
+
open: boolean;
|
|
41
|
+
editingId: string | null;
|
|
42
|
+
name: string;
|
|
43
|
+
content: string;
|
|
44
|
+
trigger: string;
|
|
45
|
+
};
|
|
46
|
+
export type SnippetsManager = {
|
|
47
|
+
snippets: Snippet[];
|
|
48
|
+
modal: SnippetModal;
|
|
49
|
+
load: () => void;
|
|
50
|
+
openModal: (editId?: string | null) => void;
|
|
51
|
+
closeModal: () => void;
|
|
52
|
+
saveSnippet: () => void;
|
|
53
|
+
deleteSnippet: (id: string) => void;
|
|
54
|
+
};
|
|
@@ -5,20 +5,50 @@
|
|
|
5
5
|
|
|
6
6
|
const SNIPPETS_STORAGE_KEY = "grove-editor-snippets";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} Snippet
|
|
10
|
+
* @property {string} id
|
|
11
|
+
* @property {string} name
|
|
12
|
+
* @property {string} content
|
|
13
|
+
* @property {string|null} trigger
|
|
14
|
+
* @property {string} [createdAt]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} SnippetModal
|
|
19
|
+
* @property {boolean} open
|
|
20
|
+
* @property {string|null} editingId
|
|
21
|
+
* @property {string} name
|
|
22
|
+
* @property {string} content
|
|
23
|
+
* @property {string} trigger
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} SnippetsManager
|
|
28
|
+
* @property {Snippet[]} snippets
|
|
29
|
+
* @property {SnippetModal} modal
|
|
30
|
+
* @property {() => void} load
|
|
31
|
+
* @property {(editId?: string|null) => void} openModal
|
|
32
|
+
* @property {() => void} closeModal
|
|
33
|
+
* @property {() => void} saveSnippet
|
|
34
|
+
* @property {(id: string) => void} deleteSnippet
|
|
35
|
+
*/
|
|
36
|
+
|
|
8
37
|
/**
|
|
9
38
|
* Creates a snippets manager with Svelte 5 runes
|
|
10
|
-
* @returns {
|
|
39
|
+
* @returns {SnippetsManager} Snippets state and controls
|
|
11
40
|
*/
|
|
12
41
|
export function useSnippets() {
|
|
42
|
+
/** @type {Snippet[]} */
|
|
13
43
|
let snippets = $state([]);
|
|
14
44
|
|
|
15
|
-
let modal = $state({
|
|
45
|
+
let modal = $state(/** @type {SnippetModal} */ ({
|
|
16
46
|
open: false,
|
|
17
47
|
editingId: null,
|
|
18
48
|
name: "",
|
|
19
49
|
content: "",
|
|
20
50
|
trigger: "",
|
|
21
|
-
});
|
|
51
|
+
}));
|
|
22
52
|
|
|
23
53
|
function load() {
|
|
24
54
|
try {
|
|
@@ -39,6 +69,7 @@ export function useSnippets() {
|
|
|
39
69
|
}
|
|
40
70
|
}
|
|
41
71
|
|
|
72
|
+
/** @param {string | null} [editId] */
|
|
42
73
|
function openModal(editId = null) {
|
|
43
74
|
if (editId) {
|
|
44
75
|
const snippet = snippets.find((s) => s.id === editId);
|
|
@@ -96,6 +127,7 @@ export function useSnippets() {
|
|
|
96
127
|
closeModal();
|
|
97
128
|
}
|
|
98
129
|
|
|
130
|
+
/** @param {string} id */
|
|
99
131
|
function deleteSnippet(id) {
|
|
100
132
|
if (confirm("Delete this snippet?")) {
|
|
101
133
|
snippets = snippets.filter((s) => s.id !== id);
|
|
@@ -2,12 +2,70 @@
|
|
|
2
2
|
* Writing Session Composable
|
|
3
3
|
* Manages campfire sessions and writing goals
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} CampfireState
|
|
7
|
+
* @property {boolean} active
|
|
8
|
+
* @property {number|null} startTime
|
|
9
|
+
* @property {number} targetMinutes
|
|
10
|
+
* @property {number} startWordCount
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} GoalState
|
|
14
|
+
* @property {boolean} enabled
|
|
15
|
+
* @property {number} targetWords
|
|
16
|
+
* @property {number} sessionWords
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} WritingSessionOptions
|
|
20
|
+
* @property {() => number} [getWordCount] - Function to get current word count
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} WritingSessionManager
|
|
24
|
+
* @property {CampfireState} campfire
|
|
25
|
+
* @property {GoalState} goal
|
|
26
|
+
* @property {boolean} isCampfireActive
|
|
27
|
+
* @property {boolean} isGoalEnabled
|
|
28
|
+
* @property {() => string} getCampfireElapsed
|
|
29
|
+
* @property {(currentWordCount: number) => number} getGoalProgress
|
|
30
|
+
* @property {(currentWordCount: number) => number} getCampfireWords
|
|
31
|
+
* @property {() => void} startCampfire
|
|
32
|
+
* @property {() => void} endCampfire
|
|
33
|
+
* @property {() => void} promptWritingGoal
|
|
34
|
+
* @property {() => void} disableGoal
|
|
35
|
+
*/
|
|
5
36
|
/**
|
|
6
37
|
* Creates a writing session manager with Svelte 5 runes
|
|
7
|
-
* @param {
|
|
8
|
-
* @
|
|
9
|
-
* @returns {object} Session state and controls
|
|
38
|
+
* @param {WritingSessionOptions} options - Configuration options
|
|
39
|
+
* @returns {WritingSessionManager} Session state and controls
|
|
10
40
|
*/
|
|
11
|
-
export function useWritingSession(options?:
|
|
12
|
-
|
|
13
|
-
|
|
41
|
+
export function useWritingSession(options?: WritingSessionOptions): WritingSessionManager;
|
|
42
|
+
export type CampfireState = {
|
|
43
|
+
active: boolean;
|
|
44
|
+
startTime: number | null;
|
|
45
|
+
targetMinutes: number;
|
|
46
|
+
startWordCount: number;
|
|
47
|
+
};
|
|
48
|
+
export type GoalState = {
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
targetWords: number;
|
|
51
|
+
sessionWords: number;
|
|
52
|
+
};
|
|
53
|
+
export type WritingSessionOptions = {
|
|
54
|
+
/**
|
|
55
|
+
* - Function to get current word count
|
|
56
|
+
*/
|
|
57
|
+
getWordCount?: (() => number) | undefined;
|
|
58
|
+
};
|
|
59
|
+
export type WritingSessionManager = {
|
|
60
|
+
campfire: CampfireState;
|
|
61
|
+
goal: GoalState;
|
|
62
|
+
isCampfireActive: boolean;
|
|
63
|
+
isGoalEnabled: boolean;
|
|
64
|
+
getCampfireElapsed: () => string;
|
|
65
|
+
getGoalProgress: (currentWordCount: number) => number;
|
|
66
|
+
getCampfireWords: (currentWordCount: number) => number;
|
|
67
|
+
startCampfire: () => void;
|
|
68
|
+
endCampfire: () => void;
|
|
69
|
+
promptWritingGoal: () => void;
|
|
70
|
+
disableGoal: () => void;
|
|
71
|
+
};
|
|
@@ -3,19 +3,54 @@
|
|
|
3
3
|
* Manages campfire sessions and writing goals
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} CampfireState
|
|
8
|
+
* @property {boolean} active
|
|
9
|
+
* @property {number|null} startTime
|
|
10
|
+
* @property {number} targetMinutes
|
|
11
|
+
* @property {number} startWordCount
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} GoalState
|
|
16
|
+
* @property {boolean} enabled
|
|
17
|
+
* @property {number} targetWords
|
|
18
|
+
* @property {number} sessionWords
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} WritingSessionOptions
|
|
23
|
+
* @property {() => number} [getWordCount] - Function to get current word count
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} WritingSessionManager
|
|
28
|
+
* @property {CampfireState} campfire
|
|
29
|
+
* @property {GoalState} goal
|
|
30
|
+
* @property {boolean} isCampfireActive
|
|
31
|
+
* @property {boolean} isGoalEnabled
|
|
32
|
+
* @property {() => string} getCampfireElapsed
|
|
33
|
+
* @property {(currentWordCount: number) => number} getGoalProgress
|
|
34
|
+
* @property {(currentWordCount: number) => number} getCampfireWords
|
|
35
|
+
* @property {() => void} startCampfire
|
|
36
|
+
* @property {() => void} endCampfire
|
|
37
|
+
* @property {() => void} promptWritingGoal
|
|
38
|
+
* @property {() => void} disableGoal
|
|
39
|
+
*/
|
|
40
|
+
|
|
6
41
|
/**
|
|
7
42
|
* Creates a writing session manager with Svelte 5 runes
|
|
8
|
-
* @param {
|
|
9
|
-
* @
|
|
10
|
-
* @returns {object} Session state and controls
|
|
43
|
+
* @param {WritingSessionOptions} options - Configuration options
|
|
44
|
+
* @returns {WritingSessionManager} Session state and controls
|
|
11
45
|
*/
|
|
12
|
-
export function useWritingSession(options = {}) {
|
|
46
|
+
export function useWritingSession(options = /** @type {WritingSessionOptions} */ ({})) {
|
|
13
47
|
const { getWordCount } = options;
|
|
14
48
|
|
|
15
49
|
// Campfire session state
|
|
16
50
|
let campfire = $state({
|
|
17
51
|
active: false,
|
|
18
|
-
|
|
52
|
+
/** @type {number | null} */
|
|
53
|
+
startTime: /** @type {number | null} */ (null),
|
|
19
54
|
targetMinutes: 25,
|
|
20
55
|
startWordCount: 0,
|
|
21
56
|
});
|
|
@@ -38,6 +73,7 @@ export function useWritingSession(options = {}) {
|
|
|
38
73
|
}
|
|
39
74
|
|
|
40
75
|
// Writing goal progress
|
|
76
|
+
/** @param {number} currentWordCount */
|
|
41
77
|
function getGoalProgress(currentWordCount) {
|
|
42
78
|
if (!goal.enabled) return 0;
|
|
43
79
|
const wordsWritten = currentWordCount - goal.sessionWords;
|
|
@@ -45,6 +81,7 @@ export function useWritingSession(options = {}) {
|
|
|
45
81
|
}
|
|
46
82
|
|
|
47
83
|
// Words written in campfire session
|
|
84
|
+
/** @param {number} currentWordCount */
|
|
48
85
|
function getCampfireWords(currentWordCount) {
|
|
49
86
|
return currentWordCount - campfire.startWordCount;
|
|
50
87
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
import { tick, untrack, onMount } from 'svelte';
|
|
3
3
|
import { browser } from '$app/environment';
|
|
4
4
|
import TableOfContents from './TableOfContents.svelte';
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
getItemsForAnchor,
|
|
12
12
|
getOrphanItems,
|
|
13
13
|
findAnchorElement,
|
|
14
|
-
parseAnchor
|
|
15
|
-
|
|
14
|
+
parseAnchor,
|
|
15
|
+
type GutterItem as GutterItemType,
|
|
16
|
+
type Header
|
|
17
|
+
} from '../../utils/gutter';
|
|
16
18
|
import '../../styles/content.css';
|
|
17
19
|
|
|
18
20
|
// Constants for positioning calculations
|
|
@@ -23,23 +25,29 @@
|
|
|
23
25
|
|
|
24
26
|
let {
|
|
25
27
|
content = '',
|
|
26
|
-
gutterContent = [],
|
|
27
|
-
headers = [],
|
|
28
|
+
gutterContent = [] as GutterItemType[],
|
|
29
|
+
headers = [] as Header[],
|
|
28
30
|
showTableOfContents = true,
|
|
29
31
|
children
|
|
30
32
|
} = $props();
|
|
31
33
|
|
|
32
34
|
// References to mobile gutter containers for each anchor
|
|
35
|
+
/** @type {Record<string, HTMLElement>} */
|
|
33
36
|
let mobileGutterRefs = $state({});
|
|
34
37
|
|
|
35
38
|
// Track content height for overflow detection
|
|
39
|
+
/** @type {HTMLElement | undefined} */
|
|
36
40
|
let contentBodyElement = $state();
|
|
37
41
|
let contentHeight = $state(0);
|
|
42
|
+
/** @type {string[]} */
|
|
38
43
|
let overflowingAnchorKeys = $state([]);
|
|
39
44
|
|
|
40
45
|
// Gutter positioning state
|
|
46
|
+
/** @type {HTMLElement | undefined} */
|
|
41
47
|
let gutterElement = $state();
|
|
48
|
+
/** @type {Record<string, number>} */
|
|
42
49
|
let itemPositions = $state({});
|
|
50
|
+
/** @type {Record<string, HTMLElement>} */
|
|
43
51
|
let anchorGroupElements = $state({});
|
|
44
52
|
|
|
45
53
|
// Compute unique anchors once as a derived value (performance optimization)
|
|
@@ -52,17 +60,27 @@
|
|
|
52
60
|
let hasGutters = $derived(hasLeftGutter || hasRightGutter);
|
|
53
61
|
let hasOverflow = $derived(overflowingAnchorKeys.length > 0);
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Helper to get anchor key with headers context
|
|
65
|
+
* @param {string} anchor
|
|
66
|
+
*/
|
|
56
67
|
function getKey(anchor) {
|
|
57
68
|
return getAnchorKey(anchor, headers);
|
|
58
69
|
}
|
|
59
70
|
|
|
60
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Get items for a specific anchor
|
|
73
|
+
* @param {string} anchor
|
|
74
|
+
*/
|
|
61
75
|
function getItems(anchor) {
|
|
62
76
|
return getItemsForAnchor(gutterContent, anchor);
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Generate unique key for a gutter item
|
|
81
|
+
* @param {GutterItem} item
|
|
82
|
+
* @param {number} index
|
|
83
|
+
*/
|
|
66
84
|
function getItemKey(item, index) {
|
|
67
85
|
// Combine item properties to create a unique identifier
|
|
68
86
|
const parts = [
|
|
@@ -88,12 +106,13 @@
|
|
|
88
106
|
const gutterRect = gutterElement.getBoundingClientRect();
|
|
89
107
|
|
|
90
108
|
let lastBottom = 0; // Track the bottom edge of the last positioned item
|
|
109
|
+
/** @type {string[]} */
|
|
91
110
|
const newOverflowingAnchors = [];
|
|
92
111
|
const newPositions = { ...itemPositions };
|
|
93
112
|
|
|
94
113
|
// Sort anchors by their position in the document
|
|
95
114
|
const anchorPositions = uniqueAnchors.map(anchor => {
|
|
96
|
-
const el = findAnchorElement(anchor, contentBodyElement, headers);
|
|
115
|
+
const el = findAnchorElement(anchor, contentBodyElement ?? null, headers);
|
|
97
116
|
if (!el && import.meta.env.DEV) {
|
|
98
117
|
console.warn(`Anchor element not found for: ${anchor}`);
|
|
99
118
|
}
|
|
@@ -149,6 +168,7 @@
|
|
|
149
168
|
|
|
150
169
|
// Setup resize listener on mount with proper cleanup
|
|
151
170
|
onMount(() => {
|
|
171
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
152
172
|
let resizeTimeoutId;
|
|
153
173
|
const handleResize = () => {
|
|
154
174
|
clearTimeout(resizeTimeoutId);
|
|
@@ -166,8 +186,9 @@
|
|
|
166
186
|
|
|
167
187
|
// Setup copy button functionality for code blocks
|
|
168
188
|
onMount(() => {
|
|
189
|
+
/** @param {Event} event */
|
|
169
190
|
const handleCopyClick = async (event) => {
|
|
170
|
-
const button = event.currentTarget;
|
|
191
|
+
const button = /** @type {HTMLElement} */ (event.currentTarget);
|
|
171
192
|
const codeText = button.getAttribute('data-code');
|
|
172
193
|
|
|
173
194
|
if (!codeText) return;
|
|
@@ -182,21 +203,21 @@
|
|
|
182
203
|
|
|
183
204
|
// Update button text and style to show success
|
|
184
205
|
const copyText = button.querySelector('.copy-text');
|
|
185
|
-
const originalText = copyText
|
|
186
|
-
copyText.textContent = 'Copied!';
|
|
206
|
+
const originalText = copyText?.textContent || 'Copy';
|
|
207
|
+
if (copyText) copyText.textContent = 'Copied!';
|
|
187
208
|
button.classList.add('copied');
|
|
188
209
|
|
|
189
210
|
// Reset after 2 seconds
|
|
190
211
|
setTimeout(() => {
|
|
191
|
-
copyText.textContent = originalText;
|
|
212
|
+
if (copyText) copyText.textContent = originalText;
|
|
192
213
|
button.classList.remove('copied');
|
|
193
214
|
}, 2000);
|
|
194
215
|
} catch (err) {
|
|
195
216
|
console.error('Failed to copy code:', err);
|
|
196
217
|
const copyText = button.querySelector('.copy-text');
|
|
197
|
-
copyText.textContent = 'Failed';
|
|
218
|
+
if (copyText) copyText.textContent = 'Failed';
|
|
198
219
|
setTimeout(() => {
|
|
199
|
-
copyText.textContent = 'Copy';
|
|
220
|
+
if (copyText) copyText.textContent = 'Copy';
|
|
200
221
|
}, 2000);
|
|
201
222
|
}
|
|
202
223
|
};
|
|
@@ -230,6 +251,7 @@
|
|
|
230
251
|
// Add IDs to headers and position mobile gutter items
|
|
231
252
|
$effect(() => {
|
|
232
253
|
// Track moved elements for cleanup
|
|
254
|
+
/** @type {Array<{ element: HTMLElement, originalParent: HTMLElement | null, originalNextSibling: Node | null }>} */
|
|
233
255
|
const movedElements = [];
|
|
234
256
|
|
|
235
257
|
untrack(() => {
|
|
@@ -239,8 +261,8 @@
|
|
|
239
261
|
if (headers && headers.length > 0) {
|
|
240
262
|
const headerElements = contentBodyElement.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
241
263
|
headerElements.forEach((el) => {
|
|
242
|
-
const text = el.textContent
|
|
243
|
-
const matchingHeader = headers.find(h => h.text === text);
|
|
264
|
+
const text = el.textContent?.trim() || '';
|
|
265
|
+
const matchingHeader = headers.find(/** @param {Header} h */ (h) => h.text === text);
|
|
244
266
|
if (matchingHeader) {
|
|
245
267
|
el.id = matchingHeader.id;
|
|
246
268
|
}
|
|
@@ -284,6 +306,7 @@
|
|
|
284
306
|
$effect(() => {
|
|
285
307
|
if (contentBodyElement) {
|
|
286
308
|
const updateHeight = () => {
|
|
309
|
+
if (!contentBodyElement) return;
|
|
287
310
|
// Get the bottom of content-body relative to the article
|
|
288
311
|
const rect = contentBodyElement.getBoundingClientRect();
|
|
289
312
|
const articleRect = contentBodyElement.closest('.content-article')?.getBoundingClientRect();
|
|
@@ -321,6 +344,10 @@
|
|
|
321
344
|
}
|
|
322
345
|
|
|
323
346
|
// Inject reference markers into content HTML for overflowing items
|
|
347
|
+
/**
|
|
348
|
+
* @param {string} html
|
|
349
|
+
* @param {string[]} overflowKeys
|
|
350
|
+
*/
|
|
324
351
|
function injectReferenceMarkers(html, overflowKeys) {
|
|
325
352
|
if (!overflowKeys || overflowKeys.length === 0 || typeof window === 'undefined') {
|
|
326
353
|
return html;
|
|
@@ -343,7 +370,7 @@
|
|
|
343
370
|
// Find header by text content
|
|
344
371
|
const allHeaders = doc.body.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
345
372
|
for (const h of allHeaders) {
|
|
346
|
-
if (h.textContent
|
|
373
|
+
if (h.textContent?.trim() === headerText) {
|
|
347
374
|
targetEl = h;
|
|
348
375
|
break;
|
|
349
376
|
}
|
|
@@ -352,9 +379,11 @@
|
|
|
352
379
|
}
|
|
353
380
|
case 'paragraph': {
|
|
354
381
|
const paragraphs = doc.body.querySelectorAll(':scope > p');
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
382
|
+
if (parsed.value != null && typeof parsed.value === 'number') {
|
|
383
|
+
const index = parsed.value - 1;
|
|
384
|
+
if (index >= 0 && index < paragraphs.length) {
|
|
385
|
+
targetEl = paragraphs[index];
|
|
386
|
+
}
|
|
358
387
|
}
|
|
359
388
|
break;
|
|
360
389
|
}
|
|
@@ -372,7 +401,7 @@
|
|
|
372
401
|
|
|
373
402
|
const link = doc.createElement('a');
|
|
374
403
|
link.href = `#overflow-${refNum}`;
|
|
375
|
-
link.textContent = refNum;
|
|
404
|
+
link.textContent = String(refNum);
|
|
376
405
|
link.title = `See gutter content for: ${getAnchorLabel(anchor)}`;
|
|
377
406
|
|
|
378
407
|
marker.appendChild(link);
|
|
@@ -399,6 +428,7 @@
|
|
|
399
428
|
let processedContent = $derived(injectReferenceMarkers(content, overflowingAnchorKeys));
|
|
400
429
|
|
|
401
430
|
// Sanitize HTML content to prevent XSS attacks (browser-only for SSR compatibility)
|
|
431
|
+
/** @type {any} */
|
|
402
432
|
let DOMPurify = $state(null);
|
|
403
433
|
|
|
404
434
|
// Load DOMPurify only in browser (avoids jsdom dependency for SSR)
|
|
@@ -410,7 +440,7 @@
|
|
|
410
440
|
});
|
|
411
441
|
|
|
412
442
|
let sanitizedContent = $derived(
|
|
413
|
-
DOMPurify
|
|
443
|
+
DOMPurify && typeof DOMPurify.sanitize === 'function'
|
|
414
444
|
? DOMPurify.sanitize(processedContent, {
|
|
415
445
|
ALLOWED_TAGS: [
|
|
416
446
|
// Headings
|
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
-
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
-
};
|
|
1
|
+
import { type GutterItem as GutterItemType, type Header } from '../../utils/gutter';
|
|
2
|
+
import '../../styles/content.css';
|
|
6
3
|
declare const ContentWithGutter: import("svelte").Component<{
|
|
7
4
|
content?: string;
|
|
8
|
-
gutterContent?:
|
|
9
|
-
headers?:
|
|
5
|
+
gutterContent?: GutterItemType[];
|
|
6
|
+
headers?: Header[];
|
|
10
7
|
showTableOfContents?: boolean;
|
|
11
8
|
children: any;
|
|
12
9
|
}, {}, "">;
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
gutterContent?: any[];
|
|
16
|
-
headers?: any[];
|
|
17
|
-
showTableOfContents?: boolean;
|
|
18
|
-
children: any;
|
|
19
|
-
};
|
|
10
|
+
type ContentWithGutter = ReturnType<typeof ContentWithGutter>;
|
|
11
|
+
export default ContentWithGutter;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import Lightbox from '../../ui/components/gallery/Lightbox.svelte';
|
|
3
3
|
import ImageGallery from '../../ui/components/gallery/ImageGallery.svelte';
|
|
4
|
-
import { sanitizeHTML } from '../../utils/sanitize
|
|
4
|
+
import { sanitizeHTML } from '../../utils/sanitize';
|
|
5
5
|
|
|
6
6
|
let { item = {} } = $props();
|
|
7
7
|
|