@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.
Files changed (112) hide show
  1. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
  2. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
  3. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
  4. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
  5. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
  6. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
  7. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
  8. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
  9. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
  10. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
  11. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
  12. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
  13. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
  14. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
  15. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
  16. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
  17. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
  18. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
  19. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
  20. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
  21. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
  22. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
  23. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
  24. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
  25. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
  26. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
  27. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
  28. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
  29. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
  30. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
  31. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
  32. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
  33. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
  34. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
  35. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
  36. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
  37. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
  38. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
  39. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
  40. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
  41. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
  42. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
  43. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
  44. package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
  45. package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
  46. package/.pnpm-cache/metadata/registry.npmjs.org/pnpm.json +1 -0
  47. package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
  48. package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
  49. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
  50. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
  51. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
  52. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
  53. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
  54. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
  55. package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
  56. package/.turbo/turbo-lint.log +4 -0
  57. package/dist/styles.css +2624 -0
  58. package/env.d.ts +11 -0
  59. package/operation-guide.md +275 -0
  60. package/package.json +43 -0
  61. package/postcss.config.cjs +7 -0
  62. package/readme.md +21 -0
  63. package/src/core/createAction.ts +5 -0
  64. package/src/core/createView.ts +28 -0
  65. package/src/core/instructions.tsx +57 -0
  66. package/src/core/nodePool.ts +127 -0
  67. package/src/core/signal.ts +130 -0
  68. package/src/core/template.tsx +25 -0
  69. package/src/index.ts +7 -0
  70. package/src/panel/ReactViewOnlinePreview.tsx +331 -0
  71. package/src/panel/ReactViewPanel.tsx +3221 -0
  72. package/src/panel/components/ElementsLayer.tsx +891 -0
  73. package/src/panel/components/MaterialSidebar.tsx +1142 -0
  74. package/src/panel/components/MoveableLayer.tsx +803 -0
  75. package/src/panel/components/PanelCanvas.tsx +660 -0
  76. package/src/panel/components/PanelConfigSidebar.tsx +3703 -0
  77. package/src/panel/components/PanelRulers.tsx +89 -0
  78. package/src/panel/components/SelectLayer.tsx +106 -0
  79. package/src/panel/components/ViewElementScopePanel.tsx +79 -0
  80. package/src/panel/components/WorkspaceConfigSidebar.tsx +158 -0
  81. package/src/panel/components/WorkspaceProjectNav.tsx +220 -0
  82. package/src/panel/components/WorkspaceStageSplit.tsx +179 -0
  83. package/src/panel/components/scope-config/ScopeConfigContext.tsx +170 -0
  84. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +250 -0
  85. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +59 -0
  86. package/src/panel/constants/messages.ts +34 -0
  87. package/src/panel/constants/zIndex.ts +6 -0
  88. package/src/panel/hooks/usePanelElements.ts +1108 -0
  89. package/src/panel/hooks/useWorkspaceProjects.ts +278 -0
  90. package/src/panel/index.tsx +3 -0
  91. package/src/panel/library/workspace-project-cache.ts +23 -0
  92. package/src/panel/library/workspace-project-db.ts +112 -0
  93. package/src/panel/library/workspace-project-sync.ts +41 -0
  94. package/src/panel/library/workspace-snapshot.ts +27 -0
  95. package/src/panel/scope/view-scope-store.ts +55 -0
  96. package/src/panel/types.ts +125 -0
  97. package/src/panel/utils/chartOptionBuilder.ts +316 -0
  98. package/src/panel/utils/gridPlacement.ts +189 -0
  99. package/src/panel/utils/mappingLayerOps.ts +142 -0
  100. package/src/panel/utils/panelElementDefaults.ts +161 -0
  101. package/src/panel/utils/panelElementNodes.ts +35 -0
  102. package/src/panel/utils/panelStateIO.ts +124 -0
  103. package/src/panel/utils/scope-autocomplete.ts +114 -0
  104. package/src/panel/utils/scope-field-labels.ts +45 -0
  105. package/src/panel/utils/scope-template-warnings.ts +135 -0
  106. package/src/panel/utils/scope-template.ts +75 -0
  107. package/src/panel/utils/updateElementDraft.ts +221 -0
  108. package/src/panel/viewportZoom.ts +26 -0
  109. package/src/tailwind.css +58 -0
  110. package/src/types/react-infinite-viewer.d.ts +39 -0
  111. package/tailwind.config.ts +7 -0
  112. package/tsconfig.json +14 -0
