@autumnsgrove/groveengine 0.1.0

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 (219) hide show
  1. package/README.md +163 -0
  2. package/dist/auth/jwt.d.ts +14 -0
  3. package/dist/auth/jwt.js +109 -0
  4. package/dist/auth/session.d.ts +42 -0
  5. package/dist/auth/session.js +105 -0
  6. package/dist/components/admin/GutterManager.svelte +910 -0
  7. package/dist/components/admin/GutterManager.svelte.d.ts +15 -0
  8. package/dist/components/admin/MarkdownEditor.svelte +3114 -0
  9. package/dist/components/admin/MarkdownEditor.svelte.d.ts +43 -0
  10. package/dist/components/custom/CollapsibleSection.svelte +74 -0
  11. package/dist/components/custom/CollapsibleSection.svelte.d.ts +15 -0
  12. package/dist/components/custom/ContentWithGutter.svelte +646 -0
  13. package/dist/components/custom/ContentWithGutter.svelte.d.ts +19 -0
  14. package/dist/components/custom/GutterItem.svelte +201 -0
  15. package/dist/components/custom/GutterItem.svelte.d.ts +11 -0
  16. package/dist/components/custom/LeftGutter.svelte +271 -0
  17. package/dist/components/custom/LeftGutter.svelte.d.ts +17 -0
  18. package/dist/components/custom/MobileTOC.svelte +273 -0
  19. package/dist/components/custom/MobileTOC.svelte.d.ts +11 -0
  20. package/dist/components/custom/TableOfContents.svelte +163 -0
  21. package/dist/components/custom/TableOfContents.svelte.d.ts +11 -0
  22. package/dist/components/gallery/ImageGallery.svelte +681 -0
  23. package/dist/components/gallery/ImageGallery.svelte.d.ts +11 -0
  24. package/dist/components/gallery/Lightbox.svelte +107 -0
  25. package/dist/components/gallery/Lightbox.svelte.d.ts +19 -0
  26. package/dist/components/gallery/LightboxCaption.svelte +25 -0
  27. package/dist/components/gallery/LightboxCaption.svelte.d.ts +11 -0
  28. package/dist/components/gallery/ZoomableImage.svelte +163 -0
  29. package/dist/components/gallery/ZoomableImage.svelte.d.ts +17 -0
  30. package/dist/components/ui/Accordion.svelte +74 -0
  31. package/dist/components/ui/Accordion.svelte.d.ts +42 -0
  32. package/dist/components/ui/Badge.svelte +48 -0
  33. package/dist/components/ui/Badge.svelte.d.ts +26 -0
  34. package/dist/components/ui/Button.svelte +74 -0
  35. package/dist/components/ui/Button.svelte.d.ts +34 -0
  36. package/dist/components/ui/Card.svelte +102 -0
  37. package/dist/components/ui/Card.svelte.d.ts +46 -0
  38. package/dist/components/ui/Dialog.svelte +91 -0
  39. package/dist/components/ui/Dialog.svelte.d.ts +43 -0
  40. package/dist/components/ui/Input.svelte +81 -0
  41. package/dist/components/ui/Input.svelte.d.ts +35 -0
  42. package/dist/components/ui/Select.svelte +69 -0
  43. package/dist/components/ui/Select.svelte.d.ts +36 -0
  44. package/dist/components/ui/Sheet.svelte +98 -0
  45. package/dist/components/ui/Sheet.svelte.d.ts +45 -0
  46. package/dist/components/ui/Skeleton.svelte +31 -0
  47. package/dist/components/ui/Skeleton.svelte.d.ts +26 -0
  48. package/dist/components/ui/Table.svelte +59 -0
  49. package/dist/components/ui/Table.svelte.d.ts +44 -0
  50. package/dist/components/ui/Tabs.svelte +76 -0
  51. package/dist/components/ui/Tabs.svelte.d.ts +41 -0
  52. package/dist/components/ui/Textarea.svelte +81 -0
  53. package/dist/components/ui/Textarea.svelte.d.ts +35 -0
  54. package/dist/components/ui/Toast.svelte +18 -0
  55. package/dist/components/ui/Toast.svelte.d.ts +7 -0
  56. package/dist/components/ui/accordion/accordion-content.svelte +24 -0
  57. package/dist/components/ui/accordion/accordion-content.svelte.d.ts +4 -0
  58. package/dist/components/ui/accordion/accordion-item.svelte +12 -0
  59. package/dist/components/ui/accordion/accordion-item.svelte.d.ts +4 -0
  60. package/dist/components/ui/accordion/accordion-trigger.svelte +29 -0
  61. package/dist/components/ui/accordion/accordion-trigger.svelte.d.ts +7 -0
  62. package/dist/components/ui/accordion/index.d.ts +6 -0
  63. package/dist/components/ui/accordion/index.js +8 -0
  64. package/dist/components/ui/badge/badge.svelte +50 -0
  65. package/dist/components/ui/badge/badge.svelte.d.ts +60 -0
  66. package/dist/components/ui/badge/index.d.ts +2 -0
  67. package/dist/components/ui/badge/index.js +2 -0
  68. package/dist/components/ui/button/button.svelte +82 -0
  69. package/dist/components/ui/button/button.svelte.d.ts +132 -0
  70. package/dist/components/ui/button/index.d.ts +2 -0
  71. package/dist/components/ui/button/index.js +4 -0
  72. package/dist/components/ui/card/card-content.svelte +16 -0
  73. package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
  74. package/dist/components/ui/card/card-description.svelte +16 -0
  75. package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
  76. package/dist/components/ui/card/card-footer.svelte +16 -0
  77. package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
  78. package/dist/components/ui/card/card-header.svelte +16 -0
  79. package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
  80. package/dist/components/ui/card/card-title.svelte +25 -0
  81. package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
  82. package/dist/components/ui/card/card.svelte +20 -0
  83. package/dist/components/ui/card/card.svelte.d.ts +5 -0
  84. package/dist/components/ui/card/index.d.ts +7 -0
  85. package/dist/components/ui/card/index.js +9 -0
  86. package/dist/components/ui/dialog/dialog-content.svelte +38 -0
  87. package/dist/components/ui/dialog/dialog-content.svelte.d.ts +9 -0
  88. package/dist/components/ui/dialog/dialog-description.svelte +16 -0
  89. package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
  90. package/dist/components/ui/dialog/dialog-footer.svelte +20 -0
  91. package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +5 -0
  92. package/dist/components/ui/dialog/dialog-header.svelte +20 -0
  93. package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
  94. package/dist/components/ui/dialog/dialog-overlay.svelte +19 -0
  95. package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
  96. package/dist/components/ui/dialog/dialog-title.svelte +16 -0
  97. package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
  98. package/dist/components/ui/dialog/index.d.ts +12 -0
  99. package/dist/components/ui/dialog/index.js +14 -0
  100. package/dist/components/ui/index.d.ts +26 -0
  101. package/dist/components/ui/index.js +29 -0
  102. package/dist/components/ui/input/index.d.ts +2 -0
  103. package/dist/components/ui/input/index.js +4 -0
  104. package/dist/components/ui/input/input.svelte +46 -0
  105. package/dist/components/ui/input/input.svelte.d.ts +13 -0
  106. package/dist/components/ui/select/index.d.ts +11 -0
  107. package/dist/components/ui/select/index.js +13 -0
  108. package/dist/components/ui/select/select-content.svelte +39 -0
  109. package/dist/components/ui/select/select-content.svelte.d.ts +7 -0
  110. package/dist/components/ui/select/select-group-heading.svelte +16 -0
  111. package/dist/components/ui/select/select-group-heading.svelte.d.ts +4 -0
  112. package/dist/components/ui/select/select-item.svelte +37 -0
  113. package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
  114. package/dist/components/ui/select/select-scroll-down-button.svelte +19 -0
  115. package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
  116. package/dist/components/ui/select/select-scroll-up-button.svelte +19 -0
  117. package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
  118. package/dist/components/ui/select/select-separator.svelte +13 -0
  119. package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
  120. package/dist/components/ui/select/select-trigger.svelte +24 -0
  121. package/dist/components/ui/select/select-trigger.svelte.d.ts +4 -0
  122. package/dist/components/ui/separator/index.d.ts +2 -0
  123. package/dist/components/ui/separator/index.js +4 -0
  124. package/dist/components/ui/separator/separator.svelte +22 -0
  125. package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
  126. package/dist/components/ui/sheet/index.d.ts +12 -0
  127. package/dist/components/ui/sheet/index.js +14 -0
  128. package/dist/components/ui/sheet/sheet-content.svelte +53 -0
  129. package/dist/components/ui/sheet/sheet-content.svelte.d.ts +62 -0
  130. package/dist/components/ui/sheet/sheet-description.svelte +16 -0
  131. package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
  132. package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
  133. package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  134. package/dist/components/ui/sheet/sheet-header.svelte +20 -0
  135. package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
  136. package/dist/components/ui/sheet/sheet-overlay.svelte +21 -0
  137. package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +6 -0
  138. package/dist/components/ui/sheet/sheet-title.svelte +16 -0
  139. package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
  140. package/dist/components/ui/skeleton/index.d.ts +2 -0
  141. package/dist/components/ui/skeleton/index.js +4 -0
  142. package/dist/components/ui/skeleton/skeleton.svelte +17 -0
  143. package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
  144. package/dist/components/ui/table/index.d.ts +9 -0
  145. package/dist/components/ui/table/index.js +11 -0
  146. package/dist/components/ui/table/table-body.svelte +16 -0
  147. package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
  148. package/dist/components/ui/table/table-caption.svelte +16 -0
  149. package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
  150. package/dist/components/ui/table/table-cell.svelte +20 -0
  151. package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
  152. package/dist/components/ui/table/table-footer.svelte +16 -0
  153. package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
  154. package/dist/components/ui/table/table-head.svelte +23 -0
  155. package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
  156. package/dist/components/ui/table/table-header.svelte +16 -0
  157. package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
  158. package/dist/components/ui/table/table-row.svelte +23 -0
  159. package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
  160. package/dist/components/ui/table/table.svelte +18 -0
  161. package/dist/components/ui/table/table.svelte.d.ts +5 -0
  162. package/dist/components/ui/tabs/index.d.ts +6 -0
  163. package/dist/components/ui/tabs/index.js +8 -0
  164. package/dist/components/ui/tabs/tabs-content.svelte +19 -0
  165. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
  166. package/dist/components/ui/tabs/tabs-list.svelte +19 -0
  167. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
  168. package/dist/components/ui/tabs/tabs-trigger.svelte +19 -0
  169. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  170. package/dist/components/ui/textarea/index.d.ts +2 -0
  171. package/dist/components/ui/textarea/index.js +4 -0
  172. package/dist/components/ui/textarea/textarea.svelte +24 -0
  173. package/dist/components/ui/textarea/textarea.svelte.d.ts +6 -0
  174. package/dist/components/ui/toast.d.ts +86 -0
  175. package/dist/components/ui/toast.js +99 -0
  176. package/dist/db/schema.sql +238 -0
  177. package/dist/index.d.ts +14 -0
  178. package/dist/index.js +20 -0
  179. package/dist/payments/index.d.ts +33 -0
  180. package/dist/payments/index.js +47 -0
  181. package/dist/payments/shop.d.ts +165 -0
  182. package/dist/payments/shop.js +588 -0
  183. package/dist/payments/stripe/client.d.ts +231 -0
  184. package/dist/payments/stripe/client.js +198 -0
  185. package/dist/payments/stripe/index.d.ts +18 -0
  186. package/dist/payments/stripe/index.js +17 -0
  187. package/dist/payments/stripe/provider.d.ts +50 -0
  188. package/dist/payments/stripe/provider.js +530 -0
  189. package/dist/payments/types.d.ts +355 -0
  190. package/dist/payments/types.js +7 -0
  191. package/dist/server/logger.d.ts +53 -0
  192. package/dist/server/logger.js +252 -0
  193. package/dist/styles/content.css +514 -0
  194. package/dist/styles/tokens.css +175 -0
  195. package/dist/utils/api.d.ts +20 -0
  196. package/dist/utils/api.js +109 -0
  197. package/dist/utils/cn.d.ts +15 -0
  198. package/dist/utils/cn.js +18 -0
  199. package/dist/utils/csrf.d.ts +22 -0
  200. package/dist/utils/csrf.js +72 -0
  201. package/dist/utils/debounce.d.ts +7 -0
  202. package/dist/utils/debounce.js +14 -0
  203. package/dist/utils/gallery.d.ts +66 -0
  204. package/dist/utils/gallery.js +181 -0
  205. package/dist/utils/gutter.d.ts +54 -0
  206. package/dist/utils/gutter.js +169 -0
  207. package/dist/utils/imageProcessor.d.ts +58 -0
  208. package/dist/utils/imageProcessor.js +205 -0
  209. package/dist/utils/json.d.ts +17 -0
  210. package/dist/utils/json.js +26 -0
  211. package/dist/utils/markdown.d.ts +101 -0
  212. package/dist/utils/markdown.js +947 -0
  213. package/dist/utils/sanitize.d.ts +25 -0
  214. package/dist/utils/sanitize.js +127 -0
  215. package/dist/utils/validation.d.ts +46 -0
  216. package/dist/utils/validation.js +169 -0
  217. package/dist/utils.d.ts +5 -0
  218. package/dist/utils.js +5 -0
  219. package/package.json +129 -0
