@arronqzy/react-view 1.0.38 → 1.0.40
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/dist/styles.css +316 -241
- package/package.json +5 -5
- package/readme.md +15 -12
- package/src/index.ts +23 -1
- package/src/panel/ReactViewPanel.tsx +140 -90
- package/src/panel/components/ElementsLayer.tsx +1 -1
- package/src/panel/components/MaterialSidebar.tsx +304 -264
- package/src/panel/components/MoveableLayer.tsx +11 -2
- package/src/panel/components/PanelCanvas.tsx +1 -0
- package/src/panel/components/PanelConfigSidebar.tsx +49 -65
- package/src/panel/components/PanelLayerDock.tsx +397 -316
- package/src/panel/components/PanelRulers.tsx +169 -42
- package/src/panel/components/SelectLayer.tsx +2 -2
- package/src/panel/components/WorkspaceProjectNav.tsx +1 -1
- package/src/panel/hooks/useWorkspaceProjects.ts +228 -52
- package/src/panel/icons.tsx +117 -0
- package/src/panel/library/abuilder-storage.ts +101 -3
- package/src/panel/library/abuilder-view-handle.ts +100 -0
- package/src/panel/library/event-subscription.ts +70 -5
- package/src/panel/library/workspace-project-sync.ts +7 -1
- package/src/panel/library/workspace-storage.ts +58 -4
- package/src/tailwind.css +66 -5
|
@@ -1,26 +1,63 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useState } from "react";
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import Ruler from "@scena/react-ruler";
|
|
3
3
|
import { PANEL_Z_INDEX } from "../constants/zIndex";
|
|
4
4
|
|
|
5
|
-
const RULER_WORLD_UNIT = 100;
|
|
6
5
|
const RULER_SEGMENT = 10;
|
|
6
|
+
/** 主刻度标签在屏幕上的目标间距(px),过密/过疏时动态调 unit */
|
|
7
|
+
const RULER_TARGET_MAJOR_PX = 78;
|
|
8
|
+
/** @scena/react-ruler 内部把刻度文字原点写死为 17px,厚度必须大于该值才看得见数字 */
|
|
9
|
+
const SCENA_TEXT_ORIGIN = 17;
|
|
7
10
|
|
|
8
11
|
export type PanelRulersProps = {
|
|
9
12
|
zoomX: number;
|
|
10
13
|
zoomY: number;
|
|
11
14
|
scrollLeft: number;
|
|
12
15
|
scrollTop: number;
|
|
16
|
+
/** 固定厚度;省略时按编辑区尺寸动态计算 */
|
|
13
17
|
size?: number;
|
|
14
18
|
};
|
|
15
19
|
|
|
20
|
+
function clamp(n: number, min: number, max: number) {
|
|
21
|
+
return Math.min(max, Math.max(min, n));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 1/2/5 × 10^n 好看步长 */
|
|
25
|
+
function niceNumber(raw: number): number {
|
|
26
|
+
if (!Number.isFinite(raw) || raw <= 0) return 1;
|
|
27
|
+
const exp = Math.floor(Math.log10(raw));
|
|
28
|
+
const frac = raw / 10 ** exp;
|
|
29
|
+
const niceFrac = frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 5 ? 5 : 10;
|
|
30
|
+
return niceFrac * 10 ** exp;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 按缩放动态主刻度单位:屏幕上主刻度间距 roughly 恒定,
|
|
35
|
+
* 缩小 → unit 变大(标签变少),放大 → unit 变小(标签变密)。
|
|
36
|
+
*/
|
|
37
|
+
export function resolveRulerUnit(zoom: number): number {
|
|
38
|
+
const z = Math.max(zoom, 0.0001);
|
|
39
|
+
return Math.max(1, niceNumber(RULER_TARGET_MAJOR_PX / z));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 按画布可视区域动态计算刻度尺厚度(保证刻度数字可读) */
|
|
43
|
+
export function resolveRulerSize(width: number, height: number): number {
|
|
44
|
+
const edge = Math.min(width || 0, height || 0);
|
|
45
|
+
if (edge <= 0) return 22;
|
|
46
|
+
// 下限 24:文字带与刻度带分离,避免数字压住刻度线
|
|
47
|
+
return Math.round(clamp(edge * 0.032, 24, 30));
|
|
48
|
+
}
|
|
49
|
+
|
|
16
50
|
export function PanelRulers({
|
|
17
51
|
zoomX,
|
|
18
52
|
zoomY,
|
|
19
53
|
scrollLeft,
|
|
20
54
|
scrollTop,
|
|
21
|
-
size
|
|
55
|
+
size: sizeProp,
|
|
22
56
|
}: PanelRulersProps) {
|
|
57
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
58
|
+
const [autoSize, setAutoSize] = useState(22);
|
|
23
59
|
const [isDark, setIsDark] = useState(false);
|
|
60
|
+
|
|
24
61
|
useEffect(() => {
|
|
25
62
|
const root = document.documentElement;
|
|
26
63
|
const update = () => setIsDark(root.classList.contains("dark"));
|
|
@@ -30,60 +67,150 @@ export function PanelRulers({
|
|
|
30
67
|
return () => observer.disconnect();
|
|
31
68
|
}, []);
|
|
32
69
|
|
|
33
|
-
|
|
34
|
-
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (typeof sizeProp === "number") return;
|
|
72
|
+
const el = rootRef.current;
|
|
73
|
+
if (!el) return;
|
|
74
|
+
const update = () => {
|
|
75
|
+
const rect = el.getBoundingClientRect();
|
|
76
|
+
setAutoSize(resolveRulerSize(rect.width, rect.height));
|
|
77
|
+
};
|
|
78
|
+
update();
|
|
79
|
+
const ro = new ResizeObserver(update);
|
|
80
|
+
ro.observe(el);
|
|
81
|
+
return () => ro.disconnect();
|
|
82
|
+
}, [sizeProp]);
|
|
35
83
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
);
|
|
84
|
+
const size = typeof sizeProp === "number" ? sizeProp : autoSize;
|
|
85
|
+
const s = `${size}px`;
|
|
86
|
+
const fontPx = Math.max(10, Math.min(11, Math.round(size * 0.38)));
|
|
87
|
+
const bg = isDark ? "rgba(15,23,42,0.98)" : "rgba(255,255,255,0.98)";
|
|
88
|
+
const lineColor = isDark ? "rgba(226,232,240,0.85)" : "rgba(15,23,42,0.55)";
|
|
89
|
+
const textColor = isDark ? "#f8fafc" : "#0f172a";
|
|
90
|
+
const seam = isDark ? "rgba(226,232,240,0.45)" : "rgba(15,23,42,0.28)";
|
|
50
91
|
|
|
51
|
-
|
|
92
|
+
// 刻度线只占内侧约 30%,数字在外侧,互不遮挡
|
|
93
|
+
const longLineSize = Math.max(4, Math.min(Math.round(size * 0.3), size - fontPx - 4));
|
|
94
|
+
const shortLineSize = Math.max(2, Math.round(longLineSize * 0.55));
|
|
95
|
+
// @scena 文字原点写死 17px
|
|
96
|
+
// end(顶/左): baseline=bottom,目标底边 ≈ fontPx+1
|
|
97
|
+
// start(底/右): baseline=top,目标顶边 ≈ size-fontPx-2
|
|
98
|
+
// 横向 offset=[沿尺, 厚度];纵向 offset=[厚度, 沿尺]
|
|
99
|
+
const textBandEnd = fontPx + 1 - (size - SCENA_TEXT_ORIGIN);
|
|
100
|
+
const textBandStart = size - fontPx - 2 - SCENA_TEXT_ORIGIN;
|
|
101
|
+
const textOffsetHEnd: [number, number] = [3, textBandEnd];
|
|
102
|
+
const textOffsetHStart: [number, number] = [3, textBandStart];
|
|
103
|
+
const textOffsetVEnd: [number, number] = [textBandEnd, 3];
|
|
104
|
+
const textOffsetVStart: [number, number] = [textBandStart, 3];
|
|
105
|
+
|
|
106
|
+
const rulerScrollLeft = scrollLeft + size * (1 - 1 / Math.max(zoomX, 0.0001));
|
|
107
|
+
const rulerScrollTop = scrollTop + size * (1 - 1 / Math.max(zoomY, 0.0001));
|
|
108
|
+
const unitX = useMemo(() => resolveRulerUnit(zoomX), [zoomX]);
|
|
109
|
+
const unitY = useMemo(() => resolveRulerUnit(zoomY), [zoomY]);
|
|
110
|
+
|
|
111
|
+
const sharedPaint = useMemo(
|
|
52
112
|
() => ({
|
|
53
|
-
zoom: zoomY,
|
|
54
|
-
unit: RULER_WORLD_UNIT,
|
|
55
113
|
segment: RULER_SEGMENT,
|
|
56
|
-
backgroundColor:
|
|
57
|
-
lineColor
|
|
58
|
-
textColor
|
|
59
|
-
font:
|
|
60
|
-
longLineSize
|
|
61
|
-
shortLineSize
|
|
114
|
+
backgroundColor: bg,
|
|
115
|
+
lineColor,
|
|
116
|
+
textColor,
|
|
117
|
+
font: `500 ${fontPx}px system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial`,
|
|
118
|
+
longLineSize,
|
|
119
|
+
shortLineSize,
|
|
120
|
+
mainLineSize: longLineSize + 2,
|
|
62
121
|
}),
|
|
63
|
-
[
|
|
122
|
+
[bg, fontPx, lineColor, longLineSize, shortLineSize, textColor]
|
|
64
123
|
);
|
|
65
124
|
|
|
125
|
+
const clipTop = `polygon(0 0, 100% 0, calc(100% - ${s}) 100%, ${s} 100%)`;
|
|
126
|
+
const clipBottom = `polygon(${s} 0, calc(100% - ${s}) 0, 100% 100%, 0 100%)`;
|
|
127
|
+
const clipLeft = `polygon(0 0, 100% ${s}, 100% calc(100% - ${s}), 0 100%)`;
|
|
128
|
+
const clipRight = `polygon(0 ${s}, 100% 0, 100% 100%, 0 calc(100% - ${s}))`;
|
|
129
|
+
|
|
66
130
|
return (
|
|
67
131
|
<div
|
|
132
|
+
ref={rootRef}
|
|
68
133
|
className="pointer-events-none absolute inset-0 isolate"
|
|
69
|
-
style={{ ["--rv-ruler-size" as string]:
|
|
134
|
+
style={{ ["--rv-ruler-size" as string]: s, zIndex: PANEL_Z_INDEX.ruler }}
|
|
70
135
|
>
|
|
71
|
-
|
|
72
|
-
|
|
136
|
+
{/* 顶:刻度朝下(end),数字在上半区 */}
|
|
137
|
+
<div
|
|
138
|
+
className="absolute inset-x-0 top-0 overflow-hidden"
|
|
139
|
+
style={{ height: s, clipPath: clipTop, backgroundColor: bg }}
|
|
140
|
+
>
|
|
141
|
+
<Ruler
|
|
142
|
+
key={`h-end-${unitX}`}
|
|
143
|
+
type="horizontal"
|
|
144
|
+
{...sharedPaint}
|
|
145
|
+
unit={unitX}
|
|
146
|
+
zoom={zoomX}
|
|
147
|
+
direction="end"
|
|
148
|
+
textOffset={textOffsetHEnd}
|
|
149
|
+
scrollPos={rulerScrollLeft}
|
|
150
|
+
/>
|
|
73
151
|
</div>
|
|
74
|
-
|
|
75
|
-
|
|
152
|
+
{/* 左:刻度朝右(end),数字在左半区 */}
|
|
153
|
+
<div
|
|
154
|
+
className="absolute inset-y-0 left-0 overflow-hidden"
|
|
155
|
+
style={{ width: s, clipPath: clipLeft, backgroundColor: bg }}
|
|
156
|
+
>
|
|
157
|
+
<Ruler
|
|
158
|
+
key={`v-end-${unitY}`}
|
|
159
|
+
type="vertical"
|
|
160
|
+
{...sharedPaint}
|
|
161
|
+
unit={unitY}
|
|
162
|
+
zoom={zoomY}
|
|
163
|
+
direction="end"
|
|
164
|
+
textOffset={textOffsetVEnd}
|
|
165
|
+
scrollPos={rulerScrollTop}
|
|
166
|
+
/>
|
|
76
167
|
</div>
|
|
77
|
-
|
|
78
|
-
|
|
168
|
+
{/* 底:刻度朝上(start),数字在下半区 */}
|
|
169
|
+
<div
|
|
170
|
+
className="absolute inset-x-0 bottom-0 overflow-hidden"
|
|
171
|
+
style={{ height: s, clipPath: clipBottom, backgroundColor: bg }}
|
|
172
|
+
>
|
|
173
|
+
<Ruler
|
|
174
|
+
key={`h-start-${unitX}`}
|
|
175
|
+
type="horizontal"
|
|
176
|
+
{...sharedPaint}
|
|
177
|
+
unit={unitX}
|
|
178
|
+
zoom={zoomX}
|
|
179
|
+
direction="start"
|
|
180
|
+
textOffset={textOffsetHStart}
|
|
181
|
+
scrollPos={rulerScrollLeft}
|
|
182
|
+
/>
|
|
79
183
|
</div>
|
|
80
|
-
|
|
81
|
-
|
|
184
|
+
{/* 右:刻度朝左(start),数字在右半区 */}
|
|
185
|
+
<div
|
|
186
|
+
className="absolute inset-y-0 right-0 overflow-hidden"
|
|
187
|
+
style={{ width: s, clipPath: clipRight, backgroundColor: bg }}
|
|
188
|
+
>
|
|
189
|
+
<Ruler
|
|
190
|
+
key={`v-start-${unitY}`}
|
|
191
|
+
type="vertical"
|
|
192
|
+
{...sharedPaint}
|
|
193
|
+
unit={unitY}
|
|
194
|
+
zoom={zoomY}
|
|
195
|
+
direction="start"
|
|
196
|
+
textOffset={textOffsetVStart}
|
|
197
|
+
scrollPos={rulerScrollTop}
|
|
198
|
+
/>
|
|
82
199
|
</div>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
200
|
+
|
|
201
|
+
{/* 四角斜缝:SVG 明确画外角→内角 */}
|
|
202
|
+
<svg aria-hidden className="pointer-events-none absolute left-0 top-0" width={size} height={size} style={{ zIndex: 1 }}>
|
|
203
|
+
<line x1={0} y1={0} x2={size} y2={size} stroke={seam} strokeWidth={1} />
|
|
204
|
+
</svg>
|
|
205
|
+
<svg aria-hidden className="pointer-events-none absolute right-0 top-0" width={size} height={size} style={{ zIndex: 1 }}>
|
|
206
|
+
<line x1={size} y1={0} x2={0} y2={size} stroke={seam} strokeWidth={1} />
|
|
207
|
+
</svg>
|
|
208
|
+
<svg aria-hidden className="pointer-events-none absolute bottom-0 left-0" width={size} height={size} style={{ zIndex: 1 }}>
|
|
209
|
+
<line x1={0} y1={size} x2={size} y2={0} stroke={seam} strokeWidth={1} />
|
|
210
|
+
</svg>
|
|
211
|
+
<svg aria-hidden className="pointer-events-none absolute bottom-0 right-0" width={size} height={size} style={{ zIndex: 1 }}>
|
|
212
|
+
<line x1={size} y1={size} x2={0} y2={0} stroke={seam} strokeWidth={1} />
|
|
213
|
+
</svg>
|
|
87
214
|
</div>
|
|
88
215
|
);
|
|
89
216
|
}
|
|
@@ -44,8 +44,8 @@ export function SelectLayer({
|
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
46
|
<Selecto
|
|
47
|
-
//
|
|
48
|
-
container={
|
|
47
|
+
// 框选层挂在未 scale 的视口上,避免选区宽高被画布 transform 放大、线宽视觉变粗
|
|
48
|
+
container={dragRoot}
|
|
49
49
|
rootContainer={dragRoot}
|
|
50
50
|
// 选择框渲染到 body,避免受滚动/缩放影响产生偏移
|
|
51
51
|
portalContainer={overlayContainer}
|
|
@@ -74,7 +74,7 @@ export function WorkspaceProjectNav({
|
|
|
74
74
|
|
|
75
75
|
return (
|
|
76
76
|
<>
|
|
77
|
-
<div className="flex items-center gap-1.5 border-l border-border pl-2">
|
|
77
|
+
<div className="flex items-center gap-1.5 border-l border-border/60 pl-2.5">
|
|
78
78
|
<TooltipProvider delayDuration={150}>
|
|
79
79
|
<Tooltip>
|
|
80
80
|
<TooltipTrigger asChild>
|