@@ -0,0 +1,179 @@
1
+ import { useEffect, useRef, useState, type ComponentRef, type ReactNode } from "react";
2
+
3
+ import {
4
+ BluePrintReactRoot,
5
+ BlueprintPanelToolbar,
6
+ type BluePrintReactRootProps,
7
+ type BlueprintDebugToolbarProps,
8
+ type BlueprintLibraryListItem,
9
+ } from "@arronqzy/react-blueprint";
10
+ import {
11
+ ResizableHandle,
12
+ ResizablePanel,
13
+ ResizablePanelGroup,
14
+ } from "@arronqzy/ui";
15
+
16
+ type WorkspaceStageSplitProps = {
17
+ blueprintOpen: boolean;
18
+ viewStage: ReactNode;
19
+ blueprintProps: BluePrintReactRootProps;
20
+ blueprintLibraryItems?: BlueprintLibraryListItem[];
21
+ activeBlueprintLibraryId?: string | null;
22
+ currentBlueprintLabel?: string;
23
+ onSelectBlueprintLibraryItem?: (id: string) => void;
24
+ onRenameBlueprintLibraryItem?: (id: string, name: string) => void;
25
+ onDeleteBlueprintLibraryItem?: (id: string) => void;
26
+ onSaveBlueprint?: () => void;
27
+ onSyncBlueprint?: () => void;
28
+ canSyncBlueprint?: boolean;
29
+ blueprintDebug?: BlueprintDebugToolbarProps;
30
+ };
31
+
32
+ const MIN_BLUEPRINT_HEIGHT = 48;
33
+
34
+ function BlueprintPanel({
35
+ blueprintOpen,
36
+ blueprintProps,
37
+ blueprintLibraryItems = [],
38
+ activeBlueprintLibraryId = null,
39
+ currentBlueprintLabel,
40
+ onSelectBlueprintLibraryItem,
41
+ onRenameBlueprintLibraryItem,
42
+ onDeleteBlueprintLibraryItem,
43
+ onSaveBlueprint,
44
+ onSyncBlueprint,
45
+ canSyncBlueprint = false,
46
+ blueprintDebug,
47
+ }: {
48
+ blueprintOpen: boolean;
49
+ blueprintProps: BluePrintReactRootProps;
50
+ blueprintLibraryItems?: BlueprintLibraryListItem[];
51
+ activeBlueprintLibraryId?: string | null;
52
+ currentBlueprintLabel?: string;
53
+ onSelectBlueprintLibraryItem?: (id: string) => void;
54
+ onRenameBlueprintLibraryItem?: (id: string, name: string) => void;
55
+ onDeleteBlueprintLibraryItem?: (id: string) => void;
56
+ onSaveBlueprint?: () => void;
57
+ onSyncBlueprint?: () => void;
58
+ canSyncBlueprint?: boolean;
59
+ blueprintDebug?: BlueprintDebugToolbarProps;
60
+ }) {
61
+ const wrapRef = useRef<HTMLDivElement>(null);
62
+ const [layoutReady, setLayoutReady] = useState(false);
63
+
64
+ useEffect(() => {
65
+ if (!blueprintOpen) {
66
+ setLayoutReady(false);
67
+ return;
68
+ }
69
+
70
+ const el = wrapRef.current;
71
+ if (!el) return;
72
+
73
+ const check = () => {
74
+ setLayoutReady(el.clientHeight >= MIN_BLUEPRINT_HEIGHT);
75
+ };
76
+
77
+ check();
78
+ const ro = new ResizeObserver(check);
79
+ ro.observe(el);
80
+ return () => ro.disconnect();
81
+ }, [blueprintOpen]);
82
+
83
+ return (
84
+ <div
85
+ data-workspace-region="blueprint"
86
+ className="flex h-full min-h-0 flex-col overflow-hidden bg-background"
87
+ >
88
+ <div className="flex shrink-0 items-center border-b border-border bg-background/90 px-2 py-1">
89
+ <BlueprintPanelToolbar
90
+ items={blueprintLibraryItems}
91
+ activeId={activeBlueprintLibraryId}
92
+ currentBlueprintLabel={currentBlueprintLabel}
93
+ onSelectItem={(id) => onSelectBlueprintLibraryItem?.(id)}
94
+ onRenameItem={(id, name) => onRenameBlueprintLibraryItem?.(id, name)}
95
+ onDeleteItem={(id) => onDeleteBlueprintLibraryItem?.(id)}
96
+ onSave={() => onSaveBlueprint?.()}
97
+ onSync={() => onSyncBlueprint?.()}
98
+ canSync={canSyncBlueprint}
99
+ debug={blueprintDebug}
100
+ />
101
+ </div>
102
+ <div ref={wrapRef} className="relative min-h-0 flex-1">
103
+ {layoutReady ? (
104
+ <BluePrintReactRoot style={{ width: "100%", height: "100%" }} {...blueprintProps} />
105
+ ) : (
106
+ <div className="flex h-full items-center justify-center text-[11px] text-muted-foreground">
107
+ {blueprintOpen ? "画布加载中…" : ""}
108
+ </div>
109
+ )}
110
+ </div>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ export function WorkspaceStageSplit({
116
+ blueprintOpen,
117
+ viewStage,
118
+ blueprintProps,
119
+ blueprintLibraryItems,
120
+ activeBlueprintLibraryId,
121
+ currentBlueprintLabel,
122
+ onSelectBlueprintLibraryItem,
123
+ onRenameBlueprintLibraryItem,
124
+ onDeleteBlueprintLibraryItem,
125
+ onSaveBlueprint,
126
+ onSyncBlueprint,
127
+ canSyncBlueprint,
128
+ blueprintDebug,
129
+ }: WorkspaceStageSplitProps) {
130
+ const blueprintPanelRef = useRef<ComponentRef<typeof ResizablePanel>>(null);
131
+
132
+ useEffect(() => {
133
+ const panel = blueprintPanelRef.current;
134
+ if (!panel) return;
135
+ if (blueprintOpen) {
136
+ panel.expand();
137
+ } else {
138
+ panel.collapse();
139
+ }
140
+ }, [blueprintOpen]);
141
+
142
+ return (
143
+ <ResizablePanelGroup
144
+ direction="vertical"
145
+ className="relative min-h-0 flex-1"
146
+ >
147
+ <ResizablePanel defaultSize={100} minSize={10} className="min-h-0">
148
+ <div className="h-full min-h-0 overflow-hidden">{viewStage}</div>
149
+ </ResizablePanel>
150
+ <ResizableHandle
151
+ withHandle
152
+ className={blueprintOpen ? undefined : "hidden"}
153
+ />
154
+ <ResizablePanel
155
+ ref={blueprintPanelRef}
156
+ collapsible
157
+ collapsedSize={0}
158
+ defaultSize={0}
159
+ minSize={15}
160
+ className="min-h-0"
161
+ >
162
+ <BlueprintPanel
163
+ blueprintOpen={blueprintOpen}
164
+ blueprintProps={blueprintProps}
165
+ blueprintLibraryItems={blueprintLibraryItems}
166
+ activeBlueprintLibraryId={activeBlueprintLibraryId}
167
+ currentBlueprintLabel={currentBlueprintLabel}
168
+ onSelectBlueprintLibraryItem={onSelectBlueprintLibraryItem}
169
+ onRenameBlueprintLibraryItem={onRenameBlueprintLibraryItem}
170
+ onDeleteBlueprintLibraryItem={onDeleteBlueprintLibraryItem}
171
+ onSaveBlueprint={onSaveBlueprint}
172
+ onSyncBlueprint={onSyncBlueprint}
173
+ canSyncBlueprint={canSyncBlueprint}
174
+ blueprintDebug={blueprintDebug}
175
+ />
176
+ </ResizablePanel>
177
+ </ResizablePanelGroup>
178
+ );
179
+ }
@@ -0,0 +1,170 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ type ReactNode,
8
+ type RefObject,
9
+ } from "react";
10
+
11
+ import type { ScopeTemplateWarning } from "../../utils/scope-template-warnings";
12
+ import { scopeFieldDomId } from "../../utils/scope-field-labels";
13
+ import { groupScopeWarningsByField } from "../../utils/scope-template-warnings";
14
+
15
+ import { ScopeTemplateAutocompleteHost } from "./ScopeTemplateAutocompleteHost";
16
+
17
+ type ScopeConfigContextValue = {
18
+ warnings: ScopeTemplateWarning[];
19
+ warningsByField: Map<string, ScopeTemplateWarning[]>;
20
+ scrollToField: (fieldId: string) => void;
21
+ };
22
+
23
+ const ScopeConfigContext = createContext<ScopeConfigContextValue | null>(null);
24
+
25
+ const INLINE_HINT_ATTR = "data-scope-inline-hint";
26
+
27
+ function findFieldAnchor(
28
+ container: HTMLElement,
29
+ fieldId: string,
30
+ fieldWarnings: ScopeTemplateWarning[]
31
+ ): HTMLElement | null {
32
+ const existing = document.getElementById(scopeFieldDomId(fieldId));
33
+ if (existing) return existing;
34
+
35
+ const inputs = container.querySelectorAll("input, textarea");
36
+ for (const input of inputs) {
37
+ if (
38
+ !(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)
39
+ ) {
40
+ continue;
41
+ }
42
+ const matched = fieldWarnings.some(
43
+ (warning) =>
44
+ input.value === warning.template ||
45
+ input.value.includes(warning.expression)
46
+ );
47
+ if (!matched) continue;
48
+ const host = input.closest("label") ?? input.parentElement;
49
+ if (host instanceof HTMLElement) {
50
+ host.id = scopeFieldDomId(fieldId);
51
+ return host;
52
+ }
53
+ }
54
+
55
+ return null;
56
+ }
57
+
58
+ function attachInlineHints(
59
+ container: HTMLElement,
60
+ warningsByField: Map<string, ScopeTemplateWarning[]>
61
+ ) {
62
+ container
63
+ .querySelectorAll(`[${INLINE_HINT_ATTR}]`)
64
+ .forEach((node) => node.remove());
65
+
66
+ for (const [fieldId, fieldWarnings] of warningsByField) {
67
+ const anchor = findFieldAnchor(container, fieldId, fieldWarnings);
68
+ if (!anchor || anchor.querySelector(`[${INLINE_HINT_ATTR}]`)) continue;
69
+
70
+ const hint = document.createElement("div");
71
+ hint.setAttribute(INLINE_HINT_ATTR, "1");
72
+ hint.className = "space-y-0.5";
73
+ for (const warning of fieldWarnings) {
74
+ const line = document.createElement("p");
75
+ line.className =
76
+ "text-[10px] leading-relaxed text-amber-700 dark:text-amber-300";
77
+ line.textContent = warning.message;
78
+ hint.appendChild(line);
79
+ }
80
+ anchor.appendChild(hint);
81
+ }
82
+ }
83
+
84
+ export function ScopeConfigProvider({
85
+ scope,
86
+ warnings,
87
+ scrollContainerRef,
88
+ children,
89
+ }: {
90
+ scope?: unknown;
91
+ warnings: ScopeTemplateWarning[];
92
+ scrollContainerRef?: RefObject<HTMLElement | null>;
93
+ children: ReactNode;
94
+ }) {
95
+ const warningsByField = useMemo(
96
+ () => groupScopeWarningsByField(warnings),
97
+ [warnings]
98
+ );
99
+
100
+ const scrollToField = useCallback(
101
+ (fieldId: string) => {
102
+ const container = scrollContainerRef?.current;
103
+ const fieldWarnings = warningsByField.get(fieldId);
104
+ if (container && fieldWarnings?.length) {
105
+ findFieldAnchor(container, fieldId, fieldWarnings);
106
+ }
107
+
108
+ const target = document.getElementById(scopeFieldDomId(fieldId));
109
+ if (!target) return;
110
+
111
+ if (container && container.contains(target)) {
112
+ const containerRect = container.getBoundingClientRect();
113
+ const targetRect = target.getBoundingClientRect();
114
+ const offset =
115
+ targetRect.top -
116
+ containerRect.top +
117
+ container.scrollTop -
118
+ container.clientHeight / 2 +
119
+ targetRect.height / 2;
120
+ container.scrollTo({ top: Math.max(0, offset), behavior: "smooth" });
121
+ } else {
122
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
123
+ }
124
+
125
+ target.classList.add("scope-field--highlight");
126
+ window.setTimeout(() => {
127
+ target.classList.remove("scope-field--highlight");
128
+ }, 1600);
129
+ },
130
+ [scrollContainerRef, warningsByField]
131
+ );
132
+
133
+ useLayoutEffect(() => {
134
+ const container = scrollContainerRef?.current;
135
+ if (!container || warnings.length === 0) {
136
+ return () => undefined;
137
+ }
138
+
139
+ attachInlineHints(container, warningsByField);
140
+ return () => {
141
+ container
142
+ .querySelectorAll(`[${INLINE_HINT_ATTR}]`)
143
+ .forEach((node) => node.remove());
144
+ };
145
+ }, [scrollContainerRef, warnings, warningsByField]);
146
+
147
+ const value = useMemo(
148
+ () => ({ warnings, warningsByField, scrollToField }),
149
+ [warnings, warningsByField, scrollToField]
150
+ );
151
+
152
+ return (
153
+ <ScopeConfigContext.Provider value={value}>
154
+ {children}
155
+ <ScopeTemplateAutocompleteHost
156
+ scope={scope}
157
+ containerRef={scrollContainerRef}
158
+ />
159
+ </ScopeConfigContext.Provider>
160
+ );
161
+ }
162
+
163
+ export function useScopeConfig() {
164
+ return useContext(ScopeConfigContext);
165
+ }
166
+
167
+ export function useScopeFieldWarnings(fieldId: string) {
168
+ const ctx = useScopeConfig();
169
+ return ctx?.warningsByField.get(fieldId) ?? [];
170
+ }
@@ -0,0 +1,250 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ type RefObject,
7
+ } from "react";
8
+ import { createPortal } from "react-dom";
9
+ import { cn } from "@arronqzy/ui";
10
+
11
+ import {
12
+ applyScopeAutocompleteSelection,
13
+ buildScopeExpression,
14
+ getScopeAutocompleteSuggestions,
15
+ parseScopeAutocomplete,
16
+ type ScopeAutocompleteState,
17
+ } from "../../utils/scope-autocomplete";
18
+
19
+ function isAutocompleteTarget(
20
+ target: EventTarget | null
21
+ ): target is HTMLInputElement | HTMLTextAreaElement {
22
+ if (target instanceof HTMLTextAreaElement) {
23
+ return target.dataset.scopeAutocomplete !== "off";
24
+ }
25
+ if (target instanceof HTMLInputElement) {
26
+ if (target.dataset.scopeAutocomplete === "off") return false;
27
+ const type = target.type;
28
+ return (
29
+ !type ||
30
+ type === "text" ||
31
+ type === "search" ||
32
+ type === "url" ||
33
+ type === "password"
34
+ );
35
+ }
36
+ return false;
37
+ }
38
+
39
+ function setNativeInputValue(
40
+ element: HTMLInputElement | HTMLTextAreaElement,
41
+ value: string
42
+ ) {
43
+ const proto =
44
+ element instanceof HTMLTextAreaElement
45
+ ? HTMLTextAreaElement.prototype
46
+ : HTMLInputElement.prototype;
47
+ const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
48
+ descriptor?.set?.call(element, value);
49
+ element.dispatchEvent(new Event("input", { bubbles: true }));
50
+ }
51
+
52
+ type DropdownState = {
53
+ input: HTMLInputElement | HTMLTextAreaElement;
54
+ state: ScopeAutocompleteState;
55
+ suggestions: string[];
56
+ activeIndex: number;
57
+ rect: DOMRect;
58
+ };
59
+
60
+ export function ScopeTemplateAutocompleteHost({
61
+ scope,
62
+ containerRef,
63
+ }: {
64
+ scope: unknown;
65
+ containerRef?: RefObject<HTMLElement | null>;
66
+ }) {
67
+ const [dropdown, setDropdown] = useState<DropdownState | null>(null);
68
+ const dropdownRef = useRef<DropdownState | null>(null);
69
+ dropdownRef.current = dropdown;
70
+
71
+ const closeDropdown = useCallback(() => {
72
+ setDropdown(null);
73
+ }, []);
74
+
75
+ const refreshForInput = useCallback(
76
+ (input: HTMLInputElement | HTMLTextAreaElement) => {
77
+ if (scope === undefined) {
78
+ closeDropdown();
79
+ return;
80
+ }
81
+ const cursor = input.selectionStart ?? input.value.length;
82
+ const parsed = parseScopeAutocomplete(input.value, cursor, scope);
83
+ if (!parsed) {
84
+ closeDropdown();
85
+ return;
86
+ }
87
+ const suggestions = getScopeAutocompleteSuggestions(scope, parsed);
88
+ if (suggestions.length === 0) {
89
+ closeDropdown();
90
+ return;
91
+ }
92
+ setDropdown({
93
+ input,
94
+ state: parsed,
95
+ suggestions,
96
+ activeIndex: 0,
97
+ rect: input.getBoundingClientRect(),
98
+ });
99
+ },
100
+ [closeDropdown, scope]
101
+ );
102
+
103
+ const applySuggestion = useCallback(
104
+ (index: number) => {
105
+ const current = dropdownRef.current;
106
+ if (!current) return;
107
+ const selected = current.suggestions[index];
108
+ if (!selected) return;
109
+
110
+ const { value, cursor } = applyScopeAutocompleteSelection(
111
+ current.input.value,
112
+ current.state,
113
+ selected
114
+ );
115
+ setNativeInputValue(current.input, value);
116
+ current.input.focus();
117
+ current.input.setSelectionRange(cursor, cursor);
118
+ closeDropdown();
119
+ window.requestAnimationFrame(() => {
120
+ refreshForInput(current.input);
121
+ });
122
+ },
123
+ [closeDropdown, refreshForInput]
124
+ );
125
+
126
+ useEffect(() => {
127
+ const container = containerRef?.current;
128
+ if (!container || scope === undefined) return;
129
+
130
+ const onFocusIn = (event: FocusEvent) => {
131
+ if (!isAutocompleteTarget(event.target)) return;
132
+ refreshForInput(event.target);
133
+ };
134
+
135
+ const onInput = (event: Event) => {
136
+ if (!isAutocompleteTarget(event.target)) return;
137
+ refreshForInput(event.target);
138
+ };
139
+
140
+ const onKeyDown = (event: KeyboardEvent) => {
141
+ const current = dropdownRef.current;
142
+ if (!current || event.target !== current.input) return;
143
+
144
+ if (event.key === "ArrowDown") {
145
+ event.preventDefault();
146
+ setDropdown((prev) =>
147
+ prev
148
+ ? {
149
+ ...prev,
150
+ activeIndex: Math.min(
151
+ prev.activeIndex + 1,
152
+ prev.suggestions.length - 1
153
+ ),
154
+ }
155
+ : prev
156
+ );
157
+ return;
158
+ }
159
+ if (event.key === "ArrowUp") {
160
+ event.preventDefault();
161
+ setDropdown((prev) =>
162
+ prev
163
+ ? { ...prev, activeIndex: Math.max(prev.activeIndex - 1, 0) }
164
+ : prev
165
+ );
166
+ return;
167
+ }
168
+ if (event.key === "Enter" || event.key === "Tab") {
169
+ if (current.suggestions.length > 0) {
170
+ event.preventDefault();
171
+ applySuggestion(current.activeIndex);
172
+ }
173
+ return;
174
+ }
175
+ if (event.key === "Escape") {
176
+ event.preventDefault();
177
+ closeDropdown();
178
+ }
179
+ };
180
+
181
+ const onPointerDown = (event: MouseEvent) => {
182
+ const target = event.target as Node | null;
183
+ if (
184
+ dropdownRef.current?.input.contains(target ?? null) ||
185
+ (target instanceof Element && target.closest("[data-scope-ac-list]"))
186
+ ) {
187
+ return;
188
+ }
189
+ if (!container.contains(target)) {
190
+ closeDropdown();
191
+ }
192
+ };
193
+
194
+ container.addEventListener("focusin", onFocusIn);
195
+ container.addEventListener("input", onInput, true);
196
+ container.addEventListener("keydown", onKeyDown, true);
197
+ window.addEventListener("pointerdown", onPointerDown, true);
198
+ window.addEventListener("scroll", closeDropdown, true);
199
+ window.addEventListener("resize", closeDropdown);
200
+
201
+ return () => {
202
+ container.removeEventListener("focusin", onFocusIn);
203
+ container.removeEventListener("input", onInput, true);
204
+ container.removeEventListener("keydown", onKeyDown, true);
205
+ window.removeEventListener("pointerdown", onPointerDown, true);
206
+ window.removeEventListener("scroll", closeDropdown, true);
207
+ window.removeEventListener("resize", closeDropdown);
208
+ };
209
+ }, [
210
+ applySuggestion,
211
+ closeDropdown,
212
+ containerRef,
213
+ refreshForInput,
214
+ scope,
215
+ ]);
216
+
217
+ if (!dropdown || scope === undefined) return null;
218
+
219
+ return createPortal(
220
+ <div
221
+ data-scope-ac-list
222
+ className="fixed z-[10120] max-h-44 min-w-[140px] overflow-auto rounded-md border border-border bg-popover py-1 text-popover-foreground shadow-md"
223
+ style={{
224
+ left: dropdown.rect.left,
225
+ top: dropdown.rect.bottom + 4,
226
+ width: Math.max(dropdown.rect.width, 160),
227
+ }}
228
+ >
229
+ {dropdown.suggestions.map((key, index) => {
230
+ const preview = buildScopeExpression(dropdown.state.path, key);
231
+ return (
232
+ <button
233
+ key={key}
234
+ type="button"
235
+ className={cn(
236
+ "flex w-full flex-col items-start gap-0.5 px-2 py-1.5 text-left text-[11px] hover:bg-accent",
237
+ index === dropdown.activeIndex && "bg-accent"
238
+ )}
239
+ onMouseDown={(event) => event.preventDefault()}
240
+ onClick={() => applySuggestion(index)}
241
+ >
242
+ <span className="font-medium text-foreground">{key}</span>
243
+ <span className="font-mono text-[10px] text-muted-foreground">{`{${preview}}`}</span>
244
+ </button>
245
+ );
246
+ })}
247
+ </div>,
248
+ document.body
249
+ );
250
+ }
@@ -0,0 +1,59 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ import { useScopeConfig } from "./ScopeConfigContext";
4
+
5
+ const WARNINGS_COLLAPSE_STORAGE_KEY = "panel:config-scope-warnings-collapsed";
6
+
7
+ export function ScopeTemplateWarningsPanel() {
8
+ const ctx = useScopeConfig();
9
+ const [isCollapsed, setIsCollapsed] = useState(false);
10
+
11
+ useEffect(() => {
12
+ const stored = window.localStorage.getItem(WARNINGS_COLLAPSE_STORAGE_KEY);
13
+ if (stored === "1") setIsCollapsed(true);
14
+ }, []);
15
+
16
+ useEffect(() => {
17
+ window.localStorage.setItem(
18
+ WARNINGS_COLLAPSE_STORAGE_KEY,
19
+ isCollapsed ? "1" : "0"
20
+ );
21
+ }, [isCollapsed]);
22
+
23
+ if (!ctx || ctx.warnings.length === 0) return null;
24
+
25
+ return (
26
+ <div className="mt-2 border-t border-border/50 pt-2">
27
+ <div className="flex items-center justify-between gap-2">
28
+ <div className="text-[11px] font-medium text-amber-700 dark:text-amber-300">
29
+ 异常调用警告
30
+ <span className="ml-1.5 rounded bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-semibold">
31
+ {ctx.warnings.length}
32
+ </span>
33
+ </div>
34
+ <button
35
+ type="button"
36
+ className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
37
+ onClick={() => setIsCollapsed((prev) => !prev)}
38
+ >
39
+ {isCollapsed ? "展开警告" : "收起警告"}
40
+ </button>
41
+ </div>
42
+ {!isCollapsed ? (
43
+ <ul className="mt-2 max-h-[200px] space-y-1 overflow-auto text-[10px] leading-relaxed">
44
+ {ctx.warnings.map((warning) => (
45
+ <li key={`${warning.fieldId}-${warning.missingPath}-${warning.expression}`}>
46
+ <button
47
+ type="button"
48
+ className="w-full rounded border border-amber-500/30 bg-amber-500/10 px-2 py-1.5 text-left text-amber-800 hover:bg-amber-500/15 dark:text-amber-200"
49
+ onClick={() => ctx.scrollToField(warning.fieldId)}
50
+ >
51
+ {warning.message}
52
+ </button>
53
+ </li>
54
+ ))}
55
+ </ul>
56
+ ) : null}
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,34 @@
1
+ export const PANEL_MESSAGES = {
2
+ nodeNotFound: "未找到节点,可能已被删除",
3
+ sourceNodeNotFound: "未找到源节点,可能已被删除",
4
+ targetLayerNotFound: "目标图层不存在",
5
+ nodeMoveSameLayer: "已在当前图层",
6
+ nodeDeleteLocked: "锁定节点不可删除,请在右侧【节点信息】中取消锁定后重试",
7
+ nodeDeleteLayerLocked: "当前节点所在图层已锁定,无法删除。请先在图层栏取消锁定",
8
+ nodeBatchDeleteContainsLocked:
9
+ "选中节点包含锁定节点,无法批量删除。请先在右侧【节点信息】中取消锁定",
10
+ nodeMoveLocked: "锁定节点不允许更换图层,请先在右侧【节点信息】中取消锁定",
11
+ nodeMoveSourceLayerLocked: "节点所在图层已锁定,无法更换图层。请先在图层栏取消锁定",
12
+ nodeMoveTargetLayerLocked: "目标图层已锁定,无法移入。请先在图层栏取消锁定",
13
+ activeLayerLockedCannotAdd: "当前图层已锁定,无法新增节点",
14
+ nodeLayerLockedCannotCopy: "当前节点所在图层已锁定,无法复制",
15
+ selectionContainsLockedLayerNode: "选中节点包含锁定图层内容,无法批量删除",
16
+ sourceLayerLockedCannotPaste: "复制源节点所在图层已锁定,无法粘贴",
17
+ nodeConfigLocked: "当前节点已锁定,配置项不可编辑。",
18
+ nodeConfigLayerLocked: "当前节点所在图层已锁定,配置项不可编辑。",
19
+ readImageFailed: "读取图片失败",
20
+ readAudioFailed: "读取音频失败",
21
+ readVideoFailed: "读取视频失败",
22
+ readRecordAudioFailed: "录音读取失败",
23
+ audioLocalSaved: "已写入本地音频(base64)",
24
+ audioRemoteUploaded: "已上传服务器并更新远程链接",
25
+ audioServerUploadFailed: "服务器上传失败,仅保留本地音频",
26
+ audioPosterSet: "已设置音频占位图",
27
+ audioRecordUnsupported: "当前环境不支持录音",
28
+ audioRecordSaved: "录音已保存,可直接播放",
29
+ audioRecording: "录音中...",
30
+ audioRecordStartFailed: "无法开始录音,请检查麦克风权限",
31
+ videoLocalSaved: "已写入本地视频(base64)",
32
+ videoRemoteUploaded: "已上传服务器并更新远程链接",
33
+ videoServerUploadFailed: "服务器上传失败,仅保留本地视频",
34
+ } as const;
@@ -0,0 +1,6 @@
1
+ export const PANEL_Z_INDEX = {
2
+ /** 仅高于画布内容层,低于弹窗蒙层 */
3
+ ruler: 100,
4
+ toolbar: 1200,
5
+ historyPopover: 1210,
6
+ } as const;