@autumnsgrove/groveengine 0.3.3 → 0.4.1

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 (270) hide show
  1. package/dist/auth/index.d.ts +2 -0
  2. package/dist/auth/index.js +5 -0
  3. package/dist/components/admin/GutterManager.svelte +4 -4
  4. package/dist/components/admin/MarkdownEditor.svelte +381 -1311
  5. package/dist/components/admin/MarkdownEditor.svelte.d.ts +2 -8
  6. package/dist/components/admin/composables/index.d.ts +7 -0
  7. package/dist/components/admin/composables/index.js +12 -0
  8. package/dist/components/admin/composables/useAmbientSounds.svelte.d.ts +53 -0
  9. package/dist/components/admin/composables/useAmbientSounds.svelte.js +192 -0
  10. package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +17 -0
  11. package/dist/components/admin/composables/useCommandPalette.svelte.js +118 -0
  12. package/dist/components/admin/composables/useDraftManager.svelte.d.ts +17 -0
  13. package/dist/components/admin/composables/useDraftManager.svelte.js +154 -0
  14. package/dist/components/admin/composables/useEditorTheme.svelte.d.ts +195 -0
  15. package/dist/components/admin/composables/useEditorTheme.svelte.js +182 -0
  16. package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +32 -0
  17. package/dist/components/admin/composables/useSlashCommands.svelte.js +166 -0
  18. package/dist/components/admin/composables/useSnippets.svelte.d.ts +5 -0
  19. package/dist/components/admin/composables/useSnippets.svelte.js +122 -0
  20. package/dist/components/admin/composables/useWritingSession.svelte.d.ts +13 -0
  21. package/dist/components/admin/composables/useWritingSession.svelte.js +100 -0
  22. package/dist/components/custom/ContentWithGutter.svelte +14 -7
  23. package/dist/components/custom/GutterItem.svelte +2 -2
  24. package/dist/config/ai-models.d.ts +25 -0
  25. package/dist/config/ai-models.js +50 -0
  26. package/dist/config/index.d.ts +1 -0
  27. package/dist/config/index.js +4 -0
  28. package/dist/index.d.ts +5 -5
  29. package/dist/index.js +6 -6
  30. package/dist/server/index.d.ts +1 -0
  31. package/dist/server/index.js +4 -0
  32. package/dist/ui/components/charts/ActivityOverview.svelte +293 -0
  33. package/dist/ui/components/charts/ActivityOverview.svelte.d.ts +12 -0
  34. package/dist/ui/components/charts/LOCBar.svelte +129 -0
  35. package/dist/ui/components/charts/LOCBar.svelte.d.ts +21 -0
  36. package/dist/ui/components/charts/RepoBreakdown.svelte +136 -0
  37. package/dist/ui/components/charts/RepoBreakdown.svelte.d.ts +16 -0
  38. package/dist/ui/components/charts/Sparkline.svelte +139 -0
  39. package/dist/ui/components/charts/Sparkline.svelte.d.ts +6 -0
  40. package/dist/ui/components/charts/index.d.ts +5 -0
  41. package/dist/ui/components/charts/index.js +11 -0
  42. package/dist/ui/components/content/PlanCard.svelte +91 -0
  43. package/dist/ui/components/content/PlanCard.svelte.d.ts +13 -0
  44. package/dist/ui/components/content/ProductCard.svelte +125 -0
  45. package/dist/ui/components/content/ProductCard.svelte.d.ts +14 -0
  46. package/dist/ui/components/content/SearchCard.svelte +60 -0
  47. package/dist/ui/components/content/SearchCard.svelte.d.ts +10 -0
  48. package/dist/ui/components/content/index.d.ts +4 -0
  49. package/dist/ui/components/content/index.js +10 -0
  50. package/dist/ui/components/forms/SearchInput.svelte +89 -0
  51. package/dist/ui/components/forms/SearchInput.svelte.d.ts +11 -0
  52. package/dist/ui/components/forms/index.d.ts +2 -0
  53. package/dist/ui/components/forms/index.js +8 -0
  54. package/dist/ui/components/gallery/index.d.ts +5 -0
  55. package/dist/ui/components/gallery/index.js +13 -0
  56. package/dist/ui/components/icons/IconLegend.svelte +83 -0
  57. package/dist/ui/components/icons/IconLegend.svelte.d.ts +11 -0
  58. package/dist/ui/components/icons/Icons.svelte +115 -0
  59. package/dist/ui/components/icons/Icons.svelte.d.ts +8 -0
  60. package/dist/ui/components/icons/index.d.ts +3 -0
  61. package/dist/ui/components/icons/index.js +9 -0
  62. package/dist/ui/components/indicators/CreditBalance.svelte +67 -0
  63. package/dist/ui/components/indicators/CreditBalance.svelte.d.ts +9 -0
  64. package/dist/ui/components/indicators/ScoreBar.svelte +63 -0
  65. package/dist/ui/components/indicators/ScoreBar.svelte.d.ts +9 -0
  66. package/dist/ui/components/indicators/StatusBadge.svelte +46 -0
  67. package/dist/ui/components/indicators/StatusBadge.svelte.d.ts +7 -0
  68. package/dist/ui/components/indicators/index.d.ts +4 -0
  69. package/dist/ui/components/indicators/index.js +10 -0
  70. package/dist/{components/ui → ui/components/primitives}/accordion/accordion-content.svelte +1 -1
  71. package/dist/{components/ui → ui/components/primitives}/accordion/accordion-item.svelte +1 -1
  72. package/dist/{components/ui → ui/components/primitives}/accordion/accordion-trigger.svelte +1 -1
  73. package/dist/ui/components/primitives/badge/badge.svelte +50 -0
  74. package/dist/ui/components/primitives/badge/badge.svelte.d.ts +60 -0
  75. package/dist/ui/components/primitives/badge/index.d.ts +2 -0
  76. package/dist/ui/components/primitives/badge/index.js +2 -0
  77. package/dist/ui/components/primitives/button/button.svelte +82 -0
  78. package/dist/ui/components/primitives/button/button.svelte.d.ts +132 -0
  79. package/dist/ui/components/primitives/button/index.d.ts +2 -0
  80. package/dist/ui/components/primitives/button/index.js +4 -0
  81. package/dist/ui/components/primitives/card/card-content.svelte +16 -0
  82. package/dist/ui/components/primitives/card/card-content.svelte.d.ts +5 -0
  83. package/dist/ui/components/primitives/card/card-description.svelte +16 -0
  84. package/dist/ui/components/primitives/card/card-description.svelte.d.ts +5 -0
  85. package/dist/ui/components/primitives/card/card-footer.svelte +16 -0
  86. package/dist/ui/components/primitives/card/card-footer.svelte.d.ts +5 -0
  87. package/dist/ui/components/primitives/card/card-header.svelte +16 -0
  88. package/dist/ui/components/primitives/card/card-header.svelte.d.ts +5 -0
  89. package/dist/ui/components/primitives/card/card-title.svelte +25 -0
  90. package/dist/ui/components/primitives/card/card-title.svelte.d.ts +8 -0
  91. package/dist/ui/components/primitives/card/card.svelte +20 -0
  92. package/dist/ui/components/primitives/card/card.svelte.d.ts +5 -0
  93. package/dist/ui/components/primitives/card/index.d.ts +7 -0
  94. package/dist/ui/components/primitives/card/index.js +9 -0
  95. package/dist/{components/ui → ui/components/primitives}/dialog/dialog-content.svelte +1 -1
  96. package/dist/{components/ui → ui/components/primitives}/dialog/dialog-description.svelte +1 -1
  97. package/dist/{components/ui → ui/components/primitives}/dialog/dialog-footer.svelte +1 -1
  98. package/dist/{components/ui → ui/components/primitives}/dialog/dialog-header.svelte +1 -1
  99. package/dist/{components/ui → ui/components/primitives}/dialog/dialog-overlay.svelte +1 -1
  100. package/dist/{components/ui → ui/components/primitives}/dialog/dialog-title.svelte +1 -1
  101. package/dist/ui/components/primitives/input/index.d.ts +2 -0
  102. package/dist/ui/components/primitives/input/index.js +4 -0
  103. package/dist/ui/components/primitives/input/input.svelte +46 -0
  104. package/dist/ui/components/primitives/input/input.svelte.d.ts +13 -0
  105. package/dist/{components/ui → ui/components/primitives}/select/select-content.svelte +1 -1
  106. package/dist/{components/ui → ui/components/primitives}/select/select-group-heading.svelte +1 -1
  107. package/dist/{components/ui → ui/components/primitives}/select/select-item.svelte +1 -1
  108. package/dist/{components/ui → ui/components/primitives}/select/select-scroll-down-button.svelte +1 -1
  109. package/dist/{components/ui → ui/components/primitives}/select/select-scroll-up-button.svelte +1 -1
  110. package/dist/{components/ui → ui/components/primitives}/select/select-separator.svelte +1 -1
  111. package/dist/{components/ui → ui/components/primitives}/select/select-trigger.svelte +1 -1
  112. package/dist/ui/components/primitives/separator/index.d.ts +2 -0
  113. package/dist/ui/components/primitives/separator/index.js +4 -0
  114. package/dist/ui/components/primitives/separator/separator.svelte +22 -0
  115. package/dist/ui/components/primitives/separator/separator.svelte.d.ts +4 -0
  116. package/dist/{components/ui → ui/components/primitives}/sheet/sheet-content.svelte +1 -1
  117. package/dist/{components/ui → ui/components/primitives}/sheet/sheet-description.svelte +1 -1
  118. package/dist/{components/ui → ui/components/primitives}/sheet/sheet-footer.svelte +1 -1
  119. package/dist/{components/ui → ui/components/primitives}/sheet/sheet-header.svelte +1 -1
  120. package/dist/{components/ui → ui/components/primitives}/sheet/sheet-overlay.svelte +1 -1
  121. package/dist/{components/ui → ui/components/primitives}/sheet/sheet-title.svelte +1 -1
  122. package/dist/ui/components/primitives/skeleton/index.d.ts +2 -0
  123. package/dist/ui/components/primitives/skeleton/index.js +4 -0
  124. package/dist/ui/components/primitives/skeleton/skeleton.svelte +17 -0
  125. package/dist/ui/components/primitives/skeleton/skeleton.svelte.d.ts +5 -0
  126. package/dist/{components/ui → ui/components/primitives}/table/table-body.svelte +1 -1
  127. package/dist/{components/ui → ui/components/primitives}/table/table-caption.svelte +1 -1
  128. package/dist/{components/ui → ui/components/primitives}/table/table-cell.svelte +1 -1
  129. package/dist/{components/ui → ui/components/primitives}/table/table-footer.svelte +1 -1
  130. package/dist/{components/ui → ui/components/primitives}/table/table-head.svelte +1 -1
  131. package/dist/{components/ui → ui/components/primitives}/table/table-header.svelte +1 -1
  132. package/dist/{components/ui → ui/components/primitives}/table/table-row.svelte +1 -1
  133. package/dist/{components/ui → ui/components/primitives}/table/table.svelte +1 -1
  134. package/dist/{components/ui → ui/components/primitives}/tabs/tabs-content.svelte +1 -1
  135. package/dist/{components/ui → ui/components/primitives}/tabs/tabs-list.svelte +1 -1
  136. package/dist/{components/ui → ui/components/primitives}/tabs/tabs-trigger.svelte +1 -1
  137. package/dist/ui/components/primitives/textarea/index.d.ts +2 -0
  138. package/dist/ui/components/primitives/textarea/index.js +4 -0
  139. package/dist/ui/components/primitives/textarea/textarea.svelte +24 -0
  140. package/dist/ui/components/primitives/textarea/textarea.svelte.d.ts +6 -0
  141. package/dist/ui/components/states/EmptyState.svelte +28 -0
  142. package/dist/ui/components/states/EmptyState.svelte.d.ts +10 -0
  143. package/dist/ui/components/states/Loading.svelte +62 -0
  144. package/dist/ui/components/states/Loading.svelte.d.ts +7 -0
  145. package/dist/ui/components/states/LoadingSkeleton.svelte +46 -0
  146. package/dist/ui/components/states/LoadingSkeleton.svelte.d.ts +8 -0
  147. package/dist/ui/components/states/ThemeToggle.svelte +138 -0
  148. package/dist/ui/components/states/ThemeToggle.svelte.d.ts +6 -0
  149. package/dist/ui/components/states/index.d.ts +5 -0
  150. package/dist/ui/components/states/index.js +11 -0
  151. package/dist/{components → ui/components}/ui/Accordion.svelte +1 -1
  152. package/dist/ui/components/ui/Badge.svelte +52 -0
  153. package/dist/ui/components/ui/Badge.svelte.d.ts +28 -0
  154. package/dist/ui/components/ui/Button.svelte +77 -0
  155. package/dist/ui/components/ui/Button.svelte.d.ts +34 -0
  156. package/dist/ui/components/ui/Card.svelte +102 -0
  157. package/dist/ui/components/ui/Card.svelte.d.ts +46 -0
  158. package/dist/ui/components/ui/CollapsibleSection.svelte +65 -0
  159. package/dist/ui/components/ui/CollapsibleSection.svelte.d.ts +10 -0
  160. package/dist/{components → ui/components}/ui/Dialog.svelte +1 -1
  161. package/dist/ui/components/ui/Input.svelte +81 -0
  162. package/dist/ui/components/ui/Input.svelte.d.ts +35 -0
  163. package/dist/{components → ui/components}/ui/Select.svelte +1 -1
  164. package/dist/{components → ui/components}/ui/Sheet.svelte +1 -1
  165. package/dist/ui/components/ui/Skeleton.svelte +31 -0
  166. package/dist/ui/components/ui/Skeleton.svelte.d.ts +26 -0
  167. package/dist/ui/components/ui/Spinner.svelte +45 -0
  168. package/dist/ui/components/ui/Spinner.svelte.d.ts +15 -0
  169. package/dist/{components → ui/components}/ui/Table.svelte +2 -2
  170. package/dist/{components → ui/components}/ui/Table.svelte.d.ts +1 -1
  171. package/dist/{components → ui/components}/ui/Tabs.svelte +2 -2
  172. package/dist/ui/components/ui/Textarea.svelte +81 -0
  173. package/dist/ui/components/ui/Textarea.svelte.d.ts +35 -0
  174. package/dist/ui/components/ui/index.d.ts +18 -0
  175. package/dist/ui/components/ui/index.js +28 -0
  176. package/dist/{components → ui/components}/ui/toast.d.ts +1 -1
  177. package/dist/{components → ui/components}/ui/toast.js +1 -1
  178. package/dist/ui/index.d.ts +10 -0
  179. package/dist/ui/index.js +22 -0
  180. package/dist/ui/stores/theme.d.ts +12 -0
  181. package/dist/ui/stores/theme.js +52 -0
  182. package/dist/ui/styles/content.css +514 -0
  183. package/dist/ui/styles/grove.css +715 -0
  184. package/dist/ui/styles/tokens.css +429 -0
  185. package/dist/ui/tailwind.preset.d.ts +340 -0
  186. package/dist/ui/tailwind.preset.js +441 -0
  187. package/dist/ui/tokens/animation.d.ts +417 -0
  188. package/dist/ui/tokens/animation.js +139 -0
  189. package/dist/ui/tokens/colors.d.ts +183 -0
  190. package/dist/ui/tokens/colors.js +97 -0
  191. package/dist/ui/tokens/effects.d.ts +111 -0
  192. package/dist/ui/tokens/effects.js +61 -0
  193. package/dist/ui/tokens/index.d.ts +6 -0
  194. package/dist/ui/tokens/index.js +19 -0
  195. package/dist/ui/tokens/spacing.d.ts +89 -0
  196. package/dist/ui/tokens/spacing.js +49 -0
  197. package/dist/ui/tokens/typography.d.ts +85 -0
  198. package/dist/ui/tokens/typography.js +68 -0
  199. package/dist/ui/utils/cn.d.ts +13 -0
  200. package/dist/ui/utils/cn.js +24 -0
  201. package/dist/ui/utils/index.d.ts +2 -0
  202. package/dist/ui/utils/index.js +8 -0
  203. package/dist/utils/index.d.ts +11 -0
  204. package/dist/utils/index.js +14 -0
  205. package/dist/utils/markdown.d.ts +11 -0
  206. package/dist/utils/markdown.js +25 -0
  207. package/dist/utils/sanitize.js +2 -3
  208. package/package.json +92 -31
  209. package/dist/components/ui/index.d.ts +0 -14
  210. package/dist/components/ui/index.js +0 -18
  211. /package/dist/{components → ui/components}/gallery/ImageGallery.svelte +0 -0
  212. /package/dist/{components → ui/components}/gallery/ImageGallery.svelte.d.ts +0 -0
  213. /package/dist/{components → ui/components}/gallery/Lightbox.svelte +0 -0
  214. /package/dist/{components → ui/components}/gallery/Lightbox.svelte.d.ts +0 -0
  215. /package/dist/{components → ui/components}/gallery/LightboxCaption.svelte +0 -0
  216. /package/dist/{components → ui/components}/gallery/LightboxCaption.svelte.d.ts +0 -0
  217. /package/dist/{components → ui/components}/gallery/ZoomableImage.svelte +0 -0
  218. /package/dist/{components → ui/components}/gallery/ZoomableImage.svelte.d.ts +0 -0
  219. /package/dist/{components/ui → ui/components/primitives}/accordion/accordion-content.svelte.d.ts +0 -0
  220. /package/dist/{components/ui → ui/components/primitives}/accordion/accordion-item.svelte.d.ts +0 -0
  221. /package/dist/{components/ui → ui/components/primitives}/accordion/accordion-trigger.svelte.d.ts +0 -0
  222. /package/dist/{components/ui → ui/components/primitives}/accordion/index.d.ts +0 -0
  223. /package/dist/{components/ui → ui/components/primitives}/accordion/index.js +0 -0
  224. /package/dist/{components/ui → ui/components/primitives}/dialog/dialog-content.svelte.d.ts +0 -0
  225. /package/dist/{components/ui → ui/components/primitives}/dialog/dialog-description.svelte.d.ts +0 -0
  226. /package/dist/{components/ui → ui/components/primitives}/dialog/dialog-footer.svelte.d.ts +0 -0
  227. /package/dist/{components/ui → ui/components/primitives}/dialog/dialog-header.svelte.d.ts +0 -0
  228. /package/dist/{components/ui → ui/components/primitives}/dialog/dialog-overlay.svelte.d.ts +0 -0
  229. /package/dist/{components/ui → ui/components/primitives}/dialog/dialog-title.svelte.d.ts +0 -0
  230. /package/dist/{components/ui → ui/components/primitives}/dialog/index.d.ts +0 -0
  231. /package/dist/{components/ui → ui/components/primitives}/dialog/index.js +0 -0
  232. /package/dist/{components/ui → ui/components/primitives}/select/index.d.ts +0 -0
  233. /package/dist/{components/ui → ui/components/primitives}/select/index.js +0 -0
  234. /package/dist/{components/ui → ui/components/primitives}/select/select-content.svelte.d.ts +0 -0
  235. /package/dist/{components/ui → ui/components/primitives}/select/select-group-heading.svelte.d.ts +0 -0
  236. /package/dist/{components/ui → ui/components/primitives}/select/select-item.svelte.d.ts +0 -0
  237. /package/dist/{components/ui → ui/components/primitives}/select/select-scroll-down-button.svelte.d.ts +0 -0
  238. /package/dist/{components/ui → ui/components/primitives}/select/select-scroll-up-button.svelte.d.ts +0 -0
  239. /package/dist/{components/ui → ui/components/primitives}/select/select-separator.svelte.d.ts +0 -0
  240. /package/dist/{components/ui → ui/components/primitives}/select/select-trigger.svelte.d.ts +0 -0
  241. /package/dist/{components/ui → ui/components/primitives}/sheet/index.d.ts +0 -0
  242. /package/dist/{components/ui → ui/components/primitives}/sheet/index.js +0 -0
  243. /package/dist/{components/ui → ui/components/primitives}/sheet/sheet-content.svelte.d.ts +0 -0
  244. /package/dist/{components/ui → ui/components/primitives}/sheet/sheet-description.svelte.d.ts +0 -0
  245. /package/dist/{components/ui → ui/components/primitives}/sheet/sheet-footer.svelte.d.ts +0 -0
  246. /package/dist/{components/ui → ui/components/primitives}/sheet/sheet-header.svelte.d.ts +0 -0
  247. /package/dist/{components/ui → ui/components/primitives}/sheet/sheet-overlay.svelte.d.ts +0 -0
  248. /package/dist/{components/ui → ui/components/primitives}/sheet/sheet-title.svelte.d.ts +0 -0
  249. /package/dist/{components/ui → ui/components/primitives}/table/index.d.ts +0 -0
  250. /package/dist/{components/ui → ui/components/primitives}/table/index.js +0 -0
  251. /package/dist/{components/ui → ui/components/primitives}/table/table-body.svelte.d.ts +0 -0
  252. /package/dist/{components/ui → ui/components/primitives}/table/table-caption.svelte.d.ts +0 -0
  253. /package/dist/{components/ui → ui/components/primitives}/table/table-cell.svelte.d.ts +0 -0
  254. /package/dist/{components/ui → ui/components/primitives}/table/table-footer.svelte.d.ts +0 -0
  255. /package/dist/{components/ui → ui/components/primitives}/table/table-head.svelte.d.ts +0 -0
  256. /package/dist/{components/ui → ui/components/primitives}/table/table-header.svelte.d.ts +0 -0
  257. /package/dist/{components/ui → ui/components/primitives}/table/table-row.svelte.d.ts +0 -0
  258. /package/dist/{components/ui → ui/components/primitives}/table/table.svelte.d.ts +0 -0
  259. /package/dist/{components/ui → ui/components/primitives}/tabs/index.d.ts +0 -0
  260. /package/dist/{components/ui → ui/components/primitives}/tabs/index.js +0 -0
  261. /package/dist/{components/ui → ui/components/primitives}/tabs/tabs-content.svelte.d.ts +0 -0
  262. /package/dist/{components/ui → ui/components/primitives}/tabs/tabs-list.svelte.d.ts +0 -0
  263. /package/dist/{components/ui → ui/components/primitives}/tabs/tabs-trigger.svelte.d.ts +0 -0
  264. /package/dist/{components → ui/components}/ui/Accordion.svelte.d.ts +0 -0
  265. /package/dist/{components → ui/components}/ui/Dialog.svelte.d.ts +0 -0
  266. /package/dist/{components → ui/components}/ui/Select.svelte.d.ts +0 -0
  267. /package/dist/{components → ui/components}/ui/Sheet.svelte.d.ts +0 -0
  268. /package/dist/{components → ui/components}/ui/Tabs.svelte.d.ts +0 -0
  269. /package/dist/{components → ui/components}/ui/Toast.svelte +0 -0
  270. /package/dist/{components → ui/components}/ui/Toast.svelte.d.ts +0 -0
