@arronqzy/react-view 0.0.1
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/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/pnpm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
- package/.turbo/turbo-lint.log +4 -0
- package/dist/styles.css +2624 -0
- package/env.d.ts +11 -0
- package/operation-guide.md +275 -0
- package/package.json +43 -0
- package/postcss.config.cjs +7 -0
- package/readme.md +21 -0
- package/src/core/createAction.ts +5 -0
- package/src/core/createView.ts +28 -0
- package/src/core/instructions.tsx +57 -0
- package/src/core/nodePool.ts +127 -0
- package/src/core/signal.ts +130 -0
- package/src/core/template.tsx +25 -0
- package/src/index.ts +7 -0
- package/src/panel/ReactViewOnlinePreview.tsx +331 -0
- package/src/panel/ReactViewPanel.tsx +3221 -0
- package/src/panel/components/ElementsLayer.tsx +891 -0
- package/src/panel/components/MaterialSidebar.tsx +1142 -0
- package/src/panel/components/MoveableLayer.tsx +803 -0
- package/src/panel/components/PanelCanvas.tsx +660 -0
- package/src/panel/components/PanelConfigSidebar.tsx +3703 -0
- package/src/panel/components/PanelRulers.tsx +89 -0
- package/src/panel/components/SelectLayer.tsx +106 -0
- package/src/panel/components/ViewElementScopePanel.tsx +79 -0
- package/src/panel/components/WorkspaceConfigSidebar.tsx +158 -0
- package/src/panel/components/WorkspaceProjectNav.tsx +220 -0
- package/src/panel/components/WorkspaceStageSplit.tsx +179 -0
- package/src/panel/components/scope-config/ScopeConfigContext.tsx +170 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +250 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +59 -0
- package/src/panel/constants/messages.ts +34 -0
- package/src/panel/constants/zIndex.ts +6 -0
- package/src/panel/hooks/usePanelElements.ts +1108 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +278 -0
- package/src/panel/index.tsx +3 -0
- package/src/panel/library/workspace-project-cache.ts +23 -0
- package/src/panel/library/workspace-project-db.ts +112 -0
- package/src/panel/library/workspace-project-sync.ts +41 -0
- package/src/panel/library/workspace-snapshot.ts +27 -0
- package/src/panel/scope/view-scope-store.ts +55 -0
- package/src/panel/types.ts +125 -0
- package/src/panel/utils/chartOptionBuilder.ts +316 -0
- package/src/panel/utils/gridPlacement.ts +189 -0
- package/src/panel/utils/mappingLayerOps.ts +142 -0
- package/src/panel/utils/panelElementDefaults.ts +161 -0
- package/src/panel/utils/panelElementNodes.ts +35 -0
- package/src/panel/utils/panelStateIO.ts +124 -0
- package/src/panel/utils/scope-autocomplete.ts +114 -0
- package/src/panel/utils/scope-field-labels.ts +45 -0
- package/src/panel/utils/scope-template-warnings.ts +135 -0
- package/src/panel/utils/scope-template.ts +75 -0
- package/src/panel/utils/updateElementDraft.ts +221 -0
- package/src/panel/viewportZoom.ts +26 -0
- package/src/tailwind.css +58 -0
- package/src/types/react-infinite-viewer.d.ts +39 -0
- package/tailwind.config.ts +7 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import Ruler from "@scena/react-ruler";
|
|
3
|
+
import { PANEL_Z_INDEX } from "../constants/zIndex";
|
|
4
|
+
|
|
5
|
+
const RULER_WORLD_UNIT = 100;
|
|
6
|
+
const RULER_SEGMENT = 10;
|
|
7
|
+
|
|
8
|
+
export type PanelRulersProps = {
|
|
9
|
+
zoomX: number;
|
|
10
|
+
zoomY: number;
|
|
11
|
+
scrollLeft: number;
|
|
12
|
+
scrollTop: number;
|
|
13
|
+
size?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function PanelRulers({
|
|
17
|
+
zoomX,
|
|
18
|
+
zoomY,
|
|
19
|
+
scrollLeft,
|
|
20
|
+
scrollTop,
|
|
21
|
+
size = 32,
|
|
22
|
+
}: PanelRulersProps) {
|
|
23
|
+
const [isDark, setIsDark] = useState(false);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const root = document.documentElement;
|
|
26
|
+
const update = () => setIsDark(root.classList.contains("dark"));
|
|
27
|
+
update();
|
|
28
|
+
const observer = new MutationObserver(update);
|
|
29
|
+
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
30
|
+
return () => observer.disconnect();
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const rulerScrollLeft = scrollLeft + size;
|
|
34
|
+
const rulerScrollTop = scrollTop + size;
|
|
35
|
+
|
|
36
|
+
const horizontalProps = useMemo(
|
|
37
|
+
() => ({
|
|
38
|
+
zoom: zoomX,
|
|
39
|
+
unit: RULER_WORLD_UNIT,
|
|
40
|
+
segment: RULER_SEGMENT,
|
|
41
|
+
backgroundColor: isDark ? "rgba(15,23,42,0.98)" : "rgba(255,255,255,0.98)",
|
|
42
|
+
lineColor: isDark ? "rgba(226,232,240,0.4)" : "rgba(15,23,42,0.22)",
|
|
43
|
+
textColor: isDark ? "rgba(248,250,252,0.95)" : "rgba(15,23,42,0.75)",
|
|
44
|
+
font: "10px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
|
|
45
|
+
longLineSize: 8,
|
|
46
|
+
shortLineSize: 4,
|
|
47
|
+
}),
|
|
48
|
+
[isDark, zoomX]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const verticalProps = useMemo(
|
|
52
|
+
() => ({
|
|
53
|
+
zoom: zoomY,
|
|
54
|
+
unit: RULER_WORLD_UNIT,
|
|
55
|
+
segment: RULER_SEGMENT,
|
|
56
|
+
backgroundColor: isDark ? "rgba(15,23,42,0.98)" : "rgba(255,255,255,0.98)",
|
|
57
|
+
lineColor: isDark ? "rgba(226,232,240,0.4)" : "rgba(15,23,42,0.22)",
|
|
58
|
+
textColor: isDark ? "rgba(248,250,252,0.95)" : "rgba(15,23,42,0.75)",
|
|
59
|
+
font: "10px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
|
|
60
|
+
longLineSize: 8,
|
|
61
|
+
shortLineSize: 4,
|
|
62
|
+
}),
|
|
63
|
+
[isDark, zoomY]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
className="pointer-events-none absolute inset-0 isolate"
|
|
69
|
+
style={{ ["--rv-ruler-size" as string]: `${size}px`, zIndex: PANEL_Z_INDEX.ruler }}
|
|
70
|
+
>
|
|
71
|
+
<div className="absolute left-[var(--rv-ruler-size)] right-[var(--rv-ruler-size)] top-0 h-[var(--rv-ruler-size)] overflow-visible">
|
|
72
|
+
<Ruler type="horizontal" {...horizontalProps} scrollPos={rulerScrollLeft} />
|
|
73
|
+
</div>
|
|
74
|
+
<div className="absolute left-0 top-[var(--rv-ruler-size)] bottom-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] overflow-visible">
|
|
75
|
+
<Ruler type="vertical" {...verticalProps} scrollPos={rulerScrollTop} />
|
|
76
|
+
</div>
|
|
77
|
+
<div className="absolute left-[var(--rv-ruler-size)] right-[var(--rv-ruler-size)] bottom-0 h-[var(--rv-ruler-size)] overflow-visible">
|
|
78
|
+
<Ruler type="horizontal" {...horizontalProps} scrollPos={rulerScrollLeft} />
|
|
79
|
+
</div>
|
|
80
|
+
<div className="absolute right-0 top-[var(--rv-ruler-size)] bottom-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] overflow-visible">
|
|
81
|
+
<Ruler type="vertical" {...verticalProps} scrollPos={rulerScrollTop} />
|
|
82
|
+
</div>
|
|
83
|
+
<div className="absolute left-0 top-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-b border-r border-border bg-background" />
|
|
84
|
+
<div className="absolute right-0 top-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-b border-l border-border bg-background" />
|
|
85
|
+
<div className="absolute left-0 bottom-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-r border-t border-border bg-background" />
|
|
86
|
+
<div className="absolute right-0 bottom-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-l border-t border-border bg-background" />
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useCallback, useMemo } from "react";
|
|
2
|
+
import Selecto from "react-selecto";
|
|
3
|
+
|
|
4
|
+
export type SelectLayerProps = {
|
|
5
|
+
container: HTMLElement | null;
|
|
6
|
+
dragContainer?: HTMLElement | null;
|
|
7
|
+
rootContainer?: HTMLElement | null;
|
|
8
|
+
selectableTargets?: string[];
|
|
9
|
+
selectedIds: string[];
|
|
10
|
+
onSelectedIdsChange: (ids: string[]) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function SelectLayer({
|
|
14
|
+
container,
|
|
15
|
+
dragContainer,
|
|
16
|
+
rootContainer,
|
|
17
|
+
selectableTargets = [".rv-selectable"],
|
|
18
|
+
selectedIds,
|
|
19
|
+
onSelectedIdsChange,
|
|
20
|
+
}: SelectLayerProps) {
|
|
21
|
+
const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
22
|
+
const dragRoot = dragContainer ?? rootContainer ?? null;
|
|
23
|
+
const overlayContainer = typeof document !== "undefined" ? document.body : null;
|
|
24
|
+
|
|
25
|
+
const handleSelect = useCallback(
|
|
26
|
+
(e: any) => {
|
|
27
|
+
const input = e?.inputEvent as MouseEvent | undefined;
|
|
28
|
+
if (input && input.button !== 0) return;
|
|
29
|
+
const selected = (e?.selected ?? []) as Array<HTMLElement | SVGElement>;
|
|
30
|
+
const idSet = new Set<string>();
|
|
31
|
+
for (const el of selected) {
|
|
32
|
+
const root = (el as Element).closest?.(".rv-selectable") as
|
|
33
|
+
| HTMLElement
|
|
34
|
+
| null;
|
|
35
|
+
const id = root?.dataset.elementId;
|
|
36
|
+
if (id) idSet.add(id);
|
|
37
|
+
}
|
|
38
|
+
onSelectedIdsChange(Array.from(idSet));
|
|
39
|
+
},
|
|
40
|
+
[onSelectedIdsChange]
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!container || !dragRoot || !overlayContainer) return null;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Selecto
|
|
47
|
+
// container 可以是 transform 过的画布;rootContainer 必须是未 transform 的真实坐标系容器
|
|
48
|
+
container={container}
|
|
49
|
+
rootContainer={dragRoot}
|
|
50
|
+
// 选择框渲染到 body,避免受滚动/缩放影响产生偏移
|
|
51
|
+
portalContainer={overlayContainer}
|
|
52
|
+
// 鼠标事件绑定到滚动容器,确保 clientX/Y 与坐标系一致
|
|
53
|
+
dragContainer={dragRoot}
|
|
54
|
+
selectableTargets={selectableTargets}
|
|
55
|
+
// 点击选中交给 ElementsLayer(支持 shift 多选),Selecto 只负责框选
|
|
56
|
+
selectByClick={false}
|
|
57
|
+
selectFromInside={false}
|
|
58
|
+
// 不要阻止在节点内部的拖动(让 Moveable 接管拖拽移动)
|
|
59
|
+
preventDragFromInside={false}
|
|
60
|
+
preventDefault={false}
|
|
61
|
+
continueSelect={false}
|
|
62
|
+
toggleContinueSelect="shift"
|
|
63
|
+
ratio={0}
|
|
64
|
+
hitRate={0}
|
|
65
|
+
dragCondition={(e: any) => {
|
|
66
|
+
const input = e?.inputEvent as MouseEvent | undefined;
|
|
67
|
+
// 只允许左键触发框选,右键不参与选区变更
|
|
68
|
+
if (input && input.button !== 0) return false;
|
|
69
|
+
const target = (input?.target as HTMLElement | null) ?? null;
|
|
70
|
+
const isShift = !!input?.shiftKey;
|
|
71
|
+
if (isShift) return true;
|
|
72
|
+
|
|
73
|
+
// 已有选中时:在选中区域/Moveable 控制框上按下,应该交给 Moveable 拖动,不启动框选
|
|
74
|
+
if (selectedIdSet.size > 0) {
|
|
75
|
+
if (
|
|
76
|
+
target?.closest(".moveable-control-box") ||
|
|
77
|
+
target?.closest(".moveable-line") ||
|
|
78
|
+
target?.closest(".moveable-control") ||
|
|
79
|
+
target?.closest(".moveable-direction")
|
|
80
|
+
) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const selectable = target?.closest(".rv-selectable") as HTMLElement | null;
|
|
84
|
+
const id = selectable?.dataset.elementId;
|
|
85
|
+
if (id && selectedIdSet.has(id)) return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}}
|
|
89
|
+
onDragStart={(e: { inputEvent: { target: unknown }; stop: () => void }) => {
|
|
90
|
+
const input = e.inputEvent as MouseEvent | undefined;
|
|
91
|
+
if (input && input.button !== 0) {
|
|
92
|
+
e.stop();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// 已有多选时,默认应该是拖动移动;只有按住 shift 才继续框选/追加
|
|
96
|
+
const target = e.inputEvent.target as HTMLElement | null;
|
|
97
|
+
const selectable = target?.closest(".rv-selectable") as HTMLElement | null;
|
|
98
|
+
const id = selectable?.dataset.elementId;
|
|
99
|
+
const isShift = (e.inputEvent as MouseEvent | undefined)?.shiftKey ?? false;
|
|
100
|
+
if (id && selectedIdSet.has(id) && !isShift) e.stop();
|
|
101
|
+
}}
|
|
102
|
+
onSelect={handleSelect}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { formatViewElementScope } from "../utils/scope-template";
|
|
4
|
+
|
|
5
|
+
const SCOPE_COLLAPSE_STORAGE_KEY = "panel:config-scope-collapsed";
|
|
6
|
+
|
|
7
|
+
export type ViewElementScopePanelProps = {
|
|
8
|
+
scope: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
|
|
12
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
13
|
+
const [scopeUpdated, setScopeUpdated] = useState(false);
|
|
14
|
+
const prevScopeSerializedRef = useRef<string | null>(null);
|
|
15
|
+
const themedScrollbarClass =
|
|
16
|
+
"scrollbar-thin [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-muted/40 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/80 [&::-webkit-scrollbar-thumb]:hover:bg-border";
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const stored = window.localStorage.getItem(SCOPE_COLLAPSE_STORAGE_KEY);
|
|
20
|
+
if (stored === "1") setIsCollapsed(true);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
window.localStorage.setItem(
|
|
25
|
+
SCOPE_COLLAPSE_STORAGE_KEY,
|
|
26
|
+
isCollapsed ? "1" : "0"
|
|
27
|
+
);
|
|
28
|
+
}, [isCollapsed]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const serialized = formatViewElementScope(scope);
|
|
32
|
+
if (
|
|
33
|
+
prevScopeSerializedRef.current !== null &&
|
|
34
|
+
prevScopeSerializedRef.current !== serialized
|
|
35
|
+
) {
|
|
36
|
+
setScopeUpdated(true);
|
|
37
|
+
}
|
|
38
|
+
prevScopeSerializedRef.current = serialized;
|
|
39
|
+
}, [scope]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="mt-2 border-t border-border/50 pt-2">
|
|
43
|
+
<div className="flex items-center justify-between gap-2">
|
|
44
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
45
|
+
<div className="text-[11px] font-medium text-foreground">Scope 数据</div>
|
|
46
|
+
{scopeUpdated ? (
|
|
47
|
+
<span className="shrink-0 rounded bg-sky-500/15 px-1.5 py-0.5 text-[10px] font-medium text-sky-700 dark:text-sky-300">
|
|
48
|
+
已更新
|
|
49
|
+
</span>
|
|
50
|
+
) : null}
|
|
51
|
+
</div>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
|
|
55
|
+
onClick={() => setIsCollapsed((prev) => !prev)}
|
|
56
|
+
>
|
|
57
|
+
{isCollapsed ? "展开 Scope" : "收起 Scope"}
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
{!isCollapsed ? (
|
|
61
|
+
<>
|
|
62
|
+
<pre
|
|
63
|
+
className={`mt-2 max-h-[200px] overflow-auto rounded-md border border-border/70 bg-muted/30 p-2 font-mono text-[10px] leading-relaxed text-foreground ${themedScrollbarClass}`}
|
|
64
|
+
>
|
|
65
|
+
{formatViewElementScope(scope)}
|
|
66
|
+
</pre>
|
|
67
|
+
<p className="mt-1.5 text-[10px] leading-relaxed text-muted-foreground">
|
|
68
|
+
表单中可使用模版字符串引用 scope,例如{" "}
|
|
69
|
+
<code className="rounded bg-muted px-1">{`{scope?.a||0}`}</code>
|
|
70
|
+
、
|
|
71
|
+
<code className="rounded bg-muted px-1">{`{scope?.name||''}`}</code>
|
|
72
|
+
。支持 <code className="rounded bg-muted px-1">?.</code> 与{" "}
|
|
73
|
+
<code className="rounded bg-muted px-1">||</code> 占位。
|
|
74
|
+
</p>
|
|
75
|
+
</>
|
|
76
|
+
) : null}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BlueprintExecutionLogPanel,
|
|
5
|
+
BlueprintNodeConfigSidebar,
|
|
6
|
+
type BlueprintGraphNode,
|
|
7
|
+
type ExecutionLogSettings,
|
|
8
|
+
type ExecutionTraceEntry,
|
|
9
|
+
} from "@arronqzy/react-blueprint";
|
|
10
|
+
import {
|
|
11
|
+
Empty,
|
|
12
|
+
EmptyDescription,
|
|
13
|
+
EmptyIcon,
|
|
14
|
+
EmptyTitle,
|
|
15
|
+
} from "@arronqzy/ui";
|
|
16
|
+
|
|
17
|
+
import type { PanelConfigSidebarProps } from "./PanelConfigSidebar";
|
|
18
|
+
import { PanelConfigSidebar } from "./PanelConfigSidebar";
|
|
19
|
+
import type { PanelElement } from "../types";
|
|
20
|
+
|
|
21
|
+
export type WorkspaceConfigFocus = "view" | "blueprint" | "blueprint-log";
|
|
22
|
+
|
|
23
|
+
export type BlueprintExecutionLogViewProps = {
|
|
24
|
+
entries: ExecutionTraceEntry[];
|
|
25
|
+
settings: ExecutionLogSettings;
|
|
26
|
+
onUpdateSettings: (patch: Partial<ExecutionLogSettings>) => void;
|
|
27
|
+
onSave: () => void;
|
|
28
|
+
onExport: () => void;
|
|
29
|
+
onClear: () => void;
|
|
30
|
+
onClearAllSaved?: () => void | Promise<void>;
|
|
31
|
+
onApplyRetention: () => void;
|
|
32
|
+
hasSavedRuns?: boolean;
|
|
33
|
+
lifecyclePhase?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type WorkspaceConfigSidebarProps = Omit<
|
|
37
|
+
PanelConfigSidebarProps,
|
|
38
|
+
"selectedElement" | "selectedElements"
|
|
39
|
+
> & {
|
|
40
|
+
configFocus: WorkspaceConfigFocus;
|
|
41
|
+
executionLog?: BlueprintExecutionLogViewProps;
|
|
42
|
+
selectedBlueprintNode: BlueprintGraphNode | null;
|
|
43
|
+
allowFalseSignalPropagation?: boolean;
|
|
44
|
+
onUpdateAllowFalseSignalPropagation?: (value: boolean) => void;
|
|
45
|
+
onUpdateBlueprintNode: (
|
|
46
|
+
nodeId: string,
|
|
47
|
+
patch: Partial<
|
|
48
|
+
Pick<
|
|
49
|
+
BlueprintGraphNode,
|
|
50
|
+
| "label"
|
|
51
|
+
| "role"
|
|
52
|
+
| "nodeType"
|
|
53
|
+
| "configSource"
|
|
54
|
+
| "viewElementId"
|
|
55
|
+
| "viewElementIds"
|
|
56
|
+
| "nestedBlueprintId"
|
|
57
|
+
| "libraryBlueprintId"
|
|
58
|
+
| "lifecyclePhase"
|
|
59
|
+
| "fetchConfig"
|
|
60
|
+
| "jsonConfig"
|
|
61
|
+
| "logicConfig"
|
|
62
|
+
| "clockConfig"
|
|
63
|
+
>
|
|
64
|
+
>
|
|
65
|
+
) => void;
|
|
66
|
+
blueprintLibraryOptions?: { id: string; label: string }[];
|
|
67
|
+
selectedElement: PanelElement | null;
|
|
68
|
+
selectedElements?: PanelElement[];
|
|
69
|
+
allViewElements: PanelElement[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function WorkspaceConfigSidebar({
|
|
73
|
+
configFocus,
|
|
74
|
+
executionLog,
|
|
75
|
+
selectedBlueprintNode,
|
|
76
|
+
allowFalseSignalPropagation = false,
|
|
77
|
+
onUpdateAllowFalseSignalPropagation,
|
|
78
|
+
onUpdateBlueprintNode,
|
|
79
|
+
blueprintLibraryOptions = [],
|
|
80
|
+
selectedElement,
|
|
81
|
+
selectedElements,
|
|
82
|
+
allViewElements,
|
|
83
|
+
...viewPanelProps
|
|
84
|
+
}: WorkspaceConfigSidebarProps) {
|
|
85
|
+
const viewElementOptions = useMemo(
|
|
86
|
+
() =>
|
|
87
|
+
allViewElements.map((el) => ({
|
|
88
|
+
id: el.id,
|
|
89
|
+
label: el.name?.trim() || el.chart?.title || el.materialType || el.id,
|
|
90
|
+
})),
|
|
91
|
+
[allViewElements]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (configFocus === "view") {
|
|
95
|
+
return (
|
|
96
|
+
<PanelConfigSidebar
|
|
97
|
+
{...viewPanelProps}
|
|
98
|
+
selectedElement={selectedElement}
|
|
99
|
+
selectedElements={selectedElements}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (configFocus === "blueprint-log" && executionLog) {
|
|
105
|
+
return (
|
|
106
|
+
<BlueprintExecutionLogPanel
|
|
107
|
+
entries={executionLog.entries}
|
|
108
|
+
settings={executionLog.settings}
|
|
109
|
+
onUpdateSettings={executionLog.onUpdateSettings}
|
|
110
|
+
onSave={() => void executionLog.onSave()}
|
|
111
|
+
onExport={executionLog.onExport}
|
|
112
|
+
onClear={executionLog.onClear}
|
|
113
|
+
onClearAllSaved={executionLog.onClearAllSaved}
|
|
114
|
+
hasSavedRuns={executionLog.hasSavedRuns}
|
|
115
|
+
onApplyRetention={() => void executionLog.onApplyRetention()}
|
|
116
|
+
lifecyclePhase={executionLog.lifecyclePhase}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!selectedBlueprintNode) {
|
|
122
|
+
return (
|
|
123
|
+
<div className="flex h-full flex-col overflow-hidden bg-background text-foreground">
|
|
124
|
+
<Empty className="py-10">
|
|
125
|
+
<EmptyIcon>
|
|
126
|
+
<svg
|
|
127
|
+
viewBox="0 0 24 24"
|
|
128
|
+
className="h-4 w-4"
|
|
129
|
+
fill="none"
|
|
130
|
+
stroke="currentColor"
|
|
131
|
+
strokeWidth="1.8"
|
|
132
|
+
aria-hidden="true"
|
|
133
|
+
>
|
|
134
|
+
<circle cx="6" cy="6" r="2" />
|
|
135
|
+
<circle cx="18" cy="18" r="2" />
|
|
136
|
+
<path d="M8 7l8 4M8 17l8-4" />
|
|
137
|
+
</svg>
|
|
138
|
+
</EmptyIcon>
|
|
139
|
+
<EmptyTitle>未选中蓝图节点</EmptyTitle>
|
|
140
|
+
<EmptyDescription>
|
|
141
|
+
在蓝图面板中选中一个节点后,这里会显示对应的配置。
|
|
142
|
+
</EmptyDescription>
|
|
143
|
+
</Empty>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<BlueprintNodeConfigSidebar
|
|
150
|
+
node={selectedBlueprintNode}
|
|
151
|
+
allowFalseSignalPropagation={allowFalseSignalPropagation}
|
|
152
|
+
onUpdateAllowFalseSignalPropagation={onUpdateAllowFalseSignalPropagation}
|
|
153
|
+
viewElementOptions={viewElementOptions}
|
|
154
|
+
blueprintLibraryOptions={blueprintLibraryOptions}
|
|
155
|
+
onUpdateNode={onUpdateBlueprintNode}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
AlertDialog,
|
|
4
|
+
AlertDialogAction,
|
|
5
|
+
AlertDialogCancel,
|
|
6
|
+
AlertDialogContent,
|
|
7
|
+
AlertDialogDescription,
|
|
8
|
+
AlertDialogFooter,
|
|
9
|
+
AlertDialogHeader,
|
|
10
|
+
AlertDialogTitle,
|
|
11
|
+
Button,
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
DropdownMenuContent,
|
|
14
|
+
DropdownMenuItem,
|
|
15
|
+
DropdownMenuSeparator,
|
|
16
|
+
DropdownMenuTrigger,
|
|
17
|
+
Tooltip,
|
|
18
|
+
TooltipContent,
|
|
19
|
+
TooltipProvider,
|
|
20
|
+
TooltipTrigger,
|
|
21
|
+
} from "@arronqzy/ui";
|
|
22
|
+
import type { WorkspaceProjectListItem } from "../library/workspace-project-db";
|
|
23
|
+
|
|
24
|
+
function formatUpdatedAt(ts: number): string {
|
|
25
|
+
try {
|
|
26
|
+
return new Date(ts).toLocaleString();
|
|
27
|
+
} catch {
|
|
28
|
+
return String(ts);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type WorkspaceProjectNavProps = {
|
|
33
|
+
projects: WorkspaceProjectListItem[];
|
|
34
|
+
activeProjectId: string | null;
|
|
35
|
+
activeProjectName: string | null;
|
|
36
|
+
dirty: boolean;
|
|
37
|
+
onCreateProject: () => Promise<{ name: string; id: string } | void>;
|
|
38
|
+
onOpenProject: (id: string) => Promise<void>;
|
|
39
|
+
onSyncProject: () => Promise<string | void>;
|
|
40
|
+
onDeleteProject: (id: string) => Promise<void>;
|
|
41
|
+
onPreviewProject: (id: string, options?: { syncFirst?: boolean }) => Promise<void>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function WorkspaceProjectNav({
|
|
45
|
+
projects,
|
|
46
|
+
activeProjectId,
|
|
47
|
+
activeProjectName,
|
|
48
|
+
dirty,
|
|
49
|
+
onCreateProject,
|
|
50
|
+
onOpenProject,
|
|
51
|
+
onSyncProject,
|
|
52
|
+
onDeleteProject,
|
|
53
|
+
onPreviewProject,
|
|
54
|
+
}: WorkspaceProjectNavProps) {
|
|
55
|
+
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
|
56
|
+
const [busy, setBusy] = useState(false);
|
|
57
|
+
|
|
58
|
+
const pendingDelete = projects.find((p) => p.id === pendingDeleteId) ?? null;
|
|
59
|
+
|
|
60
|
+
const runAction = async (action: () => Promise<unknown>) => {
|
|
61
|
+
if (busy) return;
|
|
62
|
+
setBusy(true);
|
|
63
|
+
try {
|
|
64
|
+
await action();
|
|
65
|
+
} finally {
|
|
66
|
+
setBusy(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<div className="flex items-center gap-1.5 border-l border-border pl-2">
|
|
73
|
+
<TooltipProvider delayDuration={150}>
|
|
74
|
+
<Tooltip>
|
|
75
|
+
<TooltipTrigger asChild>
|
|
76
|
+
<Button
|
|
77
|
+
type="button"
|
|
78
|
+
variant="outline"
|
|
79
|
+
size="sm"
|
|
80
|
+
className="h-7 px-2 text-xs"
|
|
81
|
+
disabled={busy}
|
|
82
|
+
onClick={() => void runAction(onCreateProject)}
|
|
83
|
+
>
|
|
84
|
+
创建工作区
|
|
85
|
+
</Button>
|
|
86
|
+
</TooltipTrigger>
|
|
87
|
+
<TooltipContent className="z-[10100]">
|
|
88
|
+
以当前产物名称新建一条 IndexedDB 工作区记录,不会覆盖已有工作区
|
|
89
|
+
</TooltipContent>
|
|
90
|
+
</Tooltip>
|
|
91
|
+
|
|
92
|
+
{activeProjectId ? (
|
|
93
|
+
<Tooltip>
|
|
94
|
+
<TooltipTrigger asChild>
|
|
95
|
+
<Button
|
|
96
|
+
type="button"
|
|
97
|
+
variant={dirty ? "default" : "outline"}
|
|
98
|
+
size="sm"
|
|
99
|
+
className="h-7 px-2 text-xs"
|
|
100
|
+
disabled={busy || !dirty}
|
|
101
|
+
onClick={() => void runAction(onSyncProject)}
|
|
102
|
+
>
|
|
103
|
+
同步{dirty ? " *" : ""}
|
|
104
|
+
</Button>
|
|
105
|
+
</TooltipTrigger>
|
|
106
|
+
<TooltipContent className="z-[10100]">
|
|
107
|
+
{dirty
|
|
108
|
+
? `同步更新「${activeProjectName ?? "当前工作区"}」到 IndexedDB`
|
|
109
|
+
: "当前工作区已与 IndexedDB 同步"}
|
|
110
|
+
</TooltipContent>
|
|
111
|
+
</Tooltip>
|
|
112
|
+
) : null}
|
|
113
|
+
|
|
114
|
+
<DropdownMenu>
|
|
115
|
+
<DropdownMenuTrigger asChild>
|
|
116
|
+
<Button
|
|
117
|
+
type="button"
|
|
118
|
+
variant="outline"
|
|
119
|
+
size="sm"
|
|
120
|
+
className="h-7 max-w-[200px] truncate px-2 text-xs"
|
|
121
|
+
disabled={busy}
|
|
122
|
+
>
|
|
123
|
+
{activeProjectName ? `工作区:${activeProjectName}` : "已保存工作区"}
|
|
124
|
+
</Button>
|
|
125
|
+
</DropdownMenuTrigger>
|
|
126
|
+
<DropdownMenuContent align="start" className="z-[10100] w-72">
|
|
127
|
+
{projects.length === 0 ? (
|
|
128
|
+
<DropdownMenuItem disabled>暂无已保存工作区</DropdownMenuItem>
|
|
129
|
+
) : (
|
|
130
|
+
projects.map((project) => (
|
|
131
|
+
<div key={project.id} className="px-1 py-0.5">
|
|
132
|
+
<div className="flex items-center gap-1">
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
className={[
|
|
136
|
+
"min-w-0 flex-1 rounded px-2 py-1.5 text-left text-xs hover:bg-accent",
|
|
137
|
+
activeProjectId === project.id ? "bg-accent/60 font-medium" : "",
|
|
138
|
+
].join(" ")}
|
|
139
|
+
onClick={() =>
|
|
140
|
+
void runAction(async () => {
|
|
141
|
+
await onOpenProject(project.id);
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
>
|
|
145
|
+
<div className="truncate">{project.name}</div>
|
|
146
|
+
<div className="truncate text-[10px] text-muted-foreground">
|
|
147
|
+
{formatUpdatedAt(project.updatedAt)}
|
|
148
|
+
</div>
|
|
149
|
+
</button>
|
|
150
|
+
<Button
|
|
151
|
+
type="button"
|
|
152
|
+
variant="ghost"
|
|
153
|
+
size="sm"
|
|
154
|
+
className="h-7 shrink-0 px-2 text-[10px]"
|
|
155
|
+
onClick={() =>
|
|
156
|
+
void runAction(async () => {
|
|
157
|
+
await onPreviewProject(project.id, {
|
|
158
|
+
syncFirst: activeProjectId === project.id,
|
|
159
|
+
});
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
>
|
|
163
|
+
预览
|
|
164
|
+
</Button>
|
|
165
|
+
<Button
|
|
166
|
+
type="button"
|
|
167
|
+
variant="ghost"
|
|
168
|
+
size="sm"
|
|
169
|
+
className="h-7 shrink-0 px-2 text-[10px] text-destructive hover:text-destructive"
|
|
170
|
+
onClick={() => setPendingDeleteId(project.id)}
|
|
171
|
+
>
|
|
172
|
+
删除
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
))
|
|
177
|
+
)}
|
|
178
|
+
{/* <DropdownMenuSeparator />
|
|
179
|
+
<DropdownMenuItem onClick={() => void runAction(onCreateProject)}>
|
|
180
|
+
创建工作区
|
|
181
|
+
</DropdownMenuItem> */}
|
|
182
|
+
</DropdownMenuContent>
|
|
183
|
+
</DropdownMenu>
|
|
184
|
+
</TooltipProvider>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<AlertDialog
|
|
188
|
+
open={!!pendingDelete}
|
|
189
|
+
onOpenChange={(open) => {
|
|
190
|
+
if (!open) setPendingDeleteId(null);
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
<AlertDialogContent className="z-[10150]">
|
|
194
|
+
<AlertDialogHeader>
|
|
195
|
+
<AlertDialogTitle>删除工作区?</AlertDialogTitle>
|
|
196
|
+
<AlertDialogDescription>
|
|
197
|
+
将永久删除 IndexedDB 中的「{pendingDelete?.name ?? ""}」,此操作不可恢复。
|
|
198
|
+
</AlertDialogDescription>
|
|
199
|
+
</AlertDialogHeader>
|
|
200
|
+
<AlertDialogFooter>
|
|
201
|
+
<AlertDialogCancel>取消</AlertDialogCancel>
|
|
202
|
+
<AlertDialogAction
|
|
203
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
204
|
+
onClick={() => {
|
|
205
|
+
const id = pendingDeleteId;
|
|
206
|
+
setPendingDeleteId(null);
|
|
207
|
+
if (!id) return;
|
|
208
|
+
void runAction(async () => {
|
|
209
|
+
await onDeleteProject(id);
|
|
210
|
+
});
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
删除
|
|
214
|
+
</AlertDialogAction>
|
|
215
|
+
</AlertDialogFooter>
|
|
216
|
+
</AlertDialogContent>
|
|
217
|
+
</AlertDialog>
|
|
218
|
+
</>
|
|
219
|
+
);
|
|
220
|
+
}
|