@kekonic/diagrams-ui 1.0.0-rc.4
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 +34 -0
- package/dist/KDiagramLive-CnGvR1jR.mjs +40 -0
- package/dist/KDiagramLive-CnGvR1jR.mjs.map +1 -0
- package/dist/KDiagramLive-Cx3hrc2_.d.mts +29 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.mjs +23 -0
- package/dist/index.mjs.map +1 -0
- package/dist/playground.d.mts +57 -0
- package/dist/playground.mjs +226 -0
- package/dist/playground.mjs.map +1 -0
- package/dist/shiki/highlight.d.mts +10 -0
- package/dist/shiki/highlight.mjs +27 -0
- package/dist/shiki/highlight.mjs.map +1 -0
- package/dist/shiki/kdiagram.d.mts +223 -0
- package/dist/shiki/kdiagram.mjs +381 -0
- package/dist/shiki/kdiagram.mjs.map +1 -0
- package/package.json +82 -0
- package/src/chrome/tokens.css +109 -0
- package/src/diagram/KDiagramLive.tsx +59 -0
- package/src/diagram/KDiagramPlayground.tsx +288 -0
- package/src/diagram/live.css +13 -0
- package/src/diagram/playground.css +261 -0
- package/src/diagram/render-static.ts +28 -0
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kekonic/diagrams-ui",
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
|
+
"description": "Shared chrome styles, KDiagram syntax highlighting, and React embeds as thin Lit wrappers.",
|
|
5
|
+
"homepage": "https://github.com/kekonic/diagrams#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/kekonic/diagrams/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/kekonic/diagrams.git",
|
|
13
|
+
"directory": "packages/diagrams-ui"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src/chrome",
|
|
18
|
+
"src/diagram"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.mts",
|
|
24
|
+
"import": "./dist/index.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./chrome.css": "./src/chrome/tokens.css",
|
|
27
|
+
"./live.css": "./src/diagram/live.css",
|
|
28
|
+
"./playground.css": "./src/diagram/playground.css",
|
|
29
|
+
"./playground": {
|
|
30
|
+
"types": "./dist/playground.d.mts",
|
|
31
|
+
"import": "./dist/playground.mjs"
|
|
32
|
+
},
|
|
33
|
+
"./highlight": {
|
|
34
|
+
"types": "./dist/shiki/highlight.d.mts",
|
|
35
|
+
"import": "./dist/shiki/highlight.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./shiki": {
|
|
38
|
+
"types": "./dist/shiki/kdiagram.d.mts",
|
|
39
|
+
"import": "./dist/shiki/kdiagram.mjs"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@lit/react": "^1.0.8",
|
|
47
|
+
"shiki": "^4.3.1",
|
|
48
|
+
"@kekonic/diagrams": "1.0.0-rc.4",
|
|
49
|
+
"@kekonic/diagrams-core": "1.0.0-rc.4",
|
|
50
|
+
"@kekonic/diagrams-element": "1.0.0-rc.4"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^26",
|
|
54
|
+
"@types/react": "^19.2.17",
|
|
55
|
+
"@types/react-dom": "^19.2.3",
|
|
56
|
+
"react": "^19.2.8",
|
|
57
|
+
"react-dom": "^19.2.8",
|
|
58
|
+
"typescript": "^5",
|
|
59
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
|
|
60
|
+
"vite-plus": "0.2.2",
|
|
61
|
+
"vitest": "^4"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
65
|
+
"react-dom": "^18.2.0 || ^19.0.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"react": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"react-dom": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"engines": {
|
|
76
|
+
"node": ">=22.18.0"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "vp pack",
|
|
80
|
+
"test": "vp test"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KDiagram chrome — shared by Studio and other product hosts.
|
|
3
|
+
* Sharp shell, purple brand accent. Diagram colors use --kd-* separately.
|
|
4
|
+
*
|
|
5
|
+
* Brand hue is OKLCH 288 — between indigo (277) and Tailwind violet (293). Past
|
|
6
|
+
* ~300 the hue runs into purple → fuchsia (322) → pink (354), so raising chroma
|
|
7
|
+
* there turns the brand pink instead of deepening it. At 288 more chroma reads
|
|
8
|
+
* as richer violet.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
:root,
|
|
12
|
+
:root[data-theme="dark"],
|
|
13
|
+
[data-theme="dark"] {
|
|
14
|
+
--bg: oklch(16% 0.012 288);
|
|
15
|
+
--bg-elevated: oklch(20% 0.014 288);
|
|
16
|
+
--bg-panel: oklch(18.5% 0.013 288);
|
|
17
|
+
--bg-input: oklch(13% 0.01 288);
|
|
18
|
+
--border: oklch(30% 0.02 288);
|
|
19
|
+
--border-soft: oklch(24% 0.014 288);
|
|
20
|
+
--text: oklch(93% 0.008 288);
|
|
21
|
+
--text-muted: oklch(68% 0.02 288);
|
|
22
|
+
--text-faint: oklch(48% 0.016 288);
|
|
23
|
+
--accent: oklch(72% 0.14 288);
|
|
24
|
+
--accent-soft: oklch(72% 0.14 288 / 0.18);
|
|
25
|
+
/* Dark-mode accent is light, so text on it must be dark. */
|
|
26
|
+
--accent-contrast: oklch(16% 0.012 288);
|
|
27
|
+
--danger: oklch(68% 0.14 25);
|
|
28
|
+
--warn: oklch(78% 0.12 85);
|
|
29
|
+
--ok: oklch(68% 0.12 155);
|
|
30
|
+
--focus: oklch(72% 0.14 288 / 0.55);
|
|
31
|
+
--shadow: 0 12px 40px oklch(0% 0 0 / 0.45);
|
|
32
|
+
color-scheme: dark;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:root[data-theme="light"],
|
|
36
|
+
[data-theme="light"] {
|
|
37
|
+
--bg: oklch(97.5% 0.008 288);
|
|
38
|
+
--bg-elevated: oklch(100% 0.004 288);
|
|
39
|
+
--bg-panel: oklch(95.5% 0.012 288);
|
|
40
|
+
--bg-input: oklch(100% 0.004 288);
|
|
41
|
+
--border: oklch(82% 0.02 288);
|
|
42
|
+
--border-soft: oklch(88% 0.014 288);
|
|
43
|
+
--text: oklch(22% 0.03 288);
|
|
44
|
+
--text-muted: oklch(42% 0.025 288);
|
|
45
|
+
--text-faint: oklch(58% 0.02 288);
|
|
46
|
+
--accent: oklch(48% 0.21 288);
|
|
47
|
+
--accent-soft: oklch(48% 0.21 288 / 0.12);
|
|
48
|
+
/* Light-mode accent is dark, so text on it must be near-white. */
|
|
49
|
+
--accent-contrast: oklch(98% 0.01 288);
|
|
50
|
+
--danger: oklch(52% 0.16 25);
|
|
51
|
+
--warn: oklch(58% 0.14 75);
|
|
52
|
+
--ok: oklch(48% 0.12 155);
|
|
53
|
+
--focus: oklch(48% 0.21 288 / 0.45);
|
|
54
|
+
--shadow: 0 12px 40px oklch(30% 0.04 288 / 0.12);
|
|
55
|
+
color-scheme: light;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:root {
|
|
59
|
+
/*
|
|
60
|
+
* Filled brand surfaces (primary buttons). Theme-independent on purpose: the
|
|
61
|
+
* per-theme --accent flips light/dark for legible link text, which makes it a
|
|
62
|
+
* poor button fill — a pale lavender button washes out and can only carry a
|
|
63
|
+
* legible label in one mode. One deeper violet works on both page backgrounds
|
|
64
|
+
* and always takes a near-white label.
|
|
65
|
+
* Label 5.8:1 at rest, 7.8:1 on hover; button edge 3.1:1 on dark, 5.8:1 on light.
|
|
66
|
+
*/
|
|
67
|
+
--accent-strong: oklch(52% 0.24 288);
|
|
68
|
+
--accent-strong-hover: oklch(45% 0.22 288);
|
|
69
|
+
--on-accent-strong: oklch(98% 0.01 288);
|
|
70
|
+
--radius: 0;
|
|
71
|
+
--font-ui: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
|
|
72
|
+
--font-mono: "IBM Plex Mono", "JetBrains Mono", "Fira Code", ui-monospace, monospace;
|
|
73
|
+
--toolbar-h: 52px;
|
|
74
|
+
--drawer-w: 300px;
|
|
75
|
+
--content-max: 72rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
* {
|
|
79
|
+
box-sizing: border-box;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
button,
|
|
83
|
+
select,
|
|
84
|
+
textarea,
|
|
85
|
+
input {
|
|
86
|
+
color: inherit;
|
|
87
|
+
font: inherit;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
button {
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
:focus {
|
|
95
|
+
outline: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
:focus-visible {
|
|
99
|
+
outline: 1px solid color-mix(in oklch, var(--accent) 55%, var(--border));
|
|
100
|
+
outline-offset: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
button:focus-visible,
|
|
104
|
+
select:focus-visible,
|
|
105
|
+
input:focus-visible,
|
|
106
|
+
textarea:focus-visible {
|
|
107
|
+
outline: none;
|
|
108
|
+
box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--accent) 55%, var(--border));
|
|
109
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { createComponent } from "@lit/react";
|
|
3
|
+
import { KDiagramElement, K_DIAGRAM_TAG } from "@kekonic/diagrams-element";
|
|
4
|
+
import type { InteractiveRenderOptions, RenderResult, ThemeMode } from "@kekonic/diagrams";
|
|
5
|
+
|
|
6
|
+
export type KDiagramLiveProps = {
|
|
7
|
+
source: string;
|
|
8
|
+
/** Diagram theme. `"auto"` follows the host page (`html[data-theme]`). */
|
|
9
|
+
theme?: ThemeMode | "auto";
|
|
10
|
+
className?: string;
|
|
11
|
+
/** Remove the host border and panel background. Controls remain independently configurable. */
|
|
12
|
+
frameless?: boolean;
|
|
13
|
+
/** Sun/moon control that dogfoods theme swaps. */
|
|
14
|
+
showThemeToggle?: boolean;
|
|
15
|
+
/** Zoom / fit / fullscreen controls. */
|
|
16
|
+
showViewControls?: boolean;
|
|
17
|
+
/** Compact render stats badge (floating). Off by default for a clean embed. */
|
|
18
|
+
showStats?: boolean;
|
|
19
|
+
/** Animation play/scrub chrome when an animation exists. Default on. */
|
|
20
|
+
showAnimationControls?: boolean;
|
|
21
|
+
/** Start playing when the diagram mounts. */
|
|
22
|
+
autoplay?: boolean;
|
|
23
|
+
/** Loop the active animation. */
|
|
24
|
+
animationLoop?: boolean;
|
|
25
|
+
/** Preferred animation name or id (authored blocks). */
|
|
26
|
+
animation?: string;
|
|
27
|
+
height?: number | string;
|
|
28
|
+
options?: Omit<InteractiveRenderOptions, "theme">;
|
|
29
|
+
/** Fired after mount / source / theme renders complete. */
|
|
30
|
+
onKDiagramRender?: (event: CustomEvent<RenderResult>) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Lit + React prop inference drifts when the element surface grows; keep React props explicit. */
|
|
34
|
+
const KDiagramReact = createComponent({
|
|
35
|
+
react: React,
|
|
36
|
+
tagName: K_DIAGRAM_TAG,
|
|
37
|
+
elementClass: KDiagramElement,
|
|
38
|
+
events: {
|
|
39
|
+
onKDiagramRender: "kdiagram-render",
|
|
40
|
+
},
|
|
41
|
+
}) as unknown as React.ForwardRefExoticComponent<
|
|
42
|
+
KDiagramLiveProps & React.RefAttributes<KDiagramElement>
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* React thin wrapper around Lit `<k-diagram>`.
|
|
47
|
+
* Same interactive host as HTML embeds — pan/zoom, theme, live `source` updates.
|
|
48
|
+
*/
|
|
49
|
+
export const KDiagramLive = React.forwardRef<KDiagramElement, KDiagramLiveProps>(
|
|
50
|
+
function KDiagramLive({ className, ...props }, ref) {
|
|
51
|
+
return (
|
|
52
|
+
<KDiagramReact
|
|
53
|
+
ref={ref}
|
|
54
|
+
className={["kd-live", className].filter(Boolean).join(" ")}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
);
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lazy,
|
|
3
|
+
Suspense,
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
type UIEvent,
|
|
10
|
+
} from "react";
|
|
11
|
+
import type { ThemeMode } from "@kekonic/diagrams";
|
|
12
|
+
import type { KDiagramLiveProps } from "./KDiagramLive.tsx";
|
|
13
|
+
|
|
14
|
+
const KDiagramLive = lazy(async () => {
|
|
15
|
+
const mod = await import("./KDiagramLive.tsx");
|
|
16
|
+
return { default: mod.KDiagramLive };
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** Kick off Shiki as soon as this island module evaluates (still lazy vs the page). */
|
|
20
|
+
const highlightModule = import("../shiki/highlight.ts");
|
|
21
|
+
|
|
22
|
+
export type KDiagramPlaygroundProps = {
|
|
23
|
+
/** Initial KDiagram source (also the Reset target). */
|
|
24
|
+
source: string;
|
|
25
|
+
theme?: ThemeMode | "auto";
|
|
26
|
+
className?: string;
|
|
27
|
+
/** Remove the live diagram's host border and panel background. */
|
|
28
|
+
frameless?: boolean;
|
|
29
|
+
/** Source pane + diagram side-by-side, stacked, or gallery (diagram-first). */
|
|
30
|
+
layout?: "split" | "stacked" | "gallery";
|
|
31
|
+
/** Debounce before `controller.update` (ms). */
|
|
32
|
+
debounceMs?: number;
|
|
33
|
+
editable?: boolean;
|
|
34
|
+
showThemeToggle?: boolean;
|
|
35
|
+
showViewControls?: boolean;
|
|
36
|
+
showStats?: boolean;
|
|
37
|
+
showAnimationControls?: boolean;
|
|
38
|
+
autoplay?: boolean;
|
|
39
|
+
animationLoop?: boolean;
|
|
40
|
+
animation?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Playground root height — both panes stretch to fill.
|
|
43
|
+
* Diagram viewport flexes inside; no separate short editor box.
|
|
44
|
+
*/
|
|
45
|
+
height?: number | string;
|
|
46
|
+
/**
|
|
47
|
+
* Minimum height for the source editor region when the root is not stretched
|
|
48
|
+
* (e.g. stacked layout without an explicit `height`).
|
|
49
|
+
*/
|
|
50
|
+
editorHeight?: number | string;
|
|
51
|
+
options?: KDiagramLiveProps["options"];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function escapeHtml(text: string): string {
|
|
55
|
+
return text
|
|
56
|
+
.replaceAll("&", "&")
|
|
57
|
+
.replaceAll("<", "<")
|
|
58
|
+
.replaceAll(">", ">")
|
|
59
|
+
.replaceAll('"', """);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cssSize(value: number | string): string {
|
|
63
|
+
return typeof value === "number" ? `${value}px` : value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function copyTextFallback(text: string): boolean {
|
|
67
|
+
const input = document.createElement("textarea");
|
|
68
|
+
input.value = text;
|
|
69
|
+
input.setAttribute("readonly", "");
|
|
70
|
+
input.style.position = "fixed";
|
|
71
|
+
input.style.opacity = "0";
|
|
72
|
+
document.body.append(input);
|
|
73
|
+
input.select();
|
|
74
|
+
const copied = document.execCommand("copy");
|
|
75
|
+
input.remove();
|
|
76
|
+
return copied;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Docs dogfood embed: editable KDiagram source (Shiki) + live diagram (`KDiagramLive`).
|
|
81
|
+
* Shiki and the render path load lazily on mount / first highlight.
|
|
82
|
+
*/
|
|
83
|
+
export function KDiagramPlayground({
|
|
84
|
+
source,
|
|
85
|
+
theme = "auto",
|
|
86
|
+
className,
|
|
87
|
+
frameless = false,
|
|
88
|
+
layout = "split",
|
|
89
|
+
debounceMs = 280,
|
|
90
|
+
editable = true,
|
|
91
|
+
showThemeToggle = true,
|
|
92
|
+
showViewControls = true,
|
|
93
|
+
showStats = false,
|
|
94
|
+
showAnimationControls = true,
|
|
95
|
+
autoplay = false,
|
|
96
|
+
animationLoop = false,
|
|
97
|
+
animation,
|
|
98
|
+
height = 360,
|
|
99
|
+
editorHeight = 280,
|
|
100
|
+
options,
|
|
101
|
+
}: KDiagramPlaygroundProps) {
|
|
102
|
+
const [draft, setDraft] = useState(source);
|
|
103
|
+
const [liveSource, setLiveSource] = useState(source);
|
|
104
|
+
const [highlightHtml, setHighlightHtml] = useState<string | null>(null);
|
|
105
|
+
const [dirty, setDirty] = useState(false);
|
|
106
|
+
const [actionStatus, setActionStatus] = useState("");
|
|
107
|
+
const highlightRef = useRef<HTMLDivElement>(null);
|
|
108
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
109
|
+
const labelId = useId();
|
|
110
|
+
const sourceGen = useRef(0);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
setDraft(source);
|
|
114
|
+
setLiveSource(source);
|
|
115
|
+
setDirty(false);
|
|
116
|
+
}, [source]);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (draft === liveSource) return;
|
|
120
|
+
const t = window.setTimeout(() => setLiveSource(draft), debounceMs);
|
|
121
|
+
return () => window.clearTimeout(t);
|
|
122
|
+
}, [draft, liveSource, debounceMs]);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
let cancelled = false;
|
|
126
|
+
const gen = ++sourceGen.current;
|
|
127
|
+
// Plain text first; upgrade once the module-level Shiki import resolves.
|
|
128
|
+
setHighlightHtml(`<pre><code>${escapeHtml(draft)}</code></pre>`);
|
|
129
|
+
void (async () => {
|
|
130
|
+
try {
|
|
131
|
+
const { highlightKDiagram } = await highlightModule;
|
|
132
|
+
const html = await highlightKDiagram(draft);
|
|
133
|
+
if (!cancelled && gen === sourceGen.current) setHighlightHtml(html);
|
|
134
|
+
} catch {
|
|
135
|
+
// Keep plain text if Shiki cannot load in this host.
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
return () => {
|
|
139
|
+
cancelled = true;
|
|
140
|
+
};
|
|
141
|
+
}, [draft]);
|
|
142
|
+
|
|
143
|
+
const syncScroll = (event: UIEvent<HTMLTextAreaElement>) => {
|
|
144
|
+
const pre = highlightRef.current;
|
|
145
|
+
if (!pre) return;
|
|
146
|
+
pre.scrollTop = event.currentTarget.scrollTop;
|
|
147
|
+
pre.scrollLeft = event.currentTarget.scrollLeft;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const reset = () => {
|
|
151
|
+
setDraft(source);
|
|
152
|
+
setLiveSource(source);
|
|
153
|
+
setDirty(false);
|
|
154
|
+
inputRef.current?.focus();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const copySource = async () => {
|
|
158
|
+
let copied = false;
|
|
159
|
+
try {
|
|
160
|
+
if (navigator.clipboard?.writeText) {
|
|
161
|
+
await navigator.clipboard.writeText(draft);
|
|
162
|
+
copied = true;
|
|
163
|
+
} else {
|
|
164
|
+
copied = copyTextFallback(draft);
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
copied = copyTextFallback(draft);
|
|
168
|
+
}
|
|
169
|
+
setActionStatus(copied ? "Copied source" : "Could not copy source");
|
|
170
|
+
window.setTimeout(() => setActionStatus(""), 1_500);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const downloadSvg = async () => {
|
|
174
|
+
const { KDiagram } = await import("@kekonic/diagrams");
|
|
175
|
+
const resolvedTheme =
|
|
176
|
+
theme === "auto"
|
|
177
|
+
? document.documentElement.dataset.theme === "light"
|
|
178
|
+
? "light"
|
|
179
|
+
: "dark"
|
|
180
|
+
: theme;
|
|
181
|
+
const result = await KDiagram.renderToSvg(draft, {
|
|
182
|
+
...options,
|
|
183
|
+
theme: resolvedTheme,
|
|
184
|
+
snapshotTheme: true,
|
|
185
|
+
});
|
|
186
|
+
if (!result.ok || !result.svg) {
|
|
187
|
+
setActionStatus("Fix diagram errors before downloading");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const url = URL.createObjectURL(new Blob([result.svg], { type: "image/svg+xml" }));
|
|
191
|
+
const link = document.createElement("a");
|
|
192
|
+
link.href = url;
|
|
193
|
+
link.download = "diagram.svg";
|
|
194
|
+
document.body.append(link);
|
|
195
|
+
link.click();
|
|
196
|
+
link.remove();
|
|
197
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 1_000);
|
|
198
|
+
setActionStatus("Downloaded SVG");
|
|
199
|
+
window.setTimeout(() => setActionStatus(""), 1_500);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const rootStyle = {
|
|
203
|
+
height: cssSize(height),
|
|
204
|
+
"--kd-play-editor-min": cssSize(editorHeight),
|
|
205
|
+
} as CSSProperties;
|
|
206
|
+
|
|
207
|
+
const hint = editable ? "Edit source — diagram updates live" : "Source + live diagram";
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div
|
|
211
|
+
className={["kd-play", className].filter(Boolean).join(" ")}
|
|
212
|
+
style={rootStyle}
|
|
213
|
+
title={hint}
|
|
214
|
+
aria-labelledby={labelId}
|
|
215
|
+
>
|
|
216
|
+
<span id={labelId} className="kd-play__sr-only">
|
|
217
|
+
{hint}
|
|
218
|
+
</span>
|
|
219
|
+
<div className="kd-play__body" data-layout={layout}>
|
|
220
|
+
<div className="kd-play__source">
|
|
221
|
+
<div className="kd-play__pane-bar">
|
|
222
|
+
<span className="kd-play__label">Source</span>
|
|
223
|
+
<div className="kd-play__actions">
|
|
224
|
+
<button type="button" className="kd-play__btn" onClick={() => void copySource()}>
|
|
225
|
+
Copy source
|
|
226
|
+
</button>
|
|
227
|
+
<button type="button" className="kd-play__btn" onClick={() => void downloadSvg()}>
|
|
228
|
+
Download SVG
|
|
229
|
+
</button>
|
|
230
|
+
{editable ? (
|
|
231
|
+
<button type="button" className="kd-play__btn" disabled={!dirty} onClick={reset}>
|
|
232
|
+
Reset
|
|
233
|
+
</button>
|
|
234
|
+
) : null}
|
|
235
|
+
</div>
|
|
236
|
+
<span className="kd-play__sr-only" role="status" aria-live="polite">
|
|
237
|
+
{actionStatus}
|
|
238
|
+
</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="kd-play__editor">
|
|
241
|
+
<div
|
|
242
|
+
ref={highlightRef}
|
|
243
|
+
className="kd-play__highlight"
|
|
244
|
+
aria-hidden
|
|
245
|
+
dangerouslySetInnerHTML={{
|
|
246
|
+
__html: highlightHtml ?? `<pre><code>${escapeHtml(draft)}</code></pre>`,
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
{editable ? (
|
|
250
|
+
<textarea
|
|
251
|
+
ref={inputRef}
|
|
252
|
+
className="kd-play__input"
|
|
253
|
+
value={draft}
|
|
254
|
+
spellCheck={false}
|
|
255
|
+
autoCapitalize="off"
|
|
256
|
+
autoCorrect="off"
|
|
257
|
+
aria-label="KDiagram source"
|
|
258
|
+
onScroll={syncScroll}
|
|
259
|
+
onChange={(event) => {
|
|
260
|
+
setDraft(event.target.value);
|
|
261
|
+
setDirty(event.target.value !== source);
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
) : null}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
<div className="kd-play__diagram">
|
|
268
|
+
<Suspense fallback={<div className="kd-play__fallback">Loading diagram…</div>}>
|
|
269
|
+
<KDiagramLive
|
|
270
|
+
source={liveSource}
|
|
271
|
+
theme={theme}
|
|
272
|
+
height="100%"
|
|
273
|
+
frameless={frameless}
|
|
274
|
+
showThemeToggle={showThemeToggle}
|
|
275
|
+
showViewControls={showViewControls}
|
|
276
|
+
showStats={showStats}
|
|
277
|
+
showAnimationControls={showAnimationControls}
|
|
278
|
+
autoplay={autoplay}
|
|
279
|
+
animationLoop={animationLoop}
|
|
280
|
+
animation={animation}
|
|
281
|
+
options={options}
|
|
282
|
+
/>
|
|
283
|
+
</Suspense>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Host layout for React embeds of `<k-diagram>`.
|
|
3
|
+
Chrome (tools, viewport, errors) lives in the Lit shadow root — do not restyle
|
|
4
|
+
`.kd-live__*` internals here; those class names no longer exist in light DOM.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
.kd-live,
|
|
8
|
+
k-diagram.kd-live {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
min-height: 0;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|