@slidev-react/client 0.2.5
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/CHANGELOG.md +42 -0
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +44 -0
- package/src/addons/AddonProvider.tsx +25 -0
- package/src/addons/g2/G2Chart.tsx +370 -0
- package/src/addons/g2/chartPresets.ts +43 -0
- package/src/addons/g2/chartThemeTokens.ts +124 -0
- package/src/addons/g2/index.ts +36 -0
- package/src/addons/g2/style.css +31 -0
- package/src/addons/insight/Insight.tsx +10 -0
- package/src/addons/insight/InsightAddonProvider.tsx +20 -0
- package/src/addons/insight/SpotlightLayout.tsx +11 -0
- package/src/addons/insight/index.ts +17 -0
- package/src/addons/insight/style.css +34 -0
- package/src/addons/mermaid/MermaidDiagram.tsx +379 -0
- package/src/addons/mermaid/index.ts +10 -0
- package/src/addons/registry.test.ts +28 -0
- package/src/addons/registry.ts +61 -0
- package/src/addons/types.ts +6 -0
- package/src/app/App.tsx +125 -0
- package/src/app/README.md +18 -0
- package/src/app/providers/SlidesNavigationProvider.tsx +82 -0
- package/src/app/usePresentationBootstrap.ts +85 -0
- package/src/features/presentation/PresentationStatus.tsx +514 -0
- package/src/features/presentation/PrintSlidesView.tsx +350 -0
- package/src/features/presentation/browser.ts +5 -0
- package/src/features/presentation/draw/DrawOverlay.tsx +170 -0
- package/src/features/presentation/draw/DrawProvider.tsx +394 -0
- package/src/features/presentation/draw/persistence.test.ts +80 -0
- package/src/features/presentation/draw/persistence.ts +54 -0
- package/src/features/presentation/exportArtifacts.test.ts +48 -0
- package/src/features/presentation/exportArtifacts.ts +6 -0
- package/src/features/presentation/location.test.ts +73 -0
- package/src/features/presentation/location.ts +113 -0
- package/src/features/presentation/navigation/KeyboardController.tsx +73 -0
- package/src/features/presentation/navigation/PresentationNavbar.tsx +162 -0
- package/src/features/presentation/navigation/ShortcutsHelpOverlay.test.tsx +24 -0
- package/src/features/presentation/navigation/ShortcutsHelpOverlay.tsx +111 -0
- package/src/features/presentation/navigation/keyboardShortcuts.test.ts +74 -0
- package/src/features/presentation/navigation/keyboardShortcuts.ts +221 -0
- package/src/features/presentation/navigation/useSlidesNavigation.ts +15 -0
- package/src/features/presentation/overview/NotesOverview.tsx +200 -0
- package/src/features/presentation/overview/QuickOverview.tsx +126 -0
- package/src/features/presentation/path.ts +137 -0
- package/src/features/presentation/presenter/FlowTimelinePreview.test.tsx +54 -0
- package/src/features/presentation/presenter/FlowTimelinePreview.tsx +274 -0
- package/src/features/presentation/presenter/PresenterModeView.tsx +93 -0
- package/src/features/presentation/presenter/PresenterShell.tsx +286 -0
- package/src/features/presentation/presenter/PresenterSidePreview.tsx +68 -0
- package/src/features/presentation/presenter/PresenterTopProgress.tsx +28 -0
- package/src/features/presentation/presenter/SpeakerNotesPanel.tsx +51 -0
- package/src/features/presentation/presenter/StandaloneModeView.tsx +36 -0
- package/src/features/presentation/presenter/persistence.test.ts +26 -0
- package/src/features/presentation/presenter/persistence.ts +31 -0
- package/src/features/presentation/presenter/presentationSyncBridge.test.ts +87 -0
- package/src/features/presentation/presenter/presentationSyncBridge.ts +82 -0
- package/src/features/presentation/presenter/stage.ts +15 -0
- package/src/features/presentation/presenter/types.ts +30 -0
- package/src/features/presentation/presenter/useFullscreen.ts +58 -0
- package/src/features/presentation/presenter/useIdleCursor.ts +37 -0
- package/src/features/presentation/presenter/usePresentationFlowRuntime.ts +238 -0
- package/src/features/presentation/presenter/usePresenterChromeRuntime.ts +358 -0
- package/src/features/presentation/presenter/usePresenterSessionState.ts +226 -0
- package/src/features/presentation/presenter/useWakeLock.ts +110 -0
- package/src/features/presentation/recordingFilename.test.ts +46 -0
- package/src/features/presentation/recordingFilename.ts +56 -0
- package/src/features/presentation/reveal/Reveal.tsx +119 -0
- package/src/features/presentation/reveal/RevealContext.tsx +29 -0
- package/src/features/presentation/reveal/useRevealStep.ts +35 -0
- package/src/features/presentation/session.test.ts +122 -0
- package/src/features/presentation/session.ts +124 -0
- package/src/features/presentation/stage/SlidePreviewSurface.tsx +92 -0
- package/src/features/presentation/stage/SlideStage.tsx +159 -0
- package/src/features/presentation/stage/slideSurface.ts +71 -0
- package/src/features/presentation/stage/slideViewport.tsx +47 -0
- package/src/features/presentation/sync/adapters/broadcastChannelTransport.ts +40 -0
- package/src/features/presentation/sync/adapters/websocketTransport.ts +128 -0
- package/src/features/presentation/sync/model/presence.test.ts +42 -0
- package/src/features/presentation/sync/model/presence.ts +33 -0
- package/src/features/presentation/sync/model/replication.test.ts +72 -0
- package/src/features/presentation/sync/model/replication.ts +113 -0
- package/src/features/presentation/sync/model/status.test.ts +52 -0
- package/src/features/presentation/sync/model/status.ts +33 -0
- package/src/features/presentation/types.ts +1 -0
- package/src/features/presentation/usePresentationRecorder.ts +194 -0
- package/src/features/presentation/usePresentationSync.ts +423 -0
- package/src/index.ts +7 -0
- package/src/main.tsx +12 -0
- package/src/theme/ThemeProvider.test.ts +36 -0
- package/src/theme/ThemeProvider.tsx +79 -0
- package/src/theme/__mocks__/active-theme.ts +3 -0
- package/src/theme/base.css +14 -0
- package/src/theme/components.css +231 -0
- package/src/theme/index.css +11 -0
- package/src/theme/layouts/center.tsx +9 -0
- package/src/theme/layouts/cover.tsx +9 -0
- package/src/theme/layouts/default.tsx +5 -0
- package/src/theme/layouts/defaultLayouts.ts +20 -0
- package/src/theme/layouts/helpers.tsx +12 -0
- package/src/theme/layouts/image-right.tsx +21 -0
- package/src/theme/layouts/immersive.tsx +9 -0
- package/src/theme/layouts/resolveLayout.ts +9 -0
- package/src/theme/layouts/section.tsx +9 -0
- package/src/theme/layouts/statement.tsx +9 -0
- package/src/theme/layouts/two-cols.tsx +21 -0
- package/src/theme/layouts/types.ts +1 -0
- package/src/theme/layouts.css +133 -0
- package/src/theme/mark.css +379 -0
- package/src/theme/print.css +106 -0
- package/src/theme/prose.css +263 -0
- package/src/theme/registry.test.ts +21 -0
- package/src/theme/registry.ts +40 -0
- package/src/theme/tokens.css +148 -0
- package/src/theme/transitions.css +141 -0
- package/src/theme/types.ts +9 -0
- package/src/theme/useResolvedLayout.ts +24 -0
- package/src/types/generated-slides.d.ts +7 -0
- package/src/types/mdx-components.ts +7 -0
- package/src/types/plantuml-encoder.d.ts +7 -0
- package/src/ui/diagrams/PlantUmlDiagram.tsx +33 -0
- package/src/ui/mdx/MagicMoveDemo.tsx +114 -0
- package/src/ui/mdx/index.ts +21 -0
- package/src/ui/primitives/Annotate.test.tsx +64 -0
- package/src/ui/primitives/Annotate.tsx +82 -0
- package/src/ui/primitives/Badge.tsx +5 -0
- package/src/ui/primitives/Callout.tsx +24 -0
- package/src/ui/primitives/ChromeIconButton.tsx +58 -0
- package/src/ui/primitives/ChromePanel.tsx +79 -0
- package/src/ui/primitives/ChromeTag.tsx +70 -0
- package/src/ui/primitives/FormSelect.tsx +51 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SlideAddonDefinition } from "../types"
|
|
2
|
+
import {
|
|
3
|
+
Chart,
|
|
4
|
+
BarChart,
|
|
5
|
+
LineChart,
|
|
6
|
+
AreaChart,
|
|
7
|
+
ScatterChart,
|
|
8
|
+
PieChart,
|
|
9
|
+
RadarChart,
|
|
10
|
+
HeatmapChart,
|
|
11
|
+
FunnelChart,
|
|
12
|
+
WordCloudChart,
|
|
13
|
+
GaugeChart,
|
|
14
|
+
TreemapChart,
|
|
15
|
+
WaterfallChart,
|
|
16
|
+
} from "./G2Chart"
|
|
17
|
+
|
|
18
|
+
export const addon: SlideAddonDefinition = {
|
|
19
|
+
id: "g2",
|
|
20
|
+
label: "G2 Charts",
|
|
21
|
+
mdxComponents: {
|
|
22
|
+
Chart,
|
|
23
|
+
BarChart,
|
|
24
|
+
LineChart,
|
|
25
|
+
AreaChart,
|
|
26
|
+
ScatterChart,
|
|
27
|
+
PieChart,
|
|
28
|
+
RadarChart,
|
|
29
|
+
HeatmapChart,
|
|
30
|
+
FunnelChart,
|
|
31
|
+
WordCloudChart,
|
|
32
|
+
GaugeChart,
|
|
33
|
+
TreemapChart,
|
|
34
|
+
WaterfallChart,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* ── G2 Chart tooltip overrides ── */
|
|
2
|
+
|
|
3
|
+
.g2-tooltip {
|
|
4
|
+
font-family: var(--font-sans, Inter, "Segoe UI", sans-serif) !important;
|
|
5
|
+
background: rgba(255, 255, 255, 0.96) !important;
|
|
6
|
+
backdrop-filter: blur(12px) !important;
|
|
7
|
+
-webkit-backdrop-filter: blur(12px) !important;
|
|
8
|
+
border: 1px solid rgba(15, 23, 42, 0.06) !important;
|
|
9
|
+
border-radius: 10px !important;
|
|
10
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.04) !important;
|
|
11
|
+
padding: 10px 14px !important;
|
|
12
|
+
font-size: 14px !important;
|
|
13
|
+
line-height: 1.6 !important;
|
|
14
|
+
color: #334155 !important;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.g2-tooltip-title {
|
|
18
|
+
font-weight: bold !important;
|
|
19
|
+
font-size: 14px !important;
|
|
20
|
+
color: #0f172a !important;
|
|
21
|
+
margin-bottom: 6px !important;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.g2-tooltip-list-item {
|
|
25
|
+
font-size: 13px !important;
|
|
26
|
+
line-height: 1.8 !important;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.g2-tooltip-list-item-marker {
|
|
30
|
+
display: none !important;
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export function Insight({ title = "Insight", children }: { title?: string; children: ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<aside className="slide-insight">
|
|
6
|
+
<div className="slide-insight-title">{title}</div>
|
|
7
|
+
<div className="slide-insight-body">{children}</div>
|
|
8
|
+
</aside>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export function InsightAddonProvider({ children }: { children: ReactNode }) {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
if (typeof document === "undefined") return;
|
|
6
|
+
|
|
7
|
+
const root = document.documentElement;
|
|
8
|
+
const attributeName = "data-slide-addon-insight";
|
|
9
|
+
const previousValue = root.getAttribute(attributeName);
|
|
10
|
+
|
|
11
|
+
root.setAttribute(attributeName, "active");
|
|
12
|
+
|
|
13
|
+
return () => {
|
|
14
|
+
if (previousValue === null) root.removeAttribute(attributeName);
|
|
15
|
+
else root.setAttribute(attributeName, previousValue);
|
|
16
|
+
};
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return <>{children}</>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export function SpotlightLayout({ children }: { children: ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<section className="slide-layout-spotlight grid size-full place-items-center">
|
|
6
|
+
<div className="spotlight-shell w-full max-w-[1440px] rounded-[32px] border border-white/60 bg-white/78 px-16 py-14 backdrop-blur">
|
|
7
|
+
{children}
|
|
8
|
+
</div>
|
|
9
|
+
</section>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { InsightAddonProvider } from "./InsightAddonProvider";
|
|
2
|
+
import { SpotlightLayout } from "./SpotlightLayout";
|
|
3
|
+
import { Insight } from "./Insight";
|
|
4
|
+
import type { SlideAddonDefinition } from "../types";
|
|
5
|
+
|
|
6
|
+
export const addon: SlideAddonDefinition = {
|
|
7
|
+
id: "insight",
|
|
8
|
+
label: "Insight",
|
|
9
|
+
experimental: true,
|
|
10
|
+
provider: InsightAddonProvider,
|
|
11
|
+
layouts: {
|
|
12
|
+
spotlight: SpotlightLayout,
|
|
13
|
+
},
|
|
14
|
+
mdxComponents: {
|
|
15
|
+
Insight,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
:root[data-slide-addon-insight="active"] {
|
|
2
|
+
--slide-insight-border: rgba(14, 116, 144, 0.24);
|
|
3
|
+
--slide-insight-bg: rgba(236, 254, 255, 0.88);
|
|
4
|
+
--slide-insight-title: #155e75;
|
|
5
|
+
--slide-insight-text: #164e63;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.slide-layout-spotlight {
|
|
9
|
+
background:
|
|
10
|
+
radial-gradient(circle at top left, rgba(103, 232, 249, 0.22), transparent 34%),
|
|
11
|
+
radial-gradient(circle at bottom right, rgba(56, 189, 248, 0.18), transparent 32%);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.slide-insight {
|
|
15
|
+
margin-top: 1.25rem;
|
|
16
|
+
border: 1px solid var(--slide-insight-border);
|
|
17
|
+
background: var(--slide-insight-bg);
|
|
18
|
+
border-radius: 1.25rem;
|
|
19
|
+
padding: 1rem 1.1rem;
|
|
20
|
+
box-shadow: 0 16px 38px rgba(14, 116, 144, 0.08);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.slide-insight-title {
|
|
24
|
+
margin-bottom: 0.45rem;
|
|
25
|
+
font-size: 0.78rem;
|
|
26
|
+
font-weight: 700;
|
|
27
|
+
letter-spacing: 0.14em;
|
|
28
|
+
text-transform: uppercase;
|
|
29
|
+
color: var(--slide-insight-title);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.slide-insight-body {
|
|
33
|
+
color: var(--slide-insight-text);
|
|
34
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import mermaid from "mermaid";
|
|
2
|
+
import { Expand, X } from "lucide-react";
|
|
3
|
+
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
|
|
6
|
+
function normalizeDiagramCode(code: string | undefined, children: ReactNode) {
|
|
7
|
+
if (typeof code === "string") return code;
|
|
8
|
+
|
|
9
|
+
if (typeof children === "string") return children;
|
|
10
|
+
|
|
11
|
+
if (Array.isArray(children)) return children.join("");
|
|
12
|
+
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let initialized = false;
|
|
17
|
+
let renderQueue = Promise.resolve();
|
|
18
|
+
const mermaidFontFamily =
|
|
19
|
+
'Inter, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif';
|
|
20
|
+
|
|
21
|
+
const themeVariables: Record<string, string> = {
|
|
22
|
+
fontFamily: mermaidFontFamily,
|
|
23
|
+
fontSize: "19px",
|
|
24
|
+
primaryColor: "#22C55E",
|
|
25
|
+
primaryTextColor: "#000000",
|
|
26
|
+
primaryBorderColor: "#16A34A",
|
|
27
|
+
lineColor: "#334155",
|
|
28
|
+
background: "#ffffff",
|
|
29
|
+
mainBkg: "#ffffff",
|
|
30
|
+
secondBkg: "#f8fafc",
|
|
31
|
+
tertiaryColor: "#f1f5f9",
|
|
32
|
+
textColor: "#000000",
|
|
33
|
+
secondaryColor: "#475569",
|
|
34
|
+
tertiaryTextColor: "#000000",
|
|
35
|
+
border1: "#cbd5e1",
|
|
36
|
+
border2: "#e2e8f0",
|
|
37
|
+
nodeBkg: "#f8fafc",
|
|
38
|
+
nodeBorder: "#94a3b8",
|
|
39
|
+
nodeTextColor: "#000000",
|
|
40
|
+
clusterBkg: "#f8fafc",
|
|
41
|
+
clusterBorder: "#cbd5e1",
|
|
42
|
+
edgeLabelBackground: "#ffffff",
|
|
43
|
+
arrowheadColor: "#475569",
|
|
44
|
+
actorBkg: "#ffffff",
|
|
45
|
+
actorBorder: "#22C55E",
|
|
46
|
+
actorTextColor: "#000000",
|
|
47
|
+
actorLineColor: "#94a3b8",
|
|
48
|
+
signalColor: "#334155",
|
|
49
|
+
signalTextColor: "#000000",
|
|
50
|
+
labelBoxBkgColor: "#f8fafc",
|
|
51
|
+
labelBoxBorderColor: "#cbd5e1",
|
|
52
|
+
labelTextColor: "#000000",
|
|
53
|
+
loopTextColor: "#000000",
|
|
54
|
+
noteBkgColor: "#fefce8",
|
|
55
|
+
noteTextColor: "#000000",
|
|
56
|
+
noteBorderColor: "#e2e8f0",
|
|
57
|
+
activationBkgColor: "#86efac",
|
|
58
|
+
activationBorderColor: "#22C55E",
|
|
59
|
+
labelColor: "#000000",
|
|
60
|
+
classText: "#000000",
|
|
61
|
+
git0: "#60a5fa",
|
|
62
|
+
git1: "#34d399",
|
|
63
|
+
git2: "#a78bfa",
|
|
64
|
+
git3: "#f472b6",
|
|
65
|
+
git4: "#fbbf24",
|
|
66
|
+
git5: "#f87171",
|
|
67
|
+
git6: "#22d3ee",
|
|
68
|
+
git7: "#fb923c",
|
|
69
|
+
gitInv0: "#ffffff",
|
|
70
|
+
gitInv1: "#ffffff",
|
|
71
|
+
gitInv2: "#ffffff",
|
|
72
|
+
gitInv3: "#ffffff",
|
|
73
|
+
gitInv4: "#ffffff",
|
|
74
|
+
gitInv5: "#ffffff",
|
|
75
|
+
gitInv6: "#ffffff",
|
|
76
|
+
gitInv7: "#ffffff",
|
|
77
|
+
commitLabelColor: "#000000",
|
|
78
|
+
commitLabelBackground: "#f8fafc",
|
|
79
|
+
fillType0: "#22C55E",
|
|
80
|
+
fillType1: "#50e3c2",
|
|
81
|
+
fillType2: "#7928ca",
|
|
82
|
+
fillType3: "#ff0080",
|
|
83
|
+
fillType4: "#f5a623",
|
|
84
|
+
fillType5: "#ff0000",
|
|
85
|
+
fillType6: "#22C55E",
|
|
86
|
+
fillType7: "#50e3c2",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type MermaidRenderVariant = "preview" | "zoom";
|
|
90
|
+
|
|
91
|
+
function createMermaidConfig(variant: MermaidRenderVariant) {
|
|
92
|
+
if (variant === "preview") {
|
|
93
|
+
return {
|
|
94
|
+
startOnLoad: false,
|
|
95
|
+
securityLevel: "loose" as const,
|
|
96
|
+
theme: "base" as const,
|
|
97
|
+
htmlLabels: false,
|
|
98
|
+
themeVariables: {
|
|
99
|
+
...themeVariables,
|
|
100
|
+
fontSize: "17px",
|
|
101
|
+
},
|
|
102
|
+
themeCSS: `
|
|
103
|
+
svg, svg * {
|
|
104
|
+
font-family: ${mermaidFontFamily};
|
|
105
|
+
}
|
|
106
|
+
.label,
|
|
107
|
+
.label text,
|
|
108
|
+
.nodeLabel,
|
|
109
|
+
.edgeLabel,
|
|
110
|
+
.cluster-label,
|
|
111
|
+
.stateLabel text,
|
|
112
|
+
foreignObject div {
|
|
113
|
+
font-family: ${mermaidFontFamily};
|
|
114
|
+
}
|
|
115
|
+
`,
|
|
116
|
+
flowchart: {
|
|
117
|
+
curve: "basis" as const,
|
|
118
|
+
padding: 15,
|
|
119
|
+
htmlLabels: false,
|
|
120
|
+
},
|
|
121
|
+
state: {} as Record<string, unknown>,
|
|
122
|
+
sequence: {
|
|
123
|
+
actorFontSize: 17,
|
|
124
|
+
noteFontSize: 16,
|
|
125
|
+
messageFontSize: 16,
|
|
126
|
+
},
|
|
127
|
+
gantt: {
|
|
128
|
+
fontSize: 16,
|
|
129
|
+
},
|
|
130
|
+
journey: {
|
|
131
|
+
taskFontSize: 16,
|
|
132
|
+
titleFontSize: "19px",
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
startOnLoad: false,
|
|
139
|
+
securityLevel: "loose" as const,
|
|
140
|
+
theme: "base" as const,
|
|
141
|
+
htmlLabels: false,
|
|
142
|
+
themeVariables,
|
|
143
|
+
themeCSS: `
|
|
144
|
+
svg, svg * {
|
|
145
|
+
font-family: ${mermaidFontFamily};
|
|
146
|
+
}
|
|
147
|
+
.label,
|
|
148
|
+
.label text,
|
|
149
|
+
.nodeLabel,
|
|
150
|
+
.edgeLabel,
|
|
151
|
+
.cluster-label,
|
|
152
|
+
.stateLabel text,
|
|
153
|
+
foreignObject div {
|
|
154
|
+
font-family: ${mermaidFontFamily};
|
|
155
|
+
}
|
|
156
|
+
`,
|
|
157
|
+
flowchart: {
|
|
158
|
+
curve: "basis" as const,
|
|
159
|
+
padding: 15,
|
|
160
|
+
htmlLabels: false,
|
|
161
|
+
},
|
|
162
|
+
state: {} as Record<string, unknown>,
|
|
163
|
+
sequence: {
|
|
164
|
+
actorFontSize: 19,
|
|
165
|
+
noteFontSize: 18,
|
|
166
|
+
messageFontSize: 18,
|
|
167
|
+
},
|
|
168
|
+
gantt: {
|
|
169
|
+
fontSize: 18,
|
|
170
|
+
},
|
|
171
|
+
journey: {
|
|
172
|
+
taskFontSize: 18,
|
|
173
|
+
titleFontSize: "21px",
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function ensureMermaid() {
|
|
179
|
+
if (initialized) return;
|
|
180
|
+
|
|
181
|
+
mermaid.initialize(createMermaidConfig("zoom"));
|
|
182
|
+
|
|
183
|
+
initialized = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function enqueueMermaidRender<T>(task: () => Promise<T>) {
|
|
187
|
+
const next = renderQueue.then(task, task);
|
|
188
|
+
renderQueue = next.then(
|
|
189
|
+
() => undefined,
|
|
190
|
+
() => undefined,
|
|
191
|
+
);
|
|
192
|
+
return next;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function renderMermaidSvg(
|
|
196
|
+
id: string,
|
|
197
|
+
source: string,
|
|
198
|
+
variant: MermaidRenderVariant,
|
|
199
|
+
) {
|
|
200
|
+
ensureMermaid();
|
|
201
|
+
mermaid.initialize(createMermaidConfig(variant));
|
|
202
|
+
const result = await mermaid.render(id, source);
|
|
203
|
+
return result.svg;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function MermaidDiagram({ code, children }: { code?: string; children?: ReactNode }) {
|
|
207
|
+
const source = normalizeDiagramCode(code, children);
|
|
208
|
+
const [previewSvg, setPreviewSvg] = useState<string>("");
|
|
209
|
+
const [zoomSvg, setZoomSvg] = useState<string>("");
|
|
210
|
+
const [error, setError] = useState<string | null>(null);
|
|
211
|
+
const [zoomed, setZoomed] = useState(false);
|
|
212
|
+
const [zoomLoading, setZoomLoading] = useState(false);
|
|
213
|
+
const previewId = useMemo(() => `mermaid-preview-${Math.random().toString(36).slice(2, 10)}`, []);
|
|
214
|
+
const zoomId = useMemo(() => `mermaid-zoom-${Math.random().toString(36).slice(2, 10)}`, []);
|
|
215
|
+
const diagramSurfaceStyle = useMemo(
|
|
216
|
+
() => ({ color: "#000000", fontFamily: mermaidFontFamily }),
|
|
217
|
+
[],
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
let cancelled = false;
|
|
222
|
+
|
|
223
|
+
const render = async () => {
|
|
224
|
+
try {
|
|
225
|
+
setPreviewSvg("");
|
|
226
|
+
setZoomSvg("");
|
|
227
|
+
setZoomLoading(false);
|
|
228
|
+
const svg = await enqueueMermaidRender(() =>
|
|
229
|
+
renderMermaidSvg(previewId, source, "preview"),
|
|
230
|
+
);
|
|
231
|
+
if (!cancelled) {
|
|
232
|
+
setPreviewSvg(svg);
|
|
233
|
+
setError(null);
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
if (!cancelled) setError(err instanceof Error ? err.message : String(err));
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
void render();
|
|
241
|
+
|
|
242
|
+
return () => {
|
|
243
|
+
cancelled = true;
|
|
244
|
+
};
|
|
245
|
+
}, [previewId, source]);
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (!zoomed || zoomSvg) return;
|
|
249
|
+
|
|
250
|
+
let cancelled = false;
|
|
251
|
+
|
|
252
|
+
const render = async () => {
|
|
253
|
+
try {
|
|
254
|
+
setZoomLoading(true);
|
|
255
|
+
const svg = await enqueueMermaidRender(() =>
|
|
256
|
+
renderMermaidSvg(zoomId, source, "zoom"),
|
|
257
|
+
);
|
|
258
|
+
if (!cancelled) {
|
|
259
|
+
setZoomSvg(svg);
|
|
260
|
+
setError(null);
|
|
261
|
+
}
|
|
262
|
+
} catch (err) {
|
|
263
|
+
if (!cancelled) setError(err instanceof Error ? err.message : String(err));
|
|
264
|
+
} finally {
|
|
265
|
+
if (!cancelled) setZoomLoading(false);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
void render();
|
|
270
|
+
|
|
271
|
+
return () => {
|
|
272
|
+
cancelled = true;
|
|
273
|
+
};
|
|
274
|
+
}, [source, zoomId, zoomSvg, zoomed]);
|
|
275
|
+
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (!zoomed) return;
|
|
278
|
+
|
|
279
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
280
|
+
if (event.key === "Escape") setZoomed(false);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const previousOverflow = document.body.style.overflow;
|
|
284
|
+
document.body.style.overflow = "hidden";
|
|
285
|
+
window.addEventListener("keydown", onKeyDown);
|
|
286
|
+
|
|
287
|
+
return () => {
|
|
288
|
+
document.body.style.overflow = previousOverflow;
|
|
289
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
290
|
+
};
|
|
291
|
+
}, [zoomed]);
|
|
292
|
+
|
|
293
|
+
if (error) {
|
|
294
|
+
return (
|
|
295
|
+
<div className="my-3 rounded-xl border border-rose-300 bg-rose-50 p-3 text-sm text-rose-900">
|
|
296
|
+
Mermaid render error: {error}
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!previewSvg) {
|
|
302
|
+
return (
|
|
303
|
+
<div className="my-3 rounded-xl border border-slate-300 bg-white/70 p-3 text-sm text-slate-700">
|
|
304
|
+
Rendering Mermaid...
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const zoomOverlay =
|
|
310
|
+
zoomed && typeof document !== "undefined"
|
|
311
|
+
? createPortal(
|
|
312
|
+
<div
|
|
313
|
+
className="fixed inset-0 z-[120] flex items-center justify-center bg-slate-100/82 p-6 backdrop-blur-sm"
|
|
314
|
+
onClick={() => setZoomed(false)}
|
|
315
|
+
>
|
|
316
|
+
<div
|
|
317
|
+
className="relative flex h-[min(92vh,1200px)] w-[min(96vw,1600px)] flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white "
|
|
318
|
+
onClick={(event) => event.stopPropagation()}
|
|
319
|
+
>
|
|
320
|
+
<div className="flex items-center justify-between border-b border-slate-200 px-5 py-3 text-sm text-slate-700">
|
|
321
|
+
<div>
|
|
322
|
+
<div className="font-semibold tracking-[0.16em] text-slate-900 uppercase">Mermaid</div>
|
|
323
|
+
<div className="mt-1 text-xs text-slate-500">Esc or click outside to close</div>
|
|
324
|
+
</div>
|
|
325
|
+
<button
|
|
326
|
+
type="button"
|
|
327
|
+
onClick={() => setZoomed(false)}
|
|
328
|
+
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 bg-slate-50 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900"
|
|
329
|
+
aria-label="Close Mermaid zoom preview"
|
|
330
|
+
title="Close"
|
|
331
|
+
>
|
|
332
|
+
<X size={18} />
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
<div className="flex-1 overflow-auto bg-slate-50 p-6">
|
|
336
|
+
{zoomSvg ? (
|
|
337
|
+
<div
|
|
338
|
+
className="inline-block min-w-full rounded-2xl border border-slate-200 bg-white p-6 [&_svg]:h-auto [&_svg]:max-w-none [&_svg_tspan]:fill-current [&_svg_text]:fill-current"
|
|
339
|
+
style={diagramSurfaceStyle}
|
|
340
|
+
dangerouslySetInnerHTML={{ __html: zoomSvg }}
|
|
341
|
+
/>
|
|
342
|
+
) : (
|
|
343
|
+
<div className="rounded-2xl border border-slate-200 bg-white p-6 text-sm text-slate-600 ">
|
|
344
|
+
{zoomLoading ? "Preparing Mermaid preview..." : "Rendering Mermaid..."}
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</div>,
|
|
350
|
+
document.body,
|
|
351
|
+
)
|
|
352
|
+
: null;
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<>
|
|
356
|
+
<div className="my-3">
|
|
357
|
+
<div className="relative w-full overflow-hidden rounded-xl border border-slate-300 bg-white p-3 shadow-sm">
|
|
358
|
+
<button
|
|
359
|
+
type="button"
|
|
360
|
+
onClick={() => setZoomed(true)}
|
|
361
|
+
className="absolute top-3 right-3 z-10 inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-300/80 bg-white/92 text-slate-700 shadow-sm transition hover:bg-white hover:text-slate-950"
|
|
362
|
+
aria-label="Open Mermaid zoom preview"
|
|
363
|
+
title="Zoom Mermaid diagram"
|
|
364
|
+
>
|
|
365
|
+
<Expand size={16} />
|
|
366
|
+
</button>
|
|
367
|
+
<div className="max-w-full overflow-x-auto pr-12">
|
|
368
|
+
<div
|
|
369
|
+
className="w-full [&_svg]:block [&_svg]:h-auto [&_svg]:w-full [&_svg]:max-w-full [&_svg_tspan]:fill-current [&_svg_text]:fill-current"
|
|
370
|
+
style={diagramSurfaceStyle}
|
|
371
|
+
dangerouslySetInnerHTML={{ __html: previewSvg }}
|
|
372
|
+
/>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
{zoomOverlay}
|
|
377
|
+
</>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { listRegisteredAddons, resolveAddonDefinitions, resolveSlideAddons } from "./registry";
|
|
3
|
+
import { Insight } from "./insight/Insight";
|
|
4
|
+
import { InsightAddonProvider } from "./insight/InsightAddonProvider";
|
|
5
|
+
import { SpotlightLayout } from "./insight/SpotlightLayout";
|
|
6
|
+
|
|
7
|
+
describe("addon registry", () => {
|
|
8
|
+
it("registers local addons discovered from the addons directory", () => {
|
|
9
|
+
const addonIds = listRegisteredAddons().map((addon) => addon.id);
|
|
10
|
+
|
|
11
|
+
expect(addonIds).toContain("insight");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("ignores missing addons while preserving known ones", () => {
|
|
15
|
+
const definitions = resolveAddonDefinitions(["insight", "missing-addon"]);
|
|
16
|
+
|
|
17
|
+
expect(definitions.map((definition) => definition.id)).toEqual(["insight"]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("merges addon layouts, mdx components, and providers", () => {
|
|
21
|
+
const addons = resolveSlideAddons(["insight"]);
|
|
22
|
+
|
|
23
|
+
expect(addons.definitions.map((definition) => definition.id)).toEqual(["insight"]);
|
|
24
|
+
expect(addons.layouts.spotlight).toBe(SpotlightLayout);
|
|
25
|
+
expect(addons.mdxComponents.Insight).toBe(Insight);
|
|
26
|
+
expect(addons.providers).toEqual([InsightAddonProvider]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ThemeMDXComponents, LayoutRegistry } from "../theme/types";
|
|
2
|
+
import type { ResolvedSlideAddons, SlideAddonDefinition } from "./types";
|
|
3
|
+
|
|
4
|
+
import.meta.glob("./*/style.css", { eager: true });
|
|
5
|
+
|
|
6
|
+
const addonModules = import.meta.glob<{ addon?: SlideAddonDefinition }>("./*/index.ts", {
|
|
7
|
+
eager: true,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const registeredAddons = Object.values(addonModules)
|
|
11
|
+
.map((module) => module.addon)
|
|
12
|
+
.filter((addon): addon is SlideAddonDefinition => Boolean(addon));
|
|
13
|
+
|
|
14
|
+
const addonMap = new Map(registeredAddons.map((addon) => [addon.id, addon]));
|
|
15
|
+
|
|
16
|
+
function normalizeAddonIds(addonIds?: string[]) {
|
|
17
|
+
return [...new Set((addonIds ?? []).map((addonId) => addonId.trim()).filter(Boolean))];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mergeLayouts(definitions: SlideAddonDefinition[]): LayoutRegistry {
|
|
21
|
+
return definitions.reduce<LayoutRegistry>(
|
|
22
|
+
(layouts, definition) => ({
|
|
23
|
+
...layouts,
|
|
24
|
+
...definition.layouts,
|
|
25
|
+
}),
|
|
26
|
+
{},
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mergeMdxComponents(definitions: SlideAddonDefinition[]): ThemeMDXComponents {
|
|
31
|
+
return definitions.reduce<ThemeMDXComponents>(
|
|
32
|
+
(components, definition) => ({
|
|
33
|
+
...components,
|
|
34
|
+
...definition.mdxComponents,
|
|
35
|
+
}),
|
|
36
|
+
{},
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function listRegisteredAddons() {
|
|
41
|
+
return [...addonMap.values()];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveAddonDefinitions(addonIds?: string[]) {
|
|
45
|
+
return normalizeAddonIds(addonIds)
|
|
46
|
+
.map((addonId) => addonMap.get(addonId))
|
|
47
|
+
.filter((addon): addon is SlideAddonDefinition => Boolean(addon));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveSlideAddons(addonIds?: string[]): ResolvedSlideAddons {
|
|
51
|
+
const definitions = resolveAddonDefinitions(addonIds);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
definitions,
|
|
55
|
+
providers: definitions
|
|
56
|
+
.map((definition) => definition.provider)
|
|
57
|
+
.filter((provider): provider is NonNullable<typeof provider> => Boolean(provider)),
|
|
58
|
+
layouts: mergeLayouts(definitions),
|
|
59
|
+
mdxComponents: mergeMdxComponents(definitions),
|
|
60
|
+
};
|
|
61
|
+
}
|