@@ -3,8 +3,21 @@
3
3
  import { onMount, tick } from "svelte";
4
4
  import { sanitizeMarkdown } from "../../utils/sanitize.js";
5
5
  import "../../styles/content.css";
6
- import { Button, Input } from '@groveengine/ui';
7
- import Dialog from "../ui/Dialog.svelte";
6
+ import { Button, Input } from '../../ui';
7
+ import Dialog from "../../ui/components/ui/Dialog.svelte";
8
+
9
+ // Import composables
10
+ import {
11
+ useAmbientSounds,
12
+ soundLibrary,
13
+ useEditorTheme,
14
+ themes,
15
+ useSnippets,
16
+ useDraftManager,
17
+ useWritingSession,
18
+ useSlashCommands,
19
+ useCommandPalette,
20
+ } from "./composables/index.js";
8
21
 
9
22
  // Props
10
23
  let {
@@ -12,19 +25,18 @@
12
25
  onSave = () => {},
13
26
  saving = false,
14
27
  readonly = false,
15
- draftKey = null, // Unique key for localStorage draft storage
16
- onDraftRestored = () => {}, // Callback when draft is restored
17
- // Optional metadata for full preview mode
28
+ draftKey = null,
29
+ onDraftRestored = () => {},
18
30
  previewTitle = "",
19
31
  previewDate = "",
20
32
  previewTags = [],
21
33
  } = $props();
22
34
 
23
- // Local state
35
+ // Core refs and state
24
36
  let textareaRef = $state(null);
25
37
  let previewRef = $state(null);
38
+ let lineNumbersRef = $state(null);
26
39
  let showPreview = $state(true);
27
- let lineNumbers = $state([]);
28
40
  let cursorLine = $state(1);
29
41
  let cursorCol = $state(1);
30
42
 
@@ -34,18 +46,10 @@
34
46
  let uploadProgress = $state("");
35
47
  let uploadError = $state(null);
36
48
 
37
- // Auto-save draft state
38
- let lastSavedContent = $state("");
39
- let draftSaveTimer = $state(null);
40
- let hasDraft = $state(false);
41
- let draftRestorePrompt = $state(false);
42
- let storedDraft = $state(null);
43
- const AUTO_SAVE_DELAY = 2000; // 2 seconds
44
-
45
- // Full preview mode state
49
+ // Full preview mode
46
50
  let showFullPreview = $state(false);
47
51
 
48
- // Editor settings (configurable, persisted to localStorage)
52
+ // Editor settings
49
53
  let editorSettings = $state({
50
54
  typewriterMode: false,
51
55
  zenMode: false,
@@ -53,262 +57,87 @@
53
57
  wordWrap: true,
54
58
  });
55
59
 
56
- // Zen mode state
60
+ // Zen mode
57
61
  let isZenMode = $state(false);
58
62
 
59
- // Campfire session state
60
- let campfireSession = $state({
61
- active: false,
62
- startTime: null,
63
- targetMinutes: 25,
64
- startWordCount: 0,
65
- });
66
-
67
- // Writing goals
68
- let writingGoal = $state({
69
- enabled: false,
70
- targetWords: 500,
71
- sessionWords: 0,
72
- });
63
+ // Initialize composables
64
+ const ambientSounds = useAmbientSounds();
65
+ const editorTheme = useEditorTheme();
66
+ const snippetsManager = useSnippets();
73
67
 
