@plures/design-dojo 0.5.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/CanvasBreadcrumb.svelte +146 -0
- package/dist/app/CanvasBreadcrumb.svelte.d.ts +19 -0
- package/dist/app/CanvasBreadcrumb.types.js +1 -0
- package/dist/app/ChatInput.svelte +296 -0
- package/dist/app/ChatInput.svelte.d.ts +15 -0
- package/dist/app/ChatView.svelte +542 -0
- package/dist/app/ChatView.svelte.d.ts +19 -0
- package/dist/app/ChatView.types.js +1 -0
- package/dist/app/ConversationGraph.svelte +471 -0
- package/dist/app/ConversationGraph.svelte.d.ts +20 -0
- package/dist/app/FirstRunWizard.svelte +542 -0
- package/dist/app/FirstRunWizard.svelte.d.ts +10 -0
- package/dist/app/FirstRunWizard.types.js +1 -0
- package/dist/app/MemorySidebar.svelte +258 -0
- package/dist/app/MemorySidebar.svelte.d.ts +9 -0
- package/dist/app/MemorySidebar.types.js +1 -0
- package/dist/app/PeerStatusPanel.svelte +464 -0
- package/dist/app/PeerStatusPanel.svelte.d.ts +16 -0
- package/dist/app/ProcedureCanvas.svelte +994 -0
- package/dist/app/ProcedureCanvas.svelte.d.ts +12 -0
- package/dist/app/ProcedureCanvas.types.js +1 -0
- package/dist/app/ProcedureEditor.svelte +494 -0
- package/dist/app/ProcedureEditor.svelte.d.ts +11 -0
- package/dist/app/ProcedureEditor.types.js +1 -0
- package/dist/app/ProcedureInspector.svelte +520 -0
- package/dist/app/ProcedureInspector.svelte.d.ts +15 -0
- package/dist/app/ProcedureNode.svelte +283 -0
- package/dist/app/ProcedureNode.svelte.d.ts +26 -0
- package/dist/app/Realm.types.js +1 -0
- package/dist/app/RealmIndicator.svelte +81 -0
- package/dist/app/RealmIndicator.svelte.d.ts +10 -0
- package/dist/app/RealmSwitcher.svelte +354 -0
- package/dist/app/RealmSwitcher.svelte.d.ts +16 -0
- package/dist/app/SemanticConversation.types.js +1 -0
- package/dist/app/SemanticSearchInput.svelte +630 -0
- package/dist/app/SemanticSearchInput.svelte.d.ts +20 -0
- package/dist/app/SemanticSearchInput.types.js +1 -0
- package/dist/app/SemanticTimeline.svelte +426 -0
- package/dist/app/SemanticTimeline.svelte.d.ts +13 -0
- package/dist/app/SettingsPanel.svelte +330 -0
- package/dist/app/SettingsPanel.svelte.d.ts +12 -0
- package/dist/app/SettingsPanel.types.js +1 -0
- package/dist/app/SubCanvas.svelte +457 -0
- package/dist/app/SubCanvas.svelte.d.ts +48 -0
- package/dist/app/SubCanvas.types.js +1 -0
- package/dist/app/Sync.types.js +1 -0
- package/dist/app/SyncIndicator.svelte +219 -0
- package/dist/app/SyncIndicator.svelte.d.ts +15 -0
- package/dist/app/SyncTimeline.svelte +299 -0
- package/dist/app/SyncTimeline.svelte.d.ts +12 -0
- package/dist/app/TagCloud.svelte +287 -0
- package/dist/app/TagCloud.svelte.d.ts +12 -0
- package/dist/app/WorkModeToggle.svelte +188 -0
- package/dist/app/WorkModeToggle.svelte.d.ts +17 -0
- package/dist/data/List.svelte +104 -0
- package/dist/data/List.svelte.d.ts +18 -0
- package/dist/data/ListItem.svelte +130 -0
- package/dist/data/ListItem.svelte.d.ts +12 -0
- package/dist/data/Table.svelte +241 -0
- package/dist/data/Table.svelte.d.ts +17 -0
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.js +3 -0
- package/dist/disclosure/Accordion.svelte +48 -0
- package/dist/disclosure/Accordion.svelte.d.ts +15 -0
- package/dist/feedback/Badge.svelte +60 -0
- package/dist/feedback/Badge.svelte.d.ts +14 -0
- package/dist/feedback/Callout.svelte +52 -0
- package/dist/feedback/Callout.svelte.d.ts +12 -0
- package/dist/feedback/EmptyState.svelte +47 -0
- package/dist/feedback/EmptyState.svelte.d.ts +12 -0
- package/dist/feedback/ProgressBar.svelte +95 -0
- package/dist/feedback/ProgressBar.svelte.d.ts +16 -0
- package/dist/forms/FileUpload.svelte +99 -0
- package/dist/forms/FileUpload.svelte.d.ts +18 -0
- package/dist/forms/RadioGroup.svelte +84 -0
- package/dist/forms/RadioGroup.svelte.d.ts +19 -0
- package/dist/icons/NerdFont.svelte +44 -0
- package/dist/icons/NerdFont.svelte.d.ts +13 -0
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +70 -6212
- package/dist/layout/Box.svelte +207 -0
- package/dist/layout/Box.svelte.d.ts +22 -0
- package/dist/layout/Sidebar.svelte +210 -0
- package/dist/layout/Sidebar.svelte.d.ts +22 -0
- package/dist/layout/SplitPane.svelte +64 -0
- package/dist/layout/SplitPane.svelte.d.ts +12 -0
- package/dist/layout/StatusBar.svelte +83 -0
- package/dist/layout/StatusBar.svelte.d.ts +12 -0
- package/dist/layout/StatusBarItem.svelte +146 -0
- package/dist/layout/StatusBarItem.svelte.d.ts +15 -0
- package/dist/layout/StatusBarSpacer.svelte +38 -0
- package/dist/layout/StatusBarSpacer.svelte.d.ts +3 -0
- package/dist/layout/Tabs.svelte +254 -0
- package/dist/layout/Tabs.svelte.d.ts +21 -0
- package/dist/layout/Tabs.types.js +1 -0
- package/dist/layout/TitleBar.svelte +422 -0
- package/dist/layout/TitleBar.svelte.d.ts +22 -0
- package/dist/layout/index.d.ts +9 -0
- package/dist/layout/index.js +8 -0
- package/dist/motion/index.d.ts +1 -0
- package/dist/motion/index.js +1 -0
- package/dist/motion/spring.js +116 -0
- package/dist/overlays/ContextMenu.svelte +268 -0
- package/dist/overlays/ContextMenu.svelte.d.ts +17 -0
- package/dist/overlays/Dialog.svelte +264 -0
- package/dist/overlays/Dialog.svelte.d.ts +20 -0
- package/dist/overlays/Menu.svelte +274 -0
- package/dist/overlays/Menu.svelte.d.ts +26 -0
- package/dist/overlays/Menu.types.js +1 -0
- package/dist/overlays/Popover.svelte +158 -0
- package/dist/overlays/Popover.svelte.d.ts +21 -0
- package/dist/overlays/Toast.svelte +179 -0
- package/dist/overlays/Toast.svelte.d.ts +19 -0
- package/dist/overlays/Tooltip.svelte +114 -0
- package/dist/overlays/Tooltip.svelte.d.ts +17 -0
- package/dist/overlays/index.d.ts +7 -0
- package/dist/overlays/index.js +6 -0
- package/dist/primitives/Button.svelte +217 -0
- package/dist/primitives/Button.svelte.d.ts +13 -0
- package/dist/primitives/ContextMenu.svelte +242 -0
- package/dist/primitives/ContextMenu.svelte.d.ts +18 -0
- package/dist/primitives/ContextMenu.types.js +1 -0
- package/dist/primitives/Input.svelte +468 -0
- package/dist/primitives/Input.svelte.d.ts +21 -0
- package/dist/primitives/MarkdownEditor.svelte +781 -0
- package/dist/primitives/MarkdownEditor.svelte.d.ts +21 -0
- package/dist/primitives/MarkdownEditor.types.js +1 -0
- package/dist/primitives/SearchInput.svelte +623 -0
- package/dist/primitives/SearchInput.svelte.d.ts +24 -0
- package/dist/primitives/Select.svelte +336 -0
- package/dist/primitives/Select.svelte.d.ts +18 -0
- package/dist/primitives/Text.svelte +177 -0
- package/dist/primitives/Text.svelte.d.ts +26 -0
- package/dist/primitives/Toggle.svelte +138 -0
- package/dist/primitives/Toggle.svelte.d.ts +9 -0
- package/dist/primitives/index.d.ts +9 -0
- package/dist/primitives/index.js +7 -0
- package/dist/primitives/search-input-types.js +1 -0
- package/dist/surfaces/ChatPane.svelte +520 -0
- package/dist/surfaces/ChatPane.svelte.d.ts +15 -0
- package/dist/surfaces/ChatPane.types.js +1 -0
- package/dist/surfaces/GlassPanel.svelte +118 -0
- package/dist/surfaces/GlassPanel.svelte.d.ts +19 -0
- package/dist/surfaces/Pane.svelte +172 -0
- package/dist/surfaces/Pane.svelte.d.ts +25 -0
- package/dist/surfaces/index.d.ts +4 -0
- package/dist/surfaces/index.js +3 -0
- package/dist/telemetry/correlation.js +26 -0
- package/dist/telemetry/index.d.ts +4 -4
- package/dist/telemetry/index.js +20 -101
- package/dist/telemetry/sampling.js +58 -0
- package/dist/telemetry/tracer.d.ts +16 -1
- package/dist/telemetry/tracer.js +112 -0
- package/dist/tokens.css +123 -0
- package/dist/tui-tokens.css +36 -0
- package/dist/useTui.js +31 -0
- package/package.json +32 -22
- package/dist/design-dojo.css +0 -1
- package/dist/enforce/index.d.ts +0 -75
- package/dist/enforce/known-components.d.ts +0 -7
- package/dist/enforce/rules/no-local-components.d.ts +0 -29
- package/dist/enforce/rules/prefer-design-dojo-imports.d.ts +0 -27
- package/dist/enforce.js +0 -132
- package/dist/lib/app/CanvasBreadcrumb.svelte.d.ts +0 -1
- package/dist/lib/app/ChatInput.svelte.d.ts +0 -1
- package/dist/lib/app/ChatView.svelte.d.ts +0 -1
- package/dist/lib/app/ConversationGraph.svelte.d.ts +0 -1
- package/dist/lib/app/FirstRunWizard.svelte.d.ts +0 -1
- package/dist/lib/app/MemorySidebar.svelte.d.ts +0 -1
- package/dist/lib/app/PeerStatusPanel.svelte.d.ts +0 -1
- package/dist/lib/app/ProcedureCanvas.svelte.d.ts +0 -1
- package/dist/lib/app/ProcedureEditor.svelte.d.ts +0 -1
- package/dist/lib/app/ProcedureInspector.svelte.d.ts +0 -1
- package/dist/lib/app/ProcedureNode.svelte.d.ts +0 -1
- package/dist/lib/app/RealmIndicator.svelte.d.ts +0 -1
- package/dist/lib/app/RealmSwitcher.svelte.d.ts +0 -1
- package/dist/lib/app/SemanticSearchInput.svelte.d.ts +0 -1
- package/dist/lib/app/SemanticTimeline.svelte.d.ts +0 -1
- package/dist/lib/app/SettingsPanel.svelte.d.ts +0 -1
- package/dist/lib/app/SubCanvas.svelte.d.ts +0 -1
- package/dist/lib/app/SyncIndicator.svelte.d.ts +0 -1
- package/dist/lib/app/SyncTimeline.svelte.d.ts +0 -1
- package/dist/lib/app/TagCloud.svelte.d.ts +0 -1
- package/dist/lib/app/WorkModeToggle.svelte.d.ts +0 -1
- package/dist/lib/data/List.svelte.d.ts +0 -1
- package/dist/lib/data/ListItem.svelte.d.ts +0 -1
- package/dist/lib/data/Table.svelte.d.ts +0 -1
- package/dist/lib/data/index.d.ts +0 -3
- package/dist/lib/disclosure/Accordion.svelte.d.ts +0 -1
- package/dist/lib/feedback/Badge.svelte.d.ts +0 -1
- package/dist/lib/feedback/Callout.svelte.d.ts +0 -1
- package/dist/lib/feedback/EmptyState.svelte.d.ts +0 -1
- package/dist/lib/feedback/ProgressBar.svelte.d.ts +0 -1
- package/dist/lib/forms/FileUpload.svelte.d.ts +0 -1
- package/dist/lib/forms/RadioGroup.svelte.d.ts +0 -1
- package/dist/lib/icons/NerdFont.svelte.d.ts +0 -1
- package/dist/lib/icons/index.d.ts +0 -1
- package/dist/lib/index.d.ts +0 -76
- package/dist/lib/layout/Box.svelte.d.ts +0 -1
- package/dist/lib/layout/Sidebar.svelte.d.ts +0 -1
- package/dist/lib/layout/SplitPane.svelte.d.ts +0 -1
- package/dist/lib/layout/StatusBar.svelte.d.ts +0 -1
- package/dist/lib/layout/StatusBarItem.svelte.d.ts +0 -1
- package/dist/lib/layout/StatusBarSpacer.svelte.d.ts +0 -1
- package/dist/lib/layout/Tabs.svelte.d.ts +0 -1
- package/dist/lib/layout/TitleBar.svelte.d.ts +0 -1
- package/dist/lib/layout/index.d.ts +0 -9
- package/dist/lib/motion/index.d.ts +0 -1
- package/dist/lib/overlays/ContextMenu.svelte.d.ts +0 -1
- package/dist/lib/overlays/Dialog.svelte.d.ts +0 -1
- package/dist/lib/overlays/Menu.svelte.d.ts +0 -1
- package/dist/lib/overlays/Popover.svelte.d.ts +0 -1
- package/dist/lib/overlays/Toast.svelte.d.ts +0 -1
- package/dist/lib/overlays/Tooltip.svelte.d.ts +0 -1
- package/dist/lib/overlays/index.d.ts +0 -7
- package/dist/lib/primitives/Button.svelte.d.ts +0 -1
- package/dist/lib/primitives/ContextMenu.svelte.d.ts +0 -1
- package/dist/lib/primitives/Input.svelte.d.ts +0 -1
- package/dist/lib/primitives/MarkdownEditor.svelte.d.ts +0 -1
- package/dist/lib/primitives/SearchInput.svelte.d.ts +0 -1
- package/dist/lib/primitives/Select.svelte.d.ts +0 -1
- package/dist/lib/primitives/Text.svelte.d.ts +0 -1
- package/dist/lib/primitives/Toggle.svelte.d.ts +0 -1
- package/dist/lib/primitives/index.d.ts +0 -9
- package/dist/lib/surfaces/ChatPane.svelte.d.ts +0 -1
- package/dist/lib/surfaces/GlassPanel.svelte.d.ts +0 -1
- package/dist/lib/surfaces/Pane.svelte.d.ts +0 -1
- package/dist/lib/surfaces/index.d.ts +0 -4
- /package/dist/{lib/app → app}/CanvasBreadcrumb.types.d.ts +0 -0
- /package/dist/{lib/app → app}/ChatView.types.d.ts +0 -0
- /package/dist/{lib/app → app}/FirstRunWizard.types.d.ts +0 -0
- /package/dist/{lib/app → app}/MemorySidebar.types.d.ts +0 -0
- /package/dist/{lib/app → app}/ProcedureCanvas.types.d.ts +0 -0
- /package/dist/{lib/app → app}/ProcedureEditor.types.d.ts +0 -0
- /package/dist/{lib/app → app}/Realm.types.d.ts +0 -0
- /package/dist/{lib/app → app}/SemanticConversation.types.d.ts +0 -0
- /package/dist/{lib/app → app}/SemanticSearchInput.types.d.ts +0 -0
- /package/dist/{lib/app → app}/SettingsPanel.types.d.ts +0 -0
- /package/dist/{lib/app → app}/SubCanvas.types.d.ts +0 -0
- /package/dist/{lib/app → app}/Sync.types.d.ts +0 -0
- /package/dist/{lib/layout → layout}/Tabs.types.d.ts +0 -0
- /package/dist/{lib/motion → motion}/spring.d.ts +0 -0
- /package/dist/{lib/overlays → overlays}/Menu.types.d.ts +0 -0
- /package/dist/{lib/primitives → primitives}/ContextMenu.types.d.ts +0 -0
- /package/dist/{lib/primitives → primitives}/MarkdownEditor.types.d.ts +0 -0
- /package/dist/{lib/primitives → primitives}/search-input-types.d.ts +0 -0
- /package/dist/{lib/surfaces → surfaces}/ChatPane.types.d.ts +0 -0
- /package/dist/{lib/useTui.d.ts → useTui.d.ts} +0 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
MarkdownEditor — Simple embeddable markdown editor control.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- GUI mode: resizable textarea with optional live preview panel
|
|
6
|
+
- TUI mode: box-drawing border multi-line text area
|
|
7
|
+
- Monospace font option for code-heavy content
|
|
8
|
+
- Preview toggle: "edit" | "preview" | "split"
|
|
9
|
+
- Keyboard shortcuts: Ctrl+B bold, Ctrl+I italic, Ctrl+` inline code
|
|
10
|
+
- Safe built-in markdown renderer (HTML-escaped, no external deps)
|
|
11
|
+
- Value binding ($bindable), placeholder, label
|
|
12
|
+
- Disabled state
|
|
13
|
+
- Events: onchange
|
|
14
|
+
-->
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { useTui } from "../useTui.js";
|
|
17
|
+
import type { MarkdownEditorMode } from "./MarkdownEditor.types.js";
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
tui?: boolean;
|
|
21
|
+
value?: string;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
/** Use monospace font in editor area (good for code/snippets). */
|
|
25
|
+
monospace?: boolean;
|
|
26
|
+
/** Show/hide the preview toggle toolbar. Default: true */
|
|
27
|
+
showToolbar?: boolean;
|
|
28
|
+
/** Active view mode. Default: "edit" */
|
|
29
|
+
mode?: MarkdownEditorMode;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
/** Minimum rows for the textarea. Default: 6 */
|
|
32
|
+
rows?: number;
|
|
33
|
+
class?: string;
|
|
34
|
+
onchange?: (value: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
tui = false,
|
|
39
|
+
value = $bindable(""),
|
|
40
|
+
placeholder = "",
|
|
41
|
+
label = "",
|
|
42
|
+
monospace = false,
|
|
43
|
+
showToolbar = true,
|
|
44
|
+
mode = $bindable<MarkdownEditorMode>("edit"),
|
|
45
|
+
disabled = false,
|
|
46
|
+
rows = 6,
|
|
47
|
+
class: className = "",
|
|
48
|
+
onchange,
|
|
49
|
+
}: Props = $props();
|
|
50
|
+
|
|
51
|
+
const getTuiCtx = useTui();
|
|
52
|
+
const isTui = $derived(tui || getTuiCtx());
|
|
53
|
+
|
|
54
|
+
let textareaEl: HTMLTextAreaElement | undefined = $state();
|
|
55
|
+
let isFocused = $state(false);
|
|
56
|
+
|
|
57
|
+
// Stable id for associating the <label> with the <textarea>
|
|
58
|
+
const editorId = `md-editor-${Math.random().toString(36).slice(2, 9)}`;
|
|
59
|
+
|
|
60
|
+
// TUI cannot render a split view; coerce "split" → "edit" when in TUI mode
|
|
61
|
+
$effect(() => {
|
|
62
|
+
if (isTui && mode === "split") {
|
|
63
|
+
mode = "edit";
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ── Markdown renderer ────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/** Escape HTML entities to prevent XSS before injecting markdown HTML. */
|
|
70
|
+
function escapeHtml(str: string): string {
|
|
71
|
+
return str
|
|
72
|
+
.replace(/&/g, "&")
|
|
73
|
+
.replace(/</g, "<")
|
|
74
|
+
.replace(/>/g, ">")
|
|
75
|
+
.replace(/"/g, """)
|
|
76
|
+
.replace(/'/g, "'");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decode a minimal set of HTML entities, including numeric ones, so that
|
|
81
|
+
* obfuscated protocols like "javascript:" are normalized before checks.
|
|
82
|
+
*/
|
|
83
|
+
function decodeHtmlEntities(str: string): string {
|
|
84
|
+
// First handle the common named entities we use elsewhere.
|
|
85
|
+
let result = str
|
|
86
|
+
.replace(/&/g, "&")
|
|
87
|
+
.replace(/</g, "<")
|
|
88
|
+
.replace(/>/g, ">")
|
|
89
|
+
.replace(/"/g, '"')
|
|
90
|
+
.replace(/'/g, "'");
|
|
91
|
+
|
|
92
|
+
// Then handle numeric (decimal and hex) character references.
|
|
93
|
+
result = result.replace(/&#(x?[0-9a-fA-F]+);?/g, (match, num) => {
|
|
94
|
+
try {
|
|
95
|
+
const codePoint =
|
|
96
|
+
typeof num === "string" && num.toLowerCase().startsWith("x")
|
|
97
|
+
? parseInt(num.slice(1), 16)
|
|
98
|
+
: parseInt(num, 10);
|
|
99
|
+
if (!Number.isFinite(codePoint) || codePoint <= 0) {
|
|
100
|
+
return match;
|
|
101
|
+
}
|
|
102
|
+
return String.fromCodePoint(codePoint);
|
|
103
|
+
} catch {
|
|
104
|
+
return match;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Allow only safe URL protocols; return empty string for dangerous ones. */
|
|
112
|
+
function sanitizeHref(href: string): string {
|
|
113
|
+
const trimmed = href.trim();
|
|
114
|
+
if (trimmed === "") {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const decoded = decodeHtmlEntities(trimmed);
|
|
119
|
+
|
|
120
|
+
// Allow fragment-only links like "#section".
|
|
121
|
+
if (decoded.startsWith("#")) {
|
|
122
|
+
return decoded;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const allowedSchemes = new Set(["http", "https", "mailto"]);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// Use a benign base so relative URLs can still be parsed.
|
|
129
|
+
const url = new URL(decoded, "http://example.com");
|
|
130
|
+
const protocol = url.protocol.replace(":", "").toLowerCase();
|
|
131
|
+
|
|
132
|
+
// If the URL has an explicit protocol, enforce the allow-list.
|
|
133
|
+
if (protocol && protocol !== "http") {
|
|
134
|
+
// If a non-http(s)/mailto protocol is present, only allow if explicitly whitelisted.
|
|
135
|
+
return allowedSchemes.has(protocol) ? decoded : "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// For http/https (including relative URLs resolved against the base), allow.
|
|
139
|
+
return decoded;
|
|
140
|
+
} catch {
|
|
141
|
+
// If parsing fails but the string looks like it starts with a scheme,
|
|
142
|
+
// reject it; otherwise treat it as a relative URL and allow.
|
|
143
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(decoded)) {
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
return decoded;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Returns true when a line should not be accumulated into a paragraph. */
|
|
151
|
+
function isSpecialMarkdownLine(line: string): boolean {
|
|
152
|
+
return (
|
|
153
|
+
line.trim() === "" ||
|
|
154
|
+
line.startsWith("#") ||
|
|
155
|
+
line.startsWith("```") ||
|
|
156
|
+
/^[ \t]*[-*+] /.test(line) ||
|
|
157
|
+
/^[ \t]*\d+\. /.test(line) ||
|
|
158
|
+
/^[-*_]{3,}$/.test(line.trim())
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Minimal safe markdown renderer.
|
|
164
|
+
* Processes: headings, bold, italic, inline code, code blocks,
|
|
165
|
+
* unordered lists, ordered lists, horizontal rules, links, paragraphs.
|
|
166
|
+
* All user content is HTML-escaped before formatting is applied.
|
|
167
|
+
*/
|
|
168
|
+
function renderMarkdown(src: string): string {
|
|
169
|
+
if (!src) return "";
|
|
170
|
+
|
|
171
|
+
const lines = src.split("\n");
|
|
172
|
+
const output: string[] = [];
|
|
173
|
+
let i = 0;
|
|
174
|
+
|
|
175
|
+
while (i < lines.length) {
|
|
176
|
+
const line = lines[i];
|
|
177
|
+
|
|
178
|
+
// Fenced code block
|
|
179
|
+
if (line.startsWith("```")) {
|
|
180
|
+
const lang = escapeHtml(line.slice(3).trim());
|
|
181
|
+
const codeLines: string[] = [];
|
|
182
|
+
i++;
|
|
183
|
+
while (i < lines.length && !lines[i].startsWith("```")) {
|
|
184
|
+
codeLines.push(escapeHtml(lines[i]));
|
|
185
|
+
i++;
|
|
186
|
+
}
|
|
187
|
+
const langAttr = lang ? ` class="language-${lang}"` : "";
|
|
188
|
+
output.push(`<pre><code${langAttr}>${codeLines.join("\n")}</code></pre>`);
|
|
189
|
+
i++; // skip closing ```
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Unordered list
|
|
194
|
+
if (/^[ \t]*[-*+] /.test(line)) {
|
|
195
|
+
const items: string[] = [];
|
|
196
|
+
while (i < lines.length && /^[ \t]*[-*+] /.test(lines[i])) {
|
|
197
|
+
items.push(`<li>${inlineMarkdown(lines[i].replace(/^[ \t]*[-*+] /, ""))}</li>`);
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
200
|
+
output.push(`<ul>${items.join("")}</ul>`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Ordered list
|
|
205
|
+
if (/^[ \t]*\d+\. /.test(line)) {
|
|
206
|
+
const items: string[] = [];
|
|
207
|
+
while (i < lines.length && /^[ \t]*\d+\. /.test(lines[i])) {
|
|
208
|
+
items.push(`<li>${inlineMarkdown(lines[i].replace(/^[ \t]*\d+\. /, ""))}</li>`);
|
|
209
|
+
i++;
|
|
210
|
+
}
|
|
211
|
+
output.push(`<ol>${items.join("")}</ol>`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Horizontal rule
|
|
216
|
+
if (/^[-*_]{3,}$/.test(line.trim())) {
|
|
217
|
+
output.push("<hr>");
|
|
218
|
+
i++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ATX headings
|
|
223
|
+
const headingMatch = line.match(/^(#{1,6}) (.*)/);
|
|
224
|
+
if (headingMatch) {
|
|
225
|
+
const level = headingMatch[1].length;
|
|
226
|
+
output.push(`<h${level}>${inlineMarkdown(headingMatch[2])}</h${level}>`);
|
|
227
|
+
i++;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Blank line → paragraph break
|
|
232
|
+
if (line.trim() === "") {
|
|
233
|
+
output.push("");
|
|
234
|
+
i++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Paragraph: collect consecutive non-blank lines
|
|
239
|
+
const paraLines: string[] = [];
|
|
240
|
+
while (i < lines.length && !isSpecialMarkdownLine(lines[i])) {
|
|
241
|
+
paraLines.push(lines[i]);
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
if (paraLines.length) {
|
|
245
|
+
output.push(`<p>${inlineMarkdown(paraLines.join(" "))}</p>`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return output.join("\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Apply inline markdown formatting to an already HTML-safe string. */
|
|
253
|
+
function inlineMarkdown(raw: string): string {
|
|
254
|
+
// Escape HTML first
|
|
255
|
+
let s = escapeHtml(raw);
|
|
256
|
+
// Inline code (must run before bold/italic to protect backtick content)
|
|
257
|
+
s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
258
|
+
// Bold
|
|
259
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
260
|
+
s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
261
|
+
// Italic
|
|
262
|
+
s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
263
|
+
s = s.replace(/_([^_]+)_/g, "<em>$1</em>");
|
|
264
|
+
// Links — href sanitized to block javascript:/vbscript:/data: URLs
|
|
265
|
+
// text is already HTML-escaped (part of `s`); use it directly to avoid double-escaping.
|
|
266
|
+
// safe href is escaped again for attribute safety (prevents quote-injection XSS).
|
|
267
|
+
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, href) => {
|
|
268
|
+
const safe = sanitizeHref(href);
|
|
269
|
+
if (!safe) return text;
|
|
270
|
+
return `<a href="${escapeHtml(safe)}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
271
|
+
});
|
|
272
|
+
return s;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const previewHtml = $derived(renderMarkdown(value ?? ""));
|
|
276
|
+
|
|
277
|
+
// ── Keyboard shortcuts ───────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
function wrapSelection(before: string, after: string) {
|
|
280
|
+
const el = textareaEl;
|
|
281
|
+
if (!el || disabled) return;
|
|
282
|
+
const start = el.selectionStart ?? 0;
|
|
283
|
+
const end = el.selectionEnd ?? 0;
|
|
284
|
+
const selected = (value ?? "").slice(start, end);
|
|
285
|
+
const newValue =
|
|
286
|
+
(value ?? "").slice(0, start) +
|
|
287
|
+
before +
|
|
288
|
+
selected +
|
|
289
|
+
after +
|
|
290
|
+
(value ?? "").slice(end);
|
|
291
|
+
value = newValue;
|
|
292
|
+
onchange?.(newValue);
|
|
293
|
+
// Restore selection inside the inserted markers
|
|
294
|
+
requestAnimationFrame(() => {
|
|
295
|
+
el.selectionStart = start + before.length;
|
|
296
|
+
el.selectionEnd = end + before.length;
|
|
297
|
+
el.focus();
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
302
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
303
|
+
if (ctrl && e.key === "b") {
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
wrapSelection("**", "**");
|
|
306
|
+
} else if (ctrl && e.key === "i") {
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
wrapSelection("*", "*");
|
|
309
|
+
} else if (ctrl && e.key === "`") {
|
|
310
|
+
e.preventDefault();
|
|
311
|
+
wrapSelection("`", "`");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function handleInput(e: Event) {
|
|
316
|
+
const target = e.target as HTMLTextAreaElement;
|
|
317
|
+
value = target.value;
|
|
318
|
+
onchange?.(value);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ── TUI helpers ──────────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
const isVisuallyFocused = $derived(isFocused);
|
|
324
|
+
|
|
325
|
+
const tuiTopBorder = $derived(
|
|
326
|
+
label
|
|
327
|
+
? (() => {
|
|
328
|
+
const maxLabelLen = 30;
|
|
329
|
+
const safeLabel = label.length > maxLabelLen ? label.substring(0, maxLabelLen) : label;
|
|
330
|
+
const section = `─ ${safeLabel} `;
|
|
331
|
+
const fill = Math.max(0, 38 - section.length);
|
|
332
|
+
return `┌${section}${"─".repeat(fill)}┐`;
|
|
333
|
+
})()
|
|
334
|
+
: `┌${"─".repeat(38)}┐`,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const tuiBottomBorder = `└${"─".repeat(38)}┘`;
|
|
338
|
+
|
|
339
|
+
const tuiModeIndicator = $derived(
|
|
340
|
+
isTui && showToolbar
|
|
341
|
+
? ` [${mode === "preview" ? "PREV" : "EDIT"}]`
|
|
342
|
+
: "",
|
|
343
|
+
);
|
|
344
|
+
</script>
|
|
345
|
+
|
|
346
|
+
{#if isTui}
|
|
347
|
+
<!-- TUI mode: box-drawing border around a real textarea -->
|
|
348
|
+
<div
|
|
349
|
+
class="tui-md {className}"
|
|
350
|
+
class:focused={isVisuallyFocused}
|
|
351
|
+
class:disabled
|
|
352
|
+
>
|
|
353
|
+
<div class="tui-top" class:focused={isVisuallyFocused}>{tuiTopBorder}{tuiModeIndicator}</div>
|
|
354
|
+
|
|
355
|
+
{#if mode !== "preview"}
|
|
356
|
+
<div class="tui-row">
|
|
357
|
+
<span class="tui-side" class:focused={isVisuallyFocused}>│</span>
|
|
358
|
+
<textarea
|
|
359
|
+
bind:this={textareaEl}
|
|
360
|
+
class="tui-textarea"
|
|
361
|
+
class:monospace
|
|
362
|
+
{rows}
|
|
363
|
+
{disabled}
|
|
364
|
+
{value}
|
|
365
|
+
aria-label={label || placeholder || "Markdown editor"}
|
|
366
|
+
aria-multiline="true"
|
|
367
|
+
oninput={handleInput}
|
|
368
|
+
onkeydown={handleKeydown}
|
|
369
|
+
onfocus={() => (isFocused = true)}
|
|
370
|
+
onblur={() => (isFocused = false)}
|
|
371
|
+
></textarea>
|
|
372
|
+
<span class="tui-side" class:focused={isVisuallyFocused}>│</span>
|
|
373
|
+
</div>
|
|
374
|
+
{/if}
|
|
375
|
+
|
|
376
|
+
{#if mode === "preview"}
|
|
377
|
+
<!-- TUI preview intentionally shows raw markdown text; HTML cannot be rendered in terminal environments -->
|
|
378
|
+
<div class="tui-row">
|
|
379
|
+
<span class="tui-side" class:focused={isVisuallyFocused}>│</span>
|
|
380
|
+
<div class="tui-preview" aria-label="Markdown preview">
|
|
381
|
+
{value ? value.split("\n").slice(0, rows).join("\n") : (placeholder || "No content")}
|
|
382
|
+
</div>
|
|
383
|
+
<span class="tui-side" class:focused={isVisuallyFocused}>│</span>
|
|
384
|
+
</div>
|
|
385
|
+
{/if}
|
|
386
|
+
|
|
387
|
+
<div class="tui-bottom" class:focused={isVisuallyFocused}>{tuiBottomBorder}</div>
|
|
388
|
+
|
|
389
|
+
{#if showToolbar}
|
|
390
|
+
<div class="tui-toolbar">
|
|
391
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
392
|
+
<span
|
|
393
|
+
role="button"
|
|
394
|
+
tabindex={disabled ? -1 : 0}
|
|
395
|
+
class="tui-tab"
|
|
396
|
+
class:active={mode === "edit"}
|
|
397
|
+
onclick={() => { mode = "edit"; }}
|
|
398
|
+
onkeydown={(e) => { if (e.key === "Enter" || e.key === " ") mode = "edit"; }}
|
|
399
|
+
aria-pressed={mode === "edit"}
|
|
400
|
+
>EDIT</span>
|
|
401
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
402
|
+
<span
|
|
403
|
+
role="button"
|
|
404
|
+
tabindex={disabled ? -1 : 0}
|
|
405
|
+
class="tui-tab"
|
|
406
|
+
class:active={mode === "preview"}
|
|
407
|
+
onclick={() => { mode = "preview"; }}
|
|
408
|
+
onkeydown={(e) => { if (e.key === "Enter" || e.key === " ") mode = "preview"; }}
|
|
409
|
+
aria-pressed={mode === "preview"}
|
|
410
|
+
>PREV</span>
|
|
411
|
+
</div>
|
|
412
|
+
{/if}
|
|
413
|
+
</div>
|
|
414
|
+
{:else}
|
|
415
|
+
<!-- GUI mode -->
|
|
416
|
+
<div
|
|
417
|
+
class="md-editor {className}"
|
|
418
|
+
class:focused={isVisuallyFocused}
|
|
419
|
+
class:disabled
|
|
420
|
+
class:split={mode === "split"}
|
|
421
|
+
>
|
|
422
|
+
{#if label}
|
|
423
|
+
<label class="md-label" for={editorId}>{label}</label>
|
|
424
|
+
{/if}
|
|
425
|
+
|
|
426
|
+
{#if showToolbar}
|
|
427
|
+
<div class="md-toolbar" role="toolbar" aria-label="Editor mode">
|
|
428
|
+
<button
|
|
429
|
+
class="md-tab"
|
|
430
|
+
class:active={mode === "edit"}
|
|
431
|
+
type="button"
|
|
432
|
+
{disabled}
|
|
433
|
+
onclick={() => (mode = "edit")}
|
|
434
|
+
aria-pressed={mode === "edit"}
|
|
435
|
+
>Edit</button>
|
|
436
|
+
<button
|
|
437
|
+
class="md-tab"
|
|
438
|
+
class:active={mode === "split"}
|
|
439
|
+
type="button"
|
|
440
|
+
{disabled}
|
|
441
|
+
onclick={() => (mode = "split")}
|
|
442
|
+
aria-pressed={mode === "split"}
|
|
443
|
+
>Split</button>
|
|
444
|
+
<button
|
|
445
|
+
class="md-tab"
|
|
446
|
+
class:active={mode === "preview"}
|
|
447
|
+
type="button"
|
|
448
|
+
{disabled}
|
|
449
|
+
onclick={() => (mode = "preview")}
|
|
450
|
+
aria-pressed={mode === "preview"}
|
|
451
|
+
>Preview</button>
|
|
452
|
+
|
|
453
|
+
<span class="md-toolbar-hint" aria-label="Keyboard shortcuts: Ctrl+B bold, Ctrl+I italic, Ctrl+backtick code">Ctrl+B bold · Ctrl+I italic · Ctrl+` code</span>
|
|
454
|
+
</div>
|
|
455
|
+
{/if}
|
|
456
|
+
|
|
457
|
+
<div class="md-body" class:split={mode === "split"}>
|
|
458
|
+
{#if mode !== "preview"}
|
|
459
|
+
<textarea
|
|
460
|
+
bind:this={textareaEl}
|
|
461
|
+
id={editorId}
|
|
462
|
+
class="md-textarea"
|
|
463
|
+
class:monospace
|
|
464
|
+
{rows}
|
|
465
|
+
{disabled}
|
|
466
|
+
{placeholder}
|
|
467
|
+
aria-label={label ? undefined : "Markdown editor"}
|
|
468
|
+
aria-multiline="true"
|
|
469
|
+
value={value}
|
|
470
|
+
oninput={handleInput}
|
|
471
|
+
onkeydown={handleKeydown}
|
|
472
|
+
onfocus={() => (isFocused = true)}
|
|
473
|
+
onblur={() => (isFocused = false)}
|
|
474
|
+
></textarea>
|
|
475
|
+
{/if}
|
|
476
|
+
|
|
477
|
+
{#if mode !== "edit"}
|
|
478
|
+
<div
|
|
479
|
+
class="md-preview"
|
|
480
|
+
class:full={mode === "preview"}
|
|
481
|
+
aria-label="Markdown preview"
|
|
482
|
+
aria-live="polite"
|
|
483
|
+
>
|
|
484
|
+
{#if value}
|
|
485
|
+
<!-- renderMarkdown HTML-escapes all user input before injecting markdown tags -->
|
|
486
|
+
{@html previewHtml}
|
|
487
|
+
{:else}
|
|
488
|
+
<span class="md-preview-empty">{placeholder || "Nothing to preview"}</span>
|
|
489
|
+
{/if}
|
|
490
|
+
</div>
|
|
491
|
+
{/if}
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
{/if}
|
|
495
|
+
|
|
496
|
+
<style>
|
|
497
|
+
/* ===== GUI mode ===== */
|
|
498
|
+
.md-editor {
|
|
499
|
+
display: flex;
|
|
500
|
+
flex-direction: column;
|
|
501
|
+
width: 100%;
|
|
502
|
+
border: 1.5px solid var(--color-border, #2a2a2a);
|
|
503
|
+
border-radius: var(--radius-md, 10px);
|
|
504
|
+
background: var(--surface-2, #1e1e1e);
|
|
505
|
+
overflow: hidden;
|
|
506
|
+
transition: border-color 0.15s ease;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.md-editor.focused {
|
|
510
|
+
border-color: var(--color-accent, #6366f1);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.md-editor.disabled {
|
|
514
|
+
opacity: 0.4;
|
|
515
|
+
pointer-events: none;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.md-label {
|
|
519
|
+
padding: var(--space-2, 8px) var(--space-3, 12px) 0;
|
|
520
|
+
font-size: var(--text-xs, 12px);
|
|
521
|
+
color: var(--color-text-muted, #888888);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.md-toolbar {
|
|
525
|
+
display: flex;
|
|
526
|
+
align-items: center;
|
|
527
|
+
gap: var(--space-1, 4px);
|
|
528
|
+
padding: var(--space-1, 4px) var(--space-2, 8px);
|
|
529
|
+
border-bottom: 1px solid var(--color-border, #2a2a2a);
|
|
530
|
+
background: var(--surface-1, #161616);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.md-tab {
|
|
534
|
+
padding: 2px var(--space-2, 8px);
|
|
535
|
+
background: transparent;
|
|
536
|
+
border: 1px solid transparent;
|
|
537
|
+
border-radius: var(--radius-sm, 6px);
|
|
538
|
+
color: var(--color-text-muted, #888888);
|
|
539
|
+
font-size: var(--text-xs, 12px);
|
|
540
|
+
cursor: pointer;
|
|
541
|
+
transition: color 0.1s, border-color 0.1s, background 0.1s;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.md-tab:hover {
|
|
545
|
+
color: var(--color-text, #e8e8e8);
|
|
546
|
+
background: var(--surface-2, #1e1e1e);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.md-tab.active {
|
|
550
|
+
color: var(--color-accent, #6366f1);
|
|
551
|
+
border-color: var(--color-accent, #6366f1);
|
|
552
|
+
background: color-mix(in srgb, var(--color-accent, #6366f1) 10%, transparent);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.md-toolbar-hint {
|
|
556
|
+
margin-left: auto;
|
|
557
|
+
font-size: 10px;
|
|
558
|
+
color: var(--color-text-muted, #888888);
|
|
559
|
+
opacity: 0.6;
|
|
560
|
+
white-space: nowrap;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.md-body {
|
|
564
|
+
display: flex;
|
|
565
|
+
flex: 1;
|
|
566
|
+
min-height: 0;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.md-body.split {
|
|
570
|
+
gap: 0;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.md-textarea {
|
|
574
|
+
flex: 1;
|
|
575
|
+
width: 100%;
|
|
576
|
+
box-sizing: border-box;
|
|
577
|
+
padding: var(--space-3, 12px);
|
|
578
|
+
background: transparent;
|
|
579
|
+
border: none;
|
|
580
|
+
color: var(--color-text, #e8e8e8);
|
|
581
|
+
font-family: inherit;
|
|
582
|
+
font-size: var(--text-sm, 14px);
|
|
583
|
+
line-height: var(--leading-normal, 1.6);
|
|
584
|
+
resize: vertical;
|
|
585
|
+
outline: none;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.md-textarea.monospace {
|
|
589
|
+
font-family: var(--font-mono, monospace);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.md-body.split .md-textarea {
|
|
593
|
+
flex: 1;
|
|
594
|
+
resize: none;
|
|
595
|
+
border-right: 1px solid var(--color-border, #2a2a2a);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.md-preview {
|
|
599
|
+
flex: 1;
|
|
600
|
+
padding: var(--space-3, 12px);
|
|
601
|
+
color: var(--color-text, #e8e8e8);
|
|
602
|
+
font-size: var(--text-sm, 14px);
|
|
603
|
+
line-height: var(--leading-normal, 1.6);
|
|
604
|
+
overflow-y: auto;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.md-preview.full {
|
|
608
|
+
min-height: 120px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.md-preview-empty {
|
|
612
|
+
color: var(--color-text-muted, #888888);
|
|
613
|
+
font-style: italic;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/* Preview typography */
|
|
617
|
+
.md-preview :global(h1),
|
|
618
|
+
.md-preview :global(h2),
|
|
619
|
+
.md-preview :global(h3),
|
|
620
|
+
.md-preview :global(h4),
|
|
621
|
+
.md-preview :global(h5),
|
|
622
|
+
.md-preview :global(h6) {
|
|
623
|
+
margin: 0.6em 0 0.3em;
|
|
624
|
+
color: var(--color-text, #e8e8e8);
|
|
625
|
+
font-weight: 600;
|
|
626
|
+
line-height: 1.25;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.md-preview :global(h1) { font-size: 1.5em; }
|
|
630
|
+
.md-preview :global(h2) { font-size: 1.25em; }
|
|
631
|
+
.md-preview :global(h3) { font-size: 1.1em; }
|
|
632
|
+
|
|
633
|
+
.md-preview :global(p) {
|
|
634
|
+
margin: 0.4em 0;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.md-preview :global(strong) {
|
|
638
|
+
font-weight: 700;
|
|
639
|
+
color: var(--color-text, #e8e8e8);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.md-preview :global(em) {
|
|
643
|
+
font-style: italic;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.md-preview :global(code) {
|
|
647
|
+
font-family: var(--font-mono, monospace);
|
|
648
|
+
font-size: 0.875em;
|
|
649
|
+
padding: 0.15em 0.35em;
|
|
650
|
+
background: var(--surface-1, #161616);
|
|
651
|
+
border-radius: var(--radius-sm, 4px);
|
|
652
|
+
color: var(--color-accent, #6366f1);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.md-preview :global(pre) {
|
|
656
|
+
background: var(--surface-1, #161616);
|
|
657
|
+
border-radius: var(--radius-sm, 6px);
|
|
658
|
+
padding: var(--space-3, 12px);
|
|
659
|
+
overflow-x: auto;
|
|
660
|
+
margin: 0.5em 0;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.md-preview :global(pre code) {
|
|
664
|
+
background: transparent;
|
|
665
|
+
padding: 0;
|
|
666
|
+
font-size: 0.85em;
|
|
667
|
+
color: var(--color-text, #e8e8e8);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.md-preview :global(ul),
|
|
671
|
+
.md-preview :global(ol) {
|
|
672
|
+
margin: 0.4em 0;
|
|
673
|
+
padding-left: 1.5em;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.md-preview :global(li) {
|
|
677
|
+
margin: 0.2em 0;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.md-preview :global(hr) {
|
|
681
|
+
border: none;
|
|
682
|
+
border-top: 1px solid var(--color-border, #2a2a2a);
|
|
683
|
+
margin: 0.8em 0;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.md-preview :global(a) {
|
|
687
|
+
color: var(--color-accent, #6366f1);
|
|
688
|
+
text-decoration: underline;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/* ===== TUI mode ===== */
|
|
692
|
+
.tui-md {
|
|
693
|
+
display: inline-flex;
|
|
694
|
+
flex-direction: column;
|
|
695
|
+
font-family: var(--font-mono, monospace);
|
|
696
|
+
font-size: var(--text-sm, 14px);
|
|
697
|
+
line-height: 1.4;
|
|
698
|
+
color: var(--tui-text, #e0e0e0);
|
|
699
|
+
background: var(--tui-surface, #16213e);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.tui-md.disabled {
|
|
703
|
+
opacity: 0.4;
|
|
704
|
+
pointer-events: none;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.tui-top,
|
|
708
|
+
.tui-bottom {
|
|
709
|
+
color: var(--tui-border, #0f3460);
|
|
710
|
+
white-space: pre;
|
|
711
|
+
user-select: none;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.tui-top.focused,
|
|
715
|
+
.tui-bottom.focused {
|
|
716
|
+
color: var(--tui-accent, #00d4ff);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.tui-row {
|
|
720
|
+
display: flex;
|
|
721
|
+
white-space: pre;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.tui-side {
|
|
725
|
+
color: var(--tui-border, #0f3460);
|
|
726
|
+
user-select: none;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.tui-side.focused {
|
|
730
|
+
color: var(--tui-accent, #00d4ff);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.tui-textarea {
|
|
734
|
+
flex: 1;
|
|
735
|
+
background: transparent;
|
|
736
|
+
border: none;
|
|
737
|
+
outline: none;
|
|
738
|
+
color: var(--tui-text, #e0e0e0);
|
|
739
|
+
font-family: var(--font-mono, monospace);
|
|
740
|
+
font-size: inherit;
|
|
741
|
+
line-height: 1.4;
|
|
742
|
+
resize: none;
|
|
743
|
+
width: 36ch;
|
|
744
|
+
padding: 0;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.tui-preview {
|
|
748
|
+
flex: 1;
|
|
749
|
+
width: 36ch;
|
|
750
|
+
padding: 0;
|
|
751
|
+
color: var(--tui-text, #e0e0e0);
|
|
752
|
+
font-family: var(--font-mono, monospace);
|
|
753
|
+
font-size: inherit;
|
|
754
|
+
line-height: 1.4;
|
|
755
|
+
white-space: pre-wrap;
|
|
756
|
+
overflow: hidden;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.tui-toolbar {
|
|
760
|
+
display: flex;
|
|
761
|
+
gap: 4px;
|
|
762
|
+
padding: 2px 0;
|
|
763
|
+
font-family: var(--font-mono, monospace);
|
|
764
|
+
font-size: var(--text-sm, 14px);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.tui-tab {
|
|
768
|
+
color: var(--tui-text-dim, #888888);
|
|
769
|
+
cursor: pointer;
|
|
770
|
+
user-select: none;
|
|
771
|
+
padding: 0 4px;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.tui-tab.active {
|
|
775
|
+
color: var(--tui-accent, #00d4ff);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.tui-tab:focus-visible {
|
|
779
|
+
outline: 1px solid var(--tui-accent, #00d4ff);
|
|
780
|
+
}
|
|
781
|
+
</style>
|