@arronqzy/vue-view 0.1.24 → 0.1.26
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 +1 -11
- package/package.json +3 -3
- package/src/index.ts +25 -1
- package/src/panel/VueViewOnlinePreview.vue +25 -0
- package/src/panel/VueViewPanel.vue +188 -63
- package/src/panel/components/ElementsLayer.vue +1 -1
- package/src/panel/components/MaterialPreview.vue +28 -27
- package/src/panel/components/MaterialSidebar.vue +288 -225
- package/src/panel/components/MaterialSidebarTreeNode.vue +15 -10
- package/src/panel/components/MoveableLayer.vue +8 -0
- package/src/panel/components/PanelCanvas.vue +1 -0
- package/src/panel/components/PanelConfigSidebar.vue +23 -41
- package/src/panel/components/PanelRulers.vue +158 -40
- package/src/panel/components/SelectLayer.vue +2 -1
- package/src/panel/components/config/ConfigFieldGroup.vue +3 -3
- package/src/panel/components/config/ConfigSection.vue +5 -5
- package/src/panel/hooks/useWorkspaceProjects.ts +209 -67
- package/src/panel/lib/panel-ruler-canvas.ts +8 -4
- package/src/panel/library/abuilder-storage.ts +101 -3
- package/src/panel/library/abuilder-view-handle.ts +100 -0
- package/src/panel/library/capture-preview-snapshot.ts +800 -0
- package/src/panel/library/event-subscription.ts +70 -5
- package/src/panel/library/preview-snapshot.ts +252 -0
- 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
|
@@ -121,24 +121,27 @@ function onDragStart(e: DragEvent) {
|
|
|
121
121
|
<template v-if="visible">
|
|
122
122
|
<div
|
|
123
123
|
:class="[
|
|
124
|
-
'
|
|
125
|
-
selected ? 'bg-primary/15 text-foreground' : 'text-
|
|
124
|
+
'group/node flex min-w-0 items-center gap-0.5 rounded-sm py-0.5 pr-1',
|
|
125
|
+
selected ? 'bg-primary/15 text-foreground' : 'text-foreground/85 hover:bg-accent/55',
|
|
126
126
|
]"
|
|
127
|
-
|
|
127
|
+
style="padding-left: 2px"
|
|
128
128
|
>
|
|
129
129
|
<button
|
|
130
130
|
v-if="hasChildren"
|
|
131
131
|
type="button"
|
|
132
|
-
class="flex h-
|
|
132
|
+
class="flex h-5 w-4 shrink-0 items-center justify-center rounded text-[11px] text-muted-foreground hover:bg-accent"
|
|
133
133
|
@click="emit('toggleExpanded', nodeKey, !isExpanded)"
|
|
134
134
|
>
|
|
135
135
|
{{ isExpanded ? "▾" : "▸" }}
|
|
136
136
|
</button>
|
|
137
|
-
<span v-else class="inline-flex h-
|
|
137
|
+
<span v-else class="inline-flex h-5 w-4 shrink-0 items-center justify-center text-[10px] text-border">
|
|
138
|
+
<span class="h-px w-2 bg-border" />
|
|
139
|
+
</span>
|
|
138
140
|
|
|
141
|
+
<div class="relative min-w-0 flex-1">
|
|
139
142
|
<button
|
|
140
143
|
type="button"
|
|
141
|
-
class="
|
|
144
|
+
class="block w-full truncate pr-1 text-left text-[12px] font-medium"
|
|
142
145
|
:title="getNodeDisplayName(node)"
|
|
143
146
|
:draggable="!node.locked"
|
|
144
147
|
@click="emit('selectNode', node.id, node.layerId)"
|
|
@@ -156,6 +159,7 @@ function onDragStart(e: DragEvent) {
|
|
|
156
159
|
{{ getNodeDisplayName(node, t) }}
|
|
157
160
|
</button>
|
|
158
161
|
|
|
162
|
+
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center gap-0.5 bg-gradient-to-l from-background via-background to-transparent pl-3 opacity-0 transition-opacity group-hover/node:pointer-events-auto group-hover/node:opacity-100 group-focus-within/node:pointer-events-auto group-focus-within/node:opacity-100">
|
|
159
163
|
<span
|
|
160
164
|
v-if="nodeHomeLayer?.isMapping"
|
|
161
165
|
class="inline-flex shrink-0 items-center rounded-md border-2 border-violet-600 bg-violet-500/20 px-1.5 py-0.5 text-[10px] font-semibold text-violet-950 shadow-sm dark:border-violet-400 dark:bg-violet-500/35 dark:text-violet-50"
|
|
@@ -261,14 +265,15 @@ function onDragStart(e: DragEvent) {
|
|
|
261
265
|
</svg>
|
|
262
266
|
</button>
|
|
263
267
|
</Tooltip>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
264
270
|
</div>
|
|
265
271
|
|
|
266
|
-
<
|
|
272
|
+
<div v-if="hasChildren && isExpanded" class="ml-[7px] border-l border-border/45 pl-1.5">
|
|
267
273
|
<template v-for="child in children" :key="`${path}->${child.id}`">
|
|
268
274
|
<div
|
|
269
275
|
v-if="nextVisited.has(child.id)"
|
|
270
|
-
class="py-1 text-[10px] text-muted-foreground/80"
|
|
271
|
-
:style="{ paddingLeft: `${6 + (level + 1) * 14}px` }"
|
|
276
|
+
class="py-0.5 pl-1 text-[10px] text-muted-foreground/80"
|
|
272
277
|
:title="t('panel.material.circularRefStopped')"
|
|
273
278
|
>
|
|
274
279
|
{{ getNodeDisplayName(child, t) }}{{ t("panel.material.circularRefSuffix") }}
|
|
@@ -297,7 +302,7 @@ function onDragStart(e: DragEvent) {
|
|
|
297
302
|
@drag-end="emit('dragEnd')"
|
|
298
303
|
/>
|
|
299
304
|
</template>
|
|
300
|
-
</
|
|
305
|
+
</div>
|
|
301
306
|
</template>
|
|
302
307
|
</template>
|
|
303
308
|
|
|
@@ -394,10 +394,18 @@ function syncLockBadge() {
|
|
|
394
394
|
};
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
function applyLiveMoveableZoom() {
|
|
398
|
+
const moveable = moveableRef.value;
|
|
399
|
+
if (!moveable) return;
|
|
400
|
+
const liveScale = readLiveCanvasScale(props.canvasContainer, propCanvasScale.value);
|
|
401
|
+
moveable.zoom = 1 / Math.max(0.0001, liveScale);
|
|
402
|
+
}
|
|
403
|
+
|
|
397
404
|
function scheduleViewportSync() {
|
|
398
405
|
if (syncRafRef.value != null) return;
|
|
399
406
|
syncRafRef.value = requestAnimationFrame(() => {
|
|
400
407
|
syncRafRef.value = null;
|
|
408
|
+
applyLiveMoveableZoom();
|
|
401
409
|
moveableRef.value?.updateRect?.();
|
|
402
410
|
syncLockBadge();
|
|
403
411
|
});
|
|
@@ -87,6 +87,7 @@ function applyCanvasTransform(scale: number) {
|
|
|
87
87
|
el.style.transform = `scale(${scale})`;
|
|
88
88
|
el.style.transformOrigin = "0 0";
|
|
89
89
|
el.dataset.canvasScale = String(scale);
|
|
90
|
+
el.style.setProperty("--rv-canvas-scale", String(scale));
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
function notifyViewportSync() {
|
|
@@ -29,8 +29,6 @@ import PanelConfigTableSection from "./table/PanelConfigTableSection.vue";
|
|
|
29
29
|
|
|
30
30
|
const { t, locale } = useI18n();
|
|
31
31
|
const msgs = () => getPanelMessages(t);
|
|
32
|
-
const SEARCH_COLLAPSE_STORAGE_KEY = "panel:config-search-collapsed";
|
|
33
|
-
|
|
34
32
|
const props = withDefaults(
|
|
35
33
|
defineProps<{
|
|
36
34
|
selectedElement: PanelElement | null;
|
|
@@ -58,7 +56,6 @@ const props = withDefaults(
|
|
|
58
56
|
);
|
|
59
57
|
|
|
60
58
|
const configSearch = ref("");
|
|
61
|
-
const isSearchCollapsed = ref(false);
|
|
62
59
|
const sidebarScrollRef = ref<HTMLElement | null>(null);
|
|
63
60
|
const configScrollPosById = new Map<string, number>();
|
|
64
61
|
const activeConfigScrollKey = ref<string | null>(null);
|
|
@@ -258,14 +255,6 @@ const visibleSectionCount = computed(() => {
|
|
|
258
255
|
return count;
|
|
259
256
|
});
|
|
260
257
|
|
|
261
|
-
onMounted(() => {
|
|
262
|
-
const saved = window.localStorage.getItem(SEARCH_COLLAPSE_STORAGE_KEY);
|
|
263
|
-
if (saved === "1") isSearchCollapsed.value = true;
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
watch(isSearchCollapsed, (next) => {
|
|
267
|
-
window.localStorage.setItem(SEARCH_COLLAPSE_STORAGE_KEY, next ? "1" : "0");
|
|
268
|
-
});
|
|
269
258
|
</script>
|
|
270
259
|
|
|
271
260
|
<template>
|
|
@@ -276,37 +265,29 @@ watch(isSearchCollapsed, (next) => {
|
|
|
276
265
|
:scroll-container-ref="sidebarScrollRef"
|
|
277
266
|
>
|
|
278
267
|
<aside
|
|
279
|
-
|
|
280
|
-
|
|
268
|
+
class="scope-config-sidebar grid h-full min-h-0 w-full overflow-hidden border-l border-border/60 bg-muted/[0.12] text-foreground [&_.scope-field--highlight]:rounded-md [&_.scope-field--highlight]:ring-2 [&_.scope-field--highlight]:ring-amber-400/80"
|
|
269
|
+
style="grid-template-rows: auto 1fr"
|
|
281
270
|
>
|
|
282
|
-
<div class="
|
|
283
|
-
<
|
|
271
|
+
<div class="border-b border-border/50 bg-background/70 px-2.5 py-1.5 backdrop-blur-sm">
|
|
272
|
+
<Input
|
|
273
|
+
v-model:value="configSearch"
|
|
274
|
+
size="small"
|
|
275
|
+
:placeholder="t('panel.config.searchPlaceholder')"
|
|
276
|
+
:aria-label="t('panel.config.searchPlaceholder')"
|
|
277
|
+
class="text-[12px]"
|
|
278
|
+
data-scope-autocomplete="off"
|
|
279
|
+
/>
|
|
280
|
+
<div v-if="hasSearch" class="mt-1 px-0.5 text-[11px] text-muted-foreground">
|
|
281
|
+
{{ t("panel.config.searching", { query: configSearch }) }}
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<div
|
|
285
|
+
ref="sidebarScrollRef"
|
|
286
|
+
class="min-h-0 overflow-auto px-2 py-1.5 text-foreground"
|
|
287
|
+
>
|
|
288
|
+
<div v-if="showScopePanel || scopeWarnings.length > 0" class="mb-2 space-y-1.5">
|
|
284
289
|
<ViewElementScopePanel v-if="showScopePanel" :scope="viewElementScope!" />
|
|
285
290
|
<ScopeTemplateWarningsPanel />
|
|
286
|
-
<div
|
|
287
|
-
:class="showScopePanel || scopeWarnings.length > 0 ? 'mt-2 border-t border-gray-200/50 pt-2' : 'mt-2'"
|
|
288
|
-
>
|
|
289
|
-
<div class="flex items-center justify-end gap-2">
|
|
290
|
-
<button
|
|
291
|
-
type="button"
|
|
292
|
-
class="rounded border border-gray-200 px-1.5 py-0.5 text-[11px] text-gray-500 hover:bg-gray-50"
|
|
293
|
-
@click="isSearchCollapsed = !isSearchCollapsed"
|
|
294
|
-
>
|
|
295
|
-
{{ isSearchCollapsed ? t("panel.config.expandSearch") : t("panel.config.collapseSearch") }}
|
|
296
|
-
</button>
|
|
297
|
-
</div>
|
|
298
|
-
<div v-if="!isSearchCollapsed" class="mt-2">
|
|
299
|
-
<Input
|
|
300
|
-
v-model:value="configSearch"
|
|
301
|
-
size="small"
|
|
302
|
-
:placeholder="t('panel.config.searchPlaceholder')"
|
|
303
|
-
data-scope-autocomplete="off"
|
|
304
|
-
/>
|
|
305
|
-
<div v-if="hasSearch" class="mt-1 text-[11px] text-gray-500">
|
|
306
|
-
{{ t("panel.config.searching", { query: configSearch }) }}
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
</div>
|
|
310
291
|
</div>
|
|
311
292
|
|
|
312
293
|
<PanelConfigMultiSelect
|
|
@@ -327,9 +308,9 @@ watch(isSearchCollapsed, (next) => {
|
|
|
327
308
|
:description="t('panel.config.emptyNoNodeDesc')"
|
|
328
309
|
/>
|
|
329
310
|
|
|
330
|
-
<div v-else class="space-y-
|
|
311
|
+
<div v-else class="space-y-2">
|
|
331
312
|
<fieldset :disabled="!isNodeEditable" :class="!isNodeEditable ? 'opacity-60' : ''">
|
|
332
|
-
<div class="space-y-
|
|
313
|
+
<div class="space-y-2 text-xs">
|
|
333
314
|
<PanelConfigNodeInfo
|
|
334
315
|
v-if="shouldShowSection('nodeInfo', t('panel.config.sectionNodeInfo'), [t('panel.config.name'), 'id', t('panel.config.type'), t('panel.layers.lockShort')])"
|
|
335
316
|
:element="selectedElement"
|
|
@@ -517,6 +498,7 @@ watch(isSearchCollapsed, (next) => {
|
|
|
517
498
|
{{ msgs().nodeConfigLayerLocked }}
|
|
518
499
|
</div>
|
|
519
500
|
</div>
|
|
501
|
+
</div>
|
|
520
502
|
</aside>
|
|
521
503
|
</ScopeConfigProvider>
|
|
522
504
|
</template>
|
|
@@ -3,8 +3,8 @@ import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|
|
3
3
|
import { PanelRuler } from "../lib/panel-ruler-canvas";
|
|
4
4
|
import { PANEL_Z_INDEX } from "../constants/zIndex";
|
|
5
5
|
|
|
6
|
-
const RULER_WORLD_UNIT = 100;
|
|
7
6
|
const RULER_SEGMENT = 10;
|
|
7
|
+
const RULER_TARGET_MAJOR_PX = 78;
|
|
8
8
|
|
|
9
9
|
const props = withDefaults(
|
|
10
10
|
defineProps<{
|
|
@@ -12,11 +12,37 @@ const props = withDefaults(
|
|
|
12
12
|
zoomY: number;
|
|
13
13
|
scrollLeft: number;
|
|
14
14
|
scrollTop: number;
|
|
15
|
+
/** 固定厚度;省略时按编辑区尺寸动态计算 */
|
|
15
16
|
size?: number;
|
|
16
17
|
}>(),
|
|
17
|
-
{
|
|
18
|
+
{}
|
|
18
19
|
);
|
|
19
20
|
|
|
21
|
+
function clamp(n: number, min: number, max: number) {
|
|
22
|
+
return Math.min(max, Math.max(min, n));
|
|
23
|
+
}
|
|
24
|
+
|
|
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
|
+
function resolveRulerUnit(zoom: number): number {
|
|
34
|
+
const z = Math.max(zoom, 0.0001);
|
|
35
|
+
return Math.max(1, niceNumber(RULER_TARGET_MAJOR_PX / z));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveRulerSize(width: number, height: number): number {
|
|
39
|
+
const edge = Math.min(width || 0, height || 0);
|
|
40
|
+
if (edge <= 0) return 22;
|
|
41
|
+
return Math.round(clamp(edge * 0.032, 24, 30));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rootRef = ref<HTMLElement | null>(null);
|
|
45
|
+
const autoSize = ref(22);
|
|
20
46
|
const isDark = ref(false);
|
|
21
47
|
const topHRef = ref<HTMLElement | null>(null);
|
|
22
48
|
const leftVRef = ref<HTMLElement | null>(null);
|
|
@@ -24,34 +50,77 @@ const bottomHRef = ref<HTMLElement | null>(null);
|
|
|
24
50
|
const rightVRef = ref<HTMLElement | null>(null);
|
|
25
51
|
|
|
26
52
|
let rulers: Array<{ ruler: PanelRuler; axis: "h" | "v" }> = [];
|
|
53
|
+
let themeObserver: MutationObserver | null = null;
|
|
54
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
27
55
|
|
|
28
|
-
const
|
|
29
|
-
|
|
56
|
+
const size = computed(() =>
|
|
57
|
+
typeof props.size === "number" ? props.size : autoSize.value
|
|
58
|
+
);
|
|
59
|
+
const sizePx = computed(() => `${size.value}px`);
|
|
60
|
+
const longLineSize = computed(() => Math.max(4, Math.round(size.value * 0.28)));
|
|
61
|
+
const shortLineSize = computed(() => Math.max(2, Math.round(size.value * 0.15)));
|
|
62
|
+
const fontPx = computed(() => Math.max(10, Math.min(12, Math.round(size.value * 0.42))));
|
|
63
|
+
const bg = computed(() =>
|
|
64
|
+
isDark.value ? "rgba(15,23,42,0.98)" : "rgba(255,255,255,0.98)"
|
|
65
|
+
);
|
|
66
|
+
const lineColor = computed(() =>
|
|
67
|
+
isDark.value ? "rgba(226,232,240,0.75)" : "rgba(15,23,42,0.5)"
|
|
68
|
+
);
|
|
69
|
+
const textColor = computed(() =>
|
|
70
|
+
isDark.value ? "rgba(248,250,252,0.95)" : "rgba(15,23,42,0.88)"
|
|
71
|
+
);
|
|
72
|
+
const seam = computed(() =>
|
|
73
|
+
isDark.value ? "rgba(226,232,240,0.45)" : "rgba(15,23,42,0.28)"
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
const rulerScrollLeft = computed(
|
|
78
|
+
() => props.scrollLeft + size.value * (1 - 1 / Math.max(props.zoomX, 0.0001))
|
|
79
|
+
);
|
|
80
|
+
const rulerScrollTop = computed(
|
|
81
|
+
() => props.scrollTop + size.value * (1 - 1 / Math.max(props.zoomY, 0.0001))
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const clipTop = computed(
|
|
85
|
+
() => `polygon(0 0, 100% 0, calc(100% - ${sizePx.value}) 100%, ${sizePx.value} 100%)`
|
|
86
|
+
);
|
|
87
|
+
const clipBottom = computed(
|
|
88
|
+
() => `polygon(${sizePx.value} 0, calc(100% - ${sizePx.value}) 0, 100% 100%, 0 100%)`
|
|
89
|
+
);
|
|
90
|
+
const clipLeft = computed(
|
|
91
|
+
() => `polygon(0 0, 100% ${sizePx.value}, 100% calc(100% - ${sizePx.value}), 0 100%)`
|
|
92
|
+
);
|
|
93
|
+
const clipRight = computed(
|
|
94
|
+
() => `polygon(0 ${sizePx.value}, 100% 0, 100% 100%, 0 calc(100% - ${sizePx.value}))`
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const unitX = computed(() => resolveRulerUnit(props.zoomX));
|
|
98
|
+
const unitY = computed(() => resolveRulerUnit(props.zoomY));
|
|
30
99
|
|
|
31
100
|
const horizontalOptions = computed(() => ({
|
|
32
101
|
type: "horizontal" as const,
|
|
33
102
|
zoom: props.zoomX,
|
|
34
|
-
unit:
|
|
103
|
+
unit: unitX.value,
|
|
35
104
|
segment: RULER_SEGMENT,
|
|
36
|
-
backgroundColor:
|
|
37
|
-
lineColor:
|
|
38
|
-
textColor:
|
|
39
|
-
font:
|
|
40
|
-
longLineSize:
|
|
41
|
-
shortLineSize:
|
|
105
|
+
backgroundColor: bg.value,
|
|
106
|
+
lineColor: lineColor.value,
|
|
107
|
+
textColor: textColor.value,
|
|
108
|
+
font: `500 ${fontPx.value}px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial`,
|
|
109
|
+
longLineSize: longLineSize.value,
|
|
110
|
+
shortLineSize: shortLineSize.value,
|
|
42
111
|
}));
|
|
43
112
|
|
|
44
113
|
const verticalOptions = computed(() => ({
|
|
45
114
|
type: "vertical" as const,
|
|
46
115
|
zoom: props.zoomY,
|
|
47
|
-
unit:
|
|
116
|
+
unit: unitY.value,
|
|
48
117
|
segment: RULER_SEGMENT,
|
|
49
|
-
backgroundColor:
|
|
50
|
-
lineColor:
|
|
51
|
-
textColor:
|
|
52
|
-
font:
|
|
53
|
-
longLineSize:
|
|
54
|
-
shortLineSize:
|
|
118
|
+
backgroundColor: bg.value,
|
|
119
|
+
lineColor: lineColor.value,
|
|
120
|
+
textColor: textColor.value,
|
|
121
|
+
font: `500 ${fontPx.value}px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial`,
|
|
122
|
+
longLineSize: longLineSize.value,
|
|
123
|
+
shortLineSize: shortLineSize.value,
|
|
55
124
|
}));
|
|
56
125
|
|
|
57
126
|
function destroyRulers() {
|
|
@@ -89,30 +158,45 @@ function syncTheme() {
|
|
|
89
158
|
const opts = axis === "h" ? horizontalOptions.value : verticalOptions.value;
|
|
90
159
|
ruler.setState({
|
|
91
160
|
zoom: opts.zoom,
|
|
161
|
+
unit: opts.unit,
|
|
162
|
+
segment: opts.segment,
|
|
92
163
|
backgroundColor: opts.backgroundColor,
|
|
93
164
|
lineColor: opts.lineColor,
|
|
94
165
|
textColor: opts.textColor,
|
|
166
|
+
font: opts.font,
|
|
167
|
+
longLineSize: opts.longLineSize,
|
|
168
|
+
shortLineSize: opts.shortLineSize,
|
|
95
169
|
});
|
|
96
170
|
}
|
|
97
171
|
syncScroll();
|
|
98
172
|
}
|
|
99
173
|
|
|
100
|
-
let themeObserver: MutationObserver | null = null;
|
|
101
|
-
|
|
102
174
|
onMounted(() => {
|
|
103
175
|
const root = document.documentElement;
|
|
104
|
-
const
|
|
176
|
+
const updateTheme = () => {
|
|
105
177
|
isDark.value = root.classList.contains("dark");
|
|
106
178
|
};
|
|
107
|
-
|
|
108
|
-
themeObserver = new MutationObserver(
|
|
179
|
+
updateTheme();
|
|
180
|
+
themeObserver = new MutationObserver(updateTheme);
|
|
109
181
|
themeObserver.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
182
|
+
|
|
183
|
+
if (rootRef.value && typeof props.size !== "number") {
|
|
184
|
+
const updateSize = () => {
|
|
185
|
+
const rect = rootRef.value!.getBoundingClientRect();
|
|
186
|
+
autoSize.value = resolveRulerSize(rect.width, rect.height);
|
|
187
|
+
};
|
|
188
|
+
updateSize();
|
|
189
|
+
resizeObserver = new ResizeObserver(updateSize);
|
|
190
|
+
resizeObserver.observe(rootRef.value);
|
|
191
|
+
}
|
|
192
|
+
|
|
110
193
|
initRulers();
|
|
111
194
|
window.addEventListener("resize", initRulers);
|
|
112
195
|
});
|
|
113
196
|
|
|
114
197
|
onUnmounted(() => {
|
|
115
198
|
themeObserver?.disconnect();
|
|
199
|
+
resizeObserver?.disconnect();
|
|
116
200
|
window.removeEventListener("resize", initRulers);
|
|
117
201
|
destroyRulers();
|
|
118
202
|
});
|
|
@@ -127,6 +211,10 @@ watch([rulerScrollLeft, rulerScrollTop], () => {
|
|
|
127
211
|
syncScroll();
|
|
128
212
|
});
|
|
129
213
|
|
|
214
|
+
watch(size, () => {
|
|
215
|
+
initRulers();
|
|
216
|
+
});
|
|
217
|
+
|
|
130
218
|
watch([topHRef, leftVRef, bottomHRef, rightVRef], () => {
|
|
131
219
|
if (!topHRef.value || !leftVRef.value || !bottomHRef.value || !rightVRef.value) return;
|
|
132
220
|
if (rulers.length === 0) initRulers();
|
|
@@ -135,43 +223,73 @@ watch([topHRef, leftVRef, bottomHRef, rightVRef], () => {
|
|
|
135
223
|
|
|
136
224
|
<template>
|
|
137
225
|
<div
|
|
226
|
+
ref="rootRef"
|
|
138
227
|
class="pointer-events-none absolute inset-0 isolate"
|
|
139
228
|
:style="{
|
|
140
|
-
'--rv-ruler-size':
|
|
229
|
+
'--rv-ruler-size': sizePx,
|
|
141
230
|
zIndex: PANEL_Z_INDEX.ruler,
|
|
142
231
|
}"
|
|
143
232
|
>
|
|
144
233
|
<div
|
|
145
|
-
class="absolute
|
|
234
|
+
class="absolute inset-x-0 top-0 overflow-hidden"
|
|
235
|
+
:style="{ height: sizePx, clipPath: clipTop, backgroundColor: bg }"
|
|
146
236
|
>
|
|
147
237
|
<div ref="topHRef" class="h-full w-full" />
|
|
148
238
|
</div>
|
|
149
239
|
<div
|
|
150
|
-
class="absolute
|
|
240
|
+
class="absolute inset-y-0 left-0 overflow-hidden"
|
|
241
|
+
:style="{ width: sizePx, clipPath: clipLeft, backgroundColor: bg }"
|
|
151
242
|
>
|
|
152
243
|
<div ref="leftVRef" class="h-full w-full" />
|
|
153
244
|
</div>
|
|
154
245
|
<div
|
|
155
|
-
class="absolute
|
|
246
|
+
class="absolute inset-x-0 bottom-0 overflow-hidden"
|
|
247
|
+
:style="{ height: sizePx, clipPath: clipBottom, backgroundColor: bg }"
|
|
156
248
|
>
|
|
157
249
|
<div ref="bottomHRef" class="h-full w-full" />
|
|
158
250
|
</div>
|
|
159
251
|
<div
|
|
160
|
-
class="absolute
|
|
252
|
+
class="absolute inset-y-0 right-0 overflow-hidden"
|
|
253
|
+
:style="{ width: sizePx, clipPath: clipRight, backgroundColor: bg }"
|
|
161
254
|
>
|
|
162
255
|
<div ref="rightVRef" class="h-full w-full" />
|
|
163
256
|
</div>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
257
|
+
|
|
258
|
+
<svg
|
|
259
|
+
aria-hidden="true"
|
|
260
|
+
class="pointer-events-none absolute left-0 top-0"
|
|
261
|
+
:width="size"
|
|
262
|
+
:height="size"
|
|
263
|
+
style="z-index: 1"
|
|
264
|
+
>
|
|
265
|
+
<line :x1="0" :y1="0" :x2="size" :y2="size" :stroke="seam" stroke-width="1" />
|
|
266
|
+
</svg>
|
|
267
|
+
<svg
|
|
268
|
+
aria-hidden="true"
|
|
269
|
+
class="pointer-events-none absolute right-0 top-0"
|
|
270
|
+
:width="size"
|
|
271
|
+
:height="size"
|
|
272
|
+
style="z-index: 1"
|
|
273
|
+
>
|
|
274
|
+
<line :x1="size" :y1="0" :x2="0" :y2="size" :stroke="seam" stroke-width="1" />
|
|
275
|
+
</svg>
|
|
276
|
+
<svg
|
|
277
|
+
aria-hidden="true"
|
|
278
|
+
class="pointer-events-none absolute bottom-0 left-0"
|
|
279
|
+
:width="size"
|
|
280
|
+
:height="size"
|
|
281
|
+
style="z-index: 1"
|
|
282
|
+
>
|
|
283
|
+
<line :x1="0" :y1="size" :x2="size" :y2="0" :stroke="seam" stroke-width="1" />
|
|
284
|
+
</svg>
|
|
285
|
+
<svg
|
|
286
|
+
aria-hidden="true"
|
|
287
|
+
class="pointer-events-none absolute bottom-0 right-0"
|
|
288
|
+
:width="size"
|
|
289
|
+
:height="size"
|
|
290
|
+
style="z-index: 1"
|
|
291
|
+
>
|
|
292
|
+
<line :x1="size" :y1="size" :x2="0" :y2="0" :stroke="seam" stroke-width="1" />
|
|
293
|
+
</svg>
|
|
176
294
|
</div>
|
|
177
295
|
</template>
|
|
@@ -44,7 +44,8 @@ function createSelecto() {
|
|
|
44
44
|
|
|
45
45
|
const selectedIdSet = new Set(props.selectedIds);
|
|
46
46
|
const selecto = new Selecto({
|
|
47
|
-
|
|
47
|
+
// 框选层挂在未 scale 的视口上,避免选区宽高被画布 transform 放大、线宽视觉变粗
|
|
48
|
+
container: dragRoot,
|
|
48
49
|
rootContainer: dragRoot,
|
|
49
50
|
portalContainer: document.body,
|
|
50
51
|
dragContainer: dragRoot,
|
|
@@ -51,8 +51,8 @@ function toggle() {
|
|
|
51
51
|
</script>
|
|
52
52
|
|
|
53
53
|
<template>
|
|
54
|
-
<div class="space-y-
|
|
55
|
-
<div class="flex items-center gap-1
|
|
54
|
+
<div class="space-y-1.5 rounded-md border border-border/50 bg-background/70 p-2">
|
|
55
|
+
<div class="flex items-center gap-1">
|
|
56
56
|
<button
|
|
57
57
|
v-if="collapsible"
|
|
58
58
|
type="button"
|
|
@@ -66,7 +66,7 @@ function toggle() {
|
|
|
66
66
|
<slot name="hint" />
|
|
67
67
|
</ConfigHintIcon>
|
|
68
68
|
</div>
|
|
69
|
-
<div v-show="isOpen" class="space-y-
|
|
69
|
+
<div v-show="isOpen" class="space-y-1.5">
|
|
70
70
|
<slot />
|
|
71
71
|
</div>
|
|
72
72
|
</div>
|
|
@@ -24,17 +24,17 @@ function toggle() {
|
|
|
24
24
|
</script>
|
|
25
25
|
|
|
26
26
|
<template>
|
|
27
|
-
<div class="rounded-
|
|
28
|
-
<div class="flex items-center gap-1
|
|
27
|
+
<div class="rounded-lg border border-border/60 bg-card/90 shadow-[0_1px_1px_hsl(var(--foreground)/0.03)]">
|
|
28
|
+
<div class="flex items-center gap-1 px-2 py-1.5">
|
|
29
29
|
<button
|
|
30
30
|
type="button"
|
|
31
|
-
class="flex h-
|
|
31
|
+
class="flex h-5 w-5 items-center justify-center rounded text-xs hover:bg-accent"
|
|
32
32
|
@click="toggle"
|
|
33
33
|
>
|
|
34
34
|
{{ isOpen ? "▾" : "▸" }}
|
|
35
35
|
</button>
|
|
36
36
|
<div class="flex min-w-0 flex-1 items-center gap-1">
|
|
37
|
-
<div class="text-[11px] font-semibold tracking-wide text-
|
|
37
|
+
<div class="text-[11px] font-semibold tracking-wide text-muted-foreground">{{ title }}</div>
|
|
38
38
|
<ConfigHintIcon v-if="$slots.hint" :label="title">
|
|
39
39
|
<slot name="hint" />
|
|
40
40
|
</ConfigHintIcon>
|
|
@@ -42,7 +42,7 @@ function toggle() {
|
|
|
42
42
|
</div>
|
|
43
43
|
<div
|
|
44
44
|
v-show="isOpen"
|
|
45
|
-
class="space-y-
|
|
45
|
+
class="space-y-2 border-t border-border/50 bg-muted/[0.08] px-2 pb-2 pt-1.5"
|
|
46
46
|
>
|
|
47
47
|
<slot />
|
|
48
48
|
</div>
|