@dooc-dev/dooc 0.0.0-stage → 0.0.2
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/README.md +26 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +311 -0
- package/dist/code.global.js +285 -0
- package/dist/compiler/bundle-urls.d.ts +3 -0
- package/dist/compiler/bundle-urls.js +294 -0
- package/dist/compiler/bundle.d.ts +2 -0
- package/dist/compiler/bundle.js +100 -0
- package/dist/compiler/check-prose.d.ts +11 -0
- package/dist/compiler/check-prose.js +138 -0
- package/dist/compiler/check-types.d.ts +44 -0
- package/dist/compiler/check-types.js +1 -0
- package/dist/compiler/check.d.ts +5 -0
- package/dist/compiler/check.js +450 -0
- package/dist/compiler/code-blocks.d.ts +17 -0
- package/dist/compiler/code-blocks.js +198 -0
- package/dist/compiler/code-languages.d.ts +10 -0
- package/dist/compiler/code-languages.js +109 -0
- package/dist/compiler/compile.d.ts +95 -0
- package/dist/compiler/compile.js +410 -0
- package/dist/compiler/config.d.ts +7 -0
- package/dist/compiler/config.js +52 -0
- package/dist/compiler/dialect.d.ts +24 -0
- package/dist/compiler/dialect.js +198 -0
- package/dist/compiler/drawio.d.ts +26 -0
- package/dist/compiler/drawio.js +176 -0
- package/dist/compiler/figures.d.ts +14 -0
- package/dist/compiler/figures.js +221 -0
- package/dist/compiler/file-cache.d.ts +14 -0
- package/dist/compiler/file-cache.js +61 -0
- package/dist/compiler/files.d.ts +29 -0
- package/dist/compiler/files.js +431 -0
- package/dist/compiler/fonts.d.ts +10 -0
- package/dist/compiler/fonts.js +54 -0
- package/dist/compiler/format.d.ts +1 -0
- package/dist/compiler/format.js +42 -0
- package/dist/compiler/from-mdast.d.ts +16 -0
- package/dist/compiler/from-mdast.js +142 -0
- package/dist/compiler/icons.d.ts +14 -0
- package/dist/compiler/icons.js +85 -0
- package/dist/compiler/language-codes.d.ts +7 -0
- package/dist/compiler/language-codes.js +17 -0
- package/dist/compiler/locales.d.ts +7 -0
- package/dist/compiler/locales.js +67 -0
- package/dist/compiler/manifest.d.ts +6 -0
- package/dist/compiler/manifest.js +30 -0
- package/dist/compiler/ops.d.ts +44 -0
- package/dist/compiler/ops.js +362 -0
- package/dist/compiler/public-file.d.ts +2 -0
- package/dist/compiler/public-file.js +93 -0
- package/dist/compiler/serialize.d.ts +3 -0
- package/dist/compiler/serialize.js +16 -0
- package/dist/compiler/terms.d.ts +9 -0
- package/dist/compiler/terms.js +44 -0
- package/dist/compiler/textfile-remote.d.ts +12 -0
- package/dist/compiler/textfile-remote.js +124 -0
- package/dist/compiler/textfile.d.ts +9 -0
- package/dist/compiler/textfile.js +113 -0
- package/dist/compiler/theme-variants.d.ts +4 -0
- package/dist/compiler/theme-variants.js +22 -0
- package/dist/compiler/zip.d.ts +6 -0
- package/dist/compiler/zip.js +48 -0
- package/dist/core/blocks.d.ts +99 -0
- package/dist/core/blocks.js +29 -0
- package/dist/core/code-file.d.ts +12 -0
- package/dist/core/code-file.js +68 -0
- package/dist/core/content-metadata.d.ts +48 -0
- package/dist/core/content-metadata.js +264 -0
- package/dist/core/content-types.d.ts +45 -0
- package/dist/core/content-types.js +1 -0
- package/dist/core/extension-package.d.ts +34 -0
- package/dist/core/extension-package.js +147 -0
- package/dist/core/figure-frame.d.ts +12 -0
- package/dist/core/figure-frame.js +108 -0
- package/dist/core/file-embed.d.ts +13 -0
- package/dist/core/file-embed.js +43 -0
- package/dist/core/files.d.ts +15 -0
- package/dist/core/files.js +59 -0
- package/dist/core/flow-base-css.generated.d.ts +1 -0
- package/dist/core/flow-base-css.generated.js +4 -0
- package/dist/core/flow-css.d.ts +9 -0
- package/dist/core/flow-css.js +54 -0
- package/dist/core/graph-common.d.ts +32 -0
- package/dist/core/graph-common.js +113 -0
- package/dist/core/graph.d.ts +117 -0
- package/dist/core/graph.js +480 -0
- package/dist/core/html.d.ts +29 -0
- package/dist/core/html.js +444 -0
- package/dist/core/icons.d.ts +2 -0
- package/dist/core/icons.js +2 -0
- package/dist/core/inline.d.ts +27 -0
- package/dist/core/inline.js +42 -0
- package/dist/core/language.d.ts +41 -0
- package/dist/core/language.js +44 -0
- package/dist/core/languages/ja.d.ts +2 -0
- package/dist/core/languages/ja.js +5 -0
- package/dist/core/markdown.d.ts +27 -0
- package/dist/core/markdown.js +154 -0
- package/dist/core/previewers.d.ts +37 -0
- package/dist/core/previewers.js +103 -0
- package/dist/core/routing.d.ts +20 -0
- package/dist/core/routing.js +64 -0
- package/dist/core/schema-comments.d.ts +14 -0
- package/dist/core/schema-comments.js +52 -0
- package/dist/core/schema-graph-flow.d.ts +9 -0
- package/dist/core/schema-graph-flow.js +379 -0
- package/dist/core/schema-graph-mount.d.ts +9 -0
- package/dist/core/schema-graph-mount.js +105 -0
- package/dist/core/schema-graph.d.ts +254 -0
- package/dist/core/schema-graph.js +981 -0
- package/dist/core/table-ui.d.ts +2 -0
- package/dist/core/table-ui.js +180 -0
- package/dist/core/taxonomy.d.ts +12 -0
- package/dist/core/taxonomy.js +49 -0
- package/dist/core/term-reference.d.ts +5 -0
- package/dist/core/term-reference.js +19 -0
- package/dist/core/terms-ui.d.ts +6 -0
- package/dist/core/terms-ui.js +22 -0
- package/dist/core/textfile.d.ts +10 -0
- package/dist/core/textfile.js +39 -0
- package/dist/core/theme-variants.d.ts +3 -0
- package/dist/core/theme-variants.js +37 -0
- package/dist/core/toc-ui.d.ts +2 -0
- package/dist/core/toc-ui.js +161 -0
- package/dist/core/video-ui.d.ts +2 -0
- package/dist/core/video-ui.js +253 -0
- package/dist/core/workspace.d.ts +15 -0
- package/dist/core/workspace.js +28 -0
- package/dist/edit.global.js +306 -0
- package/dist/flow.global.js +4959 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +31 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.js +467 -0
- package/dist/plugins/actions.d.ts +45 -0
- package/dist/plugins/actions.js +125 -0
- package/dist/plugins/blocks.d.ts +93 -0
- package/dist/plugins/blocks.js +100 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/local-storage.d.ts +18 -0
- package/dist/plugins/local-storage.js +115 -0
- package/dist/plugins/oauth.d.ts +14 -0
- package/dist/plugins/oauth.js +170 -0
- package/dist/plugins/runtime.d.ts +26 -0
- package/dist/plugins/runtime.js +395 -0
- package/dist/plugins/types.d.ts +59 -0
- package/dist/plugins/types.js +1 -0
- package/dist/plugins/view.d.ts +7 -0
- package/dist/plugins/view.js +21 -0
- package/dist/ui.global.js +78 -0
- package/dist/viewer/assets.d.ts +2 -0
- package/dist/viewer/assets.js +3 -0
- package/dist/viewer/build.d.ts +12 -0
- package/dist/viewer/build.js +55 -0
- package/dist/viewer/bundle-network.d.ts +2 -0
- package/dist/viewer/bundle-network.js +88 -0
- package/dist/viewer/code-file.d.ts +20 -0
- package/dist/viewer/code-file.js +42 -0
- package/dist/viewer/editor-tools.d.ts +38 -0
- package/dist/viewer/editor-tools.js +211 -0
- package/dist/viewer/extension-pack.d.ts +2 -0
- package/dist/viewer/extension-pack.js +36 -0
- package/dist/viewer/extensions.d.ts +10 -0
- package/dist/viewer/extensions.js +302 -0
- package/dist/viewer/file-references.d.ts +14 -0
- package/dist/viewer/file-references.js +38 -0
- package/dist/viewer/font-picker.d.ts +4 -0
- package/dist/viewer/font-picker.js +145 -0
- package/dist/viewer/font-settings.d.ts +11 -0
- package/dist/viewer/font-settings.js +50 -0
- package/dist/viewer/fonts/LICENSE.txt +119 -0
- package/dist/viewer/fonts/udev-gothic-regular.woff2 +0 -0
- package/dist/viewer/fonts.d.ts +2 -0
- package/dist/viewer/fonts.js +13 -0
- package/dist/viewer/git-diff.d.ts +3 -0
- package/dist/viewer/git-diff.js +33 -0
- package/dist/viewer/git-types.d.ts +73 -0
- package/dist/viewer/git-types.js +1 -0
- package/dist/viewer/git.d.ts +5 -0
- package/dist/viewer/git.js +337 -0
- package/dist/viewer/hmr.d.ts +53 -0
- package/dist/viewer/hmr.js +337 -0
- package/dist/viewer/i18n.d.ts +6 -0
- package/dist/viewer/i18n.js +65 -0
- package/dist/viewer/ja-lint.d.ts +3 -0
- package/dist/viewer/ja-lint.js +44 -0
- package/dist/viewer/language-picker.d.ts +2 -0
- package/dist/viewer/language-picker.js +233 -0
- package/dist/viewer/languages.d.ts +11 -0
- package/dist/viewer/languages.js +20 -0
- package/dist/viewer/locales/eng.json +317 -0
- package/dist/viewer/locales/jpn.json +317 -0
- package/dist/viewer/locales/zho.json +317 -0
- package/dist/viewer/nav-focus.d.ts +2 -0
- package/dist/viewer/nav-focus.js +64 -0
- package/dist/viewer/plugin-blocks.d.ts +3 -0
- package/dist/viewer/plugin-blocks.js +115 -0
- package/dist/viewer/plugin-config.d.ts +35 -0
- package/dist/viewer/plugin-config.js +57 -0
- package/dist/viewer/plugin-view.d.ts +2 -0
- package/dist/viewer/plugin-view.js +6 -0
- package/dist/viewer/plugins.d.ts +12 -0
- package/dist/viewer/plugins.js +267 -0
- package/dist/viewer/resize.d.ts +4 -0
- package/dist/viewer/resize.js +238 -0
- package/dist/viewer/resource-library.d.ts +29 -0
- package/dist/viewer/resource-library.js +104 -0
- package/dist/viewer/search.d.ts +16 -0
- package/dist/viewer/search.js +358 -0
- package/dist/viewer/serve.d.ts +210 -0
- package/dist/viewer/serve.js +3312 -0
- package/dist/viewer/site-icon.d.ts +8 -0
- package/dist/viewer/site-icon.js +56 -0
- package/dist/viewer/textfile.d.ts +9 -0
- package/dist/viewer/textfile.js +50 -0
- package/dist/viewer/theme-picker.d.ts +2 -0
- package/dist/viewer/theme-picker.js +135 -0
- package/dist/viewer/workspace.d.ts +4 -0
- package/dist/viewer/workspace.js +47 -0
- package/package.json +128 -5
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Interactive UI for the schema graph (`schemath`), built on React Flow. Handles zoom / pan /
|
|
3
|
+
// box drag & drop / expanded view / minimap. It starts from the same model and the same initial
|
|
4
|
+
// layout as the static SVG (html.ts), so the picture stays continuous; without JS / before
|
|
5
|
+
// mounting, the static SVG stays readable as-is (fallback).
|
|
6
|
+
//
|
|
7
|
+
// Positions and edge connections are decided by pure functions (layout / `schemaFlowEdges` in
|
|
8
|
+
// schema-graph.ts); this file only maps them onto React Flow (= the SoT for geometry and
|
|
9
|
+
// semantics isn't scattered into the UI lib side).
|
|
10
|
+
// Styling is self-contained via inline styles + `var(--doc-*)` (no extra CSS on the consumer
|
|
11
|
+
// side. React Flow's own stylesheet = the structure-only base.css is injected by the mount layer
|
|
12
|
+
// (`flow-css.ts`) — colors come from the `--doc-*` the consumer injects, so React Flow's default
|
|
13
|
+
// theme isn't loaded).
|
|
14
|
+
import { applyNodeChanges, Background, BackgroundVariant, BaseEdge, ControlButton, Controls, Handle, MarkerType, MiniMap, Position, ReactFlow, ReactFlowProvider, useReactFlow, } from "@xyflow/react";
|
|
15
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
16
|
+
import { routeSchemaEdges, schemaFlowEdges, } from "./schema-graph.js";
|
|
17
|
+
const STORE_PREFIX = "docs:sg:";
|
|
18
|
+
const FRAME_PAD = 14;
|
|
19
|
+
const FG = "var(--doc-fg, #000000)";
|
|
20
|
+
const FG2 = "var(--doc-fg-2, #2a3140)";
|
|
21
|
+
const FG3 = "var(--doc-fg-3, #5b6271)";
|
|
22
|
+
const FG4 = "var(--doc-fg-4, #9099a8)";
|
|
23
|
+
const BORDER = "var(--doc-border, #dde3eb)";
|
|
24
|
+
const PANEL = "var(--doc-panel, #ffffff)";
|
|
25
|
+
const ACCENT = "var(--doc-accent-2, #0277bd)";
|
|
26
|
+
const ACCENT2 = "var(--doc-accent, #1b4994)";
|
|
27
|
+
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
28
|
+
const readPositions = (uid) => {
|
|
29
|
+
try {
|
|
30
|
+
const raw = window.localStorage.getItem(`${STORE_PREFIX}${uid}`);
|
|
31
|
+
return raw == null ? {} : JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const writePositions = (uid, positions) => {
|
|
38
|
+
try {
|
|
39
|
+
if (Object.keys(positions).length === 0) {
|
|
40
|
+
window.localStorage.removeItem(`${STORE_PREFIX}${uid}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
window.localStorage.setItem(`${STORE_PREFIX}${uid}`, JSON.stringify(positions));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Keep interactions working even when storage can't be written (private mode etc.).
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/** Draws the same obstacle-avoiding path as the static SVG in React Flow too. */
|
|
50
|
+
const PathEdgeView = ({ id, data, style, markerEnd, interactionWidth, }) => (_jsx(BaseEdge, { id: id, path: data?.path ?? "", style: style, markerEnd: markerEnd, interactionWidth: interactionWidth }));
|
|
51
|
+
const rowType = (kind) => ({
|
|
52
|
+
fontSize: 10.5,
|
|
53
|
+
color: kind === "pk" ? ACCENT : kind === "scalar" ? FG3 : ACCENT2,
|
|
54
|
+
whiteSpace: "nowrap",
|
|
55
|
+
});
|
|
56
|
+
// Table box (same structure as the static SVG box: header / field rows / note rows). Each fk row
|
|
57
|
+
// gets a left/right pair of source handles; the edge picks one based on the target's position.
|
|
58
|
+
const TableNodeView = ({ data, selected }) => {
|
|
59
|
+
const { box, dim } = data;
|
|
60
|
+
return (_jsxs("div", { style: {
|
|
61
|
+
width: box.w,
|
|
62
|
+
minHeight: box.h,
|
|
63
|
+
fontFamily: MONO,
|
|
64
|
+
background: PANEL,
|
|
65
|
+
border: `1px solid ${selected ? ACCENT : BORDER}`,
|
|
66
|
+
borderRadius: 10,
|
|
67
|
+
boxShadow: selected ? `0 0 0 1px ${ACCENT}` : "none",
|
|
68
|
+
opacity: dim ? 0.45 : 1,
|
|
69
|
+
overflow: "hidden",
|
|
70
|
+
cursor: "grab",
|
|
71
|
+
}, children: [_jsx("div", { style: {
|
|
72
|
+
height: box.headerH,
|
|
73
|
+
lineHeight: `${box.headerH}px`,
|
|
74
|
+
padding: "0 11px",
|
|
75
|
+
fontSize: 12.5,
|
|
76
|
+
color: FG,
|
|
77
|
+
background: `color-mix(in srgb, ${ACCENT} 10%, transparent)`,
|
|
78
|
+
borderBottom: `1px solid ${BORDER}`,
|
|
79
|
+
}, children: box.name }), box.rows.map((row) => row.kind === "note" ? (_jsx("div", { style: {
|
|
80
|
+
padding: "0 11px",
|
|
81
|
+
fontSize: 10.5,
|
|
82
|
+
lineHeight: "19px",
|
|
83
|
+
color: FG4,
|
|
84
|
+
}, children: row.label }, `n:${row.label}`)) : (_jsxs("div", { style: {
|
|
85
|
+
display: "flex",
|
|
86
|
+
justifyContent: "space-between",
|
|
87
|
+
gap: 14,
|
|
88
|
+
padding: "0 11px",
|
|
89
|
+
lineHeight: "19px",
|
|
90
|
+
}, children: [_jsx("span", { style: { fontSize: 11.5, color: FG2, whiteSpace: "nowrap" }, children: row.label }), _jsx("span", { style: rowType(row.kind), children: row.type })] }, `f:${row.label}`))), box.rows
|
|
91
|
+
.filter((r) => r.fieldIndex != null && (r.kind === "fk" || r.kind === "fkList"))
|
|
92
|
+
.flatMap((r) => [
|
|
93
|
+
_jsx(Handle, { id: `f${r.fieldIndex}r`, type: "source", position: Position.Right, style: { top: r.dy, opacity: 0, width: 1, height: 1, minWidth: 1 }, isConnectable: false }, `sr${r.fieldIndex}`),
|
|
94
|
+
_jsx(Handle, { id: `f${r.fieldIndex}l`, type: "source", position: Position.Left, style: { top: r.dy, opacity: 0, width: 1, height: 1, minWidth: 1 }, isConnectable: false }, `sl${r.fieldIndex}`),
|
|
95
|
+
]), _jsx(Handle, { id: "tl", type: "target", position: Position.Left, style: { top: box.headerH / 2, opacity: 0, width: 1, height: 1, minWidth: 1 }, isConnectable: false }), _jsx(Handle, { id: "tr", type: "target", position: Position.Right, style: { top: box.headerH / 2, opacity: 0, width: 1, height: 1, minWidth: 1 }, isConnectable: false })] }));
|
|
96
|
+
};
|
|
97
|
+
// Group frame (a labeled `###`). A background node that isn't interactive.
|
|
98
|
+
const FrameNodeView = ({ data, width, height }) => (_jsxs("div", { style: { width, height, position: "relative", pointerEvents: "none" }, children: [_jsx("div", { style: {
|
|
99
|
+
position: "absolute",
|
|
100
|
+
inset: 0,
|
|
101
|
+
border: `1px dashed ${BORDER}`,
|
|
102
|
+
borderRadius: 14,
|
|
103
|
+
background: `color-mix(in srgb, ${FG} 3%, transparent)`,
|
|
104
|
+
} }), _jsx("div", { style: {
|
|
105
|
+
position: "absolute",
|
|
106
|
+
top: -18,
|
|
107
|
+
left: 4,
|
|
108
|
+
fontFamily: MONO,
|
|
109
|
+
fontSize: 11.5,
|
|
110
|
+
color: FG3,
|
|
111
|
+
}, children: data.label })] }));
|
|
112
|
+
const nodeTypes = { sgTable: TableNodeView, sgFrame: FrameNodeView };
|
|
113
|
+
const edgeTypes = { sgPath: PathEdgeView };
|
|
114
|
+
// Icons for the extra Controls buttons (drawn as SVG, since font-dependent symbols render as
|
|
115
|
+
// tofu in some environments).
|
|
116
|
+
const IconExpand = () => (_jsx("svg", { viewBox: "0 0 24 24", width: "13", height: "13", "aria-hidden": "true", children: _jsx("path", { fill: "currentColor", d: "M4 4h7v2H6v5H4V4zm9 0h7v7h-2V6h-5V4zM4 13h2v5h5v2H4v-7zm14 0h2v7h-7v-2h5v-5z" }) }));
|
|
117
|
+
const IconShrink = () => (_jsx("svg", { viewBox: "0 0 24 24", width: "13", height: "13", "aria-hidden": "true", children: _jsx("path", { fill: "currentColor", d: "M6 10h5V5H9v3H6v2zm7-5v5h5V8h-3V5h-2zM6 14v2h3v3h2v-5H6zm7 0v5h2v-3h3v-2h-5z" }) }));
|
|
118
|
+
const IconReset = () => (_jsx("svg", { viewBox: "0 0 24 24", width: "13", height: "13", "aria-hidden": "true", children: _jsx("path", { fill: "currentColor", d: "M12 5V2L7 6l5 4V7a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7z" }) }));
|
|
119
|
+
const IconSource = () => (_jsx("svg", { viewBox: "0 0 24 24", width: "13", height: "13", "aria-hidden": "true", children: _jsx("path", { fill: "currentColor", d: "M9.4 16.6 4.8 12l4.6-4.6L8 6l-6 6 6 6zm5.2 0 4.6-4.6-4.6-4.6L16 6l6 6-6 6z" }) }));
|
|
120
|
+
const srcBtnStyle = {
|
|
121
|
+
font: "inherit",
|
|
122
|
+
fontSize: 11,
|
|
123
|
+
fontFamily: MONO,
|
|
124
|
+
color: FG3,
|
|
125
|
+
background: "transparent",
|
|
126
|
+
border: `1px solid ${BORDER}`,
|
|
127
|
+
borderRadius: 6,
|
|
128
|
+
padding: "2px 8px",
|
|
129
|
+
cursor: "pointer",
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Viewer that shows the raw table DSL as-is (CodeBlock equivalent). Overlaid on the graph, it is
|
|
133
|
+
* only for reading / copying the source text (no editing = the graph never rewrites the DSL).
|
|
134
|
+
*/
|
|
135
|
+
const SourceView = ({ source, onClose }) => {
|
|
136
|
+
const [copied, setCopied] = useState(false);
|
|
137
|
+
const copy = useCallback(() => {
|
|
138
|
+
navigator.clipboard
|
|
139
|
+
?.writeText(source)
|
|
140
|
+
.then(() => {
|
|
141
|
+
setCopied(true);
|
|
142
|
+
window.setTimeout(() => setCopied(false), 1200);
|
|
143
|
+
})
|
|
144
|
+
.catch(() => {
|
|
145
|
+
/* Ignore clipboard rejection (permission / non-secure context). */
|
|
146
|
+
});
|
|
147
|
+
}, [source]);
|
|
148
|
+
return (_jsxs("div", { style: {
|
|
149
|
+
position: "absolute",
|
|
150
|
+
inset: 0,
|
|
151
|
+
zIndex: 6,
|
|
152
|
+
background: PANEL,
|
|
153
|
+
display: "flex",
|
|
154
|
+
flexDirection: "column",
|
|
155
|
+
}, children: [_jsxs("div", { style: {
|
|
156
|
+
display: "flex",
|
|
157
|
+
alignItems: "center",
|
|
158
|
+
gap: 6,
|
|
159
|
+
padding: "6px 10px",
|
|
160
|
+
borderBottom: `1px solid ${BORDER}`,
|
|
161
|
+
}, children: [_jsx("span", { style: { fontFamily: MONO, fontSize: 11, color: FG3 }, children: "table DSL" }), _jsx("span", { style: { flex: 1 } }), _jsx("button", { type: "button", onClick: copy, style: srcBtnStyle, title: "copy", children: copied ? "copied" : "copy" }), _jsx("button", { type: "button", onClick: onClose, style: srcBtnStyle, title: "Back to graph (Esc)", children: "graph" })] }), _jsx("pre", { style: {
|
|
162
|
+
margin: 0,
|
|
163
|
+
flex: 1,
|
|
164
|
+
overflow: "auto",
|
|
165
|
+
padding: "12px 14px",
|
|
166
|
+
fontFamily: MONO,
|
|
167
|
+
fontSize: 12,
|
|
168
|
+
lineHeight: 1.65,
|
|
169
|
+
color: FG2,
|
|
170
|
+
whiteSpace: "pre",
|
|
171
|
+
}, children: source })] }));
|
|
172
|
+
};
|
|
173
|
+
/** Refits each group frame to its members' bounding box (frames stay meaningful after drags). */
|
|
174
|
+
const withFrames = (nodes, data) => {
|
|
175
|
+
const tables = nodes.filter((n) => n.type === "sgTable");
|
|
176
|
+
const byName = new Map(tables.map((n) => [n.id, n]));
|
|
177
|
+
return nodes.map((node) => {
|
|
178
|
+
if (node.type !== "sgFrame") {
|
|
179
|
+
return node;
|
|
180
|
+
}
|
|
181
|
+
const group = data.groups[Number(node.id.slice("frame:".length))];
|
|
182
|
+
const members = (group?.members ?? [])
|
|
183
|
+
.map((name) => byName.get(name))
|
|
184
|
+
.filter((n) => n != null);
|
|
185
|
+
if (group == null || members.length === 0) {
|
|
186
|
+
return node;
|
|
187
|
+
}
|
|
188
|
+
const b = members.reduce((acc, n) => ({
|
|
189
|
+
x1: Math.min(acc.x1, n.position.x),
|
|
190
|
+
y1: Math.min(acc.y1, n.position.y),
|
|
191
|
+
x2: Math.max(acc.x2, n.position.x + n.data.box.w),
|
|
192
|
+
y2: Math.max(acc.y2, n.position.y + n.data.box.h),
|
|
193
|
+
}), {
|
|
194
|
+
x1: Number.POSITIVE_INFINITY,
|
|
195
|
+
y1: Number.POSITIVE_INFINITY,
|
|
196
|
+
x2: Number.NEGATIVE_INFINITY,
|
|
197
|
+
y2: Number.NEGATIVE_INFINITY,
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
...node,
|
|
201
|
+
position: { x: b.x1 - FRAME_PAD, y: b.y1 - FRAME_PAD },
|
|
202
|
+
width: b.x2 - b.x1 + FRAME_PAD * 2,
|
|
203
|
+
height: b.y2 - b.y1 + FRAME_PAD * 2,
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
/** Initial nodes (frames → tables, so frames sit behind). Uses saved positions when present. */
|
|
208
|
+
const initialNodes = (data, positions) => {
|
|
209
|
+
const frames = data.groups.map((g, i) => ({
|
|
210
|
+
id: `frame:${i}`,
|
|
211
|
+
type: "sgFrame",
|
|
212
|
+
position: { x: g.x, y: g.y },
|
|
213
|
+
width: g.w,
|
|
214
|
+
height: g.h,
|
|
215
|
+
data: { label: g.label },
|
|
216
|
+
draggable: false,
|
|
217
|
+
selectable: false,
|
|
218
|
+
focusable: false,
|
|
219
|
+
zIndex: -1,
|
|
220
|
+
}));
|
|
221
|
+
const tables = data.nodes.map((box) => {
|
|
222
|
+
const p = positions[box.name];
|
|
223
|
+
return {
|
|
224
|
+
id: box.name,
|
|
225
|
+
type: "sgTable",
|
|
226
|
+
position: p == null ? { x: box.x, y: box.y } : { x: p[0], y: p[1] },
|
|
227
|
+
data: { box, dim: false },
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
return withFrames([...frames, ...tables], data);
|
|
231
|
+
};
|
|
232
|
+
const FlowInner = ({ data, height }) => {
|
|
233
|
+
const { fitView } = useReactFlow();
|
|
234
|
+
// Fold changes in with applyNodeChanges to keep the dimensions React Flow measured
|
|
235
|
+
// (`measured`) (recreating nodes on every render leaves drag uninitialized = RF error #015).
|
|
236
|
+
const [nodes, setNodes] = useState(() => initialNodes(data, readPositions(data.uid)));
|
|
237
|
+
const [hover, setHover] = useState(null);
|
|
238
|
+
const [expanded, setExpanded] = useState(false);
|
|
239
|
+
const [showSource, setShowSource] = useState(false);
|
|
240
|
+
const onNodesChange = useCallback((changes) => {
|
|
241
|
+
setNodes((prev) => withFrames(applyNodeChanges(changes, prev), data));
|
|
242
|
+
}, [data]);
|
|
243
|
+
// Hover linkage (dimming unrelated boxes) swaps only `data` (keeps `measured` intact).
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
setNodes((prev) => {
|
|
246
|
+
const related = new Set();
|
|
247
|
+
if (hover != null) {
|
|
248
|
+
related.add(hover);
|
|
249
|
+
for (const e of data.edges) {
|
|
250
|
+
if (e.from === hover) {
|
|
251
|
+
related.add(e.to);
|
|
252
|
+
}
|
|
253
|
+
if (e.to === hover) {
|
|
254
|
+
related.add(e.from);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return prev.map((node) => {
|
|
259
|
+
if (node.type !== "sgTable") {
|
|
260
|
+
return node;
|
|
261
|
+
}
|
|
262
|
+
const dim = hover != null && !related.has(node.id);
|
|
263
|
+
return dim === node.data.dim ? node : { ...node, data: { ...node.data, dim } };
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}, [hover, data.edges]);
|
|
267
|
+
const edges = useMemo(() => {
|
|
268
|
+
const tableNodes = nodes.filter((n) => n.type === "sgTable");
|
|
269
|
+
const placed = tableNodes.map((n) => ({
|
|
270
|
+
name: n.id,
|
|
271
|
+
index: n.data.box.index,
|
|
272
|
+
x: n.position.x,
|
|
273
|
+
y: n.position.y,
|
|
274
|
+
w: n.data.box.w,
|
|
275
|
+
h: n.data.box.h,
|
|
276
|
+
headerH: n.data.box.headerH,
|
|
277
|
+
rows: n.data.box.rows.map((row) => ({
|
|
278
|
+
y: n.position.y + row.dy,
|
|
279
|
+
fieldIndex: row.fieldIndex,
|
|
280
|
+
})),
|
|
281
|
+
}));
|
|
282
|
+
const paths = routeSchemaEdges(placed, data.edges);
|
|
283
|
+
return schemaFlowEdges(placed, data.edges).map((e, i) => {
|
|
284
|
+
const active = hover != null && (e.from === hover || e.to === hover);
|
|
285
|
+
return {
|
|
286
|
+
id: e.id,
|
|
287
|
+
source: e.from,
|
|
288
|
+
target: e.to,
|
|
289
|
+
sourceHandle: `f${e.fieldIndex}${e.sourceSide}`,
|
|
290
|
+
targetHandle: e.targetSide === "l" ? "tl" : "tr",
|
|
291
|
+
type: "sgPath",
|
|
292
|
+
data: { path: paths[i]?.d ?? "" },
|
|
293
|
+
markerEnd: {
|
|
294
|
+
type: MarkerType.ArrowClosed,
|
|
295
|
+
width: 14,
|
|
296
|
+
height: 14,
|
|
297
|
+
color: active ? ACCENT : FG3,
|
|
298
|
+
},
|
|
299
|
+
style: {
|
|
300
|
+
stroke: active ? ACCENT : `color-mix(in srgb, ${FG} 30%, transparent)`,
|
|
301
|
+
strokeWidth: active ? 1.8 : 1.2,
|
|
302
|
+
strokeDasharray: e.list ? "5 4" : undefined,
|
|
303
|
+
opacity: hover == null || active ? 1 : 0.12,
|
|
304
|
+
},
|
|
305
|
+
zIndex: active ? 1 : 0,
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
}, [nodes, data.edges, hover]);
|
|
309
|
+
// Save the rearrangement when a drag ends (localStorage, per graph uid).
|
|
310
|
+
const onNodeDragStop = useCallback(() => {
|
|
311
|
+
setNodes((prev) => {
|
|
312
|
+
const positions = {};
|
|
313
|
+
for (const node of prev) {
|
|
314
|
+
if (node.type === "sgTable") {
|
|
315
|
+
positions[node.id] = [Math.round(node.position.x), Math.round(node.position.y)];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
writePositions(data.uid, positions);
|
|
319
|
+
return prev;
|
|
320
|
+
});
|
|
321
|
+
}, [data.uid]);
|
|
322
|
+
// Clamp the fit-view zoom (fitting a wide graph as-is squashes the text, so don't shrink too
|
|
323
|
+
// far: aim for a size that is readable while fitting, and let the user pan / zoom the rest).
|
|
324
|
+
const fitOptions = useMemo(() => expanded
|
|
325
|
+
? { padding: 0.04, minZoom: 0.3, maxZoom: 1.4, duration: 150 }
|
|
326
|
+
: { padding: 0.06, minZoom: 0.55, maxZoom: 1, duration: 150 }, [expanded]);
|
|
327
|
+
const reset = useCallback(() => {
|
|
328
|
+
writePositions(data.uid, {});
|
|
329
|
+
setNodes(initialNodes(data, {}));
|
|
330
|
+
window.setTimeout(() => fitView(fitOptions), 0);
|
|
331
|
+
}, [data, fitView, fitOptions]);
|
|
332
|
+
// Expanded view = full screen (Esc exits). If the source table is open, Esc closes it first.
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (!expanded && !showSource) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const onKey = (ev) => {
|
|
338
|
+
if (ev.key !== "Escape") {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (showSource) {
|
|
342
|
+
setShowSource(false);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
setExpanded(false);
|
|
346
|
+
};
|
|
347
|
+
document.addEventListener("keydown", onKey);
|
|
348
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
349
|
+
}, [expanded, showSource]);
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
// Re-fit the whole graph right after the viewport frame changes (expanded view waits a
|
|
352
|
+
// little for the layout to settle).
|
|
353
|
+
const t = window.setTimeout(() => fitView(fitOptions), expanded ? 60 : 20);
|
|
354
|
+
return () => window.clearTimeout(t);
|
|
355
|
+
}, [expanded, fitView, fitOptions]);
|
|
356
|
+
const wrapStyle = expanded
|
|
357
|
+
? {
|
|
358
|
+
position: "fixed",
|
|
359
|
+
inset: 0,
|
|
360
|
+
zIndex: 9998,
|
|
361
|
+
background: "var(--doc-bg, #ffffff)",
|
|
362
|
+
}
|
|
363
|
+
: {
|
|
364
|
+
position: "relative",
|
|
365
|
+
width: "100%",
|
|
366
|
+
height: height ?? "min(62vh, 520px)",
|
|
367
|
+
border: `1px solid ${BORDER}`,
|
|
368
|
+
borderRadius: 10,
|
|
369
|
+
overflow: "hidden",
|
|
370
|
+
};
|
|
371
|
+
return (_jsxs("div", { style: wrapStyle, children: [showSource && data.source != null ? (_jsx(SourceView, { source: data.source, onClose: () => setShowSource(false) })) : null, _jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, onNodesChange: onNodesChange, onNodeDragStop: onNodeDragStop, onNodeMouseEnter: (_, node) => setHover(node.type === "sgTable" ? node.id : null), onNodeMouseLeave: () => setHover(null), fitView: true, fitViewOptions: fitOptions, minZoom: 0.1, maxZoom: 4, nodesConnectable: false, edgesFocusable: false, proOptions: { hideAttribution: true }, style: { background: "transparent" }, children: [_jsx(Background, { variant: BackgroundVariant.Dots, gap: 22, size: 1, color: FG4 }), _jsxs(Controls, { showInteractive: false, children: [_jsx(ControlButton, { onClick: () => setExpanded((v) => !v), title: expanded ? "Close expanded view (Esc)" : "Expand view", children: expanded ? _jsx(IconShrink, {}) : _jsx(IconExpand, {}) }), _jsx(ControlButton, { onClick: reset, title: "Reset layout", children: _jsx(IconReset, {}) }), data.source == null ? null : (_jsx(ControlButton, { onClick: () => setShowSource(true), title: "Show raw tables (DSL)", children: _jsx(IconSource, {}) }))] }), _jsx(MiniMap, { pannable: true, zoomable: true, nodeStrokeWidth: 2, bgColor: PANEL, maskColor: `color-mix(in srgb, ${FG} 8%, transparent)`, nodeColor: `color-mix(in srgb, ${FG} 22%, transparent)`, nodeStrokeColor: BORDER, style: {
|
|
372
|
+
width: 150,
|
|
373
|
+
height: 96,
|
|
374
|
+
border: `1px solid ${BORDER}`,
|
|
375
|
+
borderRadius: 6,
|
|
376
|
+
} })] })] }));
|
|
377
|
+
};
|
|
378
|
+
/** Interactive UI for the schema graph (React Flow). Pass the static SVG's payload as-is. */
|
|
379
|
+
export const SchemaGraphFlow = (props) => (_jsx(ReactFlowProvider, { children: _jsx(FlowInner, { ...props }) }));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Undoes a mount (unmount + restore the static SVG). */
|
|
2
|
+
export type SchemaGraphDispose = () => void;
|
|
3
|
+
/** Replaces schema graphs under root with interactive graphs (already-mounted ones are skipped). */
|
|
4
|
+
export declare const mountSchemaGraphs: (root?: ParentNode) => SchemaGraphDispose;
|
|
5
|
+
/**
|
|
6
|
+
* Watches root and also makes graphs inserted later (HMR body replacement / edit's live render)
|
|
7
|
+
* interactive. The returned function stops watching.
|
|
8
|
+
*/
|
|
9
|
+
export declare const watchSchemaGraphs: (root?: (ParentNode & Node) | Document) => SchemaGraphDispose;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Mount layer that replaces the static SVG (output of `docHtml`) with an interactive React Flow
|
|
3
|
+
// graph. The diagram is readable without JS, so this is progressive enhancement (only graphs
|
|
4
|
+
// that mount successfully become interactive).
|
|
5
|
+
// Two kinds of consumer: script tag (viewer etc. = the global bundle calls watch) and
|
|
6
|
+
// React embed (the consumer's component calls mount). On either path, this layer injects the
|
|
7
|
+
// React Flow stylesheet (`flow-css.ts`) — consumers aren't required to ship the CSS.
|
|
8
|
+
import { createRoot } from "react-dom/client";
|
|
9
|
+
import { injectSchemaGraphCss } from "./flow-css.js";
|
|
10
|
+
import { SchemaGraphFlow } from "./schema-graph-flow.js";
|
|
11
|
+
const MOUNTED = "sgMounted";
|
|
12
|
+
const mountOne = (svg) => {
|
|
13
|
+
const payload = svg.dataset.sgLayout;
|
|
14
|
+
if (payload == null || svg.dataset[MOUNTED] === "1") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
let data;
|
|
18
|
+
try {
|
|
19
|
+
data = JSON.parse(payload);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
if (data.nodes.length === 0) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
svg.dataset[MOUNTED] = "1";
|
|
28
|
+
// Without its CSS, React Flow loses absolute positioning (boxes fly out of the frame and
|
|
29
|
+
// Controls / MiniMap stack at the top), so ensure the stylesheet before hiding the static SVG.
|
|
30
|
+
injectSchemaGraphCss(svg.ownerDocument);
|
|
31
|
+
const host = svg.ownerDocument.createElement("div");
|
|
32
|
+
host.className = "doc-sg-flow";
|
|
33
|
+
svg.parentElement?.insertBefore(host, svg);
|
|
34
|
+
// Keep the static SVG in the DOM as a fallback (print / JS disabled); only hide it.
|
|
35
|
+
svg.style.display = "none";
|
|
36
|
+
let root = createRoot(host);
|
|
37
|
+
root.render(_jsx(SchemaGraphFlow, { data: data }));
|
|
38
|
+
return () => {
|
|
39
|
+
root?.unmount();
|
|
40
|
+
root = undefined;
|
|
41
|
+
host.remove();
|
|
42
|
+
svg.style.removeProperty("display");
|
|
43
|
+
delete svg.dataset[MOUNTED];
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/** Replaces schema graphs under root with interactive graphs (already-mounted ones are skipped). */
|
|
47
|
+
export const mountSchemaGraphs = (root = document) => {
|
|
48
|
+
const disposers = [];
|
|
49
|
+
for (const svg of root.querySelectorAll("svg[data-sg-layout]")) {
|
|
50
|
+
const dispose = mountOne(svg);
|
|
51
|
+
if (dispose != null) {
|
|
52
|
+
disposers.push(dispose);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return () => {
|
|
56
|
+
for (const dispose of disposers) {
|
|
57
|
+
dispose();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Watches root and also makes graphs inserted later (HMR body replacement / edit's live render)
|
|
63
|
+
* interactive. The returned function stops watching.
|
|
64
|
+
*/
|
|
65
|
+
export const watchSchemaGraphs = (root = document) => {
|
|
66
|
+
const mounted = new Map();
|
|
67
|
+
const mount = (node) => {
|
|
68
|
+
const svgs = [...node.querySelectorAll("svg[data-sg-layout]")];
|
|
69
|
+
if (node instanceof SVGSVGElement && node.matches("svg[data-sg-layout]")) {
|
|
70
|
+
svgs.unshift(node);
|
|
71
|
+
}
|
|
72
|
+
for (const svg of svgs) {
|
|
73
|
+
const dispose = mountOne(svg);
|
|
74
|
+
if (dispose)
|
|
75
|
+
mounted.set(svg, dispose);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
mount(root);
|
|
79
|
+
const observer = new MutationObserver((records) => {
|
|
80
|
+
// Look only at added subtrees instead of scanning the whole document on every keystroke.
|
|
81
|
+
for (const [svg, dispose] of mounted) {
|
|
82
|
+
if (!root.contains(svg)) {
|
|
83
|
+
dispose();
|
|
84
|
+
mounted.delete(svg);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const record of records) {
|
|
88
|
+
for (const node of record.addedNodes) {
|
|
89
|
+
if (node instanceof Element && root.contains(node))
|
|
90
|
+
mount(node);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
observer.observe(root instanceof Document ? (root.body ?? root) : root, {
|
|
95
|
+
childList: true,
|
|
96
|
+
subtree: true,
|
|
97
|
+
});
|
|
98
|
+
return () => {
|
|
99
|
+
observer.disconnect();
|
|
100
|
+
for (const dispose of mounted.values()) {
|
|
101
|
+
dispose();
|
|
102
|
+
}
|
|
103
|
+
mounted.clear();
|
|
104
|
+
};
|
|
105
|
+
};
|