@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.
- package/README.md +163 -0
- package/dist/auth/jwt.d.ts +14 -0
- package/dist/auth/jwt.js +109 -0
- package/dist/auth/session.d.ts +42 -0
- package/dist/auth/session.js +105 -0
- package/dist/components/admin/GutterManager.svelte +910 -0
- package/dist/components/admin/GutterManager.svelte.d.ts +15 -0
- package/dist/components/admin/MarkdownEditor.svelte +3114 -0
- package/dist/components/admin/MarkdownEditor.svelte.d.ts +43 -0
- package/dist/components/custom/CollapsibleSection.svelte +74 -0
- package/dist/components/custom/CollapsibleSection.svelte.d.ts +15 -0
- package/dist/components/custom/ContentWithGutter.svelte +646 -0
- package/dist/components/custom/ContentWithGutter.svelte.d.ts +19 -0
- package/dist/components/custom/GutterItem.svelte +201 -0
- package/dist/components/custom/GutterItem.svelte.d.ts +11 -0
- package/dist/components/custom/LeftGutter.svelte +271 -0
- package/dist/components/custom/LeftGutter.svelte.d.ts +17 -0
- package/dist/components/custom/MobileTOC.svelte +273 -0
- package/dist/components/custom/MobileTOC.svelte.d.ts +11 -0
- package/dist/components/custom/TableOfContents.svelte +163 -0
- package/dist/components/custom/TableOfContents.svelte.d.ts +11 -0
- package/dist/components/gallery/ImageGallery.svelte +681 -0
- package/dist/components/gallery/ImageGallery.svelte.d.ts +11 -0
- package/dist/components/gallery/Lightbox.svelte +107 -0
- package/dist/components/gallery/Lightbox.svelte.d.ts +19 -0
- package/dist/components/gallery/LightboxCaption.svelte +25 -0
- package/dist/components/gallery/LightboxCaption.svelte.d.ts +11 -0
- package/dist/components/gallery/ZoomableImage.svelte +163 -0
- package/dist/components/gallery/ZoomableImage.svelte.d.ts +17 -0
- package/dist/components/ui/Accordion.svelte +74 -0
- package/dist/components/ui/Accordion.svelte.d.ts +42 -0
- package/dist/components/ui/Badge.svelte +48 -0
- package/dist/components/ui/Badge.svelte.d.ts +26 -0
- package/dist/components/ui/Button.svelte +74 -0
- package/dist/components/ui/Button.svelte.d.ts +34 -0
- package/dist/components/ui/Card.svelte +102 -0
- package/dist/components/ui/Card.svelte.d.ts +46 -0
- package/dist/components/ui/Dialog.svelte +91 -0
- package/dist/components/ui/Dialog.svelte.d.ts +43 -0
- package/dist/components/ui/Input.svelte +81 -0
- package/dist/components/ui/Input.svelte.d.ts +35 -0
- package/dist/components/ui/Select.svelte +69 -0
- package/dist/components/ui/Select.svelte.d.ts +36 -0
- package/dist/components/ui/Sheet.svelte +98 -0
- package/dist/components/ui/Sheet.svelte.d.ts +45 -0
- package/dist/components/ui/Skeleton.svelte +31 -0
- package/dist/components/ui/Skeleton.svelte.d.ts +26 -0
- package/dist/components/ui/Table.svelte +59 -0
- package/dist/components/ui/Table.svelte.d.ts +44 -0
- package/dist/components/ui/Tabs.svelte +76 -0
- package/dist/components/ui/Tabs.svelte.d.ts +41 -0
- package/dist/components/ui/Textarea.svelte +81 -0
- package/dist/components/ui/Textarea.svelte.d.ts +35 -0
- package/dist/components/ui/Toast.svelte +18 -0
- package/dist/components/ui/Toast.svelte.d.ts +7 -0
- package/dist/components/ui/accordion/accordion-content.svelte +24 -0
- package/dist/components/ui/accordion/accordion-content.svelte.d.ts +4 -0
- package/dist/components/ui/accordion/accordion-item.svelte +12 -0
- package/dist/components/ui/accordion/accordion-item.svelte.d.ts +4 -0
- package/dist/components/ui/accordion/accordion-trigger.svelte +29 -0
- package/dist/components/ui/accordion/accordion-trigger.svelte.d.ts +7 -0
- package/dist/components/ui/accordion/index.d.ts +6 -0
- package/dist/components/ui/accordion/index.js +8 -0
- package/dist/components/ui/badge/badge.svelte +50 -0
- package/dist/components/ui/badge/badge.svelte.d.ts +60 -0
- package/dist/components/ui/badge/index.d.ts +2 -0
- package/dist/components/ui/badge/index.js +2 -0
- package/dist/components/ui/button/button.svelte +82 -0
- package/dist/components/ui/button/button.svelte.d.ts +132 -0
- package/dist/components/ui/button/index.d.ts +2 -0
- package/dist/components/ui/button/index.js +4 -0
- package/dist/components/ui/card/card-content.svelte +16 -0
- package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-description.svelte +16 -0
- package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-footer.svelte +16 -0
- package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-header.svelte +16 -0
- package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-title.svelte +25 -0
- package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
- package/dist/components/ui/card/card.svelte +20 -0
- package/dist/components/ui/card/card.svelte.d.ts +5 -0
- package/dist/components/ui/card/index.d.ts +7 -0
- package/dist/components/ui/card/index.js +9 -0
- package/dist/components/ui/dialog/dialog-content.svelte +38 -0
- package/dist/components/ui/dialog/dialog-content.svelte.d.ts +9 -0
- package/dist/components/ui/dialog/dialog-description.svelte +16 -0
- package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog-footer.svelte +20 -0
- package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +5 -0
- package/dist/components/ui/dialog/dialog-header.svelte +20 -0
- package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
- package/dist/components/ui/dialog/dialog-overlay.svelte +19 -0
- package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog-title.svelte +16 -0
- package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/index.d.ts +12 -0
- package/dist/components/ui/dialog/index.js +14 -0
- package/dist/components/ui/index.d.ts +26 -0
- package/dist/components/ui/index.js +29 -0
- package/dist/components/ui/input/index.d.ts +2 -0
- package/dist/components/ui/input/index.js +4 -0
- package/dist/components/ui/input/input.svelte +46 -0
- package/dist/components/ui/input/input.svelte.d.ts +13 -0
- package/dist/components/ui/select/index.d.ts +11 -0
- package/dist/components/ui/select/index.js +13 -0
- package/dist/components/ui/select/select-content.svelte +39 -0
- package/dist/components/ui/select/select-content.svelte.d.ts +7 -0
- package/dist/components/ui/select/select-group-heading.svelte +16 -0
- package/dist/components/ui/select/select-group-heading.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-item.svelte +37 -0
- package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-scroll-down-button.svelte +19 -0
- package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-scroll-up-button.svelte +19 -0
- package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-separator.svelte +13 -0
- package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-trigger.svelte +24 -0
- package/dist/components/ui/select/select-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/separator/index.d.ts +2 -0
- package/dist/components/ui/separator/index.js +4 -0
- package/dist/components/ui/separator/separator.svelte +22 -0
- package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/index.d.ts +12 -0
- package/dist/components/ui/sheet/index.js +14 -0
- package/dist/components/ui/sheet/sheet-content.svelte +53 -0
- package/dist/components/ui/sheet/sheet-content.svelte.d.ts +62 -0
- package/dist/components/ui/sheet/sheet-description.svelte +16 -0
- package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
- package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-header.svelte +20 -0
- package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte +21 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +6 -0
- package/dist/components/ui/sheet/sheet-title.svelte +16 -0
- package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
- package/dist/components/ui/skeleton/index.d.ts +2 -0
- package/dist/components/ui/skeleton/index.js +4 -0
- package/dist/components/ui/skeleton/skeleton.svelte +17 -0
- package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
- package/dist/components/ui/table/index.d.ts +9 -0
- package/dist/components/ui/table/index.js +11 -0
- package/dist/components/ui/table/table-body.svelte +16 -0
- package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-caption.svelte +16 -0
- package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-cell.svelte +20 -0
- package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-footer.svelte +16 -0
- package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-head.svelte +23 -0
- package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-header.svelte +16 -0
- package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-row.svelte +23 -0
- package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
- package/dist/components/ui/table/table.svelte +18 -0
- package/dist/components/ui/table/table.svelte.d.ts +5 -0
- package/dist/components/ui/tabs/index.d.ts +6 -0
- package/dist/components/ui/tabs/index.js +8 -0
- package/dist/components/ui/tabs/tabs-content.svelte +19 -0
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-list.svelte +19 -0
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte +19 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/textarea/index.d.ts +2 -0
- package/dist/components/ui/textarea/index.js +4 -0
- package/dist/components/ui/textarea/textarea.svelte +24 -0
- package/dist/components/ui/textarea/textarea.svelte.d.ts +6 -0
- package/dist/components/ui/toast.d.ts +86 -0
- package/dist/components/ui/toast.js +99 -0
- package/dist/db/schema.sql +238 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/payments/index.d.ts +33 -0
- package/dist/payments/index.js +47 -0
- package/dist/payments/shop.d.ts +165 -0
- package/dist/payments/shop.js +588 -0
- package/dist/payments/stripe/client.d.ts +231 -0
- package/dist/payments/stripe/client.js +198 -0
- package/dist/payments/stripe/index.d.ts +18 -0
- package/dist/payments/stripe/index.js +17 -0
- package/dist/payments/stripe/provider.d.ts +50 -0
- package/dist/payments/stripe/provider.js +530 -0
- package/dist/payments/types.d.ts +355 -0
- package/dist/payments/types.js +7 -0
- package/dist/server/logger.d.ts +53 -0
- package/dist/server/logger.js +252 -0
- package/dist/styles/content.css +514 -0
- package/dist/styles/tokens.css +175 -0
- package/dist/utils/api.d.ts +20 -0
- package/dist/utils/api.js +109 -0
- package/dist/utils/cn.d.ts +15 -0
- package/dist/utils/cn.js +18 -0
- package/dist/utils/csrf.d.ts +22 -0
- package/dist/utils/csrf.js +72 -0
- package/dist/utils/debounce.d.ts +7 -0
- package/dist/utils/debounce.js +14 -0
- package/dist/utils/gallery.d.ts +66 -0
- package/dist/utils/gallery.js +181 -0
- package/dist/utils/gutter.d.ts +54 -0
- package/dist/utils/gutter.js +169 -0
- package/dist/utils/imageProcessor.d.ts +58 -0
- package/dist/utils/imageProcessor.js +205 -0
- package/dist/utils/json.d.ts +17 -0
- package/dist/utils/json.js +26 -0
- package/dist/utils/markdown.d.ts +101 -0
- package/dist/utils/markdown.js +947 -0
- package/dist/utils/sanitize.d.ts +25 -0
- package/dist/utils/sanitize.js +127 -0
- package/dist/utils/validation.d.ts +46 -0
- package/dist/utils/validation.js +169 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +5 -0
- 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: "", 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("");
|
|
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 = `\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>
|