@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.
Files changed (79) hide show
  1. package/LICENSE +378 -0
  2. package/dist/auth/jwt.d.ts +10 -4
  3. package/dist/auth/jwt.js +18 -4
  4. package/dist/auth/session.d.ts +22 -15
  5. package/dist/auth/session.js +35 -16
  6. package/dist/components/admin/GutterManager.svelte +81 -139
  7. package/dist/components/admin/GutterManager.svelte.d.ts +6 -6
  8. package/dist/components/admin/MarkdownEditor.svelte +80 -23
  9. package/dist/components/admin/MarkdownEditor.svelte.d.ts +14 -8
  10. package/dist/components/admin/composables/useAmbientSounds.svelte.d.ts +52 -2
  11. package/dist/components/admin/composables/useAmbientSounds.svelte.js +38 -4
  12. package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +80 -10
  13. package/dist/components/admin/composables/useCommandPalette.svelte.js +45 -5
  14. package/dist/components/admin/composables/useDraftManager.svelte.d.ts +76 -14
  15. package/dist/components/admin/composables/useDraftManager.svelte.js +44 -10
  16. package/dist/components/admin/composables/useEditorTheme.svelte.d.ts +168 -2
  17. package/dist/components/admin/composables/useEditorTheme.svelte.js +40 -7
  18. package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +94 -22
  19. package/dist/components/admin/composables/useSlashCommands.svelte.js +58 -9
  20. package/dist/components/admin/composables/useSnippets.svelte.d.ts +51 -2
  21. package/dist/components/admin/composables/useSnippets.svelte.js +35 -3
  22. package/dist/components/admin/composables/useWritingSession.svelte.d.ts +64 -6
  23. package/dist/components/admin/composables/useWritingSession.svelte.js +42 -5
  24. package/dist/components/custom/ContentWithGutter.svelte +53 -23
  25. package/dist/components/custom/ContentWithGutter.svelte.d.ts +6 -14
  26. package/dist/components/custom/GutterItem.svelte +1 -1
  27. package/dist/components/custom/LeftGutter.svelte +43 -13
  28. package/dist/components/custom/LeftGutter.svelte.d.ts +6 -6
  29. package/dist/config/ai-models.js +1 -1
  30. package/dist/groveauth/client.js +11 -11
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.js +2 -2
  33. package/dist/server/logger.d.ts +74 -26
  34. package/dist/server/logger.js +133 -184
  35. package/dist/server/services/cache.js +1 -10
  36. package/dist/ui/components/charts/ActivityOverview.svelte +14 -3
  37. package/dist/ui/components/charts/ActivityOverview.svelte.d.ts +10 -7
  38. package/dist/ui/components/charts/RepoBreakdown.svelte +9 -3
  39. package/dist/ui/components/charts/RepoBreakdown.svelte.d.ts +12 -11
  40. package/dist/ui/components/charts/Sparkline.svelte +18 -7
  41. package/dist/ui/components/charts/Sparkline.svelte.d.ts +21 -2
  42. package/dist/ui/components/gallery/ImageGallery.svelte +12 -8
  43. package/dist/ui/components/gallery/ImageGallery.svelte.d.ts +2 -2
  44. package/dist/ui/components/gallery/Lightbox.svelte +5 -2
  45. package/dist/ui/components/gallery/ZoomableImage.svelte +8 -5
  46. package/dist/ui/components/primitives/accordion/index.d.ts +1 -1
  47. package/dist/ui/components/primitives/input/input.svelte.d.ts +1 -1
  48. package/dist/ui/components/primitives/tabs/index.d.ts +1 -1
  49. package/dist/ui/components/primitives/textarea/textarea.svelte.d.ts +1 -1
  50. package/dist/ui/components/ui/Button.svelte +5 -0
  51. package/dist/ui/components/ui/Button.svelte.d.ts +4 -1
  52. package/dist/ui/components/ui/Input.svelte +4 -0
  53. package/dist/ui/components/ui/Input.svelte.d.ts +3 -1
  54. package/dist/ui/components/ui/Logo.svelte +86 -0
  55. package/dist/ui/components/ui/Logo.svelte.d.ts +25 -0
  56. package/dist/ui/components/ui/LogoLoader.svelte +71 -0
  57. package/dist/ui/components/ui/LogoLoader.svelte.d.ts +9 -0
  58. package/dist/ui/components/ui/index.d.ts +2 -0
  59. package/dist/ui/components/ui/index.js +2 -0
  60. package/dist/ui/tailwind.preset.js +8 -8
  61. package/dist/utils/api.js +2 -1
  62. package/dist/utils/debounce.d.ts +4 -3
  63. package/dist/utils/debounce.js +10 -6
  64. package/dist/utils/gallery.d.ts +58 -32
  65. package/dist/utils/gallery.js +111 -129
  66. package/dist/utils/gutter.d.ts +47 -26
  67. package/dist/utils/gutter.js +116 -124
  68. package/dist/utils/imageProcessor.d.ts +66 -19
  69. package/dist/utils/imageProcessor.js +31 -10
  70. package/dist/utils/index.d.ts +11 -11
  71. package/dist/utils/index.js +4 -3
  72. package/dist/utils/json.js +1 -1
  73. package/dist/utils/markdown.d.ts +183 -103
  74. package/dist/utils/markdown.js +517 -678
  75. package/dist/utils/sanitize.d.ts +22 -12
  76. package/dist/utils/sanitize.js +268 -282
  77. package/dist/utils/validation.js +4 -3
  78. package/package.json +23 -23
  79. 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 {object} options - Configuration options
29
- * @param {Function} options.getTextareaRef - Function to get textarea reference
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 {object} Snippets state and controls
29
+ * @returns {SnippetsManager} Snippets state and controls
4
30
  */
5
- export function useSnippets(): object;
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 {object} Snippets state and controls
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 {object} options - Configuration options
8
- * @param {Function} options.getWordCount - Function to get current word count
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
- getWordCount: Function;
13
- }): object;
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 {object} options - Configuration options
9
- * @param {Function} options.getWordCount - Function to get current word count
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
- startTime: null,
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
- } from '../../utils/gutter.js';
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
- // Helper to get anchor key with headers context
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
- // Get items for a specific anchor
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
- // Generate unique key for a gutter item
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.textContent;
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.trim();
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.trim() === headerText) {
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
- const index = parsed.value - 1;
356
- if (index >= 0 && index < paragraphs.length) {
357
- targetEl = paragraphs[index];
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
- export default ContentWithGutter;
2
- type ContentWithGutter = {
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?: any[];
9
- headers?: any[];
5
+ gutterContent?: GutterItemType[];
6
+ headers?: Header[];
10
7
  showTableOfContents?: boolean;
11
8
  children: any;
12
9
  }, {}, "">;
13
- type $$ComponentProps = {
14
- content?: string;
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.js';
4
+ import { sanitizeHTML } from '../../utils/sanitize';
5
5
 
6
6
  let { item = {} } = $props();
7
7