@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kekonic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @kekonic/diagrams-ui
2
+
3
+ React components and browser-facing helpers for KDiagram:
4
+
5
+ - **UI tokens** — shared application colors and typography (`@kekonic/diagrams-ui/chrome.css`)
6
+ - **Shiki grammar** — KDiagram language for Starlight / Expressive Code (`@kekonic/diagrams-ui/shiki`)
7
+ - **Embed helpers** — `renderKDiagramSvg` (SSR), `KDiagramLive` (React → Lit `<k-diagram>`), `KDiagramPlayground` (editable + live)
8
+
9
+ ## Usage
10
+
11
+ ```ts
12
+ import { renderKDiagramSvg, KDiagramLive } from "@kekonic/diagrams-ui";
13
+ import { KDiagramPlayground } from "@kekonic/diagrams-ui/playground";
14
+ import { kdiagramLanguage } from "@kekonic/diagrams-ui/shiki";
15
+ ```
16
+
17
+ ```css
18
+ @import "@kekonic/diagrams-ui/chrome.css";
19
+ @import "@kekonic/diagrams-ui/live.css";
20
+ @import "@kekonic/diagrams-ui/playground.css";
21
+ ```
22
+
23
+ `KDiagramLive` is a thin `@lit/react` wrapper around `@kekonic/diagrams-element`’s
24
+ `<k-diagram>`. Prefer the Lit element directly outside React.
25
+
26
+ The root entry contains the static helper and live React embed only. Playground and Shiki use
27
+ separate entry points so a live diagram does not pull editor/highlighter code. `KDiagramPlayground`
28
+ lazy-loads Shiki and passes animation selection, autoplay, loop, and control props to its live view;
29
+ pair it with Astro `client:visible` when the editor starts below the fold.
30
+
31
+ Both live components accept `frameless` to remove the diagram host border and panel background;
32
+ controls remain independently configurable.
33
+
34
+ Docs: [React](https://diagrams.kekonic.com/publish/react/) / [Editable playground](https://diagrams.kekonic.com/publish/react/#editable-playground).
@@ -0,0 +1,40 @@
1
+ import * as React from "react";
2
+ import { createComponent } from "@lit/react";
3
+ import { KDiagramElement, K_DIAGRAM_TAG } from "@kekonic/diagrams-element";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region \0rolldown/runtime.js
6
+ var __defProp = Object.defineProperty;
7
+ var __exportAll = (all, no_symbols) => {
8
+ let target = {};
9
+ for (var name in all) __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true
12
+ });
13
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
14
+ return target;
15
+ };
16
+ //#endregion
17
+ //#region src/diagram/KDiagramLive.tsx
18
+ var KDiagramLive_exports = /* @__PURE__ */ __exportAll({ KDiagramLive: () => KDiagramLive });
19
+ /** Lit + React prop inference drifts when the element surface grows; keep React props explicit. */
20
+ const KDiagramReact = createComponent({
21
+ react: React,
22
+ tagName: K_DIAGRAM_TAG,
23
+ elementClass: KDiagramElement,
24
+ events: { onKDiagramRender: "kdiagram-render" }
25
+ });
26
+ /**
27
+ * React thin wrapper around Lit `<k-diagram>`.
28
+ * Same interactive host as HTML embeds — pan/zoom, theme, live `source` updates.
29
+ */
30
+ const KDiagramLive = React.forwardRef(function KDiagramLive({ className, ...props }, ref) {
31
+ return /* @__PURE__ */ jsx(KDiagramReact, {
32
+ ref,
33
+ className: ["kd-live", className].filter(Boolean).join(" "),
34
+ ...props
35
+ });
36
+ });
37
+ //#endregion
38
+ export { KDiagramLive_exports as n, KDiagramLive as t };
39
+
40
+ //# sourceMappingURL=KDiagramLive-CnGvR1jR.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KDiagramLive-CnGvR1jR.mjs","names":[],"sources":["../src/diagram/KDiagramLive.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { createComponent } from \"@lit/react\";\nimport { KDiagramElement, K_DIAGRAM_TAG } from \"@kekonic/diagrams-element\";\nimport type { InteractiveRenderOptions, RenderResult, ThemeMode } from \"@kekonic/diagrams\";\n\nexport type KDiagramLiveProps = {\n source: string;\n /** Diagram theme. `\"auto\"` follows the host page (`html[data-theme]`). */\n theme?: ThemeMode | \"auto\";\n className?: string;\n /** Remove the host border and panel background. Controls remain independently configurable. */\n frameless?: boolean;\n /** Sun/moon control that dogfoods theme swaps. */\n showThemeToggle?: boolean;\n /** Zoom / fit / fullscreen controls. */\n showViewControls?: boolean;\n /** Compact render stats badge (floating). Off by default for a clean embed. */\n showStats?: boolean;\n /** Animation play/scrub chrome when an animation exists. Default on. */\n showAnimationControls?: boolean;\n /** Start playing when the diagram mounts. */\n autoplay?: boolean;\n /** Loop the active animation. */\n animationLoop?: boolean;\n /** Preferred animation name or id (authored blocks). */\n animation?: string;\n height?: number | string;\n options?: Omit<InteractiveRenderOptions, \"theme\">;\n /** Fired after mount / source / theme renders complete. */\n onKDiagramRender?: (event: CustomEvent<RenderResult>) => void;\n};\n\n/** Lit + React prop inference drifts when the element surface grows; keep React props explicit. */\nconst KDiagramReact = createComponent({\n react: React,\n tagName: K_DIAGRAM_TAG,\n elementClass: KDiagramElement,\n events: {\n onKDiagramRender: \"kdiagram-render\",\n },\n}) as unknown as React.ForwardRefExoticComponent<\n KDiagramLiveProps & React.RefAttributes<KDiagramElement>\n>;\n\n/**\n * React thin wrapper around Lit `<k-diagram>`.\n * Same interactive host as HTML embeds — pan/zoom, theme, live `source` updates.\n */\nexport const KDiagramLive = React.forwardRef<KDiagramElement, KDiagramLiveProps>(\n function KDiagramLive({ className, ...props }, ref) {\n return (\n <KDiagramReact\n ref={ref}\n className={[\"kd-live\", className].filter(Boolean).join(\" \")}\n {...props}\n />\n );\n },\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiCA,MAAM,gBAAgB,gBAAgB;CACpC,OAAO;CACP,SAAS;CACT,cAAc;CACd,QAAQ,EACN,kBAAkB,kBACpB;AACF,CAAC;;;;;AAQD,MAAa,eAAe,MAAM,WAChC,SAAS,aAAa,EAAE,WAAW,GAAG,SAAS,KAAK;CAClD,OACE,oBAAC,eAAD;EACO;EACL,WAAW,CAAC,WAAW,SAAS,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;EAC1D,GAAI;CACL,CAAA;AAEL,CACF"}
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ import { KDiagramElement } from "@kekonic/diagrams-element";
3
+ import { InteractiveRenderOptions, RenderResult, ThemeMode } from "@kekonic/diagrams";
4
+
5
+ //#region src/diagram/KDiagramLive.d.ts
6
+ type KDiagramLiveProps = {
7
+ source: string; /** Diagram theme. `"auto"` follows the host page (`html[data-theme]`). */
8
+ theme?: ThemeMode | "auto";
9
+ className?: string; /** Remove the host border and panel background. Controls remain independently configurable. */
10
+ frameless?: boolean; /** Sun/moon control that dogfoods theme swaps. */
11
+ showThemeToggle?: boolean; /** Zoom / fit / fullscreen controls. */
12
+ showViewControls?: boolean; /** Compact render stats badge (floating). Off by default for a clean embed. */
13
+ showStats?: boolean; /** Animation play/scrub chrome when an animation exists. Default on. */
14
+ showAnimationControls?: boolean; /** Start playing when the diagram mounts. */
15
+ autoplay?: boolean; /** Loop the active animation. */
16
+ animationLoop?: boolean; /** Preferred animation name or id (authored blocks). */
17
+ animation?: string;
18
+ height?: number | string;
19
+ options?: Omit<InteractiveRenderOptions, "theme">; /** Fired after mount / source / theme renders complete. */
20
+ onKDiagramRender?: (event: CustomEvent<RenderResult>) => void;
21
+ };
22
+ /**
23
+ * React thin wrapper around Lit `<k-diagram>`.
24
+ * Same interactive host as HTML embeds — pan/zoom, theme, live `source` updates.
25
+ */
26
+ declare const KDiagramLive: React.ForwardRefExoticComponent<KDiagramLiveProps & React.RefAttributes<KDiagramElement>>;
27
+ //#endregion
28
+ export { KDiagramLiveProps as n, KDiagramLive as t };
29
+ //# sourceMappingURL=KDiagramLive-Cx3hrc2_.d.mts.map
@@ -0,0 +1,17 @@
1
+ import { n as KDiagramLiveProps, t as KDiagramLive } from "./KDiagramLive-Cx3hrc2_.mjs";
2
+ import { RenderOptions, RenderResult } from "@kekonic/diagrams";
3
+
4
+ //#region src/diagram/render-static.d.ts
5
+ type StaticDiagramOptions = RenderOptions & {
6
+ /** Prefer snapshot CSS vars so SVGs look correct outside live theme CSS. */snapshotTheme?: boolean;
7
+ };
8
+ /**
9
+ * Server-safe SVG render for docs/SSR. Uses the shared font measurer in Node.
10
+ */
11
+ declare function renderKDiagramSvg(source: string, options?: StaticDiagramOptions): Promise<{
12
+ svg: string;
13
+ diagnostics: RenderResult["diagnostics"];
14
+ }>;
15
+ //#endregion
16
+ export { KDiagramLive, type KDiagramLiveProps, type StaticDiagramOptions, renderKDiagramSvg };
17
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import { t as KDiagramLive } from "./KDiagramLive-CnGvR1jR.mjs";
2
+ //#region src/diagram/render-static.ts
3
+ /**
4
+ * Server-safe SVG render for docs/SSR. Uses the shared font measurer in Node.
5
+ */
6
+ async function renderKDiagramSvg(source, options = {}) {
7
+ const { KDiagram } = await import("@kekonic/diagrams");
8
+ const { snapshotTheme = true, theme = "dark", ...rest } = options;
9
+ const result = await KDiagram.renderToSvg(source, {
10
+ ...rest,
11
+ theme,
12
+ snapshotTheme,
13
+ presentation: rest.presentation
14
+ });
15
+ return {
16
+ svg: result.svg ?? "",
17
+ diagnostics: result.diagnostics
18
+ };
19
+ }
20
+ //#endregion
21
+ export { KDiagramLive, renderKDiagramSvg };
22
+
23
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/diagram/render-static.ts"],"sourcesContent":["import type { RenderOptions, RenderResult } from \"@kekonic/diagrams\";\n\nexport type StaticDiagramOptions = RenderOptions & {\n /** Prefer snapshot CSS vars so SVGs look correct outside live theme CSS. */\n snapshotTheme?: boolean;\n};\n\n/**\n * Server-safe SVG render for docs/SSR. Uses the shared font measurer in Node.\n */\nexport async function renderKDiagramSvg(\n source: string,\n options: StaticDiagramOptions = {},\n): Promise<{\n svg: string;\n diagnostics: RenderResult[\"diagnostics\"];\n}> {\n const { KDiagram } = await import(\"@kekonic/diagrams\");\n const { snapshotTheme = true, theme = \"dark\", ...rest } = options;\n const result = await KDiagram.renderToSvg(source, {\n ...rest,\n theme,\n snapshotTheme,\n // Chromeless by default — opt into title/legend/etc. via options or the DSL.\n presentation: rest.presentation,\n });\n return { svg: result.svg ?? \"\", diagnostics: result.diagnostics };\n}\n"],"mappings":";;;;;AAUA,eAAsB,kBACpB,QACA,UAAgC,CAAC,GAIhC;CACD,MAAM,EAAE,aAAa,MAAM,OAAO;CAClC,MAAM,EAAE,gBAAgB,MAAM,QAAQ,QAAQ,GAAG,SAAS;CAC1D,MAAM,SAAS,MAAM,SAAS,YAAY,QAAQ;EAChD,GAAG;EACH;EACA;EAEA,cAAc,KAAK;CACrB,CAAC;CACD,OAAO;EAAE,KAAK,OAAO,OAAO;EAAI,aAAa,OAAO;CAAY;AAClE"}
@@ -0,0 +1,57 @@
1
+ import { n as KDiagramLiveProps } from "./KDiagramLive-Cx3hrc2_.mjs";
2
+ import { ThemeMode } from "@kekonic/diagrams";
3
+
4
+ //#region src/diagram/KDiagramPlayground.d.ts
5
+ type KDiagramPlaygroundProps = {
6
+ /** Initial KDiagram source (also the Reset target). */source: string;
7
+ theme?: ThemeMode | "auto";
8
+ className?: string; /** Remove the live diagram's host border and panel background. */
9
+ frameless?: boolean; /** Source pane + diagram side-by-side, stacked, or gallery (diagram-first). */
10
+ layout?: "split" | "stacked" | "gallery"; /** Debounce before `controller.update` (ms). */
11
+ debounceMs?: number;
12
+ editable?: boolean;
13
+ showThemeToggle?: boolean;
14
+ showViewControls?: boolean;
15
+ showStats?: boolean;
16
+ showAnimationControls?: boolean;
17
+ autoplay?: boolean;
18
+ animationLoop?: boolean;
19
+ animation?: string;
20
+ /**
21
+ * Playground root height — both panes stretch to fill.
22
+ * Diagram viewport flexes inside; no separate short editor box.
23
+ */
24
+ height?: number | string;
25
+ /**
26
+ * Minimum height for the source editor region when the root is not stretched
27
+ * (e.g. stacked layout without an explicit `height`).
28
+ */
29
+ editorHeight?: number | string;
30
+ options?: KDiagramLiveProps["options"];
31
+ };
32
+ /**
33
+ * Docs dogfood embed: editable KDiagram source (Shiki) + live diagram (`KDiagramLive`).
34
+ * Shiki and the render path load lazily on mount / first highlight.
35
+ */
36
+ declare function KDiagramPlayground({
37
+ source,
38
+ theme,
39
+ className,
40
+ frameless,
41
+ layout,
42
+ debounceMs,
43
+ editable,
44
+ showThemeToggle,
45
+ showViewControls,
46
+ showStats,
47
+ showAnimationControls,
48
+ autoplay,
49
+ animationLoop,
50
+ animation,
51
+ height,
52
+ editorHeight,
53
+ options
54
+ }: KDiagramPlaygroundProps): import("react").JSX.Element;
55
+ //#endregion
56
+ export { KDiagramPlayground, type KDiagramPlaygroundProps };
57
+ //# sourceMappingURL=playground.d.mts.map
@@ -0,0 +1,226 @@
1
+ import { Suspense, lazy, useEffect, useId, useRef, useState } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/diagram/KDiagramPlayground.tsx
4
+ const KDiagramLive = lazy(async () => {
5
+ return { default: (await import("./KDiagramLive-CnGvR1jR.mjs").then((n) => n.n)).KDiagramLive };
6
+ });
7
+ /** Kick off Shiki as soon as this island module evaluates (still lazy vs the page). */
8
+ const highlightModule = import("./shiki/highlight.mjs");
9
+ function escapeHtml(text) {
10
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
11
+ }
12
+ function cssSize(value) {
13
+ return typeof value === "number" ? `${value}px` : value;
14
+ }
15
+ function copyTextFallback(text) {
16
+ const input = document.createElement("textarea");
17
+ input.value = text;
18
+ input.setAttribute("readonly", "");
19
+ input.style.position = "fixed";
20
+ input.style.opacity = "0";
21
+ document.body.append(input);
22
+ input.select();
23
+ const copied = document.execCommand("copy");
24
+ input.remove();
25
+ return copied;
26
+ }
27
+ /**
28
+ * Docs dogfood embed: editable KDiagram source (Shiki) + live diagram (`KDiagramLive`).
29
+ * Shiki and the render path load lazily on mount / first highlight.
30
+ */
31
+ function KDiagramPlayground({ source, theme = "auto", className, frameless = false, layout = "split", debounceMs = 280, editable = true, showThemeToggle = true, showViewControls = true, showStats = false, showAnimationControls = true, autoplay = false, animationLoop = false, animation, height = 360, editorHeight = 280, options }) {
32
+ const [draft, setDraft] = useState(source);
33
+ const [liveSource, setLiveSource] = useState(source);
34
+ const [highlightHtml, setHighlightHtml] = useState(null);
35
+ const [dirty, setDirty] = useState(false);
36
+ const [actionStatus, setActionStatus] = useState("");
37
+ const highlightRef = useRef(null);
38
+ const inputRef = useRef(null);
39
+ const labelId = useId();
40
+ const sourceGen = useRef(0);
41
+ useEffect(() => {
42
+ setDraft(source);
43
+ setLiveSource(source);
44
+ setDirty(false);
45
+ }, [source]);
46
+ useEffect(() => {
47
+ if (draft === liveSource) return;
48
+ const t = window.setTimeout(() => setLiveSource(draft), debounceMs);
49
+ return () => window.clearTimeout(t);
50
+ }, [
51
+ draft,
52
+ liveSource,
53
+ debounceMs
54
+ ]);
55
+ useEffect(() => {
56
+ let cancelled = false;
57
+ const gen = ++sourceGen.current;
58
+ setHighlightHtml(`<pre><code>${escapeHtml(draft)}</code></pre>`);
59
+ (async () => {
60
+ try {
61
+ const { highlightKDiagram } = await highlightModule;
62
+ const html = await highlightKDiagram(draft);
63
+ if (!cancelled && gen === sourceGen.current) setHighlightHtml(html);
64
+ } catch {}
65
+ })();
66
+ return () => {
67
+ cancelled = true;
68
+ };
69
+ }, [draft]);
70
+ const syncScroll = (event) => {
71
+ const pre = highlightRef.current;
72
+ if (!pre) return;
73
+ pre.scrollTop = event.currentTarget.scrollTop;
74
+ pre.scrollLeft = event.currentTarget.scrollLeft;
75
+ };
76
+ const reset = () => {
77
+ setDraft(source);
78
+ setLiveSource(source);
79
+ setDirty(false);
80
+ inputRef.current?.focus();
81
+ };
82
+ const copySource = async () => {
83
+ let copied = false;
84
+ try {
85
+ if (navigator.clipboard?.writeText) {
86
+ await navigator.clipboard.writeText(draft);
87
+ copied = true;
88
+ } else copied = copyTextFallback(draft);
89
+ } catch {
90
+ copied = copyTextFallback(draft);
91
+ }
92
+ setActionStatus(copied ? "Copied source" : "Could not copy source");
93
+ window.setTimeout(() => setActionStatus(""), 1500);
94
+ };
95
+ const downloadSvg = async () => {
96
+ const { KDiagram } = await import("@kekonic/diagrams");
97
+ const resolvedTheme = theme === "auto" ? document.documentElement.dataset.theme === "light" ? "light" : "dark" : theme;
98
+ const result = await KDiagram.renderToSvg(draft, {
99
+ ...options,
100
+ theme: resolvedTheme,
101
+ snapshotTheme: true
102
+ });
103
+ if (!result.ok || !result.svg) {
104
+ setActionStatus("Fix diagram errors before downloading");
105
+ return;
106
+ }
107
+ const url = URL.createObjectURL(new Blob([result.svg], { type: "image/svg+xml" }));
108
+ const link = document.createElement("a");
109
+ link.href = url;
110
+ link.download = "diagram.svg";
111
+ document.body.append(link);
112
+ link.click();
113
+ link.remove();
114
+ window.setTimeout(() => URL.revokeObjectURL(url), 1e3);
115
+ setActionStatus("Downloaded SVG");
116
+ window.setTimeout(() => setActionStatus(""), 1500);
117
+ };
118
+ const rootStyle = {
119
+ height: cssSize(height),
120
+ "--kd-play-editor-min": cssSize(editorHeight)
121
+ };
122
+ const hint = editable ? "Edit source — diagram updates live" : "Source + live diagram";
123
+ return /* @__PURE__ */ jsxs("div", {
124
+ className: ["kd-play", className].filter(Boolean).join(" "),
125
+ style: rootStyle,
126
+ title: hint,
127
+ "aria-labelledby": labelId,
128
+ children: [/* @__PURE__ */ jsx("span", {
129
+ id: labelId,
130
+ className: "kd-play__sr-only",
131
+ children: hint
132
+ }), /* @__PURE__ */ jsxs("div", {
133
+ className: "kd-play__body",
134
+ "data-layout": layout,
135
+ children: [/* @__PURE__ */ jsxs("div", {
136
+ className: "kd-play__source",
137
+ children: [/* @__PURE__ */ jsxs("div", {
138
+ className: "kd-play__pane-bar",
139
+ children: [
140
+ /* @__PURE__ */ jsx("span", {
141
+ className: "kd-play__label",
142
+ children: "Source"
143
+ }),
144
+ /* @__PURE__ */ jsxs("div", {
145
+ className: "kd-play__actions",
146
+ children: [
147
+ /* @__PURE__ */ jsx("button", {
148
+ type: "button",
149
+ className: "kd-play__btn",
150
+ onClick: () => void copySource(),
151
+ children: "Copy source"
152
+ }),
153
+ /* @__PURE__ */ jsx("button", {
154
+ type: "button",
155
+ className: "kd-play__btn",
156
+ onClick: () => void downloadSvg(),
157
+ children: "Download SVG"
158
+ }),
159
+ editable ? /* @__PURE__ */ jsx("button", {
160
+ type: "button",
161
+ className: "kd-play__btn",
162
+ disabled: !dirty,
163
+ onClick: reset,
164
+ children: "Reset"
165
+ }) : null
166
+ ]
167
+ }),
168
+ /* @__PURE__ */ jsx("span", {
169
+ className: "kd-play__sr-only",
170
+ role: "status",
171
+ "aria-live": "polite",
172
+ children: actionStatus
173
+ })
174
+ ]
175
+ }), /* @__PURE__ */ jsxs("div", {
176
+ className: "kd-play__editor",
177
+ children: [/* @__PURE__ */ jsx("div", {
178
+ ref: highlightRef,
179
+ className: "kd-play__highlight",
180
+ "aria-hidden": true,
181
+ dangerouslySetInnerHTML: { __html: highlightHtml ?? `<pre><code>${escapeHtml(draft)}</code></pre>` }
182
+ }), editable ? /* @__PURE__ */ jsx("textarea", {
183
+ ref: inputRef,
184
+ className: "kd-play__input",
185
+ value: draft,
186
+ spellCheck: false,
187
+ autoCapitalize: "off",
188
+ autoCorrect: "off",
189
+ "aria-label": "KDiagram source",
190
+ onScroll: syncScroll,
191
+ onChange: (event) => {
192
+ setDraft(event.target.value);
193
+ setDirty(event.target.value !== source);
194
+ }
195
+ }) : null]
196
+ })]
197
+ }), /* @__PURE__ */ jsx("div", {
198
+ className: "kd-play__diagram",
199
+ children: /* @__PURE__ */ jsx(Suspense, {
200
+ fallback: /* @__PURE__ */ jsx("div", {
201
+ className: "kd-play__fallback",
202
+ children: "Loading diagram…"
203
+ }),
204
+ children: /* @__PURE__ */ jsx(KDiagramLive, {
205
+ source: liveSource,
206
+ theme,
207
+ height: "100%",
208
+ frameless,
209
+ showThemeToggle,
210
+ showViewControls,
211
+ showStats,
212
+ showAnimationControls,
213
+ autoplay,
214
+ animationLoop,
215
+ animation,
216
+ options
217
+ })
218
+ })
219
+ })]
220
+ })]
221
+ });
222
+ }
223
+ //#endregion
224
+ export { KDiagramPlayground };
225
+
226
+ //# sourceMappingURL=playground.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playground.mjs","names":[],"sources":["../src/diagram/KDiagramPlayground.tsx"],"sourcesContent":["import {\n lazy,\n Suspense,\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type UIEvent,\n} from \"react\";\nimport type { ThemeMode } from \"@kekonic/diagrams\";\nimport type { KDiagramLiveProps } from \"./KDiagramLive.tsx\";\n\nconst KDiagramLive = lazy(async () => {\n const mod = await import(\"./KDiagramLive.tsx\");\n return { default: mod.KDiagramLive };\n});\n\n/** Kick off Shiki as soon as this island module evaluates (still lazy vs the page). */\nconst highlightModule = import(\"../shiki/highlight.ts\");\n\nexport type KDiagramPlaygroundProps = {\n /** Initial KDiagram source (also the Reset target). */\n source: string;\n theme?: ThemeMode | \"auto\";\n className?: string;\n /** Remove the live diagram's host border and panel background. */\n frameless?: boolean;\n /** Source pane + diagram side-by-side, stacked, or gallery (diagram-first). */\n layout?: \"split\" | \"stacked\" | \"gallery\";\n /** Debounce before `controller.update` (ms). */\n debounceMs?: number;\n editable?: boolean;\n showThemeToggle?: boolean;\n showViewControls?: boolean;\n showStats?: boolean;\n showAnimationControls?: boolean;\n autoplay?: boolean;\n animationLoop?: boolean;\n animation?: string;\n /**\n * Playground root height — both panes stretch to fill.\n * Diagram viewport flexes inside; no separate short editor box.\n */\n height?: number | string;\n /**\n * Minimum height for the source editor region when the root is not stretched\n * (e.g. stacked layout without an explicit `height`).\n */\n editorHeight?: number | string;\n options?: KDiagramLiveProps[\"options\"];\n};\n\nfunction escapeHtml(text: string): string {\n return text\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\");\n}\n\nfunction cssSize(value: number | string): string {\n return typeof value === \"number\" ? `${value}px` : value;\n}\n\nfunction copyTextFallback(text: string): boolean {\n const input = document.createElement(\"textarea\");\n input.value = text;\n input.setAttribute(\"readonly\", \"\");\n input.style.position = \"fixed\";\n input.style.opacity = \"0\";\n document.body.append(input);\n input.select();\n const copied = document.execCommand(\"copy\");\n input.remove();\n return copied;\n}\n\n/**\n * Docs dogfood embed: editable KDiagram source (Shiki) + live diagram (`KDiagramLive`).\n * Shiki and the render path load lazily on mount / first highlight.\n */\nexport function KDiagramPlayground({\n source,\n theme = \"auto\",\n className,\n frameless = false,\n layout = \"split\",\n debounceMs = 280,\n editable = true,\n showThemeToggle = true,\n showViewControls = true,\n showStats = false,\n showAnimationControls = true,\n autoplay = false,\n animationLoop = false,\n animation,\n height = 360,\n editorHeight = 280,\n options,\n}: KDiagramPlaygroundProps) {\n const [draft, setDraft] = useState(source);\n const [liveSource, setLiveSource] = useState(source);\n const [highlightHtml, setHighlightHtml] = useState<string | null>(null);\n const [dirty, setDirty] = useState(false);\n const [actionStatus, setActionStatus] = useState(\"\");\n const highlightRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const labelId = useId();\n const sourceGen = useRef(0);\n\n useEffect(() => {\n setDraft(source);\n setLiveSource(source);\n setDirty(false);\n }, [source]);\n\n useEffect(() => {\n if (draft === liveSource) return;\n const t = window.setTimeout(() => setLiveSource(draft), debounceMs);\n return () => window.clearTimeout(t);\n }, [draft, liveSource, debounceMs]);\n\n useEffect(() => {\n let cancelled = false;\n const gen = ++sourceGen.current;\n // Plain text first; upgrade once the module-level Shiki import resolves.\n setHighlightHtml(`<pre><code>${escapeHtml(draft)}</code></pre>`);\n void (async () => {\n try {\n const { highlightKDiagram } = await highlightModule;\n const html = await highlightKDiagram(draft);\n if (!cancelled && gen === sourceGen.current) setHighlightHtml(html);\n } catch {\n // Keep plain text if Shiki cannot load in this host.\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [draft]);\n\n const syncScroll = (event: UIEvent<HTMLTextAreaElement>) => {\n const pre = highlightRef.current;\n if (!pre) return;\n pre.scrollTop = event.currentTarget.scrollTop;\n pre.scrollLeft = event.currentTarget.scrollLeft;\n };\n\n const reset = () => {\n setDraft(source);\n setLiveSource(source);\n setDirty(false);\n inputRef.current?.focus();\n };\n\n const copySource = async () => {\n let copied = false;\n try {\n if (navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(draft);\n copied = true;\n } else {\n copied = copyTextFallback(draft);\n }\n } catch {\n copied = copyTextFallback(draft);\n }\n setActionStatus(copied ? \"Copied source\" : \"Could not copy source\");\n window.setTimeout(() => setActionStatus(\"\"), 1_500);\n };\n\n const downloadSvg = async () => {\n const { KDiagram } = await import(\"@kekonic/diagrams\");\n const resolvedTheme =\n theme === \"auto\"\n ? document.documentElement.dataset.theme === \"light\"\n ? \"light\"\n : \"dark\"\n : theme;\n const result = await KDiagram.renderToSvg(draft, {\n ...options,\n theme: resolvedTheme,\n snapshotTheme: true,\n });\n if (!result.ok || !result.svg) {\n setActionStatus(\"Fix diagram errors before downloading\");\n return;\n }\n const url = URL.createObjectURL(new Blob([result.svg], { type: \"image/svg+xml\" }));\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = \"diagram.svg\";\n document.body.append(link);\n link.click();\n link.remove();\n window.setTimeout(() => URL.revokeObjectURL(url), 1_000);\n setActionStatus(\"Downloaded SVG\");\n window.setTimeout(() => setActionStatus(\"\"), 1_500);\n };\n\n const rootStyle = {\n height: cssSize(height),\n \"--kd-play-editor-min\": cssSize(editorHeight),\n } as CSSProperties;\n\n const hint = editable ? \"Edit source — diagram updates live\" : \"Source + live diagram\";\n\n return (\n <div\n className={[\"kd-play\", className].filter(Boolean).join(\" \")}\n style={rootStyle}\n title={hint}\n aria-labelledby={labelId}\n >\n <span id={labelId} className=\"kd-play__sr-only\">\n {hint}\n </span>\n <div className=\"kd-play__body\" data-layout={layout}>\n <div className=\"kd-play__source\">\n <div className=\"kd-play__pane-bar\">\n <span className=\"kd-play__label\">Source</span>\n <div className=\"kd-play__actions\">\n <button type=\"button\" className=\"kd-play__btn\" onClick={() => void copySource()}>\n Copy source\n </button>\n <button type=\"button\" className=\"kd-play__btn\" onClick={() => void downloadSvg()}>\n Download SVG\n </button>\n {editable ? (\n <button type=\"button\" className=\"kd-play__btn\" disabled={!dirty} onClick={reset}>\n Reset\n </button>\n ) : null}\n </div>\n <span className=\"kd-play__sr-only\" role=\"status\" aria-live=\"polite\">\n {actionStatus}\n </span>\n </div>\n <div className=\"kd-play__editor\">\n <div\n ref={highlightRef}\n className=\"kd-play__highlight\"\n aria-hidden\n dangerouslySetInnerHTML={{\n __html: highlightHtml ?? `<pre><code>${escapeHtml(draft)}</code></pre>`,\n }}\n />\n {editable ? (\n <textarea\n ref={inputRef}\n className=\"kd-play__input\"\n value={draft}\n spellCheck={false}\n autoCapitalize=\"off\"\n autoCorrect=\"off\"\n aria-label=\"KDiagram source\"\n onScroll={syncScroll}\n onChange={(event) => {\n setDraft(event.target.value);\n setDirty(event.target.value !== source);\n }}\n />\n ) : null}\n </div>\n </div>\n <div className=\"kd-play__diagram\">\n <Suspense fallback={<div className=\"kd-play__fallback\">Loading diagram…</div>}>\n <KDiagramLive\n source={liveSource}\n theme={theme}\n height=\"100%\"\n frameless={frameless}\n showThemeToggle={showThemeToggle}\n showViewControls={showViewControls}\n showStats={showStats}\n showAnimationControls={showAnimationControls}\n autoplay={autoplay}\n animationLoop={animationLoop}\n animation={animation}\n options={options}\n />\n </Suspense>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ //#region src/shiki/highlight.d.ts
2
+ /**
3
+ * Lazy Shiki highlighter for KDiagram source in docs embeds.
4
+ * Loaded on first highlight request — not on package import.
5
+ */
6
+ /** Dual-theme HTML (CSS vars `--shiki-dark` / `--shiki-light`) for site theme toggles. */
7
+ declare function highlightKDiagram(code: string): Promise<string>;
8
+ //#endregion
9
+ export { highlightKDiagram };
10
+ //# sourceMappingURL=highlight.d.mts.map
@@ -0,0 +1,27 @@
1
+ //#region src/shiki/highlight.ts
2
+ let highlighterPromise = null;
3
+ async function getHighlighter() {
4
+ if (!highlighterPromise) highlighterPromise = (async () => {
5
+ const [{ createHighlighter }, { kdiagramLanguage }] = await Promise.all([import("shiki"), import("./kdiagram.mjs")]);
6
+ return createHighlighter({
7
+ themes: ["github-dark", "github-light"],
8
+ langs: [kdiagramLanguage]
9
+ });
10
+ })();
11
+ return highlighterPromise;
12
+ }
13
+ /** Dual-theme HTML (CSS vars `--shiki-dark` / `--shiki-light`) for site theme toggles. */
14
+ async function highlightKDiagram(code) {
15
+ return (await getHighlighter()).codeToHtml(code.length > 0 ? code : " ", {
16
+ lang: "kdiagram",
17
+ themes: {
18
+ dark: "github-dark",
19
+ light: "github-light"
20
+ },
21
+ defaultColor: false
22
+ });
23
+ }
24
+ //#endregion
25
+ export { highlightKDiagram };
26
+
27
+ //# sourceMappingURL=highlight.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"highlight.mjs","names":[],"sources":["../../src/shiki/highlight.ts"],"sourcesContent":["/**\n * Lazy Shiki highlighter for KDiagram source in docs embeds.\n * Loaded on first highlight request — not on package import.\n */\n\ntype Highlighter = Awaited<ReturnType<(typeof import(\"shiki\"))[\"createHighlighter\"]>>;\n\nlet highlighterPromise: Promise<Highlighter> | null = null;\n\nasync function getHighlighter(): Promise<Highlighter> {\n if (!highlighterPromise) {\n highlighterPromise = (async () => {\n const [{ createHighlighter }, { kdiagramLanguage }] = await Promise.all([\n import(\"shiki\"),\n import(\"./kdiagram.ts\"),\n ]);\n return createHighlighter({\n themes: [\"github-dark\", \"github-light\"],\n // TextMate grammar object — compatible at runtime; Shiki's bundled lang union is narrower.\n langs: [kdiagramLanguage as never],\n });\n })();\n }\n return highlighterPromise;\n}\n\n/** Dual-theme HTML (CSS vars `--shiki-dark` / `--shiki-light`) for site theme toggles. */\nexport async function highlightKDiagram(code: string): Promise<string> {\n const highlighter = await getHighlighter();\n return highlighter.codeToHtml(code.length > 0 ? code : \" \", {\n lang: \"kdiagram\",\n themes: {\n dark: \"github-dark\",\n light: \"github-light\",\n },\n defaultColor: false,\n });\n}\n"],"mappings":";AAOA,IAAI,qBAAkD;AAEtD,eAAe,iBAAuC;CACpD,IAAI,CAAC,oBACH,sBAAsB,YAAY;EAChC,MAAM,CAAC,EAAE,qBAAqB,EAAE,sBAAsB,MAAM,QAAQ,IAAI,CACtE,OAAO,UACP,OAAO,iBACT,CAAC;EACD,OAAO,kBAAkB;GACvB,QAAQ,CAAC,eAAe,cAAc;GAEtC,OAAO,CAAC,gBAAyB;EACnC,CAAC;CACH,EAAA,CAAG;CAEL,OAAO;AACT;;AAGA,eAAsB,kBAAkB,MAA+B;CAErE,QAAO,MADmB,eAAe,EAAA,CACtB,WAAW,KAAK,SAAS,IAAI,OAAO,KAAK;EAC1D,MAAM;EACN,QAAQ;GACN,MAAM;GACN,OAAO;EACT;EACA,cAAc;CAChB,CAAC;AACH"}