@ltm-blueverse/alpha-semantic-hub 0.7.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/LICENSE +21 -0
- package/README.md +1674 -0
- package/explorer/README.md +282 -0
- package/explorer/src/App.css +0 -0
- package/explorer/src/App.tsx +2035 -0
- package/explorer/src/ErrorBoundary.tsx +112 -0
- package/explorer/src/ExploreWorkspaceTabs.tsx +29 -0
- package/explorer/src/assets/hero.png +0 -0
- package/explorer/src/assets/react.svg +1 -0
- package/explorer/src/assets/vite.svg +1 -0
- package/explorer/src/explorerCapabilities.ts +24 -0
- package/explorer/src/index.css +71 -0
- package/explorer/src/main.tsx +7 -0
- package/explorer/src/store/edgePairKeys.d.ts +2 -0
- package/explorer/src/store/edgePairKeys.js +7 -0
- package/explorer/src/store/graphStore.ts +183 -0
- package/explorer/src/store/registryStore.ts +78 -0
- package/explorer/src/types.d.ts +16 -0
- package/explorer/src/ui/primitives.css +414 -0
- package/explorer/src/ui/primitives.tsx +170 -0
- package/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx +279 -0
- package/explorer/src/workspaces/DiffMergeWorkspace/DiffMergeWorkspace.tsx +121 -0
- package/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx +454 -0
- package/explorer/src/workspaces/EnrichWorkspace/RegistryTab.tsx +285 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphCanvas.tsx +2673 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx +771 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx +410 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +3584 -0
- package/explorer/src/workspaces/GraphWorkspace/MarkdownContentViewer.tsx +705 -0
- package/explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx +55 -0
- package/explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx +193 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts +15 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts +25 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts +15 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts +52 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts +36 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/types.ts +41 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts +779 -0
- package/explorer/src/workspaces/GraphWorkspace/graphColorLegend.ts +36 -0
- package/explorer/src/workspaces/GraphWorkspace/graphConfig.ts +102 -0
- package/explorer/src/workspaces/GraphWorkspace/graphEntityShape.ts +34 -0
- package/explorer/src/workspaces/GraphWorkspace/graphLoading.ts +84 -0
- package/explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts +775 -0
- package/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts +2753 -0
- package/explorer/src/workspaces/GraphWorkspace/graphStructureLayer.ts +335 -0
- package/explorer/src/workspaces/GraphWorkspace/graphTheme.ts +1078 -0
- package/explorer/src/workspaces/GraphWorkspace/graphViewCopy.ts +41 -0
- package/explorer/src/workspaces/GraphWorkspace/localGraphTransition.ts +9 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownEditorState.ts +114 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownResourceClient.ts +103 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownUrlSafety.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/nodeMarkdownSync.ts +81 -0
- package/explorer/src/workspaces/GraphWorkspace/pluginRegistryPredicates.ts +24 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx +353 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx +522 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/index.ts +16 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx +120 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx +231 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx +142 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/types.ts +106 -0
- package/explorer/src/workspaces/GraphWorkspace/realtimeGraphAttributes.ts +50 -0
- package/explorer/src/workspaces/GraphWorkspace/scene.ts +79 -0
- package/explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts +421 -0
- package/explorer/src/workspaces/GraphWorkspace/smallGraphLayout.ts +135 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalLifecyclePredicates.ts +31 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalScrubberBounds.ts +46 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalSnapshotGuards.ts +113 -0
- package/explorer/src/workspaces/GraphWorkspace/types.ts +395 -0
- package/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts +746 -0
- package/explorer/src/workspaces/GraphWorkspace/useMarkdownEditor.ts +177 -0
- package/explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx +196 -0
- package/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx +177 -0
- package/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx +291 -0
- package/explorer/src/workspaces/ManageWorkspace/OntologySummaryTab.tsx +346 -0
- package/explorer/src/workspaces/MemoryWorkspace.tsx +464 -0
- package/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx +437 -0
- package/explorer/src/workspaces/OntologyWorkspace/HealthTab.tsx +214 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyEditor.tsx +770 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyLoader.tsx +913 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx +930 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx +590 -0
- package/explorer/src/workspaces/OntologyWorkspace/ProposalReview.tsx +429 -0
- package/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx +696 -0
- package/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx +363 -0
- package/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx +546 -0
- package/explorer/src/workspaces/OntologyWorkspace/api.ts +164 -0
- package/explorer/src/workspaces/OntologyWorkspace/index.tsx +101 -0
- package/explorer/src/workspaces/OntologyWorkspace/ontologyEditorModel.ts +87 -0
- package/explorer/src/workspaces/OntologyWorkspace/ontologyUrlState.ts +94 -0
- package/explorer/src/workspaces/OntologyWorkspace/types.ts +119 -0
- package/explorer/src/workspaces/ReasoningWorkspace.tsx +243 -0
- package/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx +271 -0
- package/explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx +97 -0
- package/explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx +95 -0
- package/explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx +116 -0
- package/explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx +112 -0
- package/explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx +70 -0
- package/explorer/src/workspaces/VocabularyWorkspace/queries.ts +52 -0
- package/explorer/src/workspaces/VocabularyWorkspace/types.ts +24 -0
- package/package.json +31 -0
- package/plugins/.claude-plugin/README.md +262 -0
- package/plugins/.claude-plugin/marketplace.json +20 -0
- package/plugins/.claude-plugin/plugin.json +33 -0
- package/plugins/.cline-plugin/README.md +32 -0
- package/plugins/.cline-plugin/marketplace.json +17 -0
- package/plugins/.cline-plugin/plugin.json +35 -0
- package/plugins/.codex-plugin/marketplace.json +21 -0
- package/plugins/.codex-plugin/plugin.json +35 -0
- package/plugins/.continue-plugin/README.md +34 -0
- package/plugins/.continue-plugin/marketplace.json +17 -0
- package/plugins/.continue-plugin/plugin.json +33 -0
- package/plugins/.cursor-plugin/marketplace.json +18 -0
- package/plugins/.cursor-plugin/plugin.json +32 -0
- package/plugins/.openclaw-plugin/README.md +62 -0
- package/plugins/.openclaw-plugin/marketplace.json +18 -0
- package/plugins/.openclaw-plugin/plugin.json +46 -0
- package/plugins/.pi-plugin/README.md +35 -0
- package/plugins/.vscode-plugin/README.md +48 -0
- package/plugins/.vscode-plugin/marketplace.json +17 -0
- package/plugins/.vscode-plugin/plugin.json +35 -0
- package/plugins/.windsurf-plugin/README.md +39 -0
- package/plugins/.windsurf-plugin/marketplace.json +17 -0
- package/plugins/.windsurf-plugin/plugin.json +35 -0
- package/plugins/agents/decision-advisor.md +126 -0
- package/plugins/agents/explainability.md +129 -0
- package/plugins/agents/kg-assistant.md +73 -0
- package/plugins/hooks/hooks.json +12 -0
- package/plugins/skills/causal/SKILL.md +74 -0
- package/plugins/skills/change/SKILL.md +80 -0
- package/plugins/skills/decision/SKILL.md +197 -0
- package/plugins/skills/deduplicate/SKILL.md +41 -0
- package/plugins/skills/embed/SKILL.md +230 -0
- package/plugins/skills/explain/SKILL.md +44 -0
- package/plugins/skills/export/SKILL.md +67 -0
- package/plugins/skills/extract/SKILL.md +93 -0
- package/plugins/skills/ingest/SKILL.md +38 -0
- package/plugins/skills/ontology/SKILL.md +89 -0
- package/plugins/skills/policy/SKILL.md +66 -0
- package/plugins/skills/provenance/SKILL.md +70 -0
- package/plugins/skills/query/SKILL.md +84 -0
- package/plugins/skills/reason/SKILL.md +201 -0
- package/plugins/skills/temporal/SKILL.md +164 -0
- package/plugins/skills/validate/SKILL.md +228 -0
- package/plugins/skills/visualize/SKILL.md +249 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useId,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
type KeyboardEvent,
|
|
10
|
+
} from "react";
|
|
11
|
+
import ReactMarkdown, { type Components } from "react-markdown";
|
|
12
|
+
import remarkGfm from "remark-gfm";
|
|
13
|
+
import {
|
|
14
|
+
Check,
|
|
15
|
+
Code2,
|
|
16
|
+
Copy,
|
|
17
|
+
Eye,
|
|
18
|
+
ExternalLink,
|
|
19
|
+
Image as ImageIcon,
|
|
20
|
+
Loader2,
|
|
21
|
+
Pencil,
|
|
22
|
+
RefreshCw,
|
|
23
|
+
X,
|
|
24
|
+
} from "lucide-react";
|
|
25
|
+
import { GRAPH_THEME } from "./graphTheme";
|
|
26
|
+
import type { MarkdownApplyResult } from "./markdownResourceClient";
|
|
27
|
+
import type { MarkdownResourceRef } from "./markdownEditorState";
|
|
28
|
+
import { isSafeUrl } from "./markdownUrlSafety";
|
|
29
|
+
import { useMarkdownEditor } from "./useMarkdownEditor";
|
|
30
|
+
|
|
31
|
+
export interface MarkdownContentViewerProps {
|
|
32
|
+
content?: string | null;
|
|
33
|
+
resource?: MarkdownResourceRef;
|
|
34
|
+
onApplied?: (result: MarkdownApplyResult) => void;
|
|
35
|
+
onDirtyChange?: (dirty: boolean) => void;
|
|
36
|
+
className?: string;
|
|
37
|
+
defaultMode?: "preview" | "source";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Exported for unit-testing the roving-tabindex navigation logic without a DOM.
|
|
41
|
+
// Given the ordered list of tab modes and the currently focused mode, returns
|
|
42
|
+
// the mode that should receive focus for a given keyboard key. Returns null if
|
|
43
|
+
// the key is not a navigation key so callers can handle the default case.
|
|
44
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
45
|
+
export function resolveTabNavigation(
|
|
46
|
+
current: "preview" | "source",
|
|
47
|
+
key: string,
|
|
48
|
+
): "preview" | "source" | null {
|
|
49
|
+
const order = ["preview", "source"] as const;
|
|
50
|
+
const idx = order.indexOf(current);
|
|
51
|
+
switch (key) {
|
|
52
|
+
case "ArrowRight": return order[(idx + 1) % order.length];
|
|
53
|
+
case "ArrowLeft": return order[(idx - 1 + order.length) % order.length];
|
|
54
|
+
case "Home": return order[0];
|
|
55
|
+
case "End": return order[order.length - 1];
|
|
56
|
+
default: return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function MarkdownContentViewer({
|
|
61
|
+
content,
|
|
62
|
+
resource,
|
|
63
|
+
onApplied,
|
|
64
|
+
onDirtyChange,
|
|
65
|
+
className,
|
|
66
|
+
defaultMode = "preview",
|
|
67
|
+
}: MarkdownContentViewerProps) {
|
|
68
|
+
const [activeMode, setActiveMode] = useState<"preview" | "source">(defaultMode);
|
|
69
|
+
const [copied, setCopied] = useState(false);
|
|
70
|
+
const modeBeforeEditRef = useRef<"preview" | "source">(defaultMode);
|
|
71
|
+
const resourceKey = resource ? `${resource.kind}:${resource.id}` : "";
|
|
72
|
+
const [activeResourceKey, setActiveResourceKey] = useState(resourceKey);
|
|
73
|
+
const editor = useMarkdownEditor({ resource, onApplied, onDirtyChange });
|
|
74
|
+
const {
|
|
75
|
+
session,
|
|
76
|
+
error,
|
|
77
|
+
dirty,
|
|
78
|
+
editing,
|
|
79
|
+
saving,
|
|
80
|
+
loading,
|
|
81
|
+
} = editor;
|
|
82
|
+
|
|
83
|
+
if (activeResourceKey !== resourceKey) {
|
|
84
|
+
setActiveResourceKey(resourceKey);
|
|
85
|
+
setCopied(false);
|
|
86
|
+
setActiveMode(defaultMode);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Track the content value for which the copied indicator is valid.
|
|
90
|
+
const [copiedForContent, setCopiedForContent] = useState<string | null | undefined>(content);
|
|
91
|
+
if (copiedForContent !== content) {
|
|
92
|
+
setCopiedForContent(content);
|
|
93
|
+
if (copied) setCopied(false);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// useId (not hardcoded strings) so the ids stay unique if more than one viewer
|
|
97
|
+
// is ever mounted at once — the same pattern GraphWorkspace uses for its search
|
|
98
|
+
// combobox. Hardcoded ids would collide silently in that case.
|
|
99
|
+
const baseId = useId();
|
|
100
|
+
const previewTabId = `${baseId}-tab-preview`;
|
|
101
|
+
const sourceTabId = `${baseId}-tab-source`;
|
|
102
|
+
const panelId = `${baseId}-panel`;
|
|
103
|
+
|
|
104
|
+
// Roving tabindex: the tablist is one Tab stop and arrows move focus within it.
|
|
105
|
+
// Focus is tracked separately from selection because activation is manual (see
|
|
106
|
+
// handleTabKeyDown), so a tab can hold focus without being the selected one.
|
|
107
|
+
//
|
|
108
|
+
// focusedMode is stored in a ref rather than state so that moving focus with
|
|
109
|
+
// arrow keys does NOT trigger a React re-render. A re-render here is expensive:
|
|
110
|
+
// react-markdown@10 has no internal memoisation and calls processor.parse() +
|
|
111
|
+
// processor.runSync() unconditionally on every render — measured at 385ms for a
|
|
112
|
+
// 1000-row GFM table and 1.4s at 2000 rows (#1118). Using a ref means arrow-key
|
|
113
|
+
// navigation is free of Markdown re-parses while still keeping the DOM tabIndex
|
|
114
|
+
// attributes correct via direct mutation (the same pattern used by WAI-ARIA APG
|
|
115
|
+
// keyboard examples for roving tabindex).
|
|
116
|
+
//
|
|
117
|
+
// The JSX tabIndex props use activeMode (not the ref) to satisfy the
|
|
118
|
+
// react-hooks/refs lint rule that bars ref reads during render. JSX provides the
|
|
119
|
+
// correct value on initial render and after selectMode() calls (which always keep
|
|
120
|
+
// focusedModeRef.current === activeMode at React render boundaries). A
|
|
121
|
+
// useLayoutEffect (see below) corrects any JSX overwrite that occurs when focus
|
|
122
|
+
// and selection temporarily differ during arrow navigation.
|
|
123
|
+
const focusedModeRef = useRef<"preview" | "source">(defaultMode);
|
|
124
|
+
const previewTabRef = useRef<HTMLButtonElement>(null);
|
|
125
|
+
const sourceTabRef = useRef<HTMLButtonElement>(null);
|
|
126
|
+
|
|
127
|
+
const focusTab = (mode: "preview" | "source") => {
|
|
128
|
+
focusedModeRef.current = mode;
|
|
129
|
+
// Imperatively update tabIndex on both buttons so the roving tabindex
|
|
130
|
+
// DOM state is correct without scheduling a React re-render.
|
|
131
|
+
if (previewTabRef.current) previewTabRef.current.tabIndex = mode === "preview" ? 0 : -1;
|
|
132
|
+
if (sourceTabRef.current) sourceTabRef.current.tabIndex = mode === "source" ? 0 : -1;
|
|
133
|
+
(mode === "preview" ? previewTabRef : sourceTabRef).current?.focus();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const selectMode = (mode: "preview" | "source") => {
|
|
137
|
+
// Keep the ref in sync before setActiveMode so the upcoming re-render reads
|
|
138
|
+
// the correct focusedModeRef.current when evaluating JSX tabIndex props.
|
|
139
|
+
focusedModeRef.current = mode;
|
|
140
|
+
setActiveMode(mode);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Manual activation (APG permits it, and here it is required): arrows move
|
|
144
|
+
// focus only, Enter/Space activates via the native button click. Automatic
|
|
145
|
+
// activation would re-run the full markdown parse on every arrow keypress —
|
|
146
|
+
// measured at 385ms for a 1000-row GFM table and 1.4s at 2000 rows (#1118).
|
|
147
|
+
const handleTabKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
|
148
|
+
const next = resolveTabNavigation(focusedModeRef.current, event.key);
|
|
149
|
+
if (next === null) return;
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
focusTab(next);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const copyTimeoutRef = useRef<number | undefined>(undefined);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
return () => {
|
|
157
|
+
clearTimeout(copyTimeoutRef.current);
|
|
158
|
+
};
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
// When the viewed resource/node changes, reset focusedModeRef to match the
|
|
162
|
+
// incoming defaultMode. The render-phase setActiveMode(defaultMode) above
|
|
163
|
+
// resets React state, but refs are not state and must be updated separately.
|
|
164
|
+
// useLayoutEffect fires synchronously before paint so the ref is correct before
|
|
165
|
+
// the no-deps tabIndex-correction effect (declared next) reads it.
|
|
166
|
+
// Using resourceKey as the dep means this runs exactly once per resource change,
|
|
167
|
+
// immediately after the render that detected the change.
|
|
168
|
+
useLayoutEffect(() => {
|
|
169
|
+
focusedModeRef.current = defaultMode;
|
|
170
|
+
}, [resourceKey, defaultMode]);
|
|
171
|
+
|
|
172
|
+
// After every render, restore the DOM tabIndex to match focusedModeRef.current.
|
|
173
|
+
// This is necessary because the JSX tabIndex props derive from activeMode, which
|
|
174
|
+
// is correct for initial render and for renders triggered by selectMode(). However,
|
|
175
|
+
// when focus and selection differ (i.e. after arrow-key navigation, before Enter/Space),
|
|
176
|
+
// any unrelated re-render (copy-button click, parent update, etc.) will reconcile JSX
|
|
177
|
+
// tabIndex={activeMode === X} and overwrite the imperative tabIndex values set by
|
|
178
|
+
// focusTab(). useLayoutEffect fires synchronously after React's DOM mutations, before
|
|
179
|
+
// paint, so it corrects any such overwrite before the user sees it. It does not
|
|
180
|
+
// schedule another render — the two property writes are pure DOM mutations.
|
|
181
|
+
// No deps array: intentional. The correction must run after every render, not just mount.
|
|
182
|
+
// SSR-safe: useLayoutEffect is silently skipped on the server; the JSX tabIndex from
|
|
183
|
+
// activeMode provides the correct initial value (focusedModeRef.current === activeMode
|
|
184
|
+
// at mount). Strict Mode: runs twice on remount — both runs write the same values,
|
|
185
|
+
// no state mutation, no render triggered.
|
|
186
|
+
useLayoutEffect(() => {
|
|
187
|
+
if (previewTabRef.current) {
|
|
188
|
+
previewTabRef.current.tabIndex = focusedModeRef.current === "preview" ? 0 : -1;
|
|
189
|
+
}
|
|
190
|
+
if (sourceTabRef.current) {
|
|
191
|
+
sourceTabRef.current.tabIndex = focusedModeRef.current === "source" ? 0 : -1;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const rawContent = editor.editing
|
|
196
|
+
? editor.session?.draft ?? ""
|
|
197
|
+
: (typeof content === "string" ? content : "");
|
|
198
|
+
|
|
199
|
+
const previewContent = useMemo(() => {
|
|
200
|
+
if (!editor.editing) return rawContent;
|
|
201
|
+
const lines = rawContent.split(/\r?\n/);
|
|
202
|
+
if (lines[0] !== "---") return rawContent;
|
|
203
|
+
const closingIndex = lines.findIndex((line, index) => index > 0 && line === "---");
|
|
204
|
+
return closingIndex < 0 ? rawContent : lines.slice(closingIndex + 1).join("\n").replace(/^\n/, "");
|
|
205
|
+
}, [editor.editing, rawContent]);
|
|
206
|
+
|
|
207
|
+
const hasContent = rawContent.trim().length > 0;
|
|
208
|
+
|
|
209
|
+
const renderedMarkdown = useMemo(
|
|
210
|
+
() => (
|
|
211
|
+
<ReactMarkdown remarkPlugins={REMARK_PLUGINS} components={MARKDOWN_COMPONENTS}>
|
|
212
|
+
{previewContent}
|
|
213
|
+
</ReactMarkdown>
|
|
214
|
+
),
|
|
215
|
+
[previewContent],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const handleCopy = async () => {
|
|
219
|
+
if (!hasContent) return;
|
|
220
|
+
try {
|
|
221
|
+
await navigator.clipboard.writeText(rawContent);
|
|
222
|
+
clearTimeout(copyTimeoutRef.current);
|
|
223
|
+
setCopied(true);
|
|
224
|
+
copyTimeoutRef.current = window.setTimeout(() => setCopied(false), 1500);
|
|
225
|
+
} catch {
|
|
226
|
+
// Clipboard write unavailable.
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const handleEdit = async () => {
|
|
231
|
+
modeBeforeEditRef.current = activeMode;
|
|
232
|
+
selectMode("source");
|
|
233
|
+
if (!await editor.beginEdit()) {
|
|
234
|
+
selectMode(modeBeforeEditRef.current);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const handleCancel = () => {
|
|
239
|
+
editor.discard();
|
|
240
|
+
selectMode(modeBeforeEditRef.current);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const handleApply = async () => {
|
|
244
|
+
if (await editor.save()) {
|
|
245
|
+
selectMode("preview");
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div className={className} style={viewerContainerStyle}>
|
|
251
|
+
<div style={viewerHeaderStyle}>
|
|
252
|
+
<div style={{ display: "flex", gap: 4 }} role="tablist" aria-label="Content view mode">
|
|
253
|
+
<button
|
|
254
|
+
type="button"
|
|
255
|
+
role="tab"
|
|
256
|
+
id={previewTabId}
|
|
257
|
+
ref={previewTabRef}
|
|
258
|
+
aria-selected={activeMode === "preview"}
|
|
259
|
+
aria-controls={panelId}
|
|
260
|
+
tabIndex={activeMode === "preview" ? 0 : -1}
|
|
261
|
+
onClick={() => selectMode("preview")}
|
|
262
|
+
onKeyDown={handleTabKeyDown}
|
|
263
|
+
style={{ ...tabBtnStyle, ...(activeMode === "preview" ? activeTabBtnStyle : {}) }}
|
|
264
|
+
>
|
|
265
|
+
<Eye size={12} style={{ marginRight: 5 }} aria-hidden="true" />
|
|
266
|
+
Preview
|
|
267
|
+
</button>
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
role="tab"
|
|
271
|
+
id={sourceTabId}
|
|
272
|
+
ref={sourceTabRef}
|
|
273
|
+
aria-selected={activeMode === "source"}
|
|
274
|
+
aria-controls={panelId}
|
|
275
|
+
tabIndex={activeMode === "source" ? 0 : -1}
|
|
276
|
+
onClick={() => selectMode("source")}
|
|
277
|
+
onKeyDown={handleTabKeyDown}
|
|
278
|
+
style={{ ...tabBtnStyle, ...(activeMode === "source" ? activeTabBtnStyle : {}) }}
|
|
279
|
+
>
|
|
280
|
+
<Code2 size={12} style={{ marginRight: 5 }} aria-hidden="true" />
|
|
281
|
+
Source
|
|
282
|
+
</button>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
286
|
+
{hasContent && (
|
|
287
|
+
<button type="button" onClick={() => void handleCopy()} style={copyBtnStyle} title="Copy raw content">
|
|
288
|
+
{copied ? (
|
|
289
|
+
<>
|
|
290
|
+
<Check size={12} color="#3fb950" style={{ marginRight: 4 }} aria-hidden="true" />
|
|
291
|
+
<span style={{ color: "#3fb950", fontSize: 11 }}>Copied</span>
|
|
292
|
+
</>
|
|
293
|
+
) : (
|
|
294
|
+
<>
|
|
295
|
+
<Copy size={12} style={{ marginRight: 4 }} aria-hidden="true" />
|
|
296
|
+
<span style={{ fontSize: 11 }}>Copy</span>
|
|
297
|
+
</>
|
|
298
|
+
)}
|
|
299
|
+
</button>
|
|
300
|
+
)}
|
|
301
|
+
{resource && !editing && !loading ? (
|
|
302
|
+
<button type="button" onClick={() => void handleEdit()} style={copyBtnStyle}>
|
|
303
|
+
<Pencil size={12} style={{ marginRight: 4 }} aria-hidden="true" />
|
|
304
|
+
Edit
|
|
305
|
+
</button>
|
|
306
|
+
) : null}
|
|
307
|
+
{loading ? (
|
|
308
|
+
<button type="button" disabled style={{ ...copyBtnStyle, opacity: 0.65 }}>
|
|
309
|
+
<Loader2 size={12} className="animate-spin" style={{ marginRight: 4 }} aria-hidden="true" />
|
|
310
|
+
Loading…
|
|
311
|
+
</button>
|
|
312
|
+
) : null}
|
|
313
|
+
{editing ? (
|
|
314
|
+
<>
|
|
315
|
+
<button type="button" onClick={handleCancel} disabled={saving} style={copyBtnStyle}>
|
|
316
|
+
<X size={12} style={{ marginRight: 4 }} aria-hidden="true" />
|
|
317
|
+
Cancel
|
|
318
|
+
</button>
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
onClick={() => void handleApply()}
|
|
322
|
+
disabled={saving || !dirty}
|
|
323
|
+
title={!dirty ? "Make a change before applying" : undefined}
|
|
324
|
+
style={{ ...saveBtnStyle, opacity: saving || !dirty ? 0.55 : 1 }}
|
|
325
|
+
>
|
|
326
|
+
{saving ? (
|
|
327
|
+
<Loader2 size={12} className="animate-spin" style={{ marginRight: 4 }} aria-hidden="true" />
|
|
328
|
+
) : (
|
|
329
|
+
<Check size={12} style={{ marginRight: 4 }} aria-hidden="true" />
|
|
330
|
+
)}
|
|
331
|
+
{saving ? "Applying…" : "Apply"}
|
|
332
|
+
</button>
|
|
333
|
+
</>
|
|
334
|
+
) : null}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
{error ? (
|
|
339
|
+
<div id="markdown-editor-error" role="alert" style={errorStyle}>
|
|
340
|
+
<span>{error.message}</span>
|
|
341
|
+
{error.kind === "conflict" ? (
|
|
342
|
+
<button type="button" onClick={() => void editor.reloadLatest()} style={errorActionStyle}>
|
|
343
|
+
<RefreshCw size={12} style={{ marginRight: 4 }} aria-hidden="true" />
|
|
344
|
+
Reload latest
|
|
345
|
+
</button>
|
|
346
|
+
) : null}
|
|
347
|
+
</div>
|
|
348
|
+
) : null}
|
|
349
|
+
|
|
350
|
+
{/* Both tabs point aria-controls at this one panel: only the active view is
|
|
351
|
+
ever rendered inside (edit/preview/source/empty), so per-tab panel ids
|
|
352
|
+
would leave the inactive tab referencing an element not in the DOM.
|
|
353
|
+
Wrapping all branches — including empty state and editing textarea —
|
|
354
|
+
keeps every aria-controls reference resolvable at all times.
|
|
355
|
+
tabIndex=0 because the panel is a scroll container (viewerBodyStyle caps
|
|
356
|
+
its height), so keyboard users need to be able to focus and scroll it.
|
|
357
|
+
aria-busy signals to assistive tech that the content is loading/saving. */}
|
|
358
|
+
<div
|
|
359
|
+
role="tabpanel"
|
|
360
|
+
id={panelId}
|
|
361
|
+
aria-labelledby={activeMode === "preview" ? previewTabId : sourceTabId}
|
|
362
|
+
aria-busy={saving || loading}
|
|
363
|
+
tabIndex={0}
|
|
364
|
+
style={viewerBodyStyle}
|
|
365
|
+
>
|
|
366
|
+
{activeMode === "source" && editing ? (
|
|
367
|
+
<textarea
|
|
368
|
+
aria-label="Markdown source"
|
|
369
|
+
aria-describedby={error ? "markdown-editor-error" : undefined}
|
|
370
|
+
aria-invalid={error?.kind === "validation" || undefined}
|
|
371
|
+
value={session?.draft ?? ""}
|
|
372
|
+
onChange={(event) => editor.changeDraft(event.target.value)}
|
|
373
|
+
disabled={saving}
|
|
374
|
+
spellCheck={false}
|
|
375
|
+
style={editorStyle}
|
|
376
|
+
/>
|
|
377
|
+
) : !hasContent ? (
|
|
378
|
+
<div style={emptyTextStyle}>No content available for this node.</div>
|
|
379
|
+
) : activeMode === "source" ? (
|
|
380
|
+
<pre style={sourcePreStyle}>
|
|
381
|
+
<code style={sourceCodeStyle}>{rawContent}</code>
|
|
382
|
+
</pre>
|
|
383
|
+
) : (
|
|
384
|
+
<div style={previewStyle}>{renderedMarkdown}</div>
|
|
385
|
+
)}
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* ─── Markdown rendering config ───────────────────────────────────── */
|
|
392
|
+
|
|
393
|
+
// Both props are hoisted to module scope so they keep a stable identity across
|
|
394
|
+
// renders. As inline literals they allocated a fresh plugin array and ~20 fresh
|
|
395
|
+
// arrow components on every render, which made React treat every mapped tag as a
|
|
396
|
+
// new element type and remount the entire rendered subtree instead of updating
|
|
397
|
+
// it (issue #1118). The arrow bodies only read the style constants below at call
|
|
398
|
+
// time, so declaring the map before them is safe.
|
|
399
|
+
const REMARK_PLUGINS = [remarkGfm];
|
|
400
|
+
|
|
401
|
+
const MARKDOWN_COMPONENTS: Components = {
|
|
402
|
+
// C-1: react-markdown passes a HAST `node` prop (the raw AST
|
|
403
|
+
// Element) to every custom component override via passNode:true.
|
|
404
|
+
// In React 19 any unknown prop spreads onto a native element are
|
|
405
|
+
// serialised as HTML attributes, producing node="[object Object]"
|
|
406
|
+
// on every rendered link. Fix: destructure `node` by name so it
|
|
407
|
+
// is explicitly discarded, then spread `...rest` to preserve all
|
|
408
|
+
// other legitimate HAST/remark-gfm attributes — e.g. the `id`,
|
|
409
|
+
// `aria-describedby`, `aria-label`, `data-footnote-ref`,
|
|
410
|
+
// `data-footnote-backref`, and `class` attrs that GFM footnotes
|
|
411
|
+
// require for correct in-page navigation and accessibility.
|
|
412
|
+
//
|
|
413
|
+
// C-2: fragment links (#anchor, GFM footnote backlinks) must
|
|
414
|
+
// navigate within the current document. External links continue
|
|
415
|
+
// to use target="_blank" with noopener noreferrer.
|
|
416
|
+
//
|
|
417
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
418
|
+
a: ({ href, children, title, node: _node, ...rest }) => {
|
|
419
|
+
if (!isSafeUrl(href)) {
|
|
420
|
+
return <span style={{ color: GRAPH_THEME.ui.text.muted, textDecoration: "line-through" }}>{children}</span>;
|
|
421
|
+
}
|
|
422
|
+
// isSafeUrl returning true guarantees href is a non-empty string.
|
|
423
|
+
const safeHref = href ?? "";
|
|
424
|
+
// Fragment links (#section, footnote backlinks like
|
|
425
|
+
// #user-content-fnref-1) are in-document anchors. Opening them
|
|
426
|
+
// in a new tab would break GFM footnote back-navigation.
|
|
427
|
+
const isFragment = safeHref.startsWith("#");
|
|
428
|
+
if (isFragment) {
|
|
429
|
+
return (
|
|
430
|
+
<a href={safeHref} title={title} style={linkStyle} {...rest}>
|
|
431
|
+
{children}
|
|
432
|
+
</a>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return (
|
|
436
|
+
<a href={safeHref} title={title} target="_blank" rel="noopener noreferrer" style={linkStyle} {...rest}>
|
|
437
|
+
{children}
|
|
438
|
+
<ExternalLink size={10} style={{ marginLeft: 3, verticalAlign: "middle", display: "inline" }} />
|
|
439
|
+
</a>
|
|
440
|
+
);
|
|
441
|
+
},
|
|
442
|
+
img: ({ src, alt }) => (
|
|
443
|
+
<span style={imageBadgeStyle} title={src || "Image"}>
|
|
444
|
+
<ImageIcon size={12} style={{ marginRight: 5 }} />
|
|
445
|
+
<span>Image: {alt || src || "unlabeled"}</span>
|
|
446
|
+
</span>
|
|
447
|
+
),
|
|
448
|
+
h1: ({ children }) => <h1 style={h1Style}>{children}</h1>,
|
|
449
|
+
h2: ({ children }) => <h2 style={h2Style}>{children}</h2>,
|
|
450
|
+
h3: ({ children }) => <h3 style={h3Style}>{children}</h3>,
|
|
451
|
+
h4: ({ children }) => <h4 style={h4Style}>{children}</h4>,
|
|
452
|
+
p: ({ children }) => <p style={{ margin: "0 0 8px 0" }}>{children}</p>,
|
|
453
|
+
ul: ({ children }) => <ul style={{ margin: "0 0 8px 0", paddingLeft: 18 }}>{children}</ul>,
|
|
454
|
+
ol: ({ children }) => <ol style={{ margin: "0 0 8px 0", paddingLeft: 18 }}>{children}</ol>,
|
|
455
|
+
li: ({ children }) => <li style={{ marginBottom: 3 }}>{children}</li>,
|
|
456
|
+
blockquote: ({ children }) => <blockquote style={blockquoteStyle}>{children}</blockquote>,
|
|
457
|
+
hr: () => <hr style={{ border: "none", borderTop: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`, margin: "10px 0" }} />,
|
|
458
|
+
table: ({ children }) => (
|
|
459
|
+
<div style={{ width: "100%", overflowX: "auto", margin: "8px 0", borderRadius: 6, border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}` }}>
|
|
460
|
+
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12 }}>{children}</table>
|
|
461
|
+
</div>
|
|
462
|
+
),
|
|
463
|
+
thead: ({ children }) => <thead style={{ background: "rgba(255, 255, 255, 0.04)" }}>{children}</thead>,
|
|
464
|
+
tbody: ({ children }) => <tbody>{children}</tbody>,
|
|
465
|
+
tr: ({ children }) => <tr style={{ borderBottom: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}` }}>{children}</tr>,
|
|
466
|
+
th: ({ children }) => <th style={{ padding: "6px 8px", textAlign: "left", fontWeight: 700, color: GRAPH_THEME.ui.text.strong, borderRight: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}` }}>{children}</th>,
|
|
467
|
+
td: ({ children }) => <td style={{ padding: "6px 8px", color: GRAPH_THEME.ui.text.body, borderRight: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}` }}>{children}</td>,
|
|
468
|
+
pre: ({ children }) => <pre style={preBlockStyle}>{children}</pre>,
|
|
469
|
+
// C-1: discard `node` here too — code elements are custom components
|
|
470
|
+
// and would otherwise receive node="[object Object]" in the DOM.
|
|
471
|
+
code: ({ className: codeClass, children }) => {
|
|
472
|
+
const isInline = !codeClass && typeof children === "string" && !children.includes("\n");
|
|
473
|
+
return (
|
|
474
|
+
<code style={isInline ? inlineCodeStyle : blockCodeStyle}>
|
|
475
|
+
{children}
|
|
476
|
+
</code>
|
|
477
|
+
);
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/* ─── Styles ──────────────────────────────────────────────────────── */
|
|
482
|
+
|
|
483
|
+
const viewerContainerStyle: CSSProperties = {
|
|
484
|
+
display: "flex",
|
|
485
|
+
flexDirection: "column",
|
|
486
|
+
background: "rgba(255, 255, 255, 0.025)",
|
|
487
|
+
border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
|
|
488
|
+
borderRadius: 12,
|
|
489
|
+
overflow: "hidden",
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const viewerHeaderStyle: CSSProperties = {
|
|
493
|
+
display: "flex",
|
|
494
|
+
alignItems: "center",
|
|
495
|
+
justifyContent: "space-between",
|
|
496
|
+
gap: 8,
|
|
497
|
+
flexWrap: "wrap",
|
|
498
|
+
padding: "6px 10px",
|
|
499
|
+
background: "rgba(0, 0, 0, 0.2)",
|
|
500
|
+
borderBottom: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const tabBtnStyle: CSSProperties = {
|
|
504
|
+
display: "inline-flex",
|
|
505
|
+
alignItems: "center",
|
|
506
|
+
padding: "4px 9px",
|
|
507
|
+
borderRadius: 6,
|
|
508
|
+
border: "1px solid transparent",
|
|
509
|
+
background: "transparent",
|
|
510
|
+
color: GRAPH_THEME.ui.text.muted,
|
|
511
|
+
fontSize: 12,
|
|
512
|
+
fontWeight: 600,
|
|
513
|
+
cursor: "pointer",
|
|
514
|
+
transition: "all 150ms ease",
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const activeTabBtnStyle: CSSProperties = {
|
|
518
|
+
background: GRAPH_THEME.ui.timeline.playheadSoft,
|
|
519
|
+
border: `1px solid ${GRAPH_THEME.ui.control.activeBorder}`,
|
|
520
|
+
color: GRAPH_THEME.ui.timeline.playhead,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const copyBtnStyle: CSSProperties = {
|
|
524
|
+
display: "inline-flex",
|
|
525
|
+
alignItems: "center",
|
|
526
|
+
padding: "3px 8px",
|
|
527
|
+
borderRadius: 6,
|
|
528
|
+
border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
|
|
529
|
+
background: "rgba(255, 255, 255, 0.04)",
|
|
530
|
+
color: GRAPH_THEME.ui.text.subtle,
|
|
531
|
+
fontSize: 11,
|
|
532
|
+
cursor: "pointer",
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const saveBtnStyle: CSSProperties = {
|
|
536
|
+
...copyBtnStyle,
|
|
537
|
+
background: GRAPH_THEME.ui.control.primaryBg,
|
|
538
|
+
border: `1px solid ${GRAPH_THEME.ui.control.primaryBorder}`,
|
|
539
|
+
color: GRAPH_THEME.ui.control.primaryText,
|
|
540
|
+
fontWeight: 700,
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const errorStyle: CSSProperties = {
|
|
544
|
+
display: "flex",
|
|
545
|
+
alignItems: "center",
|
|
546
|
+
justifyContent: "space-between",
|
|
547
|
+
gap: 8,
|
|
548
|
+
padding: "8px 12px",
|
|
549
|
+
color: "#ffb4ad",
|
|
550
|
+
background: "rgba(248, 81, 73, 0.1)",
|
|
551
|
+
borderBottom: "1px solid rgba(248, 81, 73, 0.25)",
|
|
552
|
+
fontSize: 12,
|
|
553
|
+
lineHeight: 1.5,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const errorActionStyle: CSSProperties = {
|
|
557
|
+
...copyBtnStyle,
|
|
558
|
+
flexShrink: 0,
|
|
559
|
+
color: "#ffb4ad",
|
|
560
|
+
border: "1px solid rgba(248, 81, 73, 0.32)",
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const viewerBodyStyle: CSSProperties = {
|
|
564
|
+
padding: 12,
|
|
565
|
+
maxHeight: 380,
|
|
566
|
+
overflowY: "auto",
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const editorStyle: CSSProperties = {
|
|
570
|
+
display: "block",
|
|
571
|
+
boxSizing: "border-box",
|
|
572
|
+
width: "100%",
|
|
573
|
+
minHeight: 280,
|
|
574
|
+
resize: "vertical",
|
|
575
|
+
padding: 10,
|
|
576
|
+
borderRadius: 8,
|
|
577
|
+
border: `1px solid ${GRAPH_THEME.ui.control.activeBorder}`,
|
|
578
|
+
background: "rgba(0, 0, 0, 0.3)",
|
|
579
|
+
color: GRAPH_THEME.ui.text.strong,
|
|
580
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
|
581
|
+
fontSize: 12,
|
|
582
|
+
lineHeight: 1.6,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const emptyTextStyle: CSSProperties = {
|
|
586
|
+
color: GRAPH_THEME.ui.text.muted,
|
|
587
|
+
fontSize: 12,
|
|
588
|
+
lineHeight: 1.5,
|
|
589
|
+
fontStyle: "italic",
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const sourcePreStyle: CSSProperties = {
|
|
593
|
+
margin: 0,
|
|
594
|
+
padding: 10,
|
|
595
|
+
borderRadius: 8,
|
|
596
|
+
background: "rgba(0, 0, 0, 0.3)",
|
|
597
|
+
border: "1px solid rgba(255, 255, 255, 0.05)",
|
|
598
|
+
overflowX: "auto",
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const sourceCodeStyle: CSSProperties = {
|
|
602
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
|
603
|
+
fontSize: 12,
|
|
604
|
+
lineHeight: 1.6,
|
|
605
|
+
color: GRAPH_THEME.ui.text.strong,
|
|
606
|
+
whiteSpace: "pre-wrap",
|
|
607
|
+
wordBreak: "break-word",
|
|
608
|
+
userSelect: "text",
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const previewStyle: CSSProperties = {
|
|
612
|
+
color: GRAPH_THEME.ui.text.body,
|
|
613
|
+
fontSize: 13,
|
|
614
|
+
lineHeight: 1.6,
|
|
615
|
+
wordBreak: "break-word",
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const h1Style: CSSProperties = {
|
|
619
|
+
fontSize: 16,
|
|
620
|
+
fontWeight: 700,
|
|
621
|
+
color: GRAPH_THEME.ui.text.strong,
|
|
622
|
+
marginTop: 8,
|
|
623
|
+
marginBottom: 6,
|
|
624
|
+
paddingBottom: 3,
|
|
625
|
+
borderBottom: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const h2Style: CSSProperties = {
|
|
629
|
+
fontSize: 14,
|
|
630
|
+
fontWeight: 700,
|
|
631
|
+
color: GRAPH_THEME.ui.text.strong,
|
|
632
|
+
marginTop: 8,
|
|
633
|
+
marginBottom: 4,
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const h3Style: CSSProperties = {
|
|
637
|
+
fontSize: 13,
|
|
638
|
+
fontWeight: 600,
|
|
639
|
+
color: GRAPH_THEME.ui.text.strong,
|
|
640
|
+
marginTop: 6,
|
|
641
|
+
marginBottom: 4,
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const h4Style: CSSProperties = {
|
|
645
|
+
fontSize: 12,
|
|
646
|
+
fontWeight: 600,
|
|
647
|
+
color: GRAPH_THEME.ui.text.strong,
|
|
648
|
+
marginTop: 4,
|
|
649
|
+
marginBottom: 2,
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const blockquoteStyle: CSSProperties = {
|
|
653
|
+
margin: "8px 0",
|
|
654
|
+
padding: "6px 12px",
|
|
655
|
+
borderLeft: `3px solid ${GRAPH_THEME.ui.timeline.playhead}`,
|
|
656
|
+
background: "rgba(98, 226, 205, 0.05)",
|
|
657
|
+
borderRadius: "0 6px 6px 0",
|
|
658
|
+
color: GRAPH_THEME.ui.text.body,
|
|
659
|
+
fontStyle: "italic",
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const linkStyle: CSSProperties = {
|
|
663
|
+
color: "#ff8fa0",
|
|
664
|
+
textDecoration: "underline",
|
|
665
|
+
textUnderlineOffset: "3px",
|
|
666
|
+
wordBreak: "break-all",
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const imageBadgeStyle: CSSProperties = {
|
|
670
|
+
display: "inline-flex",
|
|
671
|
+
alignItems: "center",
|
|
672
|
+
padding: "3px 7px",
|
|
673
|
+
background: "rgba(255, 255, 255, 0.04)",
|
|
674
|
+
border: `1px solid ${GRAPH_THEME.ui.surface.panelBorder}`,
|
|
675
|
+
borderRadius: 6,
|
|
676
|
+
color: GRAPH_THEME.ui.text.muted,
|
|
677
|
+
fontSize: 11,
|
|
678
|
+
margin: "3px 0",
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const inlineCodeStyle: CSSProperties = {
|
|
682
|
+
fontFamily: "'JetBrains Mono', monospace",
|
|
683
|
+
fontSize: 12,
|
|
684
|
+
padding: "2px 5px",
|
|
685
|
+
borderRadius: 4,
|
|
686
|
+
background: "rgba(255, 255, 255, 0.07)",
|
|
687
|
+
color: "#ffeaed",
|
|
688
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const preBlockStyle: CSSProperties = {
|
|
692
|
+
margin: "8px 0",
|
|
693
|
+
padding: 10,
|
|
694
|
+
borderRadius: 8,
|
|
695
|
+
background: "rgba(0, 0, 0, 0.35)",
|
|
696
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
697
|
+
overflowX: "auto",
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const blockCodeStyle: CSSProperties = {
|
|
701
|
+
fontFamily: "'JetBrains Mono', monospace",
|
|
702
|
+
fontSize: 12,
|
|
703
|
+
lineHeight: 1.5,
|
|
704
|
+
color: "#ffeaed",
|
|
705
|
+
};
|