74
- // Slash commands state
75
- let slashMenu = $state({
76
- open: false,
77
- query: "",
78
- position: { x: 0, y: 0 },
79
- selectedIndex: 0,
68
+ const writingSession = useWritingSession({
69
+ getWordCount: () => wordCount,
80
70
  });
81
71
 
82
- // Command palette state
83
- let commandPalette = $state({
84
- open: false,
85
- query: "",
86
- selectedIndex: 0,
72
+ const draftManager = useDraftManager({
73
+ draftKey,
74
+ getContent: () => content,
75
+ setContent: (c) => (content = c),
76
+ onDraftRestored,
77
+ readonly,
87
78
  });
88
79
 
89
- // AI Assistant state (stubs - not deployed yet)
90
- let aiAssistant = $state({
91
- enabled: false, // Keep disabled for now
92
- panelOpen: false,
93
- suggestions: [],
94
- isAnalyzing: false,
80
+ const slashCommands = useSlashCommands({
81
+ getTextareaRef: () => textareaRef,
82
+ getContent: () => content,
83
+ setContent: (c) => (content = c),
84
+ getSnippets: () => snippetsManager.snippets,
85
+ onOpenSnippetsModal: () => snippetsManager.openModal(),
95
86
  });
96
87
 
97
- // Markdown snippets state
98
- let snippets = $state([]);
99
- let snippetsModal = $state({
100
- open: false,
101
- editingId: null,
102
- name: "",
103
- content: "",
104
- trigger: "", // Optional shortcut trigger like "sig" for signature
105
- });
88
+ // Command palette actions
89
+ const basePaletteActions = [
90
+ { id: "save", label: "Save", shortcut: "⌘S", action: () => onSave() },
91
+ { id: "preview", label: "Toggle Preview", shortcut: "", action: () => (showPreview = !showPreview) },
92
+ { id: "fullPreview", label: "Full Preview", shortcut: "", action: () => (showFullPreview = true) },
93
+ { id: "zen", label: "Toggle Zen Mode", shortcut: "⌘⇧↵", action: () => toggleZenMode() },
94
+ { id: "campfire", label: "Start Campfire Session", shortcut: "", action: () => writingSession.startCampfire() },
95
+ { id: "bold", label: "Bold", shortcut: "⌘B", action: () => wrapSelection("**", "**") },
96
+ { id: "italic", label: "Italic", shortcut: "⌘I", action: () => wrapSelection("_", "_") },
97
+ { id: "code", label: "Insert Code Block", shortcut: "", action: () => insertCodeBlock() },
98
+ { id: "link", label: "Insert Link", shortcut: "", action: () => insertLink() },
99
+ { id: "image", label: "Insert Image", shortcut: "", action: () => insertImage() },
100
+ { id: "goal", label: "Set Writing Goal", shortcut: "", action: () => writingSession.promptWritingGoal() },
101
+ { id: "snippets", label: "Manage Snippets", shortcut: "", action: () => snippetsManager.openModal() },
102
+ { id: "newSnippet", label: "Create New Snippet", shortcut: "", action: () => snippetsManager.openModal() },
103
+ { id: "sounds", label: "Toggle Ambient Sounds", shortcut: "", action: () => ambientSounds.toggle() },
104
+ { id: "soundPanel", label: "Sound Settings", shortcut: "", action: () => ambientSounds.togglePanel() },
105
+ ];
106
106
 
107
- // Ambient sounds state
108
- let ambientSounds = $state({
109
- enabled: false,
110
- currentSound: "forest",
111
- volume: 0.3,
112
- showPanel: false,
107
+ const commandPalette = useCommandPalette({
108
+ getActions: () => basePaletteActions,
109
+ getThemes: () => themes,
110
+ getCurrentTheme: () => editorTheme.currentTheme,
113
111
  });
114
- let audioElement = $state(null);
115
-
116
- // Theme system
117
- const themes = {
118
- grove: {
119
- name: "grove",
120
- label: "Grove",
121
- desc: "forest green",
122
- accent: "#8bc48b",
123
- accentDim: "#7a9a7a",
124
- accentBright: "#a8dca8",
125
- accentGlow: "#c8f0c8",
126
- bg: "#1e1e1e",
127
- bgSecondary: "#252526",
128
- bgTertiary: "#1a1a1a",
129
- border: "#3a3a3a",
130
- borderAccent: "#4a7c4a",
131
- text: "#d4d4d4",
132
- textDim: "#9d9d9d",
133
- statusBg: "#2d4a2d",
134
- statusBorder: "#3d5a3d",
135
- },
136
- amber: {
137
- name: "amber",
138
- label: "Amber",
139
- desc: "classic terminal",
140
- accent: "#ffb000",
141
- accentDim: "#c98b00",
142
- accentBright: "#ffc940",
143
- accentGlow: "#ffe080",
144
- bg: "#1a1400",
145
- bgSecondary: "#241c00",
146
- bgTertiary: "#140e00",
147
- border: "#3a3000",
148
- borderAccent: "#5a4800",
149
- text: "#ffcc66",
150
- textDim: "#aa8844",
151
- statusBg: "#2a2000",
152
- statusBorder: "#3a3000",
153
- },
154
- matrix: {
155
- name: "matrix",
156
- label: "Matrix",
157
- desc: "digital rain",
158
- accent: "#00ff00",
159
- accentDim: "#00aa00",
160
- accentBright: "#44ff44",
161
- accentGlow: "#88ff88",
162
- bg: "#0a0a0a",
163
- bgSecondary: "#111111",
164
- bgTertiary: "#050505",
165
- border: "#1a3a1a",
166
- borderAccent: "#00aa00",
167
- text: "#00dd00",
168
- textDim: "#008800",
169
- statusBg: "#0a1a0a",
170
- statusBorder: "#1a3a1a",
171
- },
172
- dracula: {
173
- name: "dracula",
174
- label: "Dracula",
175
- desc: "purple night",
176
- accent: "#bd93f9",
177
- accentDim: "#9580c9",
178
- accentBright: "#d4b0ff",
179
- accentGlow: "#e8d0ff",
180
- bg: "#282a36",
181
- bgSecondary: "#343746",
182
- bgTertiary: "#21222c",
183
- border: "#44475a",
184
- borderAccent: "#6272a4",
185
- text: "#f8f8f2",
186
- textDim: "#a0a0a0",
187
- statusBg: "#3a3c4e",
188
- statusBorder: "#44475a",
189
- },
190
- nord: {
191
- name: "nord",
192
- label: "Nord",
193
- desc: "arctic frost",
194
- accent: "#88c0d0",
195
- accentDim: "#6a9aa8",
196
- accentBright: "#a3d4e2",
197
- accentGlow: "#c0e8f0",
198
- bg: "#2e3440",
199
- bgSecondary: "#3b4252",
200
- bgTertiary: "#272c36",
201
- border: "#434c5e",
202
- borderAccent: "#5e81ac",
203
- text: "#eceff4",
204
- textDim: "#a0a8b0",
205
- statusBg: "#3b4252",
206
- statusBorder: "#434c5e",
207
- },
208
- rose: {
209
- name: "rose",
210
- label: "Rose",
211
- desc: "soft pink",
212
- accent: "#f5a9b8",
213
- accentDim: "#c98a96",
214
- accentBright: "#ffccd5",
215
- accentGlow: "#ffe0e6",
216
- bg: "#1f1a1b",
217
- bgSecondary: "#2a2224",
218
- bgTertiary: "#171314",
219
- border: "#3a3234",
220
- borderAccent: "#5a4a4e",
221
- text: "#e8d8dc",
222
- textDim: "#a09498",
223
- statusBg: "#2a2224",
224
- statusBorder: "#3a3234",
225
- },
226
- };
227
-
228
- let currentTheme = $state("grove");
229
- const THEME_STORAGE_KEY = "grove-editor-theme";
230
-
231
- // Sound definitions with free ambient loops
232
- const soundLibrary = {
233
- forest: {
234
- name: "forest",
235
- key: "f",
236
- // Using freesound.org URLs for ambient sounds (CC0 licensed)
237
- // These are placeholder paths - user can provide their own audio files
238
- url: "/sounds/forest-ambience.mp3",
239
- description: "birds, wind",
240
- },
241
- rain: {
242
- name: "rain",
243
- key: "r",
244
- url: "/sounds/rain-ambience.mp3",
245
- description: "gentle rainfall",
246
- },
247
- campfire: {
248
- name: "fire",
249
- key: "i",
250
- url: "/sounds/campfire-ambience.mp3",
251
- description: "crackling embers",
252
- },
253
- night: {
254
- name: "night",
255
- key: "n",
256
- url: "/sounds/night-ambience.mp3",
257
- description: "crickets, breeze",
258
- },
259
- cafe: {
260
- name: "cafe",
261
- key: "a",
262
- url: "/sounds/cafe-ambience.mp3",
263
- description: "soft murmurs",
264
- },
265
- };
266
-
267
- // Line numbers container ref for scroll sync
268
- let lineNumbersRef = $state(null);
269
112
 
270
113
  // Computed values
271
- let wordCount = $derived(
272
- content.trim() ? content.trim().split(/\s+/).length : 0
273
- );
114
+ let wordCount = $derived(content.trim() ? content.trim().split(/\s+/).length : 0);
274
115
  let charCount = $derived(content.length);
275
116
  let lineCount = $derived(content.split("\n").length);
276
-
277
117
  let previewHtml = $derived(content ? sanitizeMarkdown(marked.parse(content)) : "");
278
118
 
279
- // Reading time estimate (average 200 words per minute)
280
- let readingTime = $derived(() => {
119
+ let readingTime = $derived.by(() => {
281
120
  const minutes = Math.ceil(wordCount / 200);
282
121
  return minutes < 1 ? "< 1 min" : `~${minutes} min read`;
283
122
  });
284
123
 
285
- // Writing goal progress
286
- let goalProgress = $derived(() => {
287
- if (!writingGoal.enabled) return 0;
288
- const wordsWritten = wordCount - writingGoal.sessionWords;
289
- return Math.min(100, Math.round((wordsWritten / writingGoal.targetWords) * 100));
290
- });
124
+ let goalProgress = $derived.by(() => writingSession.getGoalProgress(wordCount));
291
125
 
292
- // Campfire session elapsed time
293
- let campfireElapsed = $derived(() => {
294
- if (!campfireSession.active || !campfireSession.startTime) return "0:00";
295
- const now = Date.now();
296
- const elapsed = Math.floor((now - campfireSession.startTime) / 1000);
297
- const mins = Math.floor(elapsed / 60);
298
- const secs = elapsed % 60;
299
- return `${mins}:${secs.toString().padStart(2, "0")}`;
126
+ let campfireElapsed = $derived.by(() => writingSession.getCampfireElapsed());
127
+
128
+ let lineNumbers = $derived.by(() => {
129
+ const count = content.split("\n").length;
130
+ return Array.from({ length: count }, (_, i) => i + 1);
300
131
  });
301
132
 
302
- // Extract available anchors from content (headings and custom anchors)
133
+ // Extract available anchors from content
303
134
  let availableAnchors = $derived.by(() => {
304
135
  const anchors = [];
305
- // Extract headings
306
136
  const headingRegex = /^(#{1,6})\s+(.+)$/gm;
307
137
  let match;
308
138
  while ((match = headingRegex.exec(content)) !== null) {
309
139
  anchors.push(match[0].trim());
310
140
  }
311
- // Extract custom anchors
312
141
  const anchorRegex = /<!--\s*anchor:([\w-]+)\s*-->/g;
313
142
  while ((match = anchorRegex.exec(content)) !== null) {
314
143
  anchors.push(`anchor:${match[1]}`);
@@ -316,26 +145,46 @@
316
145
  return anchors;
317
146
  });
318
147
 
319
- // Public function to get available anchors
148
+ // Filtered commands for UI
149
+ let filteredSlashCommands = $derived.by(() => slashCommands.getFilteredCommands());
150
+ let filteredPaletteCommands = $derived.by(() => {
151
+ // Include theme actions that actually set the theme
152
+ const actions = basePaletteActions.filter((cmd) =>
153
+ cmd.label.toLowerCase().includes(commandPalette.query.toLowerCase())
154
+ );
155
+ const themeCommands = Object.entries(themes)
156
+ .filter(([key, theme]) =>
157
+ `Theme: ${theme.label} (${theme.desc})`.toLowerCase().includes(commandPalette.query.toLowerCase())
158
+ )
159
+ .map(([key, theme]) => ({
160
+ id: `theme-${key}`,
161
+ label: `Theme: ${theme.label} (${theme.desc})`,
162
+ shortcut: editorTheme.currentTheme === key ? "●" : "",
163
+ action: () => editorTheme.setTheme(key),
164
+ }));
165
+ return [...actions, ...themeCommands];
166
+ });
167
+
168
+ // Public exports
320
169
  export function getAvailableAnchors() {
321
170
  return availableAnchors;
322
171
  }
323
172
 
324
- // Public function to insert an anchor at cursor position
325
173
  export function insertAnchor(name) {
326
174
  insertAtCursor(`<!-- anchor:${name} -->\n`);
327
175
  }
328
176
 
329
- // Update line numbers when content changes
330
- $effect(() => {
331
- const lines = content.split("\n").length;
332
- lineNumbers = Array.from({ length: lines }, (_, i) => i + 1);
333
- });
177
+ export function clearDraft() {
178
+ draftManager.clearDraft();
179
+ }
334
180
 
335
- // Handle cursor position tracking
181
+ export function getDraftStatus() {
182
+ return draftManager.getStatus();
183
+ }
184
+
185
+ // Cursor position tracking
336
186
  function updateCursorPosition() {
337
187
  if (!textareaRef) return;
338
-
339
188
  const pos = textareaRef.selectionStart;
340
189
  const textBefore = content.substring(0, pos);
341
190
  const lines = textBefore.split("\n");
@@ -343,16 +192,16 @@
343
192
  cursorCol = lines[lines.length - 1].length + 1;
344
193
  }
345
194
 
346
- // Handle tab key for indentation
195
+ // Keyboard handlers
347
196
  function handleKeydown(e) {
348
197
  // Escape key handling
349
198
  if (e.key === "Escape") {
350
- if (slashMenu.open) {
351
- slashMenu.open = false;
199
+ if (slashCommands.isOpen) {
200
+ slashCommands.close();
352
201
  return;
353
202
  }
354
- if (commandPalette.open) {
355
- commandPalette.open = false;
203
+ if (commandPalette.isOpen) {
204
+ commandPalette.close();
356
205
  return;
357
206
  }
358
207
  if (isZenMode) {
@@ -362,46 +211,38 @@
362
211
  }
363
212
 
364
213
  // Slash commands trigger
365
- if (e.key === "/" && !slashMenu.open) {
214
+ if (e.key === "/" && !slashCommands.isOpen) {
366
215
  const pos = textareaRef.selectionStart;
367
- const textBefore = content.substring(0, pos);
368
- // Only trigger at start of line or after whitespace
369
- if (pos === 0 || /\s$/.test(textBefore)) {
370
- // Don't prevent default yet - let the slash be typed
371
- setTimeout(() => {
372
- openSlashMenu();
373
- }, 0);
216
+ if (slashCommands.shouldTrigger("/", pos, content)) {
217
+ setTimeout(() => slashCommands.open(), 0);
374
218
  }
375
219
  }
376
220
 
377
- // Close slash menu on space or enter if open
378
- if (slashMenu.open && (e.key === " " || e.key === "Enter")) {
221
+ // Close slash menu on space or enter
222
+ if (slashCommands.isOpen && (e.key === " " || e.key === "Enter")) {
379
223
  if (e.key === "Enter") {
380
224
  e.preventDefault();
381
- executeSlashCommand(slashMenu.selectedIndex);
225
+ slashCommands.execute(slashCommands.menu.selectedIndex);
382
226
  }
383
- slashMenu.open = false;
227
+ slashCommands.close();
384
228
  }
385
229
 
386
230
  // Navigate slash menu
387
- if (slashMenu.open) {
388
- const cmdCount = filteredSlashCommands.length;
231
+ if (slashCommands.isOpen) {
389
232
  if (e.key === "ArrowDown") {
390
233
  e.preventDefault();
391
- slashMenu.selectedIndex = (slashMenu.selectedIndex + 1) % cmdCount;
234
+ slashCommands.navigate("down");
392
235
  }
393
236
  if (e.key === "ArrowUp") {
394
237
  e.preventDefault();
395
- slashMenu.selectedIndex = (slashMenu.selectedIndex - 1 + cmdCount) % cmdCount;
238
+ slashCommands.navigate("up");
396
239
  }
397
240
  }
398
241
 
399
242
  // Command palette: Cmd+K
400
243
  if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
401
244
  e.preventDefault();
402
- commandPalette.open = !commandPalette.open;
403
- commandPalette.query = "";
404
- commandPalette.selectedIndex = 0;
245
+ commandPalette.toggle();
405
246
  }
406
247
 
407
248
  // Zen mode: Cmd+Shift+Enter
@@ -410,15 +251,12 @@
410
251
  toggleZenMode();
411
252
  }
412
253
 
254
+ // Tab for indentation
413
255
  if (e.key === "Tab") {
414
256
  e.preventDefault();
415
257
  const start = textareaRef.selectionStart;
416
258
  const end = textareaRef.selectionEnd;
417
-
418
- // Insert 2 spaces
419
259
  content = content.substring(0, start) + " " + content.substring(end);
420
-
421
- // Move cursor
422
260
  setTimeout(() => {
423
261
  textareaRef.selectionStart = textareaRef.selectionEnd = start + 2;
424
262
  }, 0);
@@ -443,16 +281,15 @@
443
281
  }
444
282
  }
445
283
 
446
- // Global keyboard handler for modals
447
284
  function handleGlobalKeydown(e) {
448
285
  if (e.key === "Escape") {
449
286
  if (ambientSounds.showPanel) {
450
- ambientSounds.showPanel = false;
287
+ ambientSounds.closePanel();
451
288
  e.preventDefault();
452
289
  return;
453
290
  }
454
- if (snippetsModal.open) {
455
- closeSnippetsModal();
291
+ if (snippetsManager.modal.open) {
292
+ snippetsManager.closeModal();
456
293
  e.preventDefault();
457
294
  return;
458
295
  }
@@ -463,126 +300,6 @@
463
300
  }
464
301
  }
465
302
 
466
- // Slash commands definition
467
- const slashCommands = [
468
- { id: "heading1", label: "Heading 1", insert: "# " },
469
- { id: "heading2", label: "Heading 2", insert: "## " },
470
- { id: "heading3", label: "Heading 3", insert: "### " },
471
- { id: "code", label: "Code Block", insert: "```\n\n```", cursorOffset: 4 },
472
- { id: "quote", label: "Quote", insert: "> " },
473
- { id: "list", label: "Bullet List", insert: "- " },
474
- { id: "numbered", label: "Numbered List", insert: "1. " },
475
- { id: "link", label: "Link", insert: "[](url)", cursorOffset: 1 },
476
- { id: "image", label: "Image", insert: "![alt](url)", cursorOffset: 2 },
477
- { id: "divider", label: "Divider", insert: "\n---\n" },
478
- { id: "anchor", label: "Custom Anchor", insert: "<!-- anchor:name -->\n", cursorOffset: 14 },
479
- { id: "newSnippet", label: "Create New Snippet...", insert: "", isAction: true, action: () => openSnippetsModal() },
480
- ];
481
-
482
- // Dynamic slash commands including user snippets
483
- let allSlashCommands = $derived(() => {
484
- const snippetCommands = snippets.map(s => ({
485
- id: s.id,
486
- label: `> ${s.name}`,
487
- insert: s.content,
488
- isSnippet: true,
489
- }));
490
- return [...slashCommands, ...snippetCommands];
491
- });
492
-
493
- // Filtered slash commands based on query
494
- let filteredSlashCommands = $derived(
495
- allSlashCommands().filter(cmd =>
496
- cmd.label.toLowerCase().includes(slashMenu.query.toLowerCase())
497
- )
498
- );
499
-
500
- function openSlashMenu() {
501
- slashMenu.open = true;
502
- slashMenu.query = "";
503
- slashMenu.selectedIndex = 0;
504
- }
505
-
506
- function executeSlashCommand(index) {
507
- const cmd = filteredSlashCommands[index];
508
- if (!cmd) return;
509
-
510
- // Handle action commands (like "Create New Snippet...")
511
- if (cmd.isAction && cmd.action) {
512
- // Remove the slash that triggered the menu
513
- const pos = textareaRef.selectionStart;
514
- const textBefore = content.substring(0, pos);
515
- const lastSlashIndex = textBefore.lastIndexOf("/");
516
- if (lastSlashIndex >= 0) {
517
- content = content.substring(0, lastSlashIndex) + content.substring(pos);
518
- }
519
- slashMenu.open = false;
520
- cmd.action();
521
- return;
522
- }
523
-
524
- // Remove the slash that triggered the menu
525
- const pos = textareaRef.selectionStart;
526
- const textBefore = content.substring(0, pos);
527
- const lastSlashIndex = textBefore.lastIndexOf("/");
528
-
529
- if (lastSlashIndex >= 0) {
530
- content = content.substring(0, lastSlashIndex) + cmd.insert + content.substring(pos);
531
-
532
- setTimeout(() => {
533
- const newPos = lastSlashIndex + (cmd.cursorOffset || cmd.insert.length);
534
- textareaRef.selectionStart = textareaRef.selectionEnd = newPos;
535
- textareaRef.focus();
536
- }, 0);
537
- }
538
-
539
- slashMenu.open = false;
540
- }
541
-
542
- // Command palette actions
543
- const basePaletteCommands = [
544
- { id: "save", label: "Save", shortcut: "⌘S", action: () => onSave() },
545
- { id: "preview", label: "Toggle Preview", shortcut: "", action: () => showPreview = !showPreview },
546
- { id: "fullPreview", label: "Full Preview", shortcut: "", action: () => showFullPreview = true },
547
- { id: "zen", label: "Toggle Zen Mode", shortcut: "⌘⇧↵", action: () => toggleZenMode() },
548
- { id: "campfire", label: "Start Campfire Session", shortcut: "", action: () => startCampfireSession() },
549
- { id: "bold", label: "Bold", shortcut: "⌘B", action: () => wrapSelection("**", "**") },
550
- { id: "italic", label: "Italic", shortcut: "⌘I", action: () => wrapSelection("_", "_") },
551
- { id: "code", label: "Insert Code Block", shortcut: "", action: () => insertCodeBlock() },
552
- { id: "link", label: "Insert Link", shortcut: "", action: () => insertLink() },
553
- { id: "image", label: "Insert Image", shortcut: "", action: () => insertImage() },
554
- { id: "goal", label: "Set Writing Goal", shortcut: "", action: () => promptWritingGoal() },
555
- { id: "snippets", label: "Manage Snippets", shortcut: "", action: () => openSnippetsModal() },
556
- { id: "newSnippet", label: "Create New Snippet", shortcut: "", action: () => openSnippetsModal() },
557
- { id: "sounds", label: "Toggle Ambient Sounds", shortcut: "", action: () => toggleAmbientSound() },
558
- { id: "soundPanel", label: "Sound Settings", shortcut: "", action: () => toggleSoundPanel() },
559
- ];
560
-
561
- // Add theme commands dynamically
562
- let paletteCommands = $derived(() => {
563
- const themeCommands = Object.entries(themes).map(([key, theme]) => ({
564
- id: `theme-${key}`,
565
- label: `Theme: ${theme.label} (${theme.desc})`,
566
- shortcut: currentTheme === key ? "●" : "",
567
- action: () => setTheme(key),
568
- }));
569
- return [...basePaletteCommands, ...themeCommands];
570
- });
571
-
572
- let filteredPaletteCommands = $derived(
573
- paletteCommands().filter(cmd =>
574
- cmd.label.toLowerCase().includes(commandPalette.query.toLowerCase())
575
- )
576
- );
577
-
578
- function executePaletteCommand(index) {
579
- const cmd = filteredPaletteCommands[index];
580
- if (cmd) {
581
- cmd.action();
582
- commandPalette.open = false;
583
- }
584
- }
585
-
586
303
  // Zen mode toggle
587
304
  function toggleZenMode() {
588
305
  isZenMode = !isZenMode;
@@ -591,306 +308,29 @@
591
308
  }
592
309
  }
593
310
 
594
- // Campfire session controls
595
- function startCampfireSession() {
596
- campfireSession.active = true;
597
- campfireSession.startTime = Date.now();
598
- campfireSession.startWordCount = wordCount;
599
- }
600
-
601
- function endCampfireSession() {
602
- const wordsWritten = wordCount - campfireSession.startWordCount;
603
- const elapsed = campfireSession.startTime ? Math.floor((Date.now() - campfireSession.startTime) / 1000) : 0;
604
-
605
- // Could show a summary modal here
606
- campfireSession.active = false;
607
- campfireSession.startTime = null;
608
- }
609
-
610
- // Writing goal prompt
611
- function promptWritingGoal() {
612
- const target = prompt("Set your word goal for this session:", "500");
613
- if (target && !isNaN(parseInt(target))) {
614
- writingGoal.enabled = true;
615
- writingGoal.targetWords = parseInt(target);
616
- writingGoal.sessionWords = wordCount;
617
- }
618
- }
619
-
620
- // Snippet management
621
- const SNIPPETS_STORAGE_KEY = "grove-editor-snippets";
622
-
623
- function loadSnippets() {
624
- try {
625
- const stored = localStorage.getItem(SNIPPETS_STORAGE_KEY);
626
- if (stored) {
627
- snippets = JSON.parse(stored);
628
- }
629
- } catch (e) {
630
- console.warn("Failed to load snippets:", e);
631
- }
632
- }
633
-
634
- function saveSnippets() {
635
- try {
636
- localStorage.setItem(SNIPPETS_STORAGE_KEY, JSON.stringify(snippets));
637
- } catch (e) {
638
- console.warn("Failed to save snippets:", e);
639
- }
640
- }
641
-
642
- function openSnippetsModal(editId = null) {
643
- if (editId) {
644
- const snippet = snippets.find(s => s.id === editId);
645
- if (snippet) {
646
- snippetsModal.editingId = editId;
647
- snippetsModal.name = snippet.name;
648
- snippetsModal.content = snippet.content;
649
- snippetsModal.trigger = snippet.trigger || "";
650
- }
651
- } else {
652
- snippetsModal.editingId = null;
653
- snippetsModal.name = "";
654
- snippetsModal.content = "";
655
- snippetsModal.trigger = "";
656
- }
657
- snippetsModal.open = true;
658
- commandPalette.open = false;
659
- }
660
-
661
- function closeSnippetsModal() {
662
- snippetsModal.open = false;
663
- snippetsModal.editingId = null;
664
- snippetsModal.name = "";
665
- snippetsModal.content = "";
666
- snippetsModal.trigger = "";
667
- }
668
-
669
- function saveSnippet() {
670
- if (!snippetsModal.name.trim() || !snippetsModal.content.trim()) return;
671
-
672
- if (snippetsModal.editingId) {
673
- // Update existing snippet
674
- snippets = snippets.map(s =>
675
- s.id === snippetsModal.editingId
676
- ? {
677
- ...s,
678
- name: snippetsModal.name.trim(),
679
- content: snippetsModal.content,
680
- trigger: snippetsModal.trigger.trim() || null,
681
- }
682
- : s
683
- );
684
- } else {
685
- // Create new snippet
686
- const newSnippet = {
687
- id: `snippet-${Date.now()}`,
688
- name: snippetsModal.name.trim(),
689
- content: snippetsModal.content,
690
- trigger: snippetsModal.trigger.trim() || null,
691
- createdAt: new Date().toISOString(),
692
- };
693
- snippets = [...snippets, newSnippet];
694
- }
695
-
696
- saveSnippets();
697
- closeSnippetsModal();
698
- }
699
-
700
- function deleteSnippet(id) {
701
- if (confirm("Delete this snippet?")) {
702
- snippets = snippets.filter(s => s.id !== id);
703
- saveSnippets();
704
- if (snippetsModal.editingId === id) {
705
- closeSnippetsModal();
706
- }
707
- }
708
- }
709
-
710
- function insertSnippet(snippet) {
711
- insertAtCursor(snippet.content);
712
- slashMenu.open = false;
713
- }
714
-
715
- // Ambient sound controls
716
- const SOUNDS_STORAGE_KEY = "grove-editor-sounds";
717
-
718
- function loadSoundSettings() {
719
- try {
720
- const stored = localStorage.getItem(SOUNDS_STORAGE_KEY);
721
- if (stored) {
722
- const settings = JSON.parse(stored);
723
- ambientSounds.currentSound = settings.currentSound || "forest";
724
- ambientSounds.volume = settings.volume ?? 0.3;
725
- // Don't auto-enable on load - user must click to start
726
- }
727
- } catch (e) {
728
- console.warn("Failed to load sound settings:", e);
729
- }
730
- }
731
-
732
- function saveSoundSettings() {
733
- try {
734
- localStorage.setItem(SOUNDS_STORAGE_KEY, JSON.stringify({
735
- currentSound: ambientSounds.currentSound,
736
- volume: ambientSounds.volume,
737
- }));
738
- } catch (e) {
739
- console.warn("Failed to save sound settings:", e);
740
- }
741
- }
742
-
743
- function toggleAmbientSound() {
744
- if (ambientSounds.enabled) {
745
- stopSound();
746
- } else {
747
- playSound(ambientSounds.currentSound);
748
- }
749
- }
750
-
751
- function playSound(soundKey) {
752
- const sound = soundLibrary[soundKey];
753
- if (!sound) return;
754
-
755
- // Stop current sound if playing
756
- if (audioElement) {
757
- audioElement.pause();
758
- audioElement = null;
759
- }
760
-
761
- // Create new audio element
762
- audioElement = new Audio(sound.url);
763
- audioElement.loop = true;
764
- audioElement.volume = ambientSounds.volume;
765
-
766
- // Handle playback errors gracefully
767
- audioElement.onerror = () => {
768
- console.warn(`Sound file not found: ${sound.url}`);
769
- ambientSounds.enabled = false;
770
- };
771
-
772
- audioElement.play().then(() => {
773
- ambientSounds.enabled = true;
774
- ambientSounds.currentSound = soundKey;
775
- saveSoundSettings();
776
- }).catch((e) => {
777
- console.warn("Failed to play sound:", e);
778
- ambientSounds.enabled = false;
779
- });
780
- }
781
-
782
- function stopSound() {
783
- if (audioElement) {
784
- audioElement.pause();
785
- audioElement = null;
786
- }
787
- ambientSounds.enabled = false;
788
- }
789
-
790
- function setVolume(newVolume) {
791
- ambientSounds.volume = newVolume;
792
- if (audioElement) {
793
- audioElement.volume = newVolume;
794
- }
795
- saveSoundSettings();
796
- }
797
-
798
- function selectSound(soundKey) {
799
- if (ambientSounds.enabled) {
800
- playSound(soundKey);
801
- } else {
802
- ambientSounds.currentSound = soundKey;
803
- saveSoundSettings();
804
- }
805
- }
806
-
807
- function toggleSoundPanel() {
808
- ambientSounds.showPanel = !ambientSounds.showPanel;
809
- }
810
-
811
- // Theme controls
812
- function loadTheme() {
813
- try {
814
- const stored = localStorage.getItem(THEME_STORAGE_KEY);
815
- if (stored && themes[stored]) {
816
- currentTheme = stored;
817
- applyTheme(stored);
818
- }
819
- } catch (e) {
820
- console.warn("Failed to load theme:", e);
821
- }
822
- }
823
-
824
- function saveTheme(themeName) {
825
- try {
826
- localStorage.setItem(THEME_STORAGE_KEY, themeName);
827
- } catch (e) {
828
- console.warn("Failed to save theme:", e);
829
- }
830
- }
831
-
832
- function applyTheme(themeName) {
833
- const theme = themes[themeName];
834
- if (!theme) return;
835
-
836
- const root = document.documentElement;
837
- root.style.setProperty("--editor-accent", theme.accent);
838
- root.style.setProperty("--editor-accent-dim", theme.accentDim);
839
- root.style.setProperty("--editor-accent-bright", theme.accentBright);
840
- root.style.setProperty("--editor-accent-glow", theme.accentGlow);
841
- root.style.setProperty("--editor-bg", theme.bg);
842
- root.style.setProperty("--editor-bg-secondary", theme.bgSecondary);
843
- root.style.setProperty("--editor-bg-tertiary", theme.bgTertiary);
844
- root.style.setProperty("--editor-border", theme.border);
845
- root.style.setProperty("--editor-border-accent", theme.borderAccent);
846
- root.style.setProperty("--editor-text", theme.text);
847
- root.style.setProperty("--editor-text-dim", theme.textDim);
848
- root.style.setProperty("--editor-status-bg", theme.statusBg);
849
- root.style.setProperty("--editor-status-border", theme.statusBorder);
850
- }
851
-
852
- function setTheme(themeName) {
853
- if (!themes[themeName]) return;
854
- currentTheme = themeName;
855
- applyTheme(themeName);
856
- saveTheme(themeName);
857
- commandPalette.open = false;
858
- }
859
-
860
- // Typewriter scrolling - keep cursor line centered
311
+ // Typewriter scrolling
861
312
  function applyTypewriterScroll() {
862
313
  if (!textareaRef || !editorSettings.typewriterMode) return;
863
-
864
314
  const lineHeight = parseFloat(getComputedStyle(textareaRef).lineHeight) || 24;
865
315
  const viewportHeight = textareaRef.clientHeight;
866
316
  const centerOffset = viewportHeight / 2;
867
317
  const targetScroll = (cursorLine - 1) * lineHeight - centerOffset + lineHeight / 2;
868
-
869
318
  textareaRef.scrollTop = Math.max(0, targetScroll);
870
319
  }
871
320
 
872
- // Sync line numbers scroll with textarea
873
321
  function syncLineNumbersScroll() {
874
322
  if (lineNumbersRef && textareaRef) {
875
323
  lineNumbersRef.scrollTop = textareaRef.scrollTop;
876
324
  }
877
325
  }
878
326
 
879
- // Wrap selected text with markers
327
+ // Text manipulation helpers
880
328
  function wrapSelection(before, after) {
881
329
  if (!textareaRef) return;
882
-
883
330
  const start = textareaRef.selectionStart;
884
331
  const end = textareaRef.selectionEnd;
885
332
  const selectedText = content.substring(start, end);
886
-
887
- content =
888
- content.substring(0, start) +
889
- before +
890
- selectedText +
891
- after +
892
- content.substring(end);
893
-
333
+ content = content.substring(0, start) + before + selectedText + after + content.substring(end);
894
334
  setTimeout(() => {
895
335
  textareaRef.selectionStart = start + before.length;
896
336
  textareaRef.selectionEnd = end + before.length;
@@ -898,24 +338,19 @@
898
338
  }, 0);
899
339
  }
900
340
 
901
- // Insert text at cursor
902
341
  function insertAtCursor(text) {
903
342
  if (!textareaRef) return;
904
-
905
343
  const start = textareaRef.selectionStart;
906
344
  content = content.substring(0, start) + text + content.substring(start);
907
-
908
345
  setTimeout(() => {
909
- textareaRef.selectionStart = textareaRef.selectionEnd =
910
- start + text.length;
346
+ textareaRef.selectionStart = textareaRef.selectionEnd = start + text.length;
911
347
  textareaRef.focus();
912
348
  }, 0);
913
349
  }
914
350
 
915
351
  // Toolbar actions
916
352
  function insertHeading(level) {
917
- const prefix = "#".repeat(level) + " ";
918
- insertAtCursor(prefix);
353
+ insertAtCursor("#".repeat(level) + " ");
919
354
  }
920
355
 
921
356
  function insertLink() {
@@ -928,15 +363,9 @@
928
363
 
929
364
  function insertCodeBlock() {
930
365
  const start = textareaRef.selectionStart;
931
- const selectedText = content.substring(
932
- start,
933
- textareaRef.selectionEnd
934
- );
366
+ const selectedText = content.substring(start, textareaRef.selectionEnd);
935
367
  const codeBlock = "```\n" + (selectedText || "code here") + "\n```";
936
- content =
937
- content.substring(0, start) +
938
- codeBlock +
939
- content.substring(textareaRef.selectionEnd);
368
+ content = content.substring(0, start) + codeBlock + content.substring(textareaRef.selectionEnd);
940
369
  }
941
370
 
942
371
  function insertList() {
@@ -947,18 +376,12 @@
947
376
  insertAtCursor("> ");
948
377
  }
949
378
 
950
- // Sync scroll between editor and preview (optional)
379
+ // Scroll sync
951
380
  function handleScroll() {
952
- // Sync line numbers
953
381
  syncLineNumbersScroll();
954
-
955
- // Sync preview
956
382
  if (textareaRef && previewRef && showPreview) {
957
- const scrollRatio =
958
- textareaRef.scrollTop /
959
- (textareaRef.scrollHeight - textareaRef.clientHeight);
960
- previewRef.scrollTop =
961
- scrollRatio * (previewRef.scrollHeight - previewRef.clientHeight);
383
+ const scrollRatio = textareaRef.scrollTop / (textareaRef.scrollHeight - textareaRef.clientHeight);
384
+ previewRef.scrollTop = scrollRatio * (previewRef.scrollHeight - previewRef.clientHeight);
962
385
  }
963
386
  }
964
387
 
@@ -969,12 +392,17 @@
969
392
  }
970
393
  });
971
394
 
972
- // Drag and drop image upload
395
+ // Auto-save draft effect
396
+ $effect(() => {
397
+ if (draftKey && !readonly) {
398
+ draftManager.scheduleSave(content);
399
+ }
400
+ });
401
+
402
+ // Drag and drop handlers
973
403
  function handleDragEnter(e) {
974
404
  e.preventDefault();
975
405
  if (readonly) return;
976
-
977
- // Check if dragging files
978
406
  if (e.dataTransfer?.types?.includes("Files")) {
979
407
  isDragging = true;
980
408
  }
@@ -983,7 +411,6 @@
983
411
  function handleDragOver(e) {
984
412
  e.preventDefault();
985
413
  if (readonly) return;
986
-
987
414
  if (e.dataTransfer?.types?.includes("Files")) {
988
415
  e.dataTransfer.dropEffect = "copy";
989
416
  isDragging = true;
@@ -992,7 +419,6 @@
992
419
 
993
420
  function handleDragLeave(e) {
994
421
  e.preventDefault();
995
- // Only set to false if leaving the container entirely
996
422
  if (!e.currentTarget.contains(e.relatedTarget)) {
997
423
  isDragging = false;
998
424
  }
@@ -1012,7 +438,6 @@
1012
438
  return;
1013
439
  }
1014
440
 
1015
- // Upload each image
1016
441
  for (const file of imageFiles) {
1017
442
  await uploadImage(file);
1018
443
  }
@@ -1039,7 +464,6 @@
1039
464
  throw new Error(result.message || "Upload failed");
1040
465
  }
1041
466
 
1042
- // Insert markdown image at cursor
1043
467
  const altText = file.name.replace(/\.[^/.]+$/, "").replace(/[-_]/g, " ");
1044
468
  const imageMarkdown = `![${altText}](${result.url})\n`;
1045
469
  insertAtCursor(imageMarkdown);
@@ -1054,7 +478,6 @@
1054
478
  }
1055
479
  }
1056
480
 
1057
- // Handle paste for images
1058
481
  function handlePaste(e) {
1059
482
  if (readonly) return;
1060
483
 
@@ -1065,7 +488,6 @@
1065
488
  e.preventDefault();
1066
489
  const file = imageItem.getAsFile();
1067
490
  if (file) {
1068
- // Generate a filename for pasted images
1069
491
  const timestamp = Date.now();
1070
492
  const extension = file.type.split("/")[1] || "png";
1071
493
  const renamedFile = new File([file], `pasted-${timestamp}.${extension}`, {
@@ -1076,114 +498,25 @@
1076
498
  }
1077
499
  }
1078
500
 
1079
- // Auto-save draft to localStorage
1080
- $effect(() => {
1081
- if (!draftKey || readonly) return;
1082
-
1083
- // Clear previous timer
1084
- if (draftSaveTimer) {
1085
- clearTimeout(draftSaveTimer);
1086
- }
1087
-
1088
- // Don't save if content hasn't changed from last saved version
1089
- if (content === lastSavedContent) return;
1090
-
1091
- // Schedule a draft save
1092
- draftSaveTimer = setTimeout(() => {
1093
- saveDraft();
1094
- }, AUTO_SAVE_DELAY);
1095
-
1096
- return () => {
1097
- if (draftSaveTimer) {
1098
- clearTimeout(draftSaveTimer);
1099
- }
1100
- };
1101
- });
1102
-
1103
- function saveDraft() {
1104
- if (!draftKey || readonly) return;
1105
-
1106
- try {
1107
- const draft = {
1108
- content,
1109
- savedAt: new Date().toISOString(),
1110
- };
1111
- localStorage.setItem(`draft:${draftKey}`, JSON.stringify(draft));
1112
- lastSavedContent = content;
1113
- hasDraft = true;
1114
- } catch (e) {
1115
- console.warn("Failed to save draft:", e);
1116
- }
1117
- }
1118
-
1119
- function loadDraft() {
1120
- if (!draftKey) return null;
1121
-
1122
- try {
1123
- const stored = localStorage.getItem(`draft:${draftKey}`);
1124
- if (stored) {
1125
- return JSON.parse(stored);
1126
- }
1127
- } catch (e) {
1128
- console.warn("Failed to load draft:", e);
1129
- }
1130
- return null;
1131
- }
1132
-
1133
- export function clearDraft() {
1134
- if (!draftKey) return;
1135
-
1136
- try {
1137
- localStorage.removeItem(`draft:${draftKey}`);
1138
- hasDraft = false;
1139
- storedDraft = null;
1140
- draftRestorePrompt = false;
1141
- } catch (e) {
1142
- console.warn("Failed to clear draft:", e);
1143
- }
1144
- }
1145
-
1146
- export function getDraftStatus() {
1147
- return { hasDraft, storedDraft };
1148
- }
1149
-
1150
- function restoreDraft() {
1151
- if (storedDraft) {
1152
- content = storedDraft.content;
1153
- lastSavedContent = storedDraft.content;
1154
- onDraftRestored(storedDraft);
501
+ // Command palette execution
502
+ function executePaletteCommand(index) {
503
+ const cmd = filteredPaletteCommands[index];
504
+ if (cmd && cmd.action) {
505
+ cmd.action();
506
+ commandPalette.close();
1155
507
  }
1156
- draftRestorePrompt = false;
1157
- }
1158
-
1159
- function discardDraft() {
1160
- clearDraft();
1161
- lastSavedContent = content;
1162
508
  }
1163
509
 
1164
510
  onMount(() => {
1165
511
  updateCursorPosition();
1166
- loadSnippets();
1167
- loadSoundSettings();
1168
- loadTheme();
1169
-
1170
- // Check for existing draft on mount
1171
- if (draftKey) {
1172
- const draft = loadDraft();
1173
- if (draft && draft.content !== content) {
1174
- storedDraft = draft;
1175
- draftRestorePrompt = true;
1176
- } else {
1177
- lastSavedContent = content;
1178
- }
1179
- }
512
+ snippetsManager.load();
513
+ ambientSounds.loadSettings();
514
+ editorTheme.loadTheme();
515
+ draftManager.init(content);
1180
516
 
1181
- // Cleanup audio on unmount
1182
517
  return () => {
1183
- if (audioElement) {
1184
- audioElement.pause();
1185
- audioElement = null;
1186
- }
518
+ ambientSounds.cleanup();
519
+ draftManager.cleanup();
1187
520
  };
1188
521
  });
1189
522
  </script>
@@ -1194,7 +527,7 @@
1194
527
  class="editor-container"
1195
528
  class:dragging={isDragging}
1196
529
  class:zen-mode={isZenMode}
1197
- class:campfire-mode={campfireSession.active}
530
+ class:campfire-mode={writingSession.isCampfireActive}
1198
531
  aria-label="Markdown editor with live preview"
1199
532
  ondragenter={handleDragEnter}
1200
533
  ondragover={handleDragOver}
@@ -1225,21 +558,21 @@
1225
558
  {/if}
1226
559
 
1227
560
  <!-- Draft restore prompt -->
1228
- {#if draftRestorePrompt && storedDraft}
561
+ {#if draftManager.draftRestorePrompt && draftManager.storedDraft}
1229
562
  <div class="draft-prompt">
1230
563
  <div class="draft-prompt-content">
1231
564
  <span class="draft-icon">~</span>
1232
565
  <div class="draft-message">
1233
566
  <strong>Unsaved draft found</strong>
1234
567
  <span class="draft-time">
1235
- Saved {new Date(storedDraft.savedAt).toLocaleString()}
568
+ Saved {new Date(draftManager.storedDraft.savedAt).toLocaleString()}
1236
569
  </span>
1237
570
  </div>
1238
571
  <div class="draft-actions">
1239
- <button type="button" class="draft-btn restore" onclick={restoreDraft}>
572
+ <button type="button" class="draft-btn restore" onclick={() => draftManager.restoreDraft()}>
1240
573
  [<span class="key">r</span>estore]
1241
574
  </button>
1242
- <button type="button" class="draft-btn discard" onclick={discardDraft}>
575
+ <button type="button" class="draft-btn discard" onclick={() => draftManager.discardDraft()}>
1243
576
  [<span class="key">d</span>iscard]
1244
577
  </button>
1245
578
  </div>
@@ -1250,98 +583,54 @@
1250
583
  <!-- Toolbar -->
1251
584
  <div class="toolbar">
1252
585
  <div class="toolbar-group">
1253
- <button
1254
- type="button"
1255
- class="toolbar-btn"
1256
- onclick={() => insertHeading(1)}
1257
- title="Heading 1"
1258
- disabled={readonly}
1259
- >[h<span class="key">1</span>]</button>
1260
- <button
1261
- type="button"
1262
- class="toolbar-btn"
1263
- onclick={() => insertHeading(2)}
1264
- title="Heading 2"
1265
- disabled={readonly}
1266
- >[h<span class="key">2</span>]</button>
1267
- <button
1268
- type="button"
1269
- class="toolbar-btn"
1270
- onclick={() => insertHeading(3)}
1271
- title="Heading 3"
1272
- disabled={readonly}
1273
- >[h<span class="key">3</span>]</button>
586
+ <button type="button" class="toolbar-btn" onclick={() => insertHeading(1)} title="Heading 1" disabled={readonly}>
587
+ [h<span class="key">1</span>]
588
+ </button>
589
+ <button type="button" class="toolbar-btn" onclick={() => insertHeading(2)} title="Heading 2" disabled={readonly}>
590
+ [h<span class="key">2</span>]
591
+ </button>
592
+ <button type="button" class="toolbar-btn" onclick={() => insertHeading(3)} title="Heading 3" disabled={readonly}>
593
+ [h<span class="key">3</span>]
594
+ </button>
1274
595
  </div>
1275
596
 
1276
597
  <div class="toolbar-divider">|</div>
1277
598
 
1278
599
  <div class="toolbar-group">
1279
- <button
1280
- type="button"
1281
- class="toolbar-btn"
1282
- onclick={() => wrapSelection("**", "**")}
1283
- title="Bold (Cmd+B)"
1284
- disabled={readonly}
1285
- >[<span class="key">b</span>old]</button>
1286
- <button
1287
- type="button"
1288
- class="toolbar-btn"
1289
- onclick={() => wrapSelection("_", "_")}
1290
- title="Italic (Cmd+I)"
1291
- disabled={readonly}
1292
- >[<span class="key">i</span>talic]</button>
1293
- <button
1294
- type="button"
1295
- class="toolbar-btn"
1296
- onclick={() => wrapSelection("`", "`")}
1297
- title="Inline Code"
1298
- disabled={readonly}
1299
- >[<span class="key">c</span>ode]</button>
600
+ <button type="button" class="toolbar-btn" onclick={() => wrapSelection("**", "**")} title="Bold (Cmd+B)" disabled={readonly}>
601
+ [<span class="key">b</span>old]
602
+ </button>
603
+ <button type="button" class="toolbar-btn" onclick={() => wrapSelection("_", "_")} title="Italic (Cmd+I)" disabled={readonly}>
604
+ [<span class="key">i</span>talic]
605
+ </button>
606
+ <button type="button" class="toolbar-btn" onclick={() => wrapSelection("`", "`")} title="Inline Code" disabled={readonly}>
607
+ [<span class="key">c</span>ode]
608
+ </button>
1300
609
  </div>
1301
610
 
1302
611
  <div class="toolbar-divider">|</div>
1303
612
 
1304
613
  <div class="toolbar-group">
1305
- <button
1306
- type="button"
1307
- class="toolbar-btn"
1308
- onclick={insertLink}
1309
- title="Link"
1310
- disabled={readonly}
1311
- >[<span class="key">l</span>ink]</button>
1312
- <button
1313
- type="button"
1314
- class="toolbar-btn"
1315
- onclick={insertImage}
1316
- title="Image"
1317
- disabled={readonly}
1318
- >[i<span class="key">m</span>g]</button>
1319
- <button
1320
- type="button"
1321
- class="toolbar-btn"
1322
- onclick={insertCodeBlock}
1323
- title="Code Block"
1324
- disabled={readonly}
1325
- >[bloc<span class="key">k</span>]</button>
614
+ <button type="button" class="toolbar-btn" onclick={insertLink} title="Link" disabled={readonly}>
615
+ [<span class="key">l</span>ink]
616
+ </button>
617
+ <button type="button" class="toolbar-btn" onclick={insertImage} title="Image" disabled={readonly}>
618
+ [i<span class="key">m</span>g]
619
+ </button>
620
+ <button type="button" class="toolbar-btn" onclick={insertCodeBlock} title="Code Block" disabled={readonly}>
621
+ [bloc<span class="key">k</span>]
622
+ </button>
1326
623
  </div>
1327
624
 
1328
625
  <div class="toolbar-divider">|</div>
1329
626
 
1330
627
  <div class="toolbar-group">
1331
- <button
1332
- type="button"
1333
- class="toolbar-btn"
1334
- onclick={insertList}
1335
- title="List"
1336
- disabled={readonly}
1337
- >[lis<span class="key">t</span>]</button>
1338
- <button
1339
- type="button"
1340
- class="toolbar-btn"
1341
- onclick={insertQuote}
1342
- title="Quote"
1343
- disabled={readonly}
1344
- >[<span class="key">q</span>uote]</button>
628
+ <button type="button" class="toolbar-btn" onclick={insertList} title="List" disabled={readonly}>
629
+ [lis<span class="key">t</span>]
630
+ </button>
631
+ <button type="button" class="toolbar-btn" onclick={insertQuote} title="Quote" disabled={readonly}>
632
+ [<span class="key">q</span>uote]
633
+ </button>
1345
634
  </div>
1346
635
 
1347
636
  <div class="toolbar-spacer"></div>
@@ -1353,13 +642,17 @@
1353
642
  class:active={showPreview}
1354
643
  onclick={() => (showPreview = !showPreview)}
1355
644
  title="Toggle Preview"
1356
- >{#if showPreview}[hide <span class="key">p</span>review]{:else}[show <span class="key">p</span>review]{/if}</button>
645
+ >
646
+ {#if showPreview}[hide <span class="key">p</span>review]{:else}[show <span class="key">p</span>review]{/if}
647
+ </button>
1357
648
  <button
1358
649
  type="button"
1359
650
  class="toolbar-btn full-preview-btn"
1360
651
  onclick={() => (showFullPreview = true)}
1361
652
  title="Open Full Preview (site styling)"
1362
- >[<span class="key">f</span>ull]</button>
653
+ >
654
+ [<span class="key">f</span>ull]
655
+ </button>
1363
656
  </div>
1364
657
  </div>
1365
658
 
@@ -1412,26 +705,20 @@
1412
705
  <!-- Status Bar -->
1413
706
  <div class="status-bar">
1414
707
  <div class="status-left">
1415
- <span class="status-item">
1416
- Ln {cursorLine}, Col {cursorCol}
1417
- </span>
708
+ <span class="status-item">Ln {cursorLine}, Col {cursorCol}</span>
1418
709
  <span class="status-divider">|</span>
1419
710
  <span class="status-item">{lineCount} lines</span>
1420
711
  <span class="status-divider">|</span>
1421
712
  <span class="status-item">{wordCount} words</span>
1422
713
  <span class="status-divider">|</span>
1423
- <span class="status-item">{readingTime()}</span>
1424
- {#if writingGoal.enabled}
714
+ <span class="status-item">{readingTime}</span>
715
+ {#if writingSession.isGoalEnabled}
1425
716
  <span class="status-divider">|</span>
1426
- <span class="status-goal">
1427
- Goal: {goalProgress()}%
1428
- </span>
717
+ <span class="status-goal">Goal: {goalProgress}%</span>
1429
718
  {/if}
1430
- {#if campfireSession.active}
719
+ {#if writingSession.isCampfireActive}
1431
720
  <span class="status-divider">|</span>
1432
- <span class="status-campfire">
1433
- ~ {campfireElapsed()}
1434
- </span>
721
+ <span class="status-campfire">~ {campfireElapsed}</span>
1435
722
  {/if}
1436
723
  </div>
1437
724
  <div class="status-right">
@@ -1439,7 +726,7 @@
1439
726
  type="button"
1440
727
  class="status-sound-btn"
1441
728
  class:playing={ambientSounds.enabled}
1442
- onclick={toggleSoundPanel}
729
+ onclick={() => ambientSounds.togglePanel()}
1443
730
  title="Ambient sounds"
1444
731
  >
1445
732
  [{soundLibrary[ambientSounds.currentSound]?.name || "snd"}]{#if ambientSounds.enabled}<span class="sound-wave">~</span>{/if}
@@ -1451,7 +738,7 @@
1451
738
  {/if}
1452
739
  {#if saving}
1453
740
  <span class="status-saving">Saving...</span>
1454
- {:else if draftKey && content !== lastSavedContent}
741
+ {:else if draftKey && draftManager.hasUnsavedChanges(content)}
1455
742
  <span class="status-draft">Draft saving...</span>
1456
743
  {:else}
1457
744
  <span class="status-item">Markdown</span>
@@ -1461,15 +748,15 @@
1461
748
  </div>
1462
749
 
1463
750
  <!-- Slash Commands Menu -->
1464
- {#if slashMenu.open}
751
+ {#if slashCommands.isOpen}
1465
752
  <div class="slash-menu">
1466
753
  <div class="slash-menu-header">:: commands</div>
1467
754
  {#each filteredSlashCommands as cmd, i}
1468
755
  <button
1469
756
  type="button"
1470
757
  class="slash-menu-item"
1471
- class:selected={i === slashMenu.selectedIndex}
1472
- onclick={() => executeSlashCommand(i)}
758
+ class:selected={i === slashCommands.menu.selectedIndex}
759
+ onclick={() => slashCommands.execute(i)}
1473
760
  >
1474
761
  <span class="slash-cmd-label">{cmd.label}</span>
1475
762
  </button>
@@ -1481,29 +768,30 @@
1481
768
  {/if}
1482
769
 
1483
770
  <!-- Command Palette -->
1484
- {#if commandPalette.open}
1485
- <div class="command-palette-overlay" onclick={() => commandPalette.open = false}>
771
+ {#if commandPalette.isOpen}
772
+ <div class="command-palette-overlay" onclick={() => commandPalette.close()}>
1486
773
  <div class="command-palette" onclick={(e) => e.stopPropagation()}>
1487
774
  <input
1488
775
  type="text"
1489
776
  class="command-palette-input"
1490
777
  placeholder="> type a command..."
1491
- bind:value={commandPalette.query}
778
+ value={commandPalette.query}
779
+ oninput={(e) => commandPalette.setQuery(e.target.value)}
1492
780
  onkeydown={(e) => {
1493
781
  if (e.key === "ArrowDown") {
1494
782
  e.preventDefault();
1495
- commandPalette.selectedIndex = (commandPalette.selectedIndex + 1) % filteredPaletteCommands.length;
783
+ commandPalette.navigate("down");
1496
784
  }
1497
785
  if (e.key === "ArrowUp") {
1498
786
  e.preventDefault();
1499
- commandPalette.selectedIndex = (commandPalette.selectedIndex - 1 + filteredPaletteCommands.length) % filteredPaletteCommands.length;
787
+ commandPalette.navigate("up");
1500
788
  }
1501
789
  if (e.key === "Enter") {
1502
790
  e.preventDefault();
1503
791
  executePaletteCommand(commandPalette.selectedIndex);
1504
792
  }
1505
793
  if (e.key === "Escape") {
1506
- commandPalette.open = false;
794
+ commandPalette.close();
1507
795
  }
1508
796
  }}
1509
797
  />
@@ -1526,23 +814,23 @@
1526
814
  </div>
1527
815
  {/if}
1528
816
 
1529
- <!-- Campfire Session Controls (when active) -->
1530
- {#if campfireSession.active}
817
+ <!-- Campfire Session Controls -->
818
+ {#if writingSession.isCampfireActive}
1531
819
  <div class="campfire-controls">
1532
820
  <div class="campfire-ember"></div>
1533
821
  <div class="campfire-stats">
1534
- <span class="campfire-time">{campfireElapsed()}</span>
1535
- <span class="campfire-words">+{wordCount - campfireSession.startWordCount} words</span>
822
+ <span class="campfire-time">{campfireElapsed}</span>
823
+ <span class="campfire-words">+{writingSession.getCampfireWords(wordCount)} words</span>
1536
824
  </div>
1537
- <button type="button" class="campfire-end" onclick={endCampfireSession}>
825
+ <button type="button" class="campfire-end" onclick={() => writingSession.endCampfire()}>
1538
826
  [<span class="key">e</span>nd]
1539
827
  </button>
1540
828
  </div>
1541
829
  {/if}
1542
830
 
1543
831
  <!-- Snippets Modal -->
1544
- <Dialog bind:open={snippetsModal.open}>
1545
- <h3 slot="title">:: {snippetsModal.editingId ? "edit snippet" : "new snippet"}</h3>
832
+ <Dialog bind:open={snippetsManager.modal.open}>
833
+ <h3 slot="title">:: {snippetsManager.modal.editingId ? "edit snippet" : "new snippet"}</h3>
1546
834
 
1547
835
  <div class="snippets-modal-body">
1548
836
  <div class="snippets-form">
@@ -1551,7 +839,7 @@
1551
839
  <Input
1552
840
  id="snippet-name"
1553
841
  type="text"
1554
- bind:value={snippetsModal.name}
842
+ bind:value={snippetsManager.modal.name}
1555
843
  placeholder="e.g., Blog signature"
1556
844
  />
1557
845
  </div>
@@ -1561,55 +849,52 @@
1561
849
  <Input
1562
850
  id="snippet-trigger"
1563
851
  type="text"
1564
- bind:value={snippetsModal.trigger}
852
+ bind:value={snippetsManager.modal.trigger}
1565
853
  placeholder="e.g., sig"
1566
854
  />
1567
855
  <span class="field-hint">Type /trigger to quickly insert</span>
1568
856
  </div>
1569
857
 
1570
- <div class="snippet-field">
1571
- <label for="snippet-content">Content</label>
1572
- <textarea
1573
- id="snippet-content"
1574
- bind:value={snippetsModal.content}
1575
- placeholder="Enter your markdown snippet..."
1576
- rows="6"
1577
- ></textarea>
1578
- </div>
858
+ <div class="snippet-field">
859
+ <label for="snippet-content">Content</label>
860
+ <textarea
861
+ id="snippet-content"
862
+ bind:value={snippetsManager.modal.content}
863
+ placeholder="Enter your markdown snippet..."
864
+ rows="6"
865
+ ></textarea>
866
+ </div>
1579
867
 
1580
868
  <div class="snippet-actions">
1581
- {#if snippetsModal.editingId}
1582
- <Button
1583
- variant="danger"
1584
- onclick={() => deleteSnippet(snippetsModal.editingId)}
1585
- >
869
+ {#if snippetsManager.modal.editingId}
870
+ <Button variant="danger" onclick={() => snippetsManager.deleteSnippet(snippetsManager.modal.editingId)}>
1586
871
  [<span class="key">d</span>elete]
1587
872
  </Button>
1588
873
  {/if}
1589
874
  <div class="snippet-actions-right">
1590
- <Button variant="outline" onclick={closeSnippetsModal}>
875
+ <Button variant="outline" onclick={() => snippetsManager.closeModal()}>
1591
876
  [<span class="key">c</span>ancel]
1592
877
  </Button>
1593
878
  <Button
1594
- onclick={saveSnippet}
1595
- disabled={!snippetsModal.name.trim() || !snippetsModal.content.trim()}
879
+ onclick={() => snippetsManager.saveSnippet()}
880
+ disabled={!snippetsManager.modal.name.trim() || !snippetsManager.modal.content.trim()}
1596
881
  >
1597
- {#if snippetsModal.editingId}[<span class="key">u</span>pdate]{:else}[<span class="key">s</span>ave]{/if}
882
+ {#if snippetsManager.modal.editingId}[<span class="key">u</span>pdate]{:else}[<span class="key">s</span>ave]{/if}
1598
883
  </Button>
1599
884
  </div>
1600
885
  </div>
1601
886
  </div>
1602
887
 
1603
- {#if snippets.length > 0 && !snippetsModal.editingId}
888
+ {#if snippetsManager.snippets.length > 0 && !snippetsManager.modal.editingId}
1604
889
  <div class="snippets-list-divider">
1605
890
  <span>:: your snippets</span>
1606
891
  </div>
1607
892
  <div class="snippets-list">
1608
- {#each snippets as snippet}
893
+ {#each snippetsManager.snippets as snippet}
1609
894
  <button
1610
895
  type="button"
1611
896
  class="snippet-list-item"
1612
- onclick={() => openSnippetsModal(snippet.id)}
897
+ onclick={() => snippetsManager.openModal(snippet.id)}
1613
898
  >
1614
899
  <span class="snippet-name">{snippet.name}</span>
1615
900
  {#if snippet.trigger}
@@ -1627,11 +912,9 @@
1627
912
  <div class="sound-panel">
1628
913
  <div class="sound-panel-header">
1629
914
  <span class="sound-panel-title">:: ambient sounds</span>
1630
- <button
1631
- type="button"
1632
- class="sound-panel-close"
1633
- onclick={() => ambientSounds.showPanel = false}
1634
- >[x]</button>
915
+ <button type="button" class="sound-panel-close" onclick={() => ambientSounds.closePanel()}>
916
+ [x]
917
+ </button>
1635
918
  </div>
1636
919
 
1637
920
  <div class="sound-options">
@@ -1641,7 +924,7 @@
1641
924
  class="sound-option"
1642
925
  class:active={ambientSounds.currentSound === key}
1643
926
  class:playing={ambientSounds.enabled && ambientSounds.currentSound === key}
1644
- onclick={() => selectSound(key)}
927
+ onclick={() => ambientSounds.selectSound(key)}
1645
928
  >
1646
929
  [<span class="key">{sound.key}</span>] {sound.name}
1647
930
  </button>
@@ -1657,7 +940,7 @@
1657
940
  max="1"
1658
941
  step="0.05"
1659
942
  value={ambientSounds.volume}
1660
- oninput={(e) => setVolume(parseFloat(e.target.value))}
943
+ oninput={(e) => ambientSounds.setVolume(parseFloat(e.target.value))}
1661
944
  class="volume-slider"
1662
945
  />
1663
946
  </label>
@@ -1666,7 +949,7 @@
1666
949
  type="button"
1667
950
  class="sound-play-btn"
1668
951
  class:playing={ambientSounds.enabled}
1669
- onclick={toggleAmbientSound}
952
+ onclick={() => ambientSounds.toggle()}
1670
953
  >
1671
954
  {#if ambientSounds.enabled}[<span class="key">s</span>top]{:else}[<span class="key">p</span>lay]{/if}
1672
955
  </button>
@@ -1686,18 +969,13 @@
1686
969
  <header class="full-preview-header">
1687
970
  <h2>:: full preview</h2>
1688
971
  <div class="full-preview-actions">
1689
- <button
1690
- type="button"
1691
- class="full-preview-close"
1692
- onclick={() => (showFullPreview = false)}
1693
- >
972
+ <button type="button" class="full-preview-close" onclick={() => (showFullPreview = false)}>
1694
973
  [<span class="key">c</span>lose]
1695
974
  </button>
1696
975
  </div>
1697
976
  </header>
1698
977
  <div class="full-preview-scroll">
1699
978
  <article class="full-preview-article">
1700
- <!-- Post Header -->
1701
979
  {#if previewTitle || previewDate || previewTags.length > 0}
1702
980
  <header class="content-header">
1703
981
  {#if previewTitle}
@@ -1726,7 +1004,6 @@
1726
1004
  </header>
1727
1005
  {/if}
1728
1006
 
1729
- <!-- Rendered Content -->
1730
1007
  <div class="content-body">
1731
1008
  {#if previewHtml}
1732
1009
  {@html previewHtml}
@@ -1752,12 +1029,12 @@
1752
1029
  overflow: hidden;
1753
1030
  font-family: "JetBrains Mono", "Fira Code", "SF Mono", Consolas, monospace;
1754
1031
  position: relative;
1032
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
1755
1033
  }
1756
1034
  .editor-container.dragging {
1757
1035
  border-color: var(--editor-accent, #8bc48b);
1758
1036
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--editor-accent, #8bc48b) 30%, transparent);
1759
1037
  }
1760
- /* Drag overlay */
1761
1038
  .drag-overlay {
1762
1039
  position: absolute;
1763
1040
  inset: 0;
@@ -1791,7 +1068,6 @@
1791
1068
  font-size: 1.1rem;
1792
1069
  font-weight: 500;
1793
1070
  }
1794
- /* Upload status */
1795
1071
  .upload-status {
1796
1072
  position: absolute;
1797
1073
  top: 50%;
@@ -1835,11 +1111,8 @@
1835
1111
  font-weight: bold;
1836
1112
  }
1837
1113
  @keyframes spin {
1838
- to {
1839
- transform: rotate(360deg);
1840
- }
1114
+ to { transform: rotate(360deg); }
1841
1115
  }
1842
- /* Draft prompt */
1843
1116
  .draft-prompt {
1844
1117
  position: absolute;
1845
1118
  top: 0;
@@ -1901,13 +1174,11 @@
1901
1174
  .draft-btn.discard:hover {
1902
1175
  color: #d4d4d4;
1903
1176
  }
1904
- /* Terminal Key Highlight */
1905
1177
  .key {
1906
1178
  color: var(--editor-accent, #8bc48b);
1907
1179
  font-weight: bold;
1908
1180
  text-decoration: underline;
1909
1181
  }
1910
- /* Toolbar */
1911
1182
  .toolbar {
1912
1183
  display: flex;
1913
1184
  align-items: center;
@@ -1917,6 +1188,7 @@
1917
1188
  border-bottom: 1px solid var(--editor-border, var(--light-border-primary));
1918
1189
  flex-wrap: wrap;
1919
1190
  font-family: "JetBrains Mono", "Fira Code", monospace;
1191
+ transition: opacity 0.3s ease;
1920
1192
  }
1921
1193
  .toolbar-group {
1922
1194
  display: flex;
@@ -1972,7 +1244,6 @@
1972
1244
  .toolbar-spacer {
1973
1245
  flex: 1;
1974
1246
  }
1975
- /* Editor Area */
1976
1247
  .editor-area {
1977
1248
  display: flex;
1978
1249
  flex: 1;
@@ -1996,7 +1267,6 @@
1996
1267
  min-height: 0;
1997
1268
  overflow: hidden;
1998
1269
  }
1999
- /* Line Numbers */
2000
1270
  .line-numbers {
2001
1271
  display: flex;
2002
1272
  flex-direction: column;
@@ -2021,7 +1291,6 @@
2021
1291
  color: var(--editor-accent, #8bc48b);
2022
1292
  background: color-mix(in srgb, var(--editor-accent, #8bc48b) 10%, transparent);
2023
1293
  }
2024
- /* Editor Textarea */
2025
1294
  .editor-textarea {
2026
1295
  flex: 1;
2027
1296
  padding: 1rem;
@@ -2047,7 +1316,6 @@
2047
1316
  opacity: 0.7;
2048
1317
  cursor: not-allowed;
2049
1318
  }
2050
- /* Preview Panel */
2051
1319
  .preview-panel {
2052
1320
  width: 50%;
2053
1321
  display: flex;
@@ -2070,12 +1338,7 @@
2070
1338
  padding: 1rem;
2071
1339
  overflow-y: auto;
2072
1340
  color: #d4d4d4;
2073
- font-family:
2074
- -apple-system,
2075
- BlinkMacSystemFont,
2076
- "Segoe UI",
2077
- Roboto,
2078
- sans-serif;
1341
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
2079
1342
  font-size: 0.95rem;
2080
1343
  line-height: 1.7;
2081
1344
  }
@@ -2083,7 +1346,6 @@
2083
1346
  color: #5a5a5a;
2084
1347
  font-style: italic;
2085
1348
  }
2086
- /* Preview content styles */
2087
1349
  .preview-content :global(h1),
2088
1350
  .preview-content :global(h2),
2089
1351
  .preview-content :global(h3),
@@ -2157,7 +1419,6 @@
2157
1419
  max-width: 100%;
2158
1420
  border-radius: 4px;
2159
1421
  }
2160
- /* Status Bar */
2161
1422
  .status-bar {
2162
1423
  display: flex;
2163
1424
  justify-content: space-between;
@@ -2167,6 +1428,7 @@
2167
1428
  border-top: 1px solid var(--editor-status-border, var(--light-border-secondary));
2168
1429
  font-size: 0.75rem;
2169
1430
  color: var(--editor-accent-bright, #a8dca8);
1431
+ transition: opacity 0.3s ease;
2170
1432
  }
2171
1433
  .status-left,
2172
1434
  .status-right {
@@ -2188,16 +1450,21 @@
2188
1450
  color: #7a9a7a;
2189
1451
  font-style: italic;
2190
1452
  }
1453
+ .status-goal {
1454
+ color: var(--editor-accent, #8bc48b);
1455
+ font-weight: 500;
1456
+ }
1457
+ .status-campfire {
1458
+ color: #f0a060;
1459
+ }
1460
+ .status-mode {
1461
+ color: #7ab3ff;
1462
+ font-size: 0.75rem;
1463
+ }
2191
1464
  @keyframes pulse {
2192
- 0%,
2193
- 100% {
2194
- opacity: 1;
2195
- }
2196
- 50% {
2197
- opacity: 0.5;
2198
- }
1465
+ 0%, 100% { opacity: 1; }
1466
+ 50% { opacity: 0.5; }
2199
1467
  }
2200
- /* Responsive */
2201
1468
  @media (max-width: 768px) {
2202
1469
  .editor-area.split {
2203
1470
  flex-direction: column;
@@ -2220,136 +1487,6 @@
2220
1487
  font-size: 0.75rem;
2221
1488
  }
2222
1489
  }
2223
- /* Full Preview Button */
2224
- .full-preview-btn {
2225
- background: #2d3a4d;
2226
- color: #7ab3ff;
2227
- border-color: #3d4a5d;
2228
- }
2229
- .full-preview-btn:hover {
2230
- background: #3d4a5d;
2231
- color: #9ac5ff;
2232
- }
2233
- /* Full Preview Modal */
2234
- .full-preview-modal {
2235
- position: fixed;
2236
- inset: 0;
2237
- z-index: 1000;
2238
- display: flex;
2239
- align-items: center;
2240
- justify-content: center;
2241
- }
2242
- .full-preview-backdrop {
2243
- position: absolute;
2244
- inset: 0;
2245
- background: rgba(0, 0, 0, 0.7);
2246
- }
2247
- .full-preview-container {
2248
- position: relative;
2249
- width: 90%;
2250
- max-width: 900px;
2251
- height: 90vh;
2252
- background: var(--color-bg, var(--light-bg-primary));
2253
- border-radius: 12px;
2254
- display: flex;
2255
- flex-direction: column;
2256
- overflow: hidden;
2257
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
2258
- }
2259
- :global(.dark) .full-preview-container {
2260
- background: var(--color-bg-dark, #0d1117);
2261
- }
2262
- .full-preview-header {
2263
- display: flex;
2264
- justify-content: space-between;
2265
- align-items: center;
2266
- padding: 1rem 1.5rem;
2267
- background: var(--color-bg-secondary, var(--light-bg-tertiary));
2268
- border-bottom: 1px solid var(--color-border, var(--light-border-primary));
2269
- flex-shrink: 0;
2270
- }
2271
- :global(.dark) .full-preview-header {
2272
- background: var(--color-bg-secondary-dark, var(--light-bg-primary));
2273
- border-color: var(--color-border-dark, var(--light-border-secondary));
2274
- }
2275
- .full-preview-header h2 {
2276
- margin: 0;
2277
- font-size: 0.9rem;
2278
- font-weight: 500;
2279
- font-family: "JetBrains Mono", "Fira Code", monospace;
2280
- color: #8bc48b;
2281
- }
2282
- :global(.dark) .full-preview-header h2 {
2283
- color: #8bc48b;
2284
- }
2285
- .full-preview-close {
2286
- padding: 0.3rem 0.5rem;
2287
- background: transparent;
2288
- color: #7a9a7a;
2289
- border: none;
2290
- font-size: 0.85rem;
2291
- font-family: "JetBrains Mono", "Fira Code", monospace;
2292
- cursor: pointer;
2293
- transition: color 0.1s ease;
2294
- }
2295
- .full-preview-close:hover {
2296
- color: #a8dca8;
2297
- }
2298
- .full-preview-scroll {
2299
- flex: 1;
2300
- overflow-y: auto;
2301
- padding: 2rem;
2302
- }
2303
- .full-preview-article {
2304
- max-width: 800px;
2305
- margin: 0 auto;
2306
- }
2307
- /* Post meta styling in full preview */
2308
- .full-preview-article .post-meta {
2309
- display: flex;
2310
- align-items: center;
2311
- gap: 1rem;
2312
- flex-wrap: wrap;
2313
- margin-top: 1rem;
2314
- }
2315
- .full-preview-article time {
2316
- color: var(--light-text-light);
2317
- font-size: 1rem;
2318
- transition: color 0.3s ease;
2319
- }
2320
- :global(.dark) .full-preview-article time {
2321
- color: var(--color-text-subtle-dark, #666);
2322
- }
2323
- .full-preview-article .tags {
2324
- display: flex;
2325
- gap: 0.5rem;
2326
- flex-wrap: wrap;
2327
- }
2328
- .full-preview-article .tag {
2329
- padding: 0.25rem 0.75rem;
2330
- background: var(--tag-bg, #2c5f2d);
2331
- color: white;
2332
- border-radius: 12px;
2333
- font-size: 0.8rem;
2334
- font-weight: 500;
2335
- }
2336
- /* Line numbers scroll sync */
2337
- .line-numbers {
2338
- overflow: hidden;
2339
- }
2340
- /* Status bar enhancements */
2341
- .status-goal {
2342
- color: var(--editor-accent, #8bc48b);
2343
- font-weight: 500;
2344
- }
2345
- .status-campfire {
2346
- color: #f0a060;
2347
- }
2348
- .status-mode {
2349
- color: #7ab3ff;
2350
- font-size: 0.75rem;
2351
- }
2352
- /* Zen Mode Styles */
2353
1490
  .editor-container.zen-mode {
2354
1491
  position: fixed;
2355
1492
  inset: 0;
@@ -2359,14 +1496,12 @@
2359
1496
  }
2360
1497
  .editor-container.zen-mode .toolbar {
2361
1498
  opacity: 0.3;
2362
- transition: opacity 0.3s ease;
2363
1499
  }
2364
1500
  .editor-container.zen-mode .toolbar:hover {
2365
1501
  opacity: 1;
2366
1502
  }
2367
1503
  .editor-container.zen-mode .status-bar {
2368
1504
  opacity: 0.5;
2369
- transition: opacity 0.3s ease;
2370
1505
  }
2371
1506
  .editor-container.zen-mode .status-bar:hover {
2372
1507
  opacity: 1;
@@ -2374,7 +1509,6 @@
2374
1509
  .editor-container.zen-mode .editor-area {
2375
1510
  height: calc(100vh - 80px);
2376
1511
  }
2377
- /* Campfire Mode Styles */
2378
1512
  .editor-container.campfire-mode {
2379
1513
  border-color: #8b5a2b;
2380
1514
  box-shadow: 0 0 30px rgba(240, 160, 96, 0.15);
@@ -2393,6 +1527,7 @@
2393
1527
  color: #f0d0a0;
2394
1528
  z-index: 1000;
2395
1529
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
1530
+ animation: fade-in 0.3s ease;
2396
1531
  }
2397
1532
  .campfire-ember {
2398
1533
  width: 12px;
@@ -2402,12 +1537,8 @@
2402
1537
  animation: ember-glow 2s ease-in-out infinite;
2403
1538
  }
2404
1539
  @keyframes ember-glow {
2405
- 0%, 100% {
2406
- box-shadow: 0 0 8px #ff6b35, 0 0 16px rgba(240, 107, 53, 0.5);
2407
- }
2408
- 50% {
2409
- box-shadow: 0 0 12px #f0a060, 0 0 24px rgba(240, 160, 96, 0.6);
2410
- }
1540
+ 0%, 100% { box-shadow: 0 0 8px #ff6b35, 0 0 16px rgba(240, 107, 53, 0.5); }
1541
+ 50% { box-shadow: 0 0 12px #f0a060, 0 0 24px rgba(240, 160, 96, 0.6); }
2411
1542
  }
2412
1543
  .campfire-stats {
2413
1544
  display: flex;
@@ -2436,7 +1567,6 @@
2436
1567
  .campfire-end:hover {
2437
1568
  color: #f0d0a0;
2438
1569
  }
2439
- /* Slash Commands Menu */
2440
1570
  .slash-menu {
2441
1571
  position: fixed;
2442
1572
  top: 50%;
@@ -2450,6 +1580,7 @@
2450
1580
  border-radius: 8px;
2451
1581
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
2452
1582
  z-index: 1001;
1583
+ animation: scale-in 0.15s ease;
2453
1584
  }
2454
1585
  .slash-menu-header {
2455
1586
  padding: 0.5rem 0.75rem;
@@ -2486,7 +1617,6 @@
2486
1617
  font-size: 0.8rem;
2487
1618
  text-align: center;
2488
1619
  }
2489
- /* Command Palette */
2490
1620
  .command-palette-overlay {
2491
1621
  position: fixed;
2492
1622
  inset: 0;
@@ -2505,6 +1635,7 @@
2505
1635
  border-radius: 8px;
2506
1636
  box-shadow: 0 16px 64px rgba(0, 0, 0, 0.6);
2507
1637
  overflow: hidden;
1638
+ animation: slide-down 0.2s ease;
2508
1639
  }
2509
1640
  .command-palette-input {
2510
1641
  width: 100%;
@@ -2554,107 +1685,17 @@
2554
1685
  color: #6a6a6a;
2555
1686
  font-family: "JetBrains Mono", monospace;
2556
1687
  }
2557
- /* Mode Transitions */
2558
- .editor-container {
2559
- transition: border-color 0.3s ease, box-shadow 0.3s ease;
2560
- }
2561
- .toolbar,
2562
- .status-bar {
2563
- transition: opacity 0.3s ease;
2564
- }
2565
- .campfire-controls {
2566
- animation: fade-in 0.3s ease;
2567
- }
2568
1688
  @keyframes fade-in {
2569
- from {
2570
- opacity: 0;
2571
- transform: translateY(10px);
2572
- }
2573
- to {
2574
- opacity: 1;
2575
- transform: translateY(0);
2576
- }
2577
- }
2578
- .slash-menu,
2579
- .command-palette {
2580
- animation: scale-in 0.15s ease;
1689
+ from { opacity: 0; transform: translateY(10px); }
1690
+ to { opacity: 1; transform: translateY(0); }
2581
1691
  }
2582
1692
  @keyframes scale-in {
2583
- from {
2584
- opacity: 0;
2585
- transform: translate(-50%, -50%) scale(0.95);
2586
- }
2587
- to {
2588
- opacity: 1;
2589
- transform: translate(-50%, -50%) scale(1);
2590
- }
2591
- }
2592
- .command-palette {
2593
- animation: slide-down 0.2s ease;
1693
+ from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
1694
+ to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
2594
1695
  }
2595
1696
  @keyframes slide-down {
2596
- from {
2597
- opacity: 0;
2598
- transform: translateY(-10px);
2599
- }
2600
- to {
2601
- opacity: 1;
2602
- transform: translateY(0);
2603
- }
2604
- }
2605
- /* Snippets Modal */
2606
- .snippets-modal-overlay {
2607
- position: fixed;
2608
- inset: 0;
2609
- background: rgba(0, 0, 0, 0.6);
2610
- display: flex;
2611
- align-items: center;
2612
- justify-content: center;
2613
- z-index: 1003;
2614
- animation: fade-in 0.2s ease;
2615
- }
2616
- .snippets-modal {
2617
- width: 90%;
2618
- max-width: 500px;
2619
- max-height: 80vh;
2620
- background: var(--light-bg-primary);
2621
- border: 1px solid var(--light-border-primary);
2622
- border-radius: 12px;
2623
- display: flex;
2624
- flex-direction: column;
2625
- overflow: hidden;
2626
- box-shadow: 0 16px 64px rgba(0, 0, 0, 0.5);
2627
- animation: scale-in 0.2s ease;
2628
- }
2629
- .snippets-modal-header {
2630
- display: flex;
2631
- justify-content: space-between;
2632
- align-items: center;
2633
- padding: 1rem 1.25rem;
2634
- background: #252526;
2635
- border-bottom: 1px solid var(--light-border-primary);
2636
- }
2637
- .snippets-modal-header h3 {
2638
- margin: 0;
2639
- font-size: 0.9rem;
2640
- font-weight: 500;
2641
- font-family: "JetBrains Mono", "Fira Code", monospace;
2642
- color: #8bc48b;
2643
- }
2644
- .snippets-modal-close {
2645
- display: flex;
2646
- align-items: center;
2647
- justify-content: center;
2648
- background: transparent;
2649
- border: none;
2650
- color: #7a9a7a;
2651
- font-size: 0.85rem;
2652
- font-family: "JetBrains Mono", "Fira Code", monospace;
2653
- cursor: pointer;
2654
- transition: color 0.1s ease;
2655
- }
2656
- .snippets-modal-close:hover {
2657
- color: #a8dca8;
1697
+ from { opacity: 0; transform: translateY(-10px); }
1698
+ to { opacity: 1; transform: translateY(0); }
2658
1699
  }
2659
1700
  .snippets-modal-body {
2660
1701
  padding: 1.25rem;
@@ -2675,28 +1716,23 @@
2675
1716
  font-weight: 500;
2676
1717
  color: #a8dca8;
2677
1718
  }
2678
- .snippet-field input,
2679
1719
  .snippet-field textarea {
2680
1720
  padding: 0.6rem 0.75rem;
2681
1721
  background: #252526;
2682
1722
  border: 1px solid var(--light-border-primary);
2683
1723
  border-radius: 6px;
2684
1724
  color: #d4d4d4;
2685
- font-family: inherit;
1725
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2686
1726
  font-size: 0.9rem;
1727
+ line-height: 1.5;
1728
+ resize: vertical;
1729
+ min-height: 100px;
2687
1730
  transition: border-color 0.2s ease;
2688
1731
  }
2689
- .snippet-field input:focus,
2690
1732
  .snippet-field textarea:focus {
2691
1733
  outline: none;
2692
1734
  border-color: #4a7c4a;
2693
1735
  }
2694
- .snippet-field textarea {
2695
- resize: vertical;
2696
- min-height: 100px;
2697
- font-family: "JetBrains Mono", "Fira Code", monospace;
2698
- line-height: 1.5;
2699
- }
2700
1736
  .field-hint {
2701
1737
  font-size: 0.75rem;
2702
1738
  color: #6a6a6a;
@@ -2715,38 +1751,6 @@
2715
1751
  gap: 0.5rem;
2716
1752
  margin-left: auto;
2717
1753
  }
2718
- .snippet-btn {
2719
- padding: 0.3rem 0.5rem;
2720
- border-radius: 0;
2721
- font-size: 0.85rem;
2722
- font-family: "JetBrains Mono", "Fira Code", monospace;
2723
- cursor: pointer;
2724
- transition: color 0.1s ease;
2725
- background: transparent;
2726
- border: none;
2727
- }
2728
- .snippet-btn.save {
2729
- color: #8bc48b;
2730
- }
2731
- .snippet-btn.save:hover:not(:disabled) {
2732
- color: #c8f0c8;
2733
- }
2734
- .snippet-btn.save:disabled {
2735
- opacity: 0.4;
2736
- cursor: not-allowed;
2737
- }
2738
- .snippet-btn.cancel {
2739
- color: #9d9d9d;
2740
- }
2741
- .snippet-btn.cancel:hover {
2742
- color: #d4d4d4;
2743
- }
2744
- .snippet-btn.delete {
2745
- color: #e08080;
2746
- }
2747
- .snippet-btn.delete:hover {
2748
- color: #ff9090;
2749
- }
2750
1754
  .snippets-list-divider {
2751
1755
  display: flex;
2752
1756
  align-items: center;
@@ -2800,7 +1804,6 @@
2800
1804
  padding: 0.15rem 0.4rem;
2801
1805
  border-radius: 3px;
2802
1806
  }
2803
- /* Status Bar Sound Button */
2804
1807
  .status-sound-btn {
2805
1808
  display: flex;
2806
1809
  align-items: center;
@@ -2830,16 +1833,9 @@
2830
1833
  animation: sound-pulse 1.5s ease-in-out infinite;
2831
1834
  }
2832
1835
  @keyframes sound-pulse {
2833
- 0%, 100% {
2834
- opacity: 0.4;
2835
- transform: scale(0.8);
2836
- }
2837
- 50% {
2838
- opacity: 1;
2839
- transform: scale(1);
2840
- }
1836
+ 0%, 100% { opacity: 0.4; transform: scale(0.8); }
1837
+ 50% { opacity: 1; transform: scale(1); }
2841
1838
  }
2842
- /* Sound Panel */
2843
1839
  .sound-panel {
2844
1840
  position: fixed;
2845
1841
  bottom: 3.5rem;
@@ -2853,14 +1849,8 @@
2853
1849
  animation: slide-up 0.2s ease;
2854
1850
  }
2855
1851
  @keyframes slide-up {
2856
- from {
2857
- opacity: 0;
2858
- transform: translateY(10px);
2859
- }
2860
- to {
2861
- opacity: 1;
2862
- transform: translateY(0);
2863
- }
1852
+ from { opacity: 0; transform: translateY(10px); }
1853
+ to { opacity: 1; transform: translateY(0); }
2864
1854
  }
2865
1855
  .sound-panel-header {
2866
1856
  display: flex;
@@ -2907,6 +1897,8 @@
2907
1897
  border-radius: 8px;
2908
1898
  cursor: pointer;
2909
1899
  transition: all 0.15s ease;
1900
+ font-size: 0.65rem;
1901
+ color: #9d9d9d;
2910
1902
  }
2911
1903
  .sound-option:hover {
2912
1904
  background: var(--light-bg-tertiary);
@@ -2915,22 +1907,12 @@
2915
1907
  .sound-option.active {
2916
1908
  background: var(--light-border-secondary);
2917
1909
  border-color: #4a7c4a;
1910
+ color: #a8dca8;
2918
1911
  }
2919
1912
  .sound-option.playing {
2920
1913
  border-color: #8bc48b;
2921
1914
  box-shadow: 0 0 8px rgba(139, 196, 139, 0.3);
2922
1915
  }
2923
- .sound-icon {
2924
- font-size: 1.25rem;
2925
- }
2926
- .sound-name {
2927
- font-size: 0.65rem;
2928
- color: #9d9d9d;
2929
- text-align: center;
2930
- }
2931
- .sound-option.active .sound-name {
2932
- color: #a8dca8;
2933
- }
2934
1916
  .sound-controls {
2935
1917
  display: flex;
2936
1918
  align-items: center;
@@ -3011,14 +1993,102 @@
3011
1993
  font-size: 0.7rem;
3012
1994
  color: #6a6a6a;
3013
1995
  }
3014
- .sound-note-icon {
1996
+ .full-preview-modal {
1997
+ position: fixed;
1998
+ inset: 0;
1999
+ z-index: 1000;
2000
+ display: flex;
2001
+ align-items: center;
2002
+ justify-content: center;
2003
+ }
2004
+ .full-preview-backdrop {
2005
+ position: absolute;
2006
+ inset: 0;
2007
+ background: rgba(0, 0, 0, 0.7);
2008
+ }
2009
+ .full-preview-container {
2010
+ position: relative;
2011
+ width: 90%;
2012
+ max-width: 900px;
2013
+ height: 90vh;
2014
+ background: var(--color-bg, var(--light-bg-primary));
2015
+ border-radius: 12px;
2016
+ display: flex;
2017
+ flex-direction: column;
2018
+ overflow: hidden;
2019
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
2020
+ }
2021
+ :global(.dark) .full-preview-container {
2022
+ background: var(--color-bg-dark, #0d1117);
2023
+ }
2024
+ .full-preview-header {
2025
+ display: flex;
2026
+ justify-content: space-between;
2027
+ align-items: center;
2028
+ padding: 1rem 1.5rem;
2029
+ background: var(--color-bg-secondary, var(--light-bg-tertiary));
2030
+ border-bottom: 1px solid var(--color-border, var(--light-border-primary));
2031
+ flex-shrink: 0;
2032
+ }
2033
+ :global(.dark) .full-preview-header {
2034
+ background: var(--color-bg-secondary-dark, var(--light-bg-primary));
2035
+ border-color: var(--color-border-dark, var(--light-border-secondary));
2036
+ }
2037
+ .full-preview-header h2 {
2038
+ margin: 0;
2039
+ font-size: 0.9rem;
2040
+ font-weight: 500;
2041
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2042
+ color: #8bc48b;
2043
+ }
2044
+ .full-preview-close {
2045
+ padding: 0.3rem 0.5rem;
2046
+ background: transparent;
2047
+ color: #7a9a7a;
2048
+ border: none;
3015
2049
  font-size: 0.85rem;
2050
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2051
+ cursor: pointer;
2052
+ transition: color 0.1s ease;
3016
2053
  }
3017
- .sound-note code {
3018
- background: var(--light-bg-primary);
3019
- padding: 0.1rem 0.3rem;
3020
- border-radius: 3px;
3021
- font-family: "JetBrains Mono", monospace;
3022
- font-size: 0.65rem;
2054
+ .full-preview-close:hover {
2055
+ color: #a8dca8;
2056
+ }
2057
+ .full-preview-scroll {
2058
+ flex: 1;
2059
+ overflow-y: auto;
2060
+ padding: 2rem;
2061
+ }
2062
+ .full-preview-article {
2063
+ max-width: 800px;
2064
+ margin: 0 auto;
2065
+ }
2066
+ .full-preview-article .post-meta {
2067
+ display: flex;
2068
+ align-items: center;
2069
+ gap: 1rem;
2070
+ flex-wrap: wrap;
2071
+ margin-top: 1rem;
2072
+ }
2073
+ .full-preview-article time {
2074
+ color: var(--light-text-light);
2075
+ font-size: 1rem;
2076
+ transition: color 0.3s ease;
2077
+ }
2078
+ :global(.dark) .full-preview-article time {
2079
+ color: var(--color-text-subtle-dark, #666);
2080
+ }
2081
+ .full-preview-article .tags {
2082
+ display: flex;
2083
+ gap: 0.5rem;
2084
+ flex-wrap: wrap;
2085
+ }
2086
+ .full-preview-article .tag {
2087
+ padding: 0.25rem 0.75rem;
2088
+ background: var(--tag-bg, #2c5f2d);
2089
+ color: white;
2090
+ border-radius: 12px;
2091
+ font-size: 0.8rem;
2092
+ font-weight: 500;
3023
2093
  }
3024
2094
  </style>