@@ -0,0 +1,3114 @@
1
+ <script>
2
+ import { marked } from "marked";
3
+ import mermaid from "mermaid";
4
+ import { onMount, tick } from "svelte";
5
+ import { sanitizeMarkdown } from "../../utils/sanitize.js";
6
+ import "../../styles/content.css";
7
+ import Dialog from "../ui/Dialog.svelte";
8
+ import Button from "../ui/Button.svelte";
9
+ import Input from "../ui/Input.svelte";
10
+
11
+ // Initialize mermaid with grove-themed dark config
12
+ mermaid.initialize({
13
+ startOnLoad: false,
14
+ theme: "dark",
15
+ themeVariables: {
16
+ primaryColor: "#2d5a2d",
17
+ primaryTextColor: "#d4d4d4",
18
+ primaryBorderColor: "#4a7c4a",
19
+ lineColor: "#8bc48b",
20
+ secondaryColor: "#1e3a1e",
21
+ tertiaryColor: "#2a2a2a",
22
+ background: "#1e1e1e",
23
+ mainBkg: "#252526",
24
+ secondBkg: "#1e1e1e",
25
+ nodeBorder: "#4a7c4a",
26
+ clusterBkg: "#1a2a1a",
27
+ titleColor: "#8bc48b",
28
+ edgeLabelBackground: "#252526",
29
+ },
30
+ flowchart: {
31
+ curve: "basis",
32
+ padding: 15,
33
+ },
34
+ sequence: {
35
+ actorMargin: 50,
36
+ boxMargin: 10,
37
+ },
38
+ });
39
+
40
+ // Props
41
+ let {
42
+ content = $bindable(""),
43
+ onSave = () => {},
44
+ saving = false,
45
+ readonly = false,
46
+ draftKey = null, // Unique key for localStorage draft storage
47
+ onDraftRestored = () => {}, // Callback when draft is restored
48
+ // Optional metadata for full preview mode
49
+ previewTitle = "",
50
+ previewDate = "",
51
+ previewTags = [],
52
+ } = $props();
53
+
54
+ // Local state
55
+ let textareaRef = $state(null);
56
+ let previewRef = $state(null);
57
+ let showPreview = $state(true);
58
+ let lineNumbers = $state([]);
59
+ let cursorLine = $state(1);
60
+ let cursorCol = $state(1);
61
+
62
+ // Image upload state
63
+ let isDragging = $state(false);
64
+ let isUploading = $state(false);
65
+ let uploadProgress = $state("");
66
+ let uploadError = $state(null);
67
+
68
+ // Auto-save draft state
69
+ let lastSavedContent = $state("");
70
+ let draftSaveTimer = $state(null);
71
+ let hasDraft = $state(false);
72
+ let draftRestorePrompt = $state(false);
73
+ let storedDraft = $state(null);
74
+ const AUTO_SAVE_DELAY = 2000; // 2 seconds
75
+
76
+ // Full preview mode state
77
+ let showFullPreview = $state(false);
78
+
79
+ // Editor settings (configurable, persisted to localStorage)
80
+ let editorSettings = $state({
81
+ typewriterMode: false,
82
+ zenMode: false,
83
+ showLineNumbers: true,
84
+ wordWrap: true,
85
+ });
86
+
87
+ // Zen mode state
88
+ let isZenMode = $state(false);
89
+
90
+ // Campfire session state
91
+ let campfireSession = $state({
92
+ active: false,
93
+ startTime: null,
94
+ targetMinutes: 25,
95
+ startWordCount: 0,
96
+ });
97
+
98
+ // Writing goals
99
+ let writingGoal = $state({
100
+ enabled: false,
101
+ targetWords: 500,
102
+ sessionWords: 0,
103
+ });
104
+
105
+ // Slash commands state
106
+ let slashMenu = $state({
107
+ open: false,
108
+ query: "",
109
+ position: { x: 0, y: 0 },
110
+ selectedIndex: 0,
111
+ });
112
+
113
+ // Command palette state
114
+ let commandPalette = $state({
115
+ open: false,
116
+ query: "",
117
+ selectedIndex: 0,
118
+ });
119
+
120
+ // AI Assistant state (stubs - not deployed yet)
121
+ let aiAssistant = $state({
122
+ enabled: false, // Keep disabled for now
123
+ panelOpen: false,
124
+ suggestions: [],
125
+ isAnalyzing: false,
126
+ });
127
+
128
+ // Markdown snippets state
129
+ let snippets = $state([]);
130
+ let snippetsModal = $state({
131
+ open: false,
132
+ editingId: null,
133
+ name: "",
134
+ content: "",
135
+ trigger: "", // Optional shortcut trigger like "sig" for signature
136
+ });
137
+
138
+ // Ambient sounds state
139
+ let ambientSounds = $state({
140
+ enabled: false,
141
+ currentSound: "forest",
142
+ volume: 0.3,
143
+ showPanel: false,
144
+ });
145
+ let audioElement = $state(null);
146
+
147
+ // Theme system
148
+ const themes = {
149
+ grove: {
150
+ name: "grove",
151
+ label: "Grove",
152
+ desc: "forest green",
153
+ accent: "#8bc48b",
154
+ accentDim: "#7a9a7a",
155
+ accentBright: "#a8dca8",
156
+ accentGlow: "#c8f0c8",
157
+ bg: "#1e1e1e",
158
+ bgSecondary: "#252526",
159
+ bgTertiary: "#1a1a1a",
160
+ border: "#3a3a3a",
161
+ borderAccent: "#4a7c4a",
162
+ text: "#d4d4d4",
163
+ textDim: "#9d9d9d",
164
+ statusBg: "#2d4a2d",
165
+ statusBorder: "#3d5a3d",
166
+ },
167
+ amber: {
168
+ name: "amber",
169
+ label: "Amber",
170
+ desc: "classic terminal",
171
+ accent: "#ffb000",
172
+ accentDim: "#c98b00",
173
+ accentBright: "#ffc940",
174
+ accentGlow: "#ffe080",
175
+ bg: "#1a1400",
176
+ bgSecondary: "#241c00",
177
+ bgTertiary: "#140e00",
178
+ border: "#3a3000",
179
+ borderAccent: "#5a4800",
180
+ text: "#ffcc66",
181
+ textDim: "#aa8844",
182
+ statusBg: "#2a2000",
183
+ statusBorder: "#3a3000",
184
+ },
185
+ matrix: {
186
+ name: "matrix",
187
+ label: "Matrix",
188
+ desc: "digital rain",
189
+ accent: "#00ff00",
190
+ accentDim: "#00aa00",
191
+ accentBright: "#44ff44",
192
+ accentGlow: "#88ff88",
193
+ bg: "#0a0a0a",
194
+ bgSecondary: "#111111",
195
+ bgTertiary: "#050505",
196
+ border: "#1a3a1a",
197
+ borderAccent: "#00aa00",
198
+ text: "#00dd00",
199
+ textDim: "#008800",
200
+ statusBg: "#0a1a0a",
201
+ statusBorder: "#1a3a1a",
202
+ },
203
+ dracula: {
204
+ name: "dracula",
205
+ label: "Dracula",
206
+ desc: "purple night",
207
+ accent: "#bd93f9",
208
+ accentDim: "#9580c9",
209
+ accentBright: "#d4b0ff",
210
+ accentGlow: "#e8d0ff",
211
+ bg: "#282a36",
212
+ bgSecondary: "#343746",
213
+ bgTertiary: "#21222c",
214
+ border: "#44475a",
215
+ borderAccent: "#6272a4",
216
+ text: "#f8f8f2",
217
+ textDim: "#a0a0a0",
218
+ statusBg: "#3a3c4e",
219
+ statusBorder: "#44475a",
220
+ },
221
+ nord: {
222
+ name: "nord",
223
+ label: "Nord",
224
+ desc: "arctic frost",
225
+ accent: "#88c0d0",
226
+ accentDim: "#6a9aa8",
227
+ accentBright: "#a3d4e2",
228
+ accentGlow: "#c0e8f0",
229
+ bg: "#2e3440",
230
+ bgSecondary: "#3b4252",
231
+ bgTertiary: "#272c36",
232
+ border: "#434c5e",
233
+ borderAccent: "#5e81ac",
234
+ text: "#eceff4",
235
+ textDim: "#a0a8b0",
236
+ statusBg: "#3b4252",
237
+ statusBorder: "#434c5e",
238
+ },
239
+ rose: {
240
+ name: "rose",
241
+ label: "Rose",
242
+ desc: "soft pink",
243
+ accent: "#f5a9b8",
244
+ accentDim: "#c98a96",
245
+ accentBright: "#ffccd5",
246
+ accentGlow: "#ffe0e6",
247
+ bg: "#1f1a1b",
248
+ bgSecondary: "#2a2224",
249
+ bgTertiary: "#171314",
250
+ border: "#3a3234",
251
+ borderAccent: "#5a4a4e",
252
+ text: "#e8d8dc",
253
+ textDim: "#a09498",
254
+ statusBg: "#2a2224",
255
+ statusBorder: "#3a3234",
256
+ },
257
+ };
258
+
259
+ let currentTheme = $state("grove");
260
+ const THEME_STORAGE_KEY = "grove-editor-theme";
261
+
262
+ // Sound definitions with free ambient loops
263
+ const soundLibrary = {
264
+ forest: {
265
+ name: "forest",
266
+ key: "f",
267
+ // Using freesound.org URLs for ambient sounds (CC0 licensed)
268
+ // These are placeholder paths - user can provide their own audio files
269
+ url: "/sounds/forest-ambience.mp3",
270
+ description: "birds, wind",
271
+ },
272
+ rain: {
273
+ name: "rain",
274
+ key: "r",
275
+ url: "/sounds/rain-ambience.mp3",
276
+ description: "gentle rainfall",
277
+ },
278
+ campfire: {
279
+ name: "fire",
280
+ key: "i",
281
+ url: "/sounds/campfire-ambience.mp3",
282
+ description: "crackling embers",
283
+ },
284
+ night: {
285
+ name: "night",
286
+ key: "n",
287
+ url: "/sounds/night-ambience.mp3",
288
+ description: "crickets, breeze",
289
+ },
290
+ cafe: {
291
+ name: "cafe",
292
+ key: "a",
293
+ url: "/sounds/cafe-ambience.mp3",
294
+ description: "soft murmurs",
295
+ },
296
+ };
297
+
298
+ // Line numbers container ref for scroll sync
299
+ let lineNumbersRef = $state(null);
300
+
301
+ // Computed values
302
+ let wordCount = $derived(
303
+ content.trim() ? content.trim().split(/\s+/).length : 0
304
+ );
305
+ let charCount = $derived(content.length);
306
+ let lineCount = $derived(content.split("\n").length);
307
+ // Custom marked renderer for mermaid blocks
308
+ const renderer = new marked.Renderer();
309
+ const originalCodeRenderer = renderer.code.bind(renderer);
310
+
311
+ renderer.code = function ({ text, lang }) {
312
+ if (lang === "mermaid") {
313
+ // Wrap mermaid code in a special container for rendering
314
+ const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
315
+ return `<div class="mermaid-container"><pre class="mermaid" id="${id}">${text}</pre></div>`;
316
+ }
317
+ return originalCodeRenderer({ text, lang });
318
+ };
319
+
320
+ marked.use({ renderer });
321
+
322
+ let previewHtml = $derived(content ? sanitizeMarkdown(marked.parse(content)) : "");
323
+
324
+ // Render mermaid diagrams after preview updates
325
+ async function renderMermaidDiagrams() {
326
+ await tick();
327
+ const mermaidElements = document.querySelectorAll(".preview-content .mermaid, .full-preview-scroll .mermaid");
328
+ if (mermaidElements.length > 0) {
329
+ try {
330
+ await mermaid.run({ nodes: mermaidElements });
331
+ } catch (e) {
332
+ console.warn("Mermaid rendering error:", e);
333
+ }
334
+ }
335
+ }
336
+
337
+ // Trigger mermaid rendering when preview HTML changes
338
+ $effect(() => {
339
+ if (previewHtml && (showPreview || showFullPreview)) {
340
+ renderMermaidDiagrams();
341
+ }
342
+ });
343
+
344
+ // Reading time estimate (average 200 words per minute)
345
+ let readingTime = $derived(() => {
346
+ const minutes = Math.ceil(wordCount / 200);
347
+ return minutes < 1 ? "< 1 min" : `~${minutes} min read`;
348
+ });
349
+
350
+ // Writing goal progress
351
+ let goalProgress = $derived(() => {
352
+ if (!writingGoal.enabled) return 0;
353
+ const wordsWritten = wordCount - writingGoal.sessionWords;
354
+ return Math.min(100, Math.round((wordsWritten / writingGoal.targetWords) * 100));
355
+ });
356
+
357
+ // Campfire session elapsed time
358
+ let campfireElapsed = $derived(() => {
359
+ if (!campfireSession.active || !campfireSession.startTime) return "0:00";
360
+ const now = Date.now();
361
+ const elapsed = Math.floor((now - campfireSession.startTime) / 1000);
362
+ const mins = Math.floor(elapsed / 60);
363
+ const secs = elapsed % 60;
364
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
365
+ });
366
+
367
+ // Extract available anchors from content (headings and custom anchors)
368
+ let availableAnchors = $derived.by(() => {
369
+ const anchors = [];
370
+ // Extract headings
371
+ const headingRegex = /^(#{1,6})\s+(.+)$/gm;
372
+ let match;
373
+ while ((match = headingRegex.exec(content)) !== null) {
374
+ anchors.push(match[0].trim());
375
+ }
376
+ // Extract custom anchors
377
+ const anchorRegex = /<!--\s*anchor:([\w-]+)\s*-->/g;
378
+ while ((match = anchorRegex.exec(content)) !== null) {
379
+ anchors.push(`anchor:${match[1]}`);
380
+ }
381
+ return anchors;
382
+ });
383
+
384
+ // Public function to get available anchors
385
+ export function getAvailableAnchors() {
386
+ return availableAnchors;
387
+ }
388
+
389
+ // Public function to insert an anchor at cursor position
390
+ export function insertAnchor(name) {
391
+ insertAtCursor(`<!-- anchor:${name} -->\n`);
392
+ }
393
+
394
+ // Update line numbers when content changes
395
+ $effect(() => {
396
+ const lines = content.split("\n").length;
397
+ lineNumbers = Array.from({ length: lines }, (_, i) => i + 1);
398
+ });
399
+
400
+ // Handle cursor position tracking
401
+ function updateCursorPosition() {
402
+ if (!textareaRef) return;
403
+
404
+ const pos = textareaRef.selectionStart;
405
+ const textBefore = content.substring(0, pos);
406
+ const lines = textBefore.split("\n");
407
+ cursorLine = lines.length;
408
+ cursorCol = lines[lines.length - 1].length + 1;
409
+ }
410
+
411
+ // Handle tab key for indentation
412
+ function handleKeydown(e) {
413
+ // Escape key handling
414
+ if (e.key === "Escape") {
415
+ if (slashMenu.open) {
416
+ slashMenu.open = false;
417
+ return;
418
+ }
419
+ if (commandPalette.open) {
420
+ commandPalette.open = false;
421
+ return;
422
+ }
423
+ if (isZenMode) {
424
+ isZenMode = false;
425
+ return;
426
+ }
427
+ }
428
+
429
+ // Slash commands trigger
430
+ if (e.key === "/" && !slashMenu.open) {
431
+ const pos = textareaRef.selectionStart;
432
+ const textBefore = content.substring(0, pos);
433
+ // Only trigger at start of line or after whitespace
434
+ if (pos === 0 || /\s$/.test(textBefore)) {
435
+ // Don't prevent default yet - let the slash be typed
436
+ setTimeout(() => {
437
+ openSlashMenu();
438
+ }, 0);
439
+ }
440
+ }
441
+
442
+ // Close slash menu on space or enter if open
443
+ if (slashMenu.open && (e.key === " " || e.key === "Enter")) {
444
+ if (e.key === "Enter") {
445
+ e.preventDefault();
446
+ executeSlashCommand(slashMenu.selectedIndex);
447
+ }
448
+ slashMenu.open = false;
449
+ }
450
+
451
+ // Navigate slash menu
452
+ if (slashMenu.open) {
453
+ const cmdCount = filteredSlashCommands.length;
454
+ if (e.key === "ArrowDown") {
455
+ e.preventDefault();
456
+ slashMenu.selectedIndex = (slashMenu.selectedIndex + 1) % cmdCount;
457
+ }
458
+ if (e.key === "ArrowUp") {
459
+ e.preventDefault();
460
+ slashMenu.selectedIndex = (slashMenu.selectedIndex - 1 + cmdCount) % cmdCount;
461
+ }
462
+ }
463
+
464
+ // Command palette: Cmd+K
465
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
466
+ e.preventDefault();
467
+ commandPalette.open = !commandPalette.open;
468
+ commandPalette.query = "";
469
+ commandPalette.selectedIndex = 0;
470
+ }
471
+
472
+ // Zen mode: Cmd+Shift+Enter
473
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && e.shiftKey) {
474
+ e.preventDefault();
475
+ toggleZenMode();
476
+ }
477
+
478
+ if (e.key === "Tab") {
479
+ e.preventDefault();
480
+ const start = textareaRef.selectionStart;
481
+ const end = textareaRef.selectionEnd;
482
+
483
+ // Insert 2 spaces
484
+ content = content.substring(0, start) + " " + content.substring(end);
485
+
486
+ // Move cursor
487
+ setTimeout(() => {
488
+ textareaRef.selectionStart = textareaRef.selectionEnd = start + 2;
489
+ }, 0);
490
+ }
491
+
492
+ // Cmd/Ctrl + S to save
493
+ if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
494
+ e.preventDefault();
495
+ onSave();
496
+ }
497
+
498
+ // Cmd/Ctrl + B for bold
499
+ if (e.key === "b" && (e.metaKey || e.ctrlKey)) {
500
+ e.preventDefault();
501
+ wrapSelection("**", "**");
502
+ }
503
+
504
+ // Cmd/Ctrl + I for italic
505
+ if (e.key === "i" && (e.metaKey || e.ctrlKey)) {
506
+ e.preventDefault();
507
+ wrapSelection("_", "_");
508
+ }
509
+ }
510
+
511
+ // Global keyboard handler for modals
512
+ function handleGlobalKeydown(e) {
513
+ if (e.key === "Escape") {
514
+ if (ambientSounds.showPanel) {
515
+ ambientSounds.showPanel = false;
516
+ e.preventDefault();
517
+ return;
518
+ }
519
+ if (snippetsModal.open) {
520
+ closeSnippetsModal();
521
+ e.preventDefault();
522
+ return;
523
+ }
524
+ if (showFullPreview) {
525
+ showFullPreview = false;
526
+ e.preventDefault();
527
+ }
528
+ }
529
+ }
530
+
531
+ // Slash commands definition
532
+ const slashCommands = [
533
+ { id: "heading1", label: "Heading 1", insert: "# " },
534
+ { id: "heading2", label: "Heading 2", insert: "## " },
535
+ { id: "heading3", label: "Heading 3", insert: "### " },
536
+ { id: "code", label: "Code Block", insert: "```\n\n```", cursorOffset: 4 },
537
+ { id: "mermaid", label: "Mermaid Diagram", insert: "```mermaid\nflowchart TD\n A[Start] --> B[End]\n```", cursorOffset: 32 },
538
+ { id: "quote", label: "Quote", insert: "> " },
539
+ { id: "list", label: "Bullet List", insert: "- " },
540
+ { id: "numbered", label: "Numbered List", insert: "1. " },
541
+ { id: "link", label: "Link", insert: "[](url)", cursorOffset: 1 },
542
+ { id: "image", label: "Image", insert: "![alt](url)", cursorOffset: 2 },
543
+ { id: "divider", label: "Divider", insert: "\n---\n" },
544
+ { id: "anchor", label: "Custom Anchor", insert: "<!-- anchor:name -->\n", cursorOffset: 14 },
545
+ { id: "newSnippet", label: "Create New Snippet...", insert: "", isAction: true, action: () => openSnippetsModal() },
546
+ ];
547
+
548
+ // Dynamic slash commands including user snippets
549
+ let allSlashCommands = $derived(() => {
550
+ const snippetCommands = snippets.map(s => ({
551
+ id: s.id,
552
+ label: `> ${s.name}`,
553
+ insert: s.content,
554
+ isSnippet: true,
555
+ }));
556
+ return [...slashCommands, ...snippetCommands];
557
+ });
558
+
559
+ // Filtered slash commands based on query
560
+ let filteredSlashCommands = $derived(
561
+ allSlashCommands().filter(cmd =>
562
+ cmd.label.toLowerCase().includes(slashMenu.query.toLowerCase())
563
+ )
564
+ );
565
+
566
+ function openSlashMenu() {
567
+ slashMenu.open = true;
568
+ slashMenu.query = "";
569
+ slashMenu.selectedIndex = 0;
570
+ }
571
+
572
+ function executeSlashCommand(index) {
573
+ const cmd = filteredSlashCommands[index];
574
+ if (!cmd) return;
575
+
576
+ // Handle action commands (like "Create New Snippet...")
577
+ if (cmd.isAction && cmd.action) {
578
+ // Remove the slash that triggered the menu
579
+ const pos = textareaRef.selectionStart;
580
+ const textBefore = content.substring(0, pos);
581
+ const lastSlashIndex = textBefore.lastIndexOf("/");
582
+ if (lastSlashIndex >= 0) {
583
+ content = content.substring(0, lastSlashIndex) + content.substring(pos);
584
+ }
585
+ slashMenu.open = false;
586
+ cmd.action();
587
+ return;
588
+ }
589
+
590
+ // Remove the slash that triggered the menu
591
+ const pos = textareaRef.selectionStart;
592
+ const textBefore = content.substring(0, pos);
593
+ const lastSlashIndex = textBefore.lastIndexOf("/");
594
+
595
+ if (lastSlashIndex >= 0) {
596
+ content = content.substring(0, lastSlashIndex) + cmd.insert + content.substring(pos);
597
+
598
+ setTimeout(() => {
599
+ const newPos = lastSlashIndex + (cmd.cursorOffset || cmd.insert.length);
600
+ textareaRef.selectionStart = textareaRef.selectionEnd = newPos;
601
+ textareaRef.focus();
602
+ }, 0);
603
+ }
604
+
605
+ slashMenu.open = false;
606
+ }
607
+
608
+ // Command palette actions
609
+ const basePaletteCommands = [
610
+ { id: "save", label: "Save", shortcut: "⌘S", action: () => onSave() },
611
+ { id: "preview", label: "Toggle Preview", shortcut: "", action: () => showPreview = !showPreview },
612
+ { id: "fullPreview", label: "Full Preview", shortcut: "", action: () => showFullPreview = true },
613
+ { id: "zen", label: "Toggle Zen Mode", shortcut: "⌘⇧↵", action: () => toggleZenMode() },
614
+ { id: "campfire", label: "Start Campfire Session", shortcut: "", action: () => startCampfireSession() },
615
+ { id: "bold", label: "Bold", shortcut: "⌘B", action: () => wrapSelection("**", "**") },
616
+ { id: "italic", label: "Italic", shortcut: "⌘I", action: () => wrapSelection("_", "_") },
617
+ { id: "code", label: "Insert Code Block", shortcut: "", action: () => insertCodeBlock() },
618
+ { id: "link", label: "Insert Link", shortcut: "", action: () => insertLink() },
619
+ { id: "image", label: "Insert Image", shortcut: "", action: () => insertImage() },
620
+ { id: "goal", label: "Set Writing Goal", shortcut: "", action: () => promptWritingGoal() },
621
+ { id: "snippets", label: "Manage Snippets", shortcut: "", action: () => openSnippetsModal() },
622
+ { id: "newSnippet", label: "Create New Snippet", shortcut: "", action: () => openSnippetsModal() },
623
+ { id: "sounds", label: "Toggle Ambient Sounds", shortcut: "", action: () => toggleAmbientSound() },
624
+ { id: "soundPanel", label: "Sound Settings", shortcut: "", action: () => toggleSoundPanel() },
625
+ ];
626
+
627
+ // Add theme commands dynamically
628
+ let paletteCommands = $derived(() => {
629
+ const themeCommands = Object.entries(themes).map(([key, theme]) => ({
630
+ id: `theme-${key}`,
631
+ label: `Theme: ${theme.label} (${theme.desc})`,
632
+ shortcut: currentTheme === key ? "●" : "",
633
+ action: () => setTheme(key),
634
+ }));
635
+ return [...basePaletteCommands, ...themeCommands];
636
+ });
637
+
638
+ let filteredPaletteCommands = $derived(
639
+ paletteCommands().filter(cmd =>
640
+ cmd.label.toLowerCase().includes(commandPalette.query.toLowerCase())
641
+ )
642
+ );
643
+
644
+ function executePaletteCommand(index) {
645
+ const cmd = filteredPaletteCommands[index];
646
+ if (cmd) {
647
+ cmd.action();
648
+ commandPalette.open = false;
649
+ }
650
+ }
651
+
652
+ // Zen mode toggle
653
+ function toggleZenMode() {
654
+ isZenMode = !isZenMode;
655
+ if (isZenMode) {
656
+ editorSettings.typewriterMode = true;
657
+ }
658
+ }
659
+
660
+ // Campfire session controls
661
+ function startCampfireSession() {
662
+ campfireSession.active = true;
663
+ campfireSession.startTime = Date.now();
664
+ campfireSession.startWordCount = wordCount;
665
+ }
666
+
667
+ function endCampfireSession() {
668
+ const wordsWritten = wordCount - campfireSession.startWordCount;
669
+ const elapsed = campfireSession.startTime ? Math.floor((Date.now() - campfireSession.startTime) / 1000) : 0;
670
+
671
+ // Could show a summary modal here
672
+ campfireSession.active = false;
673
+ campfireSession.startTime = null;
674
+ }
675
+
676
+ // Writing goal prompt
677
+ function promptWritingGoal() {
678
+ const target = prompt("Set your word goal for this session:", "500");
679
+ if (target && !isNaN(parseInt(target))) {
680
+ writingGoal.enabled = true;
681
+ writingGoal.targetWords = parseInt(target);
682
+ writingGoal.sessionWords = wordCount;
683
+ }
684
+ }
685
+
686
+ // Snippet management
687
+ const SNIPPETS_STORAGE_KEY = "grove-editor-snippets";
688
+
689
+ function loadSnippets() {
690
+ try {
691
+ const stored = localStorage.getItem(SNIPPETS_STORAGE_KEY);
692
+ if (stored) {
693
+ snippets = JSON.parse(stored);
694
+ }
695
+ } catch (e) {
696
+ console.warn("Failed to load snippets:", e);
697
+ }
698
+ }
699
+
700
+ function saveSnippets() {
701
+ try {
702
+ localStorage.setItem(SNIPPETS_STORAGE_KEY, JSON.stringify(snippets));
703
+ } catch (e) {
704
+ console.warn("Failed to save snippets:", e);
705
+ }
706
+ }
707
+
708
+ function openSnippetsModal(editId = null) {
709
+ if (editId) {
710
+ const snippet = snippets.find(s => s.id === editId);
711
+ if (snippet) {
712
+ snippetsModal.editingId = editId;
713
+ snippetsModal.name = snippet.name;
714
+ snippetsModal.content = snippet.content;
715
+ snippetsModal.trigger = snippet.trigger || "";
716
+ }
717
+ } else {
718
+ snippetsModal.editingId = null;
719
+ snippetsModal.name = "";
720
+ snippetsModal.content = "";
721
+ snippetsModal.trigger = "";
722
+ }
723
+ snippetsModal.open = true;
724
+ commandPalette.open = false;
725
+ }
726
+
727
+ function closeSnippetsModal() {
728
+ snippetsModal.open = false;
729
+ snippetsModal.editingId = null;
730
+ snippetsModal.name = "";
731
+ snippetsModal.content = "";
732
+ snippetsModal.trigger = "";
733
+ }
734
+
735
+ function saveSnippet() {
736
+ if (!snippetsModal.name.trim() || !snippetsModal.content.trim()) return;
737
+
738
+ if (snippetsModal.editingId) {
739
+ // Update existing snippet
740
+ snippets = snippets.map(s =>
741
+ s.id === snippetsModal.editingId
742
+ ? {
743
+ ...s,
744
+ name: snippetsModal.name.trim(),
745
+ content: snippetsModal.content,
746
+ trigger: snippetsModal.trigger.trim() || null,
747
+ }
748
+ : s
749
+ );
750
+ } else {
751
+ // Create new snippet
752
+ const newSnippet = {
753
+ id: `snippet-${Date.now()}`,
754
+ name: snippetsModal.name.trim(),
755
+ content: snippetsModal.content,
756
+ trigger: snippetsModal.trigger.trim() || null,
757
+ createdAt: new Date().toISOString(),
758
+ };
759
+ snippets = [...snippets, newSnippet];
760
+ }
761
+
762
+ saveSnippets();
763
+ closeSnippetsModal();
764
+ }
765
+
766
+ function deleteSnippet(id) {
767
+ if (confirm("Delete this snippet?")) {
768
+ snippets = snippets.filter(s => s.id !== id);
769
+ saveSnippets();
770
+ if (snippetsModal.editingId === id) {
771
+ closeSnippetsModal();
772
+ }
773
+ }
774
+ }
775
+
776
+ function insertSnippet(snippet) {
777
+ insertAtCursor(snippet.content);
778
+ slashMenu.open = false;
779
+ }
780
+
781
+ // Ambient sound controls
782
+ const SOUNDS_STORAGE_KEY = "grove-editor-sounds";
783
+
784
+ function loadSoundSettings() {
785
+ try {
786
+ const stored = localStorage.getItem(SOUNDS_STORAGE_KEY);
787
+ if (stored) {
788
+ const settings = JSON.parse(stored);
789
+ ambientSounds.currentSound = settings.currentSound || "forest";
790
+ ambientSounds.volume = settings.volume ?? 0.3;
791
+ // Don't auto-enable on load - user must click to start
792
+ }
793
+ } catch (e) {
794
+ console.warn("Failed to load sound settings:", e);
795
+ }
796
+ }
797
+
798
+ function saveSoundSettings() {
799
+ try {
800
+ localStorage.setItem(SOUNDS_STORAGE_KEY, JSON.stringify({
801
+ currentSound: ambientSounds.currentSound,
802
+ volume: ambientSounds.volume,
803
+ }));
804
+ } catch (e) {
805
+ console.warn("Failed to save sound settings:", e);
806
+ }
807
+ }
808
+
809
+ function toggleAmbientSound() {
810
+ if (ambientSounds.enabled) {
811
+ stopSound();
812
+ } else {
813
+ playSound(ambientSounds.currentSound);
814
+ }
815
+ }
816
+
817
+ function playSound(soundKey) {
818
+ const sound = soundLibrary[soundKey];
819
+ if (!sound) return;
820
+
821
+ // Stop current sound if playing
822
+ if (audioElement) {
823
+ audioElement.pause();
824
+ audioElement = null;
825
+ }
826
+
827
+ // Create new audio element
828
+ audioElement = new Audio(sound.url);
829
+ audioElement.loop = true;
830
+ audioElement.volume = ambientSounds.volume;
831
+
832
+ // Handle playback errors gracefully
833
+ audioElement.onerror = () => {
834
+ console.warn(`Sound file not found: ${sound.url}`);
835
+ ambientSounds.enabled = false;
836
+ };
837
+
838
+ audioElement.play().then(() => {
839
+ ambientSounds.enabled = true;
840
+ ambientSounds.currentSound = soundKey;
841
+ saveSoundSettings();
842
+ }).catch((e) => {
843
+ console.warn("Failed to play sound:", e);
844
+ ambientSounds.enabled = false;
845
+ });
846
+ }
847
+
848
+ function stopSound() {
849
+ if (audioElement) {
850
+ audioElement.pause();
851
+ audioElement = null;
852
+ }
853
+ ambientSounds.enabled = false;
854
+ }
855
+
856
+ function setVolume(newVolume) {
857
+ ambientSounds.volume = newVolume;
858
+ if (audioElement) {
859
+ audioElement.volume = newVolume;
860
+ }
861
+ saveSoundSettings();
862
+ }
863
+
864
+ function selectSound(soundKey) {
865
+ if (ambientSounds.enabled) {
866
+ playSound(soundKey);
867
+ } else {
868
+ ambientSounds.currentSound = soundKey;
869
+ saveSoundSettings();
870
+ }
871
+ }
872
+
873
+ function toggleSoundPanel() {
874
+ ambientSounds.showPanel = !ambientSounds.showPanel;
875
+ }
876
+
877
+ // Theme controls
878
+ function loadTheme() {
879
+ try {
880
+ const stored = localStorage.getItem(THEME_STORAGE_KEY);
881
+ if (stored && themes[stored]) {
882
+ currentTheme = stored;
883
+ applyTheme(stored);
884
+ }
885
+ } catch (e) {
886
+ console.warn("Failed to load theme:", e);
887
+ }
888
+ }
889
+
890
+ function saveTheme(themeName) {
891
+ try {
892
+ localStorage.setItem(THEME_STORAGE_KEY, themeName);
893
+ } catch (e) {
894
+ console.warn("Failed to save theme:", e);
895
+ }
896
+ }
897
+
898
+ function applyTheme(themeName) {
899
+ const theme = themes[themeName];
900
+ if (!theme) return;
901
+
902
+ const root = document.documentElement;
903
+ root.style.setProperty("--editor-accent", theme.accent);
904
+ root.style.setProperty("--editor-accent-dim", theme.accentDim);
905
+ root.style.setProperty("--editor-accent-bright", theme.accentBright);
906
+ root.style.setProperty("--editor-accent-glow", theme.accentGlow);
907
+ root.style.setProperty("--editor-bg", theme.bg);
908
+ root.style.setProperty("--editor-bg-secondary", theme.bgSecondary);
909
+ root.style.setProperty("--editor-bg-tertiary", theme.bgTertiary);
910
+ root.style.setProperty("--editor-border", theme.border);
911
+ root.style.setProperty("--editor-border-accent", theme.borderAccent);
912
+ root.style.setProperty("--editor-text", theme.text);
913
+ root.style.setProperty("--editor-text-dim", theme.textDim);
914
+ root.style.setProperty("--editor-status-bg", theme.statusBg);
915
+ root.style.setProperty("--editor-status-border", theme.statusBorder);
916
+ }
917
+
918
+ function setTheme(themeName) {
919
+ if (!themes[themeName]) return;
920
+ currentTheme = themeName;
921
+ applyTheme(themeName);
922
+ saveTheme(themeName);
923
+ commandPalette.open = false;
924
+ }
925
+
926
+ // Typewriter scrolling - keep cursor line centered
927
+ function applyTypewriterScroll() {
928
+ if (!textareaRef || !editorSettings.typewriterMode) return;
929
+
930
+ const lineHeight = parseFloat(getComputedStyle(textareaRef).lineHeight) || 24;
931
+ const viewportHeight = textareaRef.clientHeight;
932
+ const centerOffset = viewportHeight / 2;
933
+ const targetScroll = (cursorLine - 1) * lineHeight - centerOffset + lineHeight / 2;
934
+
935
+ textareaRef.scrollTop = Math.max(0, targetScroll);
936
+ }
937
+
938
+ // Sync line numbers scroll with textarea
939
+ function syncLineNumbersScroll() {
940
+ if (lineNumbersRef && textareaRef) {
941
+ lineNumbersRef.scrollTop = textareaRef.scrollTop;
942
+ }
943
+ }
944
+
945
+ // Wrap selected text with markers
946
+ function wrapSelection(before, after) {
947
+ if (!textareaRef) return;
948
+
949
+ const start = textareaRef.selectionStart;
950
+ const end = textareaRef.selectionEnd;
951
+ const selectedText = content.substring(start, end);
952
+
953
+ content =
954
+ content.substring(0, start) +
955
+ before +
956
+ selectedText +
957
+ after +
958
+ content.substring(end);
959
+
960
+ setTimeout(() => {
961
+ textareaRef.selectionStart = start + before.length;
962
+ textareaRef.selectionEnd = end + before.length;
963
+ textareaRef.focus();
964
+ }, 0);
965
+ }
966
+
967
+ // Insert text at cursor
968
+ function insertAtCursor(text) {
969
+ if (!textareaRef) return;
970
+
971
+ const start = textareaRef.selectionStart;
972
+ content = content.substring(0, start) + text + content.substring(start);
973
+
974
+ setTimeout(() => {
975
+ textareaRef.selectionStart = textareaRef.selectionEnd =
976
+ start + text.length;
977
+ textareaRef.focus();
978
+ }, 0);
979
+ }
980
+
981
+ // Toolbar actions
982
+ function insertHeading(level) {
983
+ const prefix = "#".repeat(level) + " ";
984
+ insertAtCursor(prefix);
985
+ }
986
+
987
+ function insertLink() {
988
+ wrapSelection("[", "](url)");
989
+ }
990
+
991
+ function insertImage() {
992
+ insertAtCursor("![alt text](image-url)");
993
+ }
994
+
995
+ function insertCodeBlock() {
996
+ const start = textareaRef.selectionStart;
997
+ const selectedText = content.substring(
998
+ start,
999
+ textareaRef.selectionEnd
1000
+ );
1001
+ const codeBlock = "```\n" + (selectedText || "code here") + "\n```";
1002
+ content =
1003
+ content.substring(0, start) +
1004
+ codeBlock +
1005
+ content.substring(textareaRef.selectionEnd);
1006
+ }
1007
+
1008
+ function insertList() {
1009
+ insertAtCursor("- ");
1010
+ }
1011
+
1012
+ function insertQuote() {
1013
+ insertAtCursor("> ");
1014
+ }
1015
+
1016
+ // Sync scroll between editor and preview (optional)
1017
+ function handleScroll() {
1018
+ // Sync line numbers
1019
+ syncLineNumbersScroll();
1020
+
1021
+ // Sync preview
1022
+ if (textareaRef && previewRef && showPreview) {
1023
+ const scrollRatio =
1024
+ textareaRef.scrollTop /
1025
+ (textareaRef.scrollHeight - textareaRef.clientHeight);
1026
+ previewRef.scrollTop =
1027
+ scrollRatio * (previewRef.scrollHeight - previewRef.clientHeight);
1028
+ }
1029
+ }
1030
+
1031
+ // Apply typewriter scroll when cursor moves
1032
+ $effect(() => {
1033
+ if (editorSettings.typewriterMode && cursorLine) {
1034
+ applyTypewriterScroll();
1035
+ }
1036
+ });
1037
+
1038
+ // Drag and drop image upload
1039
+ function handleDragEnter(e) {
1040
+ e.preventDefault();
1041
+ if (readonly) return;
1042
+
1043
+ // Check if dragging files
1044
+ if (e.dataTransfer?.types?.includes("Files")) {
1045
+ isDragging = true;
1046
+ }
1047
+ }
1048
+
1049
+ function handleDragOver(e) {
1050
+ e.preventDefault();
1051
+ if (readonly) return;
1052
+
1053
+ if (e.dataTransfer?.types?.includes("Files")) {
1054
+ e.dataTransfer.dropEffect = "copy";
1055
+ isDragging = true;
1056
+ }
1057
+ }
1058
+
1059
+ function handleDragLeave(e) {
1060
+ e.preventDefault();
1061
+ // Only set to false if leaving the container entirely
1062
+ if (!e.currentTarget.contains(e.relatedTarget)) {
1063
+ isDragging = false;
1064
+ }
1065
+ }
1066
+
1067
+ async function handleDrop(e) {
1068
+ e.preventDefault();
1069
+ isDragging = false;
1070
+ if (readonly) return;
1071
+
1072
+ const files = Array.from(e.dataTransfer?.files || []);
1073
+ const imageFiles = files.filter((f) => f.type.startsWith("image/"));
1074
+
1075
+ if (imageFiles.length === 0) {
1076
+ uploadError = "No image files detected";
1077
+ setTimeout(() => (uploadError = null), 3000);
1078
+ return;
1079
+ }
1080
+
1081
+ // Upload each image
1082
+ for (const file of imageFiles) {
1083
+ await uploadImage(file);
1084
+ }
1085
+ }
1086
+
1087
+ async function uploadImage(file) {
1088
+ isUploading = true;
1089
+ uploadProgress = `Uploading ${file.name}...`;
1090
+ uploadError = null;
1091
+
1092
+ try {
1093
+ const formData = new FormData();
1094
+ formData.append("file", file);
1095
+ formData.append("folder", "blog");
1096
+
1097
+ const response = await fetch("/api/images/upload", {
1098
+ method: "POST",
1099
+ body: formData,
1100
+ });
1101
+
1102
+ const result = await response.json();
1103
+
1104
+ if (!response.ok) {
1105
+ throw new Error(result.message || "Upload failed");
1106
+ }
1107
+
1108
+ // Insert markdown image at cursor
1109
+ const altText = file.name.replace(/\.[^/.]+$/, "").replace(/[-_]/g, " ");
1110
+ const imageMarkdown = `![${altText}](${result.url})\n`;
1111
+ insertAtCursor(imageMarkdown);
1112
+
1113
+ uploadProgress = "";
1114
+ } catch (err) {
1115
+ uploadError = err.message;
1116
+ setTimeout(() => (uploadError = null), 5000);
1117
+ } finally {
1118
+ isUploading = false;
1119
+ uploadProgress = "";
1120
+ }
1121
+ }
1122
+
1123
+ // Handle paste for images
1124
+ function handlePaste(e) {
1125
+ if (readonly) return;
1126
+
1127
+ const items = Array.from(e.clipboardData?.items || []);
1128
+ const imageItem = items.find((item) => item.type.startsWith("image/"));
1129
+
1130
+ if (imageItem) {
1131
+ e.preventDefault();
1132
+ const file = imageItem.getAsFile();
1133
+ if (file) {
1134
+ // Generate a filename for pasted images
1135
+ const timestamp = Date.now();
1136
+ const extension = file.type.split("/")[1] || "png";
1137
+ const renamedFile = new File([file], `pasted-${timestamp}.${extension}`, {
1138
+ type: file.type,
1139
+ });
1140
+ uploadImage(renamedFile);
1141
+ }
1142
+ }
1143
+ }
1144
+
1145
+ // Auto-save draft to localStorage
1146
+ $effect(() => {
1147
+ if (!draftKey || readonly) return;
1148
+
1149
+ // Clear previous timer
1150
+ if (draftSaveTimer) {
1151
+ clearTimeout(draftSaveTimer);
1152
+ }
1153
+
1154
+ // Don't save if content hasn't changed from last saved version
1155
+ if (content === lastSavedContent) return;
1156
+
1157
+ // Schedule a draft save
1158
+ draftSaveTimer = setTimeout(() => {
1159
+ saveDraft();
1160
+ }, AUTO_SAVE_DELAY);
1161
+
1162
+ return () => {
1163
+ if (draftSaveTimer) {
1164
+ clearTimeout(draftSaveTimer);
1165
+ }
1166
+ };
1167
+ });
1168
+
1169
+ function saveDraft() {
1170
+ if (!draftKey || readonly) return;
1171
+
1172
+ try {
1173
+ const draft = {
1174
+ content,
1175
+ savedAt: new Date().toISOString(),
1176
+ };
1177
+ localStorage.setItem(`draft:${draftKey}`, JSON.stringify(draft));
1178
+ lastSavedContent = content;
1179
+ hasDraft = true;
1180
+ } catch (e) {
1181
+ console.warn("Failed to save draft:", e);
1182
+ }
1183
+ }
1184
+
1185
+ function loadDraft() {
1186
+ if (!draftKey) return null;
1187
+
1188
+ try {
1189
+ const stored = localStorage.getItem(`draft:${draftKey}`);
1190
+ if (stored) {
1191
+ return JSON.parse(stored);
1192
+ }
1193
+ } catch (e) {
1194
+ console.warn("Failed to load draft:", e);
1195
+ }
1196
+ return null;
1197
+ }
1198
+
1199
+ export function clearDraft() {
1200
+ if (!draftKey) return;
1201
+
1202
+ try {
1203
+ localStorage.removeItem(`draft:${draftKey}`);
1204
+ hasDraft = false;
1205
+ storedDraft = null;
1206
+ draftRestorePrompt = false;
1207
+ } catch (e) {
1208
+ console.warn("Failed to clear draft:", e);
1209
+ }
1210
+ }
1211
+
1212
+ export function getDraftStatus() {
1213
+ return { hasDraft, storedDraft };
1214
+ }
1215
+
1216
+ function restoreDraft() {
1217
+ if (storedDraft) {
1218
+ content = storedDraft.content;
1219
+ lastSavedContent = storedDraft.content;
1220
+ onDraftRestored(storedDraft);
1221
+ }
1222
+ draftRestorePrompt = false;
1223
+ }
1224
+
1225
+ function discardDraft() {
1226
+ clearDraft();
1227
+ lastSavedContent = content;
1228
+ }
1229
+
1230
+ onMount(() => {
1231
+ updateCursorPosition();
1232
+ loadSnippets();
1233
+ loadSoundSettings();
1234
+ loadTheme();
1235
+
1236
+ // Check for existing draft on mount
1237
+ if (draftKey) {
1238
+ const draft = loadDraft();
1239
+ if (draft && draft.content !== content) {
1240
+ storedDraft = draft;
1241
+ draftRestorePrompt = true;
1242
+ } else {
1243
+ lastSavedContent = content;
1244
+ }
1245
+ }
1246
+
1247
+ // Cleanup audio on unmount
1248
+ return () => {
1249
+ if (audioElement) {
1250
+ audioElement.pause();
1251
+ audioElement = null;
1252
+ }
1253
+ };
1254
+ });
1255
+ </script>
1256
+
1257
+ <svelte:window onkeydown={handleGlobalKeydown} />
1258
+
1259
+ <div
1260
+ class="editor-container"
1261
+ class:dragging={isDragging}
1262
+ class:zen-mode={isZenMode}
1263
+ class:campfire-mode={campfireSession.active}
1264
+ aria-label="Markdown editor with live preview"
1265
+ ondragenter={handleDragEnter}
1266
+ ondragover={handleDragOver}
1267
+ ondragleave={handleDragLeave}
1268
+ ondrop={handleDrop}
1269
+ >
1270
+ <!-- Drag overlay -->
1271
+ {#if isDragging}
1272
+ <div class="drag-overlay">
1273
+ <div class="drag-overlay-content">
1274
+ <span class="drag-icon">+</span>
1275
+ <span class="drag-text">Drop image to upload</span>
1276
+ </div>
1277
+ </div>
1278
+ {/if}
1279
+
1280
+ <!-- Upload status -->
1281
+ {#if isUploading || uploadError}
1282
+ <div class="upload-status" class:error={uploadError}>
1283
+ {#if isUploading}
1284
+ <span class="upload-spinner"></span>
1285
+ <span>{uploadProgress}</span>
1286
+ {:else if uploadError}
1287
+ <span class="upload-error-icon">!</span>
1288
+ <span>{uploadError}</span>
1289
+ {/if}
1290
+ </div>
1291
+ {/if}
1292
+
1293
+ <!-- Draft restore prompt -->
1294
+ {#if draftRestorePrompt && storedDraft}
1295
+ <div class="draft-prompt">
1296
+ <div class="draft-prompt-content">
1297
+ <span class="draft-icon">~</span>
1298
+ <div class="draft-message">
1299
+ <strong>Unsaved draft found</strong>
1300
+ <span class="draft-time">
1301
+ Saved {new Date(storedDraft.savedAt).toLocaleString()}
1302
+ </span>
1303
+ </div>
1304
+ <div class="draft-actions">
1305
+ <button type="button" class="draft-btn restore" onclick={restoreDraft}>
1306
+ [<span class="key">r</span>estore]
1307
+ </button>
1308
+ <button type="button" class="draft-btn discard" onclick={discardDraft}>
1309
+ [<span class="key">d</span>iscard]
1310
+ </button>
1311
+ </div>
1312
+ </div>
1313
+ </div>
1314
+ {/if}
1315
+
1316
+ <!-- Toolbar -->
1317
+ <div class="toolbar">
1318
+ <div class="toolbar-group">
1319
+ <button
1320
+ type="button"
1321
+ class="toolbar-btn"
1322
+ onclick={() => insertHeading(1)}
1323
+ title="Heading 1"
1324
+ disabled={readonly}
1325
+ >[h<span class="key">1</span>]</button>
1326
+ <button
1327
+ type="button"
1328
+ class="toolbar-btn"
1329
+ onclick={() => insertHeading(2)}
1330
+ title="Heading 2"
1331
+ disabled={readonly}
1332
+ >[h<span class="key">2</span>]</button>
1333
+ <button
1334
+ type="button"
1335
+ class="toolbar-btn"
1336
+ onclick={() => insertHeading(3)}
1337
+ title="Heading 3"
1338
+ disabled={readonly}
1339
+ >[h<span class="key">3</span>]</button>
1340
+ </div>
1341
+
1342
+ <div class="toolbar-divider">|</div>
1343
+
1344
+ <div class="toolbar-group">
1345
+ <button
1346
+ type="button"
1347
+ class="toolbar-btn"
1348
+ onclick={() => wrapSelection("**", "**")}
1349
+ title="Bold (Cmd+B)"
1350
+ disabled={readonly}
1351
+ >[<span class="key">b</span>old]</button>
1352
+ <button
1353
+ type="button"
1354
+ class="toolbar-btn"
1355
+ onclick={() => wrapSelection("_", "_")}
1356
+ title="Italic (Cmd+I)"
1357
+ disabled={readonly}
1358
+ >[<span class="key">i</span>talic]</button>
1359
+ <button
1360
+ type="button"
1361
+ class="toolbar-btn"
1362
+ onclick={() => wrapSelection("`", "`")}
1363
+ title="Inline Code"
1364
+ disabled={readonly}
1365
+ >[<span class="key">c</span>ode]</button>
1366
+ </div>
1367
+
1368
+ <div class="toolbar-divider">|</div>
1369
+
1370
+ <div class="toolbar-group">
1371
+ <button
1372
+ type="button"
1373
+ class="toolbar-btn"
1374
+ onclick={insertLink}
1375
+ title="Link"
1376
+ disabled={readonly}
1377
+ >[<span class="key">l</span>ink]</button>
1378
+ <button
1379
+ type="button"
1380
+ class="toolbar-btn"
1381
+ onclick={insertImage}
1382
+ title="Image"
1383
+ disabled={readonly}
1384
+ >[i<span class="key">m</span>g]</button>
1385
+ <button
1386
+ type="button"
1387
+ class="toolbar-btn"
1388
+ onclick={insertCodeBlock}
1389
+ title="Code Block"
1390
+ disabled={readonly}
1391
+ >[bloc<span class="key">k</span>]</button>
1392
+ </div>
1393
+
1394
+ <div class="toolbar-divider">|</div>
1395
+
1396
+ <div class="toolbar-group">
1397
+ <button
1398
+ type="button"
1399
+ class="toolbar-btn"
1400
+ onclick={insertList}
1401
+ title="List"
1402
+ disabled={readonly}
1403
+ >[lis<span class="key">t</span>]</button>
1404
+ <button
1405
+ type="button"
1406
+ class="toolbar-btn"
1407
+ onclick={insertQuote}
1408
+ title="Quote"
1409
+ disabled={readonly}
1410
+ >[<span class="key">q</span>uote]</button>
1411
+ </div>
1412
+
1413
+ <div class="toolbar-spacer"></div>
1414
+
1415
+ <div class="toolbar-group">
1416
+ <button
1417
+ type="button"
1418
+ class="toolbar-btn toggle-btn"
1419
+ class:active={showPreview}
1420
+ onclick={() => (showPreview = !showPreview)}
1421
+ title="Toggle Preview"
1422
+ >{#if showPreview}[hide <span class="key">p</span>review]{:else}[show <span class="key">p</span>review]{/if}</button>
1423
+ <button
1424
+ type="button"
1425
+ class="toolbar-btn full-preview-btn"
1426
+ onclick={() => (showFullPreview = true)}
1427
+ title="Open Full Preview (site styling)"
1428
+ >[<span class="key">f</span>ull]</button>
1429
+ </div>
1430
+ </div>
1431
+
1432
+ <!-- Editor Area -->
1433
+ <div class="editor-area" class:split={showPreview}>
1434
+ <!-- Editor Panel -->
1435
+ <div class="editor-panel">
1436
+ <div class="editor-wrapper">
1437
+ <div class="line-numbers" aria-hidden="true" bind:this={lineNumbersRef}>
1438
+ {#each lineNumbers as num}
1439
+ <span class:current={num === cursorLine}>{num}</span>
1440
+ {/each}
1441
+ </div>
1442
+ <textarea
1443
+ bind:this={textareaRef}
1444
+ bind:value={content}
1445
+ oninput={updateCursorPosition}
1446
+ onclick={updateCursorPosition}
1447
+ onkeyup={updateCursorPosition}
1448
+ onkeydown={handleKeydown}
1449
+ onscroll={handleScroll}
1450
+ onpaste={handlePaste}
1451
+ placeholder="Start writing your post... (Drag & drop or paste images)"
1452
+ spellcheck="true"
1453
+ disabled={readonly}
1454
+ class="editor-textarea"
1455
+ ></textarea>
1456
+ </div>
1457
+ </div>
1458
+
1459
+ <!-- Preview Panel -->
1460
+ {#if showPreview}
1461
+ <div class="preview-panel">
1462
+ <div class="preview-header">
1463
+ <span class="preview-label">:: preview</span>
1464
+ </div>
1465
+ <div class="preview-content" bind:this={previewRef}>
1466
+ {#if previewHtml}
1467
+ {@html previewHtml}
1468
+ {:else}
1469
+ <p class="preview-placeholder">
1470
+ Your rendered markdown will appear here...
1471
+ </p>
1472
+ {/if}
1473
+ </div>
1474
+ </div>
1475
+ {/if}
1476
+ </div>
1477
+
1478
+ <!-- Status Bar -->
1479
+ <div class="status-bar">
1480
+ <div class="status-left">
1481
+ <span class="status-item">
1482
+ Ln {cursorLine}, Col {cursorCol}
1483
+ </span>
1484
+ <span class="status-divider">|</span>
1485
+ <span class="status-item">{lineCount} lines</span>
1486
+ <span class="status-divider">|</span>
1487
+ <span class="status-item">{wordCount} words</span>
1488
+ <span class="status-divider">|</span>
1489
+ <span class="status-item">{readingTime()}</span>
1490
+ {#if writingGoal.enabled}
1491
+ <span class="status-divider">|</span>
1492
+ <span class="status-goal">
1493
+ Goal: {goalProgress()}%
1494
+ </span>
1495
+ {/if}
1496
+ {#if campfireSession.active}
1497
+ <span class="status-divider">|</span>
1498
+ <span class="status-campfire">
1499
+ ~ {campfireElapsed()}
1500
+ </span>
1501
+ {/if}
1502
+ </div>
1503
+ <div class="status-right">
1504
+ <button
1505
+ type="button"
1506
+ class="status-sound-btn"
1507
+ class:playing={ambientSounds.enabled}
1508
+ onclick={toggleSoundPanel}
1509
+ title="Ambient sounds"
1510
+ >
1511
+ [{soundLibrary[ambientSounds.currentSound]?.name || "snd"}]{#if ambientSounds.enabled}<span class="sound-wave">~</span>{/if}
1512
+ </button>
1513
+ <span class="status-divider">|</span>
1514
+ {#if editorSettings.typewriterMode}
1515
+ <span class="status-mode">Typewriter</span>
1516
+ <span class="status-divider">|</span>
1517
+ {/if}
1518
+ {#if saving}
1519
+ <span class="status-saving">Saving...</span>
1520
+ {:else if draftKey && content !== lastSavedContent}
1521
+ <span class="status-draft">Draft saving...</span>
1522
+ {:else}
1523
+ <span class="status-item">Markdown</span>
1524
+ {/if}
1525
+ </div>
1526
+ </div>
1527
+ </div>
1528
+
1529
+ <!-- Slash Commands Menu -->
1530
+ {#if slashMenu.open}
1531
+ <div class="slash-menu">
1532
+ <div class="slash-menu-header">:: commands</div>
1533
+ {#each filteredSlashCommands as cmd, i}
1534
+ <button
1535
+ type="button"
1536
+ class="slash-menu-item"
1537
+ class:selected={i === slashMenu.selectedIndex}
1538
+ onclick={() => executeSlashCommand(i)}
1539
+ >
1540
+ <span class="slash-cmd-label">{cmd.label}</span>
1541
+ </button>
1542
+ {/each}
1543
+ {#if filteredSlashCommands.length === 0}
1544
+ <div class="slash-menu-empty">; no commands found</div>
1545
+ {/if}
1546
+ </div>
1547
+ {/if}
1548
+
1549
+ <!-- Command Palette -->
1550
+ {#if commandPalette.open}
1551
+ <div class="command-palette-overlay" onclick={() => commandPalette.open = false}>
1552
+ <div class="command-palette" onclick={(e) => e.stopPropagation()}>
1553
+ <input
1554
+ type="text"
1555
+ class="command-palette-input"
1556
+ placeholder="> type a command..."
1557
+ bind:value={commandPalette.query}
1558
+ onkeydown={(e) => {
1559
+ if (e.key === "ArrowDown") {
1560
+ e.preventDefault();
1561
+ commandPalette.selectedIndex = (commandPalette.selectedIndex + 1) % filteredPaletteCommands.length;
1562
+ }
1563
+ if (e.key === "ArrowUp") {
1564
+ e.preventDefault();
1565
+ commandPalette.selectedIndex = (commandPalette.selectedIndex - 1 + filteredPaletteCommands.length) % filteredPaletteCommands.length;
1566
+ }
1567
+ if (e.key === "Enter") {
1568
+ e.preventDefault();
1569
+ executePaletteCommand(commandPalette.selectedIndex);
1570
+ }
1571
+ if (e.key === "Escape") {
1572
+ commandPalette.open = false;
1573
+ }
1574
+ }}
1575
+ />
1576
+ <div class="command-palette-list">
1577
+ {#each filteredPaletteCommands as cmd, i}
1578
+ <button
1579
+ type="button"
1580
+ class="command-palette-item"
1581
+ class:selected={i === commandPalette.selectedIndex}
1582
+ onclick={() => executePaletteCommand(i)}
1583
+ >
1584
+ <span class="palette-cmd-label">{cmd.label}</span>
1585
+ {#if cmd.shortcut}
1586
+ <span class="palette-cmd-shortcut">{cmd.shortcut}</span>
1587
+ {/if}
1588
+ </button>
1589
+ {/each}
1590
+ </div>
1591
+ </div>
1592
+ </div>
1593
+ {/if}
1594
+
1595
+ <!-- Campfire Session Controls (when active) -->
1596
+ {#if campfireSession.active}
1597
+ <div class="campfire-controls">
1598
+ <div class="campfire-ember"></div>
1599
+ <div class="campfire-stats">
1600
+ <span class="campfire-time">{campfireElapsed()}</span>
1601
+ <span class="campfire-words">+{wordCount - campfireSession.startWordCount} words</span>
1602
+ </div>
1603
+ <button type="button" class="campfire-end" onclick={endCampfireSession}>
1604
+ [<span class="key">e</span>nd]
1605
+ </button>
1606
+ </div>
1607
+ {/if}
1608
+
1609
+ <!-- Snippets Modal -->
1610
+ <Dialog bind:open={snippetsModal.open}>
1611
+ <h3 slot="title">:: {snippetsModal.editingId ? "edit snippet" : "new snippet"}</h3>
1612
+
1613
+ <div class="snippets-modal-body">
1614
+ <div class="snippets-form">
1615
+ <div class="snippet-field">
1616
+ <label for="snippet-name">Name</label>
1617
+ <Input
1618
+ id="snippet-name"
1619
+ type="text"
1620
+ bind:value={snippetsModal.name}
1621
+ placeholder="e.g., Blog signature"
1622
+ />
1623
+ </div>
1624
+
1625
+ <div class="snippet-field">
1626
+ <label for="snippet-trigger">Trigger (optional)</label>
1627
+ <Input
1628
+ id="snippet-trigger"
1629
+ type="text"
1630
+ bind:value={snippetsModal.trigger}
1631
+ placeholder="e.g., sig"
1632
+ />
1633
+ <span class="field-hint">Type /trigger to quickly insert</span>
1634
+ </div>
1635
+
1636
+ <div class="snippet-field">
1637
+ <label for="snippet-content">Content</label>
1638
+ <textarea
1639
+ id="snippet-content"
1640
+ bind:value={snippetsModal.content}
1641
+ placeholder="Enter your markdown snippet..."
1642
+ rows="6"
1643
+ ></textarea>
1644
+ </div>
1645
+
1646
+ <div class="snippet-actions">
1647
+ {#if snippetsModal.editingId}
1648
+ <Button
1649
+ variant="danger"
1650
+ onclick={() => deleteSnippet(snippetsModal.editingId)}
1651
+ >
1652
+ [<span class="key">d</span>elete]
1653
+ </Button>
1654
+ {/if}
1655
+ <div class="snippet-actions-right">
1656
+ <Button variant="outline" onclick={closeSnippetsModal}>
1657
+ [<span class="key">c</span>ancel]
1658
+ </Button>
1659
+ <Button
1660
+ onclick={saveSnippet}
1661
+ disabled={!snippetsModal.name.trim() || !snippetsModal.content.trim()}
1662
+ >
1663
+ {#if snippetsModal.editingId}[<span class="key">u</span>pdate]{:else}[<span class="key">s</span>ave]{/if}
1664
+ </Button>
1665
+ </div>
1666
+ </div>
1667
+ </div>
1668
+
1669
+ {#if snippets.length > 0 && !snippetsModal.editingId}
1670
+ <div class="snippets-list-divider">
1671
+ <span>:: your snippets</span>
1672
+ </div>
1673
+ <div class="snippets-list">
1674
+ {#each snippets as snippet}
1675
+ <button
1676
+ type="button"
1677
+ class="snippet-list-item"
1678
+ onclick={() => openSnippetsModal(snippet.id)}
1679
+ >
1680
+ <span class="snippet-name">{snippet.name}</span>
1681
+ {#if snippet.trigger}
1682
+ <span class="snippet-trigger">/{snippet.trigger}</span>
1683
+ {/if}
1684
+ </button>
1685
+ {/each}
1686
+ </div>
1687
+ {/if}
1688
+ </div>
1689
+ </Dialog>
1690
+
1691
+ <!-- Ambient Sound Panel -->
1692
+ {#if ambientSounds.showPanel}
1693
+ <div class="sound-panel">
1694
+ <div class="sound-panel-header">
1695
+ <span class="sound-panel-title">:: ambient sounds</span>
1696
+ <button
1697
+ type="button"
1698
+ class="sound-panel-close"
1699
+ onclick={() => ambientSounds.showPanel = false}
1700
+ >[x]</button>
1701
+ </div>
1702
+
1703
+ <div class="sound-options">
1704
+ {#each Object.entries(soundLibrary) as [key, sound]}
1705
+ <button
1706
+ type="button"
1707
+ class="sound-option"
1708
+ class:active={ambientSounds.currentSound === key}
1709
+ class:playing={ambientSounds.enabled && ambientSounds.currentSound === key}
1710
+ onclick={() => selectSound(key)}
1711
+ >
1712
+ [<span class="key">{sound.key}</span>] {sound.name}
1713
+ </button>
1714
+ {/each}
1715
+ </div>
1716
+
1717
+ <div class="sound-controls">
1718
+ <label class="volume-label">
1719
+ <span>vol:</span>
1720
+ <input
1721
+ type="range"
1722
+ min="0"
1723
+ max="1"
1724
+ step="0.05"
1725
+ value={ambientSounds.volume}
1726
+ oninput={(e) => setVolume(parseFloat(e.target.value))}
1727
+ class="volume-slider"
1728
+ />
1729
+ </label>
1730
+
1731
+ <button
1732
+ type="button"
1733
+ class="sound-play-btn"
1734
+ class:playing={ambientSounds.enabled}
1735
+ onclick={toggleAmbientSound}
1736
+ >
1737
+ {#if ambientSounds.enabled}[<span class="key">s</span>top]{:else}[<span class="key">p</span>lay]{/if}
1738
+ </button>
1739
+ </div>
1740
+
1741
+ <div class="sound-note">
1742
+ <span>; add audio to /static/sounds/</span>
1743
+ </div>
1744
+ </div>
1745
+ {/if}
1746
+
1747
+ <!-- Full Preview Modal -->
1748
+ {#if showFullPreview}
1749
+ <div class="full-preview-modal" role="dialog" aria-modal="true">
1750
+ <div class="full-preview-backdrop" onclick={() => (showFullPreview = false)}></div>
1751
+ <div class="full-preview-container">
1752
+ <header class="full-preview-header">
1753
+ <h2>:: full preview</h2>
1754
+ <div class="full-preview-actions">
1755
+ <button
1756
+ type="button"
1757
+ class="full-preview-close"
1758
+ onclick={() => (showFullPreview = false)}
1759
+ >
1760
+ [<span class="key">c</span>lose]
1761
+ </button>
1762
+ </div>
1763
+ </header>
1764
+ <div class="full-preview-scroll">
1765
+ <article class="full-preview-article">
1766
+ <!-- Post Header -->
1767
+ {#if previewTitle || previewDate || previewTags.length > 0}
1768
+ <header class="content-header">
1769
+ {#if previewTitle}
1770
+ <h1>{previewTitle}</h1>
1771
+ {/if}
1772
+ {#if previewDate || previewTags.length > 0}
1773
+ <div class="post-meta">
1774
+ {#if previewDate}
1775
+ <time datetime={previewDate}>
1776
+ {new Date(previewDate).toLocaleDateString("en-US", {
1777
+ year: "numeric",
1778
+ month: "long",
1779
+ day: "numeric",
1780
+ })}
1781
+ </time>
1782
+ {/if}
1783
+ {#if previewTags.length > 0}
1784
+ <div class="tags">
1785
+ {#each previewTags as tag}
1786
+ <span class="tag">{tag}</span>
1787
+ {/each}
1788
+ </div>
1789
+ {/if}
1790
+ </div>
1791
+ {/if}
1792
+ </header>
1793
+ {/if}
1794
+
1795
+ <!-- Rendered Content -->
1796
+ <div class="content-body">
1797
+ {#if previewHtml}
1798
+ {@html previewHtml}
1799
+ {:else}
1800
+ <p class="preview-placeholder">Start writing to see your content here...</p>
1801
+ {/if}
1802
+ </div>
1803
+ </article>
1804
+ </div>
1805
+ </div>
1806
+ </div>
1807
+ {/if}
1808
+
1809
+ <style>
1810
+ .editor-container {
1811
+ display: flex;
1812
+ flex-direction: column;
1813
+ height: 100%;
1814
+ min-height: 500px;
1815
+ background: var(--editor-bg, var(--light-bg-primary));
1816
+ border: 1px solid var(--editor-border, var(--light-border-primary));
1817
+ border-radius: 8px;
1818
+ overflow: hidden;
1819
+ font-family: "JetBrains Mono", "Fira Code", "SF Mono", Consolas, monospace;
1820
+ position: relative;
1821
+ }
1822
+ .editor-container.dragging {
1823
+ border-color: var(--editor-accent, #8bc48b);
1824
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--editor-accent, #8bc48b) 30%, transparent);
1825
+ }
1826
+ /* Drag overlay */
1827
+ .drag-overlay {
1828
+ position: absolute;
1829
+ inset: 0;
1830
+ background: color-mix(in srgb, var(--editor-bg, var(--light-bg-primary)) 95%, transparent);
1831
+ display: flex;
1832
+ align-items: center;
1833
+ justify-content: center;
1834
+ z-index: 100;
1835
+ border: 3px dashed var(--editor-accent, #8bc48b);
1836
+ border-radius: 8px;
1837
+ }
1838
+ .drag-overlay-content {
1839
+ display: flex;
1840
+ flex-direction: column;
1841
+ align-items: center;
1842
+ gap: 1rem;
1843
+ color: var(--editor-accent, #8bc48b);
1844
+ }
1845
+ .drag-icon {
1846
+ font-size: 3rem;
1847
+ font-weight: 300;
1848
+ width: 80px;
1849
+ height: 80px;
1850
+ display: flex;
1851
+ align-items: center;
1852
+ justify-content: center;
1853
+ border: 2px dashed var(--editor-accent, #8bc48b);
1854
+ border-radius: 50%;
1855
+ }
1856
+ .drag-text {
1857
+ font-size: 1.1rem;
1858
+ font-weight: 500;
1859
+ }
1860
+ /* Upload status */
1861
+ .upload-status {
1862
+ position: absolute;
1863
+ top: 50%;
1864
+ left: 50%;
1865
+ transform: translate(-50%, -50%);
1866
+ display: flex;
1867
+ align-items: center;
1868
+ gap: 0.75rem;
1869
+ padding: 0.75rem 1.25rem;
1870
+ background: rgba(45, 74, 45, 0.95);
1871
+ border: 1px solid #4a7c4a;
1872
+ border-radius: 6px;
1873
+ color: #a8dca8;
1874
+ font-size: 0.9rem;
1875
+ z-index: 99;
1876
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
1877
+ }
1878
+ .upload-status.error {
1879
+ background: rgba(80, 40, 40, 0.95);
1880
+ border-color: #a85050;
1881
+ color: #ffb0b0;
1882
+ }
1883
+ .upload-spinner {
1884
+ width: 18px;
1885
+ height: 18px;
1886
+ border: 2px solid #4a7c4a;
1887
+ border-top-color: #a8dca8;
1888
+ border-radius: 50%;
1889
+ animation: spin 0.8s linear infinite;
1890
+ }
1891
+ .upload-error-icon {
1892
+ display: flex;
1893
+ align-items: center;
1894
+ justify-content: center;
1895
+ width: 20px;
1896
+ height: 20px;
1897
+ background: #a85050;
1898
+ color: white;
1899
+ border-radius: 50%;
1900
+ font-size: 0.75rem;
1901
+ font-weight: bold;
1902
+ }
1903
+ @keyframes spin {
1904
+ to {
1905
+ transform: rotate(360deg);
1906
+ }
1907
+ }
1908
+ /* Draft prompt */
1909
+ .draft-prompt {
1910
+ position: absolute;
1911
+ top: 0;
1912
+ left: 0;
1913
+ right: 0;
1914
+ background: rgba(45, 60, 45, 0.98);
1915
+ border-bottom: 1px solid #4a7c4a;
1916
+ z-index: 98;
1917
+ padding: 0.5rem 0.75rem;
1918
+ }
1919
+ .draft-prompt-content {
1920
+ display: flex;
1921
+ align-items: center;
1922
+ gap: 0.75rem;
1923
+ font-size: 0.85rem;
1924
+ }
1925
+ .draft-icon {
1926
+ font-size: 1.25rem;
1927
+ color: #8bc48b;
1928
+ font-weight: bold;
1929
+ }
1930
+ .draft-message {
1931
+ display: flex;
1932
+ flex-direction: column;
1933
+ gap: 0.15rem;
1934
+ color: #d4d4d4;
1935
+ flex: 1;
1936
+ }
1937
+ .draft-message strong {
1938
+ color: #a8dca8;
1939
+ }
1940
+ .draft-time {
1941
+ font-size: 0.75rem;
1942
+ color: #7a9a7a;
1943
+ }
1944
+ .draft-actions {
1945
+ display: flex;
1946
+ gap: 0.5rem;
1947
+ }
1948
+ .draft-btn {
1949
+ padding: 0.25rem 0.5rem;
1950
+ border-radius: 0;
1951
+ font-size: 0.8rem;
1952
+ font-family: "JetBrains Mono", "Fira Code", monospace;
1953
+ cursor: pointer;
1954
+ transition: color 0.1s ease;
1955
+ background: transparent;
1956
+ border: none;
1957
+ }
1958
+ .draft-btn.restore {
1959
+ color: #8bc48b;
1960
+ }
1961
+ .draft-btn.restore:hover {
1962
+ color: #c8f0c8;
1963
+ }
1964
+ .draft-btn.discard {
1965
+ color: #9d9d9d;
1966
+ }
1967
+ .draft-btn.discard:hover {
1968
+ color: #d4d4d4;
1969
+ }
1970
+ /* Terminal Key Highlight */
1971
+ .key {
1972
+ color: var(--editor-accent, #8bc48b);
1973
+ font-weight: bold;
1974
+ text-decoration: underline;
1975
+ }
1976
+ /* Toolbar */
1977
+ .toolbar {
1978
+ display: flex;
1979
+ align-items: center;
1980
+ gap: 0.15rem;
1981
+ padding: 0.4rem 0.75rem;
1982
+ background: var(--editor-bg-tertiary, var(--light-bg-primary));
1983
+ border-bottom: 1px solid var(--editor-border, var(--light-border-primary));
1984
+ flex-wrap: wrap;
1985
+ font-family: "JetBrains Mono", "Fira Code", monospace;
1986
+ }
1987
+ .toolbar-group {
1988
+ display: flex;
1989
+ gap: 0.1rem;
1990
+ }
1991
+ .toolbar-btn {
1992
+ padding: 0.2rem 0.35rem;
1993
+ background: transparent;
1994
+ border: none;
1995
+ border-radius: 0;
1996
+ color: var(--editor-accent-dim, #7a9a7a);
1997
+ font-family: inherit;
1998
+ font-size: 0.8rem;
1999
+ cursor: pointer;
2000
+ transition: color 0.1s ease;
2001
+ white-space: nowrap;
2002
+ }
2003
+ .toolbar-btn:hover:not(:disabled) {
2004
+ color: var(--editor-accent-bright, #a8dca8);
2005
+ background: transparent;
2006
+ }
2007
+ .toolbar-btn:hover:not(:disabled) .key {
2008
+ color: var(--editor-accent-glow, #c8f0c8);
2009
+ }
2010
+ .toolbar-btn:disabled {
2011
+ opacity: 0.3;
2012
+ cursor: not-allowed;
2013
+ }
2014
+ .toolbar-btn.toggle-btn {
2015
+ color: var(--editor-accent, #8bc48b);
2016
+ }
2017
+ .toolbar-btn.toggle-btn:hover {
2018
+ color: var(--editor-accent-glow, #c8f0c8);
2019
+ }
2020
+ .toolbar-btn.toggle-btn.active {
2021
+ color: var(--editor-accent-bright, #a8dca8);
2022
+ text-shadow: 0 0 8px color-mix(in srgb, var(--editor-accent, #8bc48b) 50%, transparent);
2023
+ }
2024
+ .toolbar-btn.full-preview-btn {
2025
+ color: #7ab3ff;
2026
+ }
2027
+ .toolbar-btn.full-preview-btn:hover {
2028
+ color: #9ac5ff;
2029
+ }
2030
+ .toolbar-btn.full-preview-btn .key {
2031
+ color: #9ac5ff;
2032
+ }
2033
+ .toolbar-divider {
2034
+ color: #4a4a4a;
2035
+ margin: 0 0.25rem;
2036
+ font-size: 0.8rem;
2037
+ }
2038
+ .toolbar-spacer {
2039
+ flex: 1;
2040
+ }
2041
+ /* Editor Area */
2042
+ .editor-area {
2043
+ display: flex;
2044
+ flex: 1;
2045
+ min-height: 0;
2046
+ }
2047
+ .editor-area.split .editor-panel {
2048
+ width: 50%;
2049
+ border-right: 1px solid var(--light-border-primary);
2050
+ }
2051
+ .editor-area:not(.split) .editor-panel {
2052
+ width: 100%;
2053
+ }
2054
+ .editor-panel {
2055
+ display: flex;
2056
+ flex-direction: column;
2057
+ min-height: 0;
2058
+ }
2059
+ .editor-wrapper {
2060
+ display: flex;
2061
+ flex: 1;
2062
+ min-height: 0;
2063
+ overflow: hidden;
2064
+ }
2065
+ /* Line Numbers */
2066
+ .line-numbers {
2067
+ display: flex;
2068
+ flex-direction: column;
2069
+ padding: 1rem 0;
2070
+ background: var(--editor-bg-tertiary, var(--light-bg-primary));
2071
+ border-right: 1px solid var(--editor-border, var(--light-bg-tertiary));
2072
+ min-width: 3rem;
2073
+ text-align: right;
2074
+ -webkit-user-select: none;
2075
+ -moz-user-select: none;
2076
+ user-select: none;
2077
+ overflow: hidden;
2078
+ }
2079
+ .line-numbers span {
2080
+ padding: 0 0.75rem;
2081
+ color: var(--editor-text-dim, #5a5a5a);
2082
+ font-size: 0.85rem;
2083
+ line-height: 1.6;
2084
+ height: 1.6em;
2085
+ }
2086
+ .line-numbers span.current {
2087
+ color: var(--editor-accent, #8bc48b);
2088
+ background: color-mix(in srgb, var(--editor-accent, #8bc48b) 10%, transparent);
2089
+ }
2090
+ /* Editor Textarea */
2091
+ .editor-textarea {
2092
+ flex: 1;
2093
+ padding: 1rem;
2094
+ background: var(--editor-bg, var(--light-bg-primary));
2095
+ border: none;
2096
+ color: var(--editor-text, #d4d4d4);
2097
+ font-family: inherit;
2098
+ font-size: 0.9rem;
2099
+ line-height: 1.6;
2100
+ resize: none;
2101
+ outline: none;
2102
+ overflow-y: auto;
2103
+ }
2104
+ .editor-textarea::-moz-placeholder {
2105
+ color: var(--editor-text-dim, #5a5a5a);
2106
+ font-style: italic;
2107
+ }
2108
+ .editor-textarea::placeholder {
2109
+ color: var(--editor-text-dim, #5a5a5a);
2110
+ font-style: italic;
2111
+ }
2112
+ .editor-textarea:disabled {
2113
+ opacity: 0.7;
2114
+ cursor: not-allowed;
2115
+ }
2116
+ /* Preview Panel */
2117
+ .preview-panel {
2118
+ width: 50%;
2119
+ display: flex;
2120
+ flex-direction: column;
2121
+ background: #252526;
2122
+ min-height: 0;
2123
+ }
2124
+ .preview-header {
2125
+ padding: 0.5rem 1rem;
2126
+ background: #2d2d2d;
2127
+ border-bottom: 1px solid var(--light-border-primary);
2128
+ }
2129
+ .preview-label {
2130
+ color: #8bc48b;
2131
+ font-size: 0.85rem;
2132
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2133
+ }
2134
+ .preview-content {
2135
+ flex: 1;
2136
+ padding: 1rem;
2137
+ overflow-y: auto;
2138
+ color: #d4d4d4;
2139
+ font-family:
2140
+ -apple-system,
2141
+ BlinkMacSystemFont,
2142
+ "Segoe UI",
2143
+ Roboto,
2144
+ sans-serif;
2145
+ font-size: 0.95rem;
2146
+ line-height: 1.7;
2147
+ }
2148
+ .preview-placeholder {
2149
+ color: #5a5a5a;
2150
+ font-style: italic;
2151
+ }
2152
+ /* Preview content styles */
2153
+ .preview-content :global(h1),
2154
+ .preview-content :global(h2),
2155
+ .preview-content :global(h3),
2156
+ .preview-content :global(h4),
2157
+ .preview-content :global(h5),
2158
+ .preview-content :global(h6) {
2159
+ color: #8bc48b;
2160
+ margin-top: 1.5rem;
2161
+ margin-bottom: 0.75rem;
2162
+ font-weight: 600;
2163
+ }
2164
+ .preview-content :global(h1) {
2165
+ font-size: 1.75rem;
2166
+ border-bottom: 1px solid var(--light-border-primary);
2167
+ padding-bottom: 0.5rem;
2168
+ }
2169
+ .preview-content :global(h2) {
2170
+ font-size: 1.5rem;
2171
+ }
2172
+ .preview-content :global(h3) {
2173
+ font-size: 1.25rem;
2174
+ }
2175
+ .preview-content :global(p) {
2176
+ margin: 0.75rem 0;
2177
+ }
2178
+ .preview-content :global(a) {
2179
+ color: #6cb36c;
2180
+ text-decoration: underline;
2181
+ }
2182
+ .preview-content :global(code) {
2183
+ background: var(--light-bg-primary);
2184
+ padding: 0.15rem 0.4rem;
2185
+ border-radius: 3px;
2186
+ font-family: inherit;
2187
+ font-size: 0.9em;
2188
+ color: #ce9178;
2189
+ }
2190
+ .preview-content :global(pre) {
2191
+ background: var(--light-bg-primary);
2192
+ padding: 1rem;
2193
+ border-radius: 4px;
2194
+ overflow-x: auto;
2195
+ border: 1px solid var(--light-bg-tertiary);
2196
+ }
2197
+ .preview-content :global(pre code) {
2198
+ background: none;
2199
+ padding: 0;
2200
+ color: #d4d4d4;
2201
+ }
2202
+ .preview-content :global(blockquote) {
2203
+ border-left: 3px solid #4a7c4a;
2204
+ margin: 1rem 0;
2205
+ padding-left: 1rem;
2206
+ color: #9d9d9d;
2207
+ font-style: italic;
2208
+ }
2209
+ .preview-content :global(ul),
2210
+ .preview-content :global(ol) {
2211
+ margin: 0.75rem 0;
2212
+ padding-left: 1.5rem;
2213
+ }
2214
+ .preview-content :global(li) {
2215
+ margin: 0.25rem 0;
2216
+ }
2217
+ .preview-content :global(hr) {
2218
+ border: none;
2219
+ border-top: 1px solid var(--light-border-primary);
2220
+ margin: 1.5rem 0;
2221
+ }
2222
+ .preview-content :global(img) {
2223
+ max-width: 100%;
2224
+ border-radius: 4px;
2225
+ }
2226
+ /* Status Bar */
2227
+ .status-bar {
2228
+ display: flex;
2229
+ justify-content: space-between;
2230
+ align-items: center;
2231
+ padding: 0.35rem 0.75rem;
2232
+ background: var(--editor-status-bg, var(--light-border-secondary));
2233
+ border-top: 1px solid var(--editor-status-border, var(--light-border-secondary));
2234
+ font-size: 0.75rem;
2235
+ color: var(--editor-accent-bright, #a8dca8);
2236
+ }
2237
+ .status-left,
2238
+ .status-right {
2239
+ display: flex;
2240
+ align-items: center;
2241
+ gap: 0.5rem;
2242
+ }
2243
+ .status-item {
2244
+ opacity: 0.9;
2245
+ }
2246
+ .status-divider {
2247
+ opacity: 0.4;
2248
+ }
2249
+ .status-saving {
2250
+ color: #f0c674;
2251
+ animation: pulse 1s ease-in-out infinite;
2252
+ }
2253
+ .status-draft {
2254
+ color: #7a9a7a;
2255
+ font-style: italic;
2256
+ }
2257
+ @keyframes pulse {
2258
+ 0%,
2259
+ 100% {
2260
+ opacity: 1;
2261
+ }
2262
+ 50% {
2263
+ opacity: 0.5;
2264
+ }
2265
+ }
2266
+ /* Responsive */
2267
+ @media (max-width: 768px) {
2268
+ .editor-area.split {
2269
+ flex-direction: column;
2270
+ }
2271
+ .editor-area.split .editor-panel {
2272
+ width: 100%;
2273
+ border-right: none;
2274
+ border-bottom: 1px solid var(--light-border-primary);
2275
+ height: 50%;
2276
+ }
2277
+ .editor-area.split .preview-panel {
2278
+ width: 100%;
2279
+ height: 50%;
2280
+ }
2281
+ .toolbar {
2282
+ padding: 0.5rem;
2283
+ }
2284
+ .toolbar-btn {
2285
+ padding: 0.3rem 0.5rem;
2286
+ font-size: 0.75rem;
2287
+ }
2288
+ }
2289
+ /* Full Preview Button */
2290
+ .full-preview-btn {
2291
+ background: #2d3a4d;
2292
+ color: #7ab3ff;
2293
+ border-color: #3d4a5d;
2294
+ }
2295
+ .full-preview-btn:hover {
2296
+ background: #3d4a5d;
2297
+ color: #9ac5ff;
2298
+ }
2299
+ /* Full Preview Modal */
2300
+ .full-preview-modal {
2301
+ position: fixed;
2302
+ inset: 0;
2303
+ z-index: 1000;
2304
+ display: flex;
2305
+ align-items: center;
2306
+ justify-content: center;
2307
+ }
2308
+ .full-preview-backdrop {
2309
+ position: absolute;
2310
+ inset: 0;
2311
+ background: rgba(0, 0, 0, 0.7);
2312
+ }
2313
+ .full-preview-container {
2314
+ position: relative;
2315
+ width: 90%;
2316
+ max-width: 900px;
2317
+ height: 90vh;
2318
+ background: var(--color-bg, var(--light-bg-primary));
2319
+ border-radius: 12px;
2320
+ display: flex;
2321
+ flex-direction: column;
2322
+ overflow: hidden;
2323
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
2324
+ }
2325
+ :global(.dark) .full-preview-container {
2326
+ background: var(--color-bg-dark, #0d1117);
2327
+ }
2328
+ .full-preview-header {
2329
+ display: flex;
2330
+ justify-content: space-between;
2331
+ align-items: center;
2332
+ padding: 1rem 1.5rem;
2333
+ background: var(--color-bg-secondary, var(--light-bg-tertiary));
2334
+ border-bottom: 1px solid var(--color-border, var(--light-border-primary));
2335
+ flex-shrink: 0;
2336
+ }
2337
+ :global(.dark) .full-preview-header {
2338
+ background: var(--color-bg-secondary-dark, var(--light-bg-primary));
2339
+ border-color: var(--color-border-dark, var(--light-border-secondary));
2340
+ }
2341
+ .full-preview-header h2 {
2342
+ margin: 0;
2343
+ font-size: 0.9rem;
2344
+ font-weight: 500;
2345
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2346
+ color: #8bc48b;
2347
+ }
2348
+ :global(.dark) .full-preview-header h2 {
2349
+ color: #8bc48b;
2350
+ }
2351
+ .full-preview-close {
2352
+ padding: 0.3rem 0.5rem;
2353
+ background: transparent;
2354
+ color: #7a9a7a;
2355
+ border: none;
2356
+ font-size: 0.85rem;
2357
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2358
+ cursor: pointer;
2359
+ transition: color 0.1s ease;
2360
+ }
2361
+ .full-preview-close:hover {
2362
+ color: #a8dca8;
2363
+ }
2364
+ .full-preview-scroll {
2365
+ flex: 1;
2366
+ overflow-y: auto;
2367
+ padding: 2rem;
2368
+ }
2369
+ .full-preview-article {
2370
+ max-width: 800px;
2371
+ margin: 0 auto;
2372
+ }
2373
+ /* Post meta styling in full preview */
2374
+ .full-preview-article .post-meta {
2375
+ display: flex;
2376
+ align-items: center;
2377
+ gap: 1rem;
2378
+ flex-wrap: wrap;
2379
+ margin-top: 1rem;
2380
+ }
2381
+ .full-preview-article time {
2382
+ color: var(--light-text-light);
2383
+ font-size: 1rem;
2384
+ transition: color 0.3s ease;
2385
+ }
2386
+ :global(.dark) .full-preview-article time {
2387
+ color: var(--color-text-subtle-dark, #666);
2388
+ }
2389
+ .full-preview-article .tags {
2390
+ display: flex;
2391
+ gap: 0.5rem;
2392
+ flex-wrap: wrap;
2393
+ }
2394
+ .full-preview-article .tag {
2395
+ padding: 0.25rem 0.75rem;
2396
+ background: var(--tag-bg, #2c5f2d);
2397
+ color: white;
2398
+ border-radius: 12px;
2399
+ font-size: 0.8rem;
2400
+ font-weight: 500;
2401
+ }
2402
+ /* Line numbers scroll sync */
2403
+ .line-numbers {
2404
+ overflow: hidden;
2405
+ }
2406
+ /* Status bar enhancements */
2407
+ .status-goal {
2408
+ color: var(--editor-accent, #8bc48b);
2409
+ font-weight: 500;
2410
+ }
2411
+ .status-campfire {
2412
+ color: #f0a060;
2413
+ }
2414
+ .status-mode {
2415
+ color: #7ab3ff;
2416
+ font-size: 0.75rem;
2417
+ }
2418
+ /* Zen Mode Styles */
2419
+ .editor-container.zen-mode {
2420
+ position: fixed;
2421
+ inset: 0;
2422
+ z-index: 9999;
2423
+ border-radius: 0;
2424
+ border: none;
2425
+ }
2426
+ .editor-container.zen-mode .toolbar {
2427
+ opacity: 0.3;
2428
+ transition: opacity 0.3s ease;
2429
+ }
2430
+ .editor-container.zen-mode .toolbar:hover {
2431
+ opacity: 1;
2432
+ }
2433
+ .editor-container.zen-mode .status-bar {
2434
+ opacity: 0.5;
2435
+ transition: opacity 0.3s ease;
2436
+ }
2437
+ .editor-container.zen-mode .status-bar:hover {
2438
+ opacity: 1;
2439
+ }
2440
+ .editor-container.zen-mode .editor-area {
2441
+ height: calc(100vh - 80px);
2442
+ }
2443
+ /* Campfire Mode Styles */
2444
+ .editor-container.campfire-mode {
2445
+ border-color: #8b5a2b;
2446
+ box-shadow: 0 0 30px rgba(240, 160, 96, 0.15);
2447
+ }
2448
+ .campfire-controls {
2449
+ position: fixed;
2450
+ bottom: 2rem;
2451
+ right: 2rem;
2452
+ display: flex;
2453
+ align-items: center;
2454
+ gap: 1rem;
2455
+ padding: 0.75rem 1.25rem;
2456
+ background: rgba(40, 30, 20, 0.95);
2457
+ border: 1px solid #8b5a2b;
2458
+ border-radius: 8px;
2459
+ color: #f0d0a0;
2460
+ z-index: 1000;
2461
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
2462
+ }
2463
+ .campfire-ember {
2464
+ width: 12px;
2465
+ height: 12px;
2466
+ background: linear-gradient(135deg, #ff6b35, #f0a060);
2467
+ border-radius: 50%;
2468
+ animation: ember-glow 2s ease-in-out infinite;
2469
+ }
2470
+ @keyframes ember-glow {
2471
+ 0%, 100% {
2472
+ box-shadow: 0 0 8px #ff6b35, 0 0 16px rgba(240, 107, 53, 0.5);
2473
+ }
2474
+ 50% {
2475
+ box-shadow: 0 0 12px #f0a060, 0 0 24px rgba(240, 160, 96, 0.6);
2476
+ }
2477
+ }
2478
+ .campfire-stats {
2479
+ display: flex;
2480
+ flex-direction: column;
2481
+ gap: 0.15rem;
2482
+ }
2483
+ .campfire-time {
2484
+ font-size: 1.1rem;
2485
+ font-weight: 600;
2486
+ font-family: "JetBrains Mono", monospace;
2487
+ }
2488
+ .campfire-words {
2489
+ font-size: 0.75rem;
2490
+ color: #c0a080;
2491
+ }
2492
+ .campfire-end {
2493
+ padding: 0.3rem 0.5rem;
2494
+ background: transparent;
2495
+ border: none;
2496
+ color: #c0a080;
2497
+ font-size: 0.8rem;
2498
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2499
+ cursor: pointer;
2500
+ transition: color 0.1s ease;
2501
+ }
2502
+ .campfire-end:hover {
2503
+ color: #f0d0a0;
2504
+ }
2505
+ /* Slash Commands Menu */
2506
+ .slash-menu {
2507
+ position: fixed;
2508
+ top: 50%;
2509
+ left: 50%;
2510
+ transform: translate(-50%, -50%);
2511
+ min-width: 220px;
2512
+ max-height: 300px;
2513
+ overflow-y: auto;
2514
+ background: #252526;
2515
+ border: 1px solid var(--light-border-primary);
2516
+ border-radius: 8px;
2517
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
2518
+ z-index: 1001;
2519
+ }
2520
+ .slash-menu-header {
2521
+ padding: 0.5rem 0.75rem;
2522
+ font-size: 0.8rem;
2523
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2524
+ color: #8bc48b;
2525
+ border-bottom: 1px solid var(--light-border-primary);
2526
+ }
2527
+ .slash-menu-item {
2528
+ display: flex;
2529
+ align-items: center;
2530
+ width: 100%;
2531
+ padding: 0.6rem 0.75rem;
2532
+ background: transparent;
2533
+ border: none;
2534
+ color: #d4d4d4;
2535
+ font-size: 0.85rem;
2536
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2537
+ text-align: left;
2538
+ cursor: pointer;
2539
+ transition: background-color 0.1s ease;
2540
+ }
2541
+ .slash-menu-item:hover,
2542
+ .slash-menu-item.selected {
2543
+ background: var(--light-border-primary);
2544
+ }
2545
+ .slash-menu-item.selected {
2546
+ color: #8bc48b;
2547
+ }
2548
+ .slash-menu-empty {
2549
+ padding: 0.75rem;
2550
+ color: #7a9a7a;
2551
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2552
+ font-size: 0.8rem;
2553
+ text-align: center;
2554
+ }
2555
+ /* Command Palette */
2556
+ .command-palette-overlay {
2557
+ position: fixed;
2558
+ inset: 0;
2559
+ background: rgba(0, 0, 0, 0.5);
2560
+ display: flex;
2561
+ align-items: flex-start;
2562
+ justify-content: center;
2563
+ padding-top: 15vh;
2564
+ z-index: 1002;
2565
+ }
2566
+ .command-palette {
2567
+ width: 100%;
2568
+ max-width: 500px;
2569
+ background: var(--light-bg-primary);
2570
+ border: 1px solid var(--light-border-primary);
2571
+ border-radius: 8px;
2572
+ box-shadow: 0 16px 64px rgba(0, 0, 0, 0.6);
2573
+ overflow: hidden;
2574
+ }
2575
+ .command-palette-input {
2576
+ width: 100%;
2577
+ padding: 1rem;
2578
+ background: transparent;
2579
+ border: none;
2580
+ border-bottom: 1px solid var(--light-border-primary);
2581
+ color: #d4d4d4;
2582
+ font-size: 1rem;
2583
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2584
+ outline: none;
2585
+ }
2586
+ .command-palette-input::-moz-placeholder {
2587
+ color: #7a9a7a;
2588
+ }
2589
+ .command-palette-input::placeholder {
2590
+ color: #7a9a7a;
2591
+ }
2592
+ .command-palette-list {
2593
+ max-height: 300px;
2594
+ overflow-y: auto;
2595
+ }
2596
+ .command-palette-item {
2597
+ display: flex;
2598
+ align-items: center;
2599
+ justify-content: space-between;
2600
+ width: 100%;
2601
+ padding: 0.75rem 1rem;
2602
+ background: transparent;
2603
+ border: none;
2604
+ color: #d4d4d4;
2605
+ font-size: 0.9rem;
2606
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2607
+ text-align: left;
2608
+ cursor: pointer;
2609
+ transition: background-color 0.1s ease;
2610
+ }
2611
+ .command-palette-item:hover,
2612
+ .command-palette-item.selected {
2613
+ background: var(--light-bg-tertiary);
2614
+ }
2615
+ .command-palette-item.selected {
2616
+ color: #8bc48b;
2617
+ }
2618
+ .palette-cmd-shortcut {
2619
+ font-size: 0.75rem;
2620
+ color: #6a6a6a;
2621
+ font-family: "JetBrains Mono", monospace;
2622
+ }
2623
+ /* Mermaid Diagram Styles */
2624
+ :global(.mermaid-container) {
2625
+ margin: 1.5rem 0;
2626
+ padding: 1rem;
2627
+ background: var(--light-bg-primary);
2628
+ border: 1px solid var(--light-border-primary);
2629
+ border-radius: 8px;
2630
+ overflow-x: auto;
2631
+ }
2632
+ :global(.mermaid) {
2633
+ display: flex;
2634
+ justify-content: center;
2635
+ }
2636
+ :global(.mermaid svg) {
2637
+ max-width: 100%;
2638
+ height: auto;
2639
+ }
2640
+ /* Mermaid error styling */
2641
+ :global(.mermaid-container .error) {
2642
+ color: #e07030;
2643
+ padding: 0.5rem;
2644
+ font-family: monospace;
2645
+ font-size: 0.85rem;
2646
+ }
2647
+ /* Mode Transitions */
2648
+ .editor-container {
2649
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
2650
+ }
2651
+ .toolbar,
2652
+ .status-bar {
2653
+ transition: opacity 0.3s ease;
2654
+ }
2655
+ .campfire-controls {
2656
+ animation: fade-in 0.3s ease;
2657
+ }
2658
+ @keyframes fade-in {
2659
+ from {
2660
+ opacity: 0;
2661
+ transform: translateY(10px);
2662
+ }
2663
+ to {
2664
+ opacity: 1;
2665
+ transform: translateY(0);
2666
+ }
2667
+ }
2668
+ .slash-menu,
2669
+ .command-palette {
2670
+ animation: scale-in 0.15s ease;
2671
+ }
2672
+ @keyframes scale-in {
2673
+ from {
2674
+ opacity: 0;
2675
+ transform: translate(-50%, -50%) scale(0.95);
2676
+ }
2677
+ to {
2678
+ opacity: 1;
2679
+ transform: translate(-50%, -50%) scale(1);
2680
+ }
2681
+ }
2682
+ .command-palette {
2683
+ animation: slide-down 0.2s ease;
2684
+ }
2685
+ @keyframes slide-down {
2686
+ from {
2687
+ opacity: 0;
2688
+ transform: translateY(-10px);
2689
+ }
2690
+ to {
2691
+ opacity: 1;
2692
+ transform: translateY(0);
2693
+ }
2694
+ }
2695
+ /* Snippets Modal */
2696
+ .snippets-modal-overlay {
2697
+ position: fixed;
2698
+ inset: 0;
2699
+ background: rgba(0, 0, 0, 0.6);
2700
+ display: flex;
2701
+ align-items: center;
2702
+ justify-content: center;
2703
+ z-index: 1003;
2704
+ animation: fade-in 0.2s ease;
2705
+ }
2706
+ .snippets-modal {
2707
+ width: 90%;
2708
+ max-width: 500px;
2709
+ max-height: 80vh;
2710
+ background: var(--light-bg-primary);
2711
+ border: 1px solid var(--light-border-primary);
2712
+ border-radius: 12px;
2713
+ display: flex;
2714
+ flex-direction: column;
2715
+ overflow: hidden;
2716
+ box-shadow: 0 16px 64px rgba(0, 0, 0, 0.5);
2717
+ animation: scale-in 0.2s ease;
2718
+ }
2719
+ .snippets-modal-header {
2720
+ display: flex;
2721
+ justify-content: space-between;
2722
+ align-items: center;
2723
+ padding: 1rem 1.25rem;
2724
+ background: #252526;
2725
+ border-bottom: 1px solid var(--light-border-primary);
2726
+ }
2727
+ .snippets-modal-header h3 {
2728
+ margin: 0;
2729
+ font-size: 0.9rem;
2730
+ font-weight: 500;
2731
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2732
+ color: #8bc48b;
2733
+ }
2734
+ .snippets-modal-close {
2735
+ display: flex;
2736
+ align-items: center;
2737
+ justify-content: center;
2738
+ background: transparent;
2739
+ border: none;
2740
+ color: #7a9a7a;
2741
+ font-size: 0.85rem;
2742
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2743
+ cursor: pointer;
2744
+ transition: color 0.1s ease;
2745
+ }
2746
+ .snippets-modal-close:hover {
2747
+ color: #a8dca8;
2748
+ }
2749
+ .snippets-modal-body {
2750
+ padding: 1.25rem;
2751
+ overflow-y: auto;
2752
+ }
2753
+ .snippets-form {
2754
+ display: flex;
2755
+ flex-direction: column;
2756
+ gap: 1rem;
2757
+ }
2758
+ .snippet-field {
2759
+ display: flex;
2760
+ flex-direction: column;
2761
+ gap: 0.4rem;
2762
+ }
2763
+ .snippet-field label {
2764
+ font-size: 0.85rem;
2765
+ font-weight: 500;
2766
+ color: #a8dca8;
2767
+ }
2768
+ .snippet-field input,
2769
+ .snippet-field textarea {
2770
+ padding: 0.6rem 0.75rem;
2771
+ background: #252526;
2772
+ border: 1px solid var(--light-border-primary);
2773
+ border-radius: 6px;
2774
+ color: #d4d4d4;
2775
+ font-family: inherit;
2776
+ font-size: 0.9rem;
2777
+ transition: border-color 0.2s ease;
2778
+ }
2779
+ .snippet-field input:focus,
2780
+ .snippet-field textarea:focus {
2781
+ outline: none;
2782
+ border-color: #4a7c4a;
2783
+ }
2784
+ .snippet-field textarea {
2785
+ resize: vertical;
2786
+ min-height: 100px;
2787
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2788
+ line-height: 1.5;
2789
+ }
2790
+ .field-hint {
2791
+ font-size: 0.75rem;
2792
+ color: #6a6a6a;
2793
+ font-style: italic;
2794
+ }
2795
+ .snippet-actions {
2796
+ display: flex;
2797
+ justify-content: space-between;
2798
+ align-items: center;
2799
+ margin-top: 0.5rem;
2800
+ padding-top: 1rem;
2801
+ border-top: 1px solid var(--light-bg-tertiary);
2802
+ }
2803
+ .snippet-actions-right {
2804
+ display: flex;
2805
+ gap: 0.5rem;
2806
+ margin-left: auto;
2807
+ }
2808
+ .snippet-btn {
2809
+ padding: 0.3rem 0.5rem;
2810
+ border-radius: 0;
2811
+ font-size: 0.85rem;
2812
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2813
+ cursor: pointer;
2814
+ transition: color 0.1s ease;
2815
+ background: transparent;
2816
+ border: none;
2817
+ }
2818
+ .snippet-btn.save {
2819
+ color: #8bc48b;
2820
+ }
2821
+ .snippet-btn.save:hover:not(:disabled) {
2822
+ color: #c8f0c8;
2823
+ }
2824
+ .snippet-btn.save:disabled {
2825
+ opacity: 0.4;
2826
+ cursor: not-allowed;
2827
+ }
2828
+ .snippet-btn.cancel {
2829
+ color: #9d9d9d;
2830
+ }
2831
+ .snippet-btn.cancel:hover {
2832
+ color: #d4d4d4;
2833
+ }
2834
+ .snippet-btn.delete {
2835
+ color: #e08080;
2836
+ }
2837
+ .snippet-btn.delete:hover {
2838
+ color: #ff9090;
2839
+ }
2840
+ .snippets-list-divider {
2841
+ display: flex;
2842
+ align-items: center;
2843
+ margin: 1.25rem 0 0.75rem;
2844
+ color: #8bc48b;
2845
+ font-size: 0.8rem;
2846
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2847
+ }
2848
+ .snippets-list-divider::before,
2849
+ .snippets-list-divider::after {
2850
+ content: "";
2851
+ flex: 1;
2852
+ height: 1px;
2853
+ background: var(--light-border-primary);
2854
+ }
2855
+ .snippets-list-divider span {
2856
+ padding: 0 0.75rem;
2857
+ }
2858
+ .snippets-list {
2859
+ display: flex;
2860
+ flex-direction: column;
2861
+ gap: 0.25rem;
2862
+ }
2863
+ .snippet-list-item {
2864
+ display: flex;
2865
+ justify-content: space-between;
2866
+ align-items: center;
2867
+ width: 100%;
2868
+ padding: 0.6rem 0.75rem;
2869
+ background: #252526;
2870
+ border: 1px solid transparent;
2871
+ border-radius: 6px;
2872
+ color: #d4d4d4;
2873
+ font-size: 0.9rem;
2874
+ text-align: left;
2875
+ cursor: pointer;
2876
+ transition: all 0.15s ease;
2877
+ }
2878
+ .snippet-list-item:hover {
2879
+ background: var(--light-bg-tertiary);
2880
+ border-color: var(--light-border-primary);
2881
+ }
2882
+ .snippet-name {
2883
+ font-weight: 500;
2884
+ }
2885
+ .snippet-trigger {
2886
+ font-size: 0.75rem;
2887
+ color: #7ab3ff;
2888
+ font-family: "JetBrains Mono", monospace;
2889
+ background: #1a2a3a;
2890
+ padding: 0.15rem 0.4rem;
2891
+ border-radius: 3px;
2892
+ }
2893
+ /* Status Bar Sound Button */
2894
+ .status-sound-btn {
2895
+ display: flex;
2896
+ align-items: center;
2897
+ gap: 0.25rem;
2898
+ padding: 0.15rem 0.4rem;
2899
+ background: transparent;
2900
+ border: 1px solid transparent;
2901
+ border-radius: 4px;
2902
+ color: #7a9a7a;
2903
+ font-size: 0.85rem;
2904
+ cursor: pointer;
2905
+ transition: all 0.15s ease;
2906
+ position: relative;
2907
+ }
2908
+ .status-sound-btn:hover {
2909
+ background: rgba(139, 196, 139, 0.1);
2910
+ color: #a8dca8;
2911
+ }
2912
+ .status-sound-btn.playing {
2913
+ color: #8bc48b;
2914
+ }
2915
+ .sound-wave {
2916
+ width: 10px;
2917
+ height: 10px;
2918
+ border-radius: 50%;
2919
+ background: #8bc48b;
2920
+ animation: sound-pulse 1.5s ease-in-out infinite;
2921
+ }
2922
+ @keyframes sound-pulse {
2923
+ 0%, 100% {
2924
+ opacity: 0.4;
2925
+ transform: scale(0.8);
2926
+ }
2927
+ 50% {
2928
+ opacity: 1;
2929
+ transform: scale(1);
2930
+ }
2931
+ }
2932
+ /* Sound Panel */
2933
+ .sound-panel {
2934
+ position: fixed;
2935
+ bottom: 3.5rem;
2936
+ right: 1rem;
2937
+ width: 280px;
2938
+ background: var(--light-bg-primary);
2939
+ border: 1px solid var(--light-border-primary);
2940
+ border-radius: 12px;
2941
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
2942
+ z-index: 1001;
2943
+ animation: slide-up 0.2s ease;
2944
+ }
2945
+ @keyframes slide-up {
2946
+ from {
2947
+ opacity: 0;
2948
+ transform: translateY(10px);
2949
+ }
2950
+ to {
2951
+ opacity: 1;
2952
+ transform: translateY(0);
2953
+ }
2954
+ }
2955
+ .sound-panel-header {
2956
+ display: flex;
2957
+ justify-content: space-between;
2958
+ align-items: center;
2959
+ padding: 0.75rem 1rem;
2960
+ border-bottom: 1px solid var(--light-border-primary);
2961
+ }
2962
+ .sound-panel-title {
2963
+ font-size: 0.85rem;
2964
+ font-weight: 500;
2965
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2966
+ color: #8bc48b;
2967
+ }
2968
+ .sound-panel-close {
2969
+ display: flex;
2970
+ align-items: center;
2971
+ justify-content: center;
2972
+ background: transparent;
2973
+ border: none;
2974
+ color: #7a9a7a;
2975
+ font-size: 0.85rem;
2976
+ font-family: "JetBrains Mono", "Fira Code", monospace;
2977
+ cursor: pointer;
2978
+ transition: color 0.1s ease;
2979
+ }
2980
+ .sound-panel-close:hover {
2981
+ color: #a8dca8;
2982
+ }
2983
+ .sound-options {
2984
+ display: grid;
2985
+ grid-template-columns: repeat(5, 1fr);
2986
+ gap: 0.5rem;
2987
+ padding: 1rem;
2988
+ }
2989
+ .sound-option {
2990
+ display: flex;
2991
+ flex-direction: column;
2992
+ align-items: center;
2993
+ gap: 0.25rem;
2994
+ padding: 0.5rem 0.25rem;
2995
+ background: #252526;
2996
+ border: 1px solid transparent;
2997
+ border-radius: 8px;
2998
+ cursor: pointer;
2999
+ transition: all 0.15s ease;
3000
+ }
3001
+ .sound-option:hover {
3002
+ background: var(--light-bg-tertiary);
3003
+ border-color: var(--light-border-primary);
3004
+ }
3005
+ .sound-option.active {
3006
+ background: var(--light-border-secondary);
3007
+ border-color: #4a7c4a;
3008
+ }
3009
+ .sound-option.playing {
3010
+ border-color: #8bc48b;
3011
+ box-shadow: 0 0 8px rgba(139, 196, 139, 0.3);
3012
+ }
3013
+ .sound-icon {
3014
+ font-size: 1.25rem;
3015
+ }
3016
+ .sound-name {
3017
+ font-size: 0.65rem;
3018
+ color: #9d9d9d;
3019
+ text-align: center;
3020
+ }
3021
+ .sound-option.active .sound-name {
3022
+ color: #a8dca8;
3023
+ }
3024
+ .sound-controls {
3025
+ display: flex;
3026
+ align-items: center;
3027
+ gap: 1rem;
3028
+ padding: 0 1rem 1rem;
3029
+ }
3030
+ .volume-label {
3031
+ flex: 1;
3032
+ display: flex;
3033
+ flex-direction: column;
3034
+ gap: 0.35rem;
3035
+ }
3036
+ .volume-label span {
3037
+ font-size: 0.75rem;
3038
+ color: #7a9a7a;
3039
+ }
3040
+ .volume-slider {
3041
+ width: 100%;
3042
+ height: 4px;
3043
+ -webkit-appearance: none;
3044
+ -moz-appearance: none;
3045
+ appearance: none;
3046
+ background: var(--light-border-primary);
3047
+ border-radius: 2px;
3048
+ cursor: pointer;
3049
+ }
3050
+ .volume-slider::-webkit-slider-thumb {
3051
+ -webkit-appearance: none;
3052
+ width: 14px;
3053
+ height: 14px;
3054
+ background: #8bc48b;
3055
+ border-radius: 50%;
3056
+ cursor: pointer;
3057
+ -webkit-transition: transform 0.15s ease;
3058
+ transition: transform 0.15s ease;
3059
+ }
3060
+ .volume-slider::-webkit-slider-thumb:hover {
3061
+ transform: scale(1.2);
3062
+ }
3063
+ .volume-slider::-moz-range-thumb {
3064
+ width: 14px;
3065
+ height: 14px;
3066
+ background: #8bc48b;
3067
+ border-radius: 50%;
3068
+ cursor: pointer;
3069
+ border: none;
3070
+ }
3071
+ .sound-play-btn {
3072
+ display: flex;
3073
+ align-items: center;
3074
+ gap: 0.25rem;
3075
+ padding: 0.3rem 0.5rem;
3076
+ background: transparent;
3077
+ border: none;
3078
+ color: #7a9a7a;
3079
+ font-size: 0.8rem;
3080
+ font-family: "JetBrains Mono", "Fira Code", monospace;
3081
+ cursor: pointer;
3082
+ transition: color 0.1s ease;
3083
+ }
3084
+ .sound-play-btn:hover {
3085
+ color: #a8dca8;
3086
+ }
3087
+ .sound-play-btn.playing {
3088
+ color: #8bc48b;
3089
+ }
3090
+ .sound-play-btn.playing:hover {
3091
+ color: #c8f0c8;
3092
+ }
3093
+ .sound-note {
3094
+ display: flex;
3095
+ align-items: center;
3096
+ gap: 0.5rem;
3097
+ padding: 0.75rem 1rem;
3098
+ background: #252526;
3099
+ border-top: 1px solid var(--light-border-primary);
3100
+ border-radius: 0 0 12px 12px;
3101
+ font-size: 0.7rem;
3102
+ color: #6a6a6a;
3103
+ }
3104
+ .sound-note-icon {
3105
+ font-size: 0.85rem;
3106
+ }
3107
+ .sound-note code {
3108
+ background: var(--light-bg-primary);
3109
+ padding: 0.1rem 0.3rem;
3110
+ border-radius: 3px;
3111
+ font-family: "JetBrains Mono", monospace;
3112
+ font-size: 0.65rem;
3113
+ }
3114
+ </style>