@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.
@@ -121,24 +121,27 @@ function onDragStart(e: DragEvent) {
121
121
  <template v-if="visible">
122
122
  <div
123
123
  :class="[
124
- 'mb-1 flex items-center gap-1 rounded py-1',
125
- selected ? 'bg-primary/15 text-foreground' : 'text-muted-foreground hover:bg-accent/60',
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
- :style="{ paddingLeft: `${6 + level * 14}px`, paddingRight: '6px' }"
127
+ style="padding-left: 2px"
128
128
  >
129
129
  <button
130
130
  v-if="hasChildren"
131
131
  type="button"
132
- class="flex h-7 w-7 items-center justify-center rounded text-sm hover:bg-accent"
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-7 w-7 items-center justify-center text-sm opacity-40">•</span>
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="min-w-0 flex-1 truncate text-left"
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
- <template v-if="hasChildren && isExpanded">
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
- </template>
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
- ref="sidebarScrollRef"
280
- class="scope-config-sidebar h-full overflow-auto border-l border-gray-200 bg-gray-50/30 px-3 py-3 text-gray-900 [&_.scope-field--highlight]:rounded-md [&_.scope-field--highlight]:ring-2 [&_.scope-field--highlight]:ring-amber-400/80"
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="sticky top-0 z-20 mb-3 rounded-lg border border-gray-200/80 bg-white px-2.5 py-2 shadow-sm">
283
- <div class="text-xs font-semibold tracking-wide">{{ t("panel.config.panelTitle") }}</div>
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-3">
311
+ <div v-else class="space-y-2">
331
312
  <fieldset :disabled="!isNodeEditable" :class="!isNodeEditable ? 'opacity-60' : ''">
332
- <div class="space-y-3.5 text-xs">
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
- { size: 32 }
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 rulerScrollLeft = computed(() => props.scrollLeft + props.size);
29
- const rulerScrollTop = computed(() => props.scrollTop + props.size);
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: RULER_WORLD_UNIT,
103
+ unit: unitX.value,
35
104
  segment: RULER_SEGMENT,
36
- backgroundColor: isDark.value ? "rgba(15,23,42,0.98)" : "rgba(255,255,255,0.98)",
37
- lineColor: isDark.value ? "rgba(226,232,240,0.4)" : "rgba(15,23,42,0.22)",
38
- textColor: isDark.value ? "rgba(248,250,252,0.95)" : "rgba(15,23,42,0.75)",
39
- font: "10px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
40
- longLineSize: 8,
41
- shortLineSize: 4,
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: RULER_WORLD_UNIT,
116
+ unit: unitY.value,
48
117
  segment: RULER_SEGMENT,
49
- backgroundColor: isDark.value ? "rgba(15,23,42,0.98)" : "rgba(255,255,255,0.98)",
50
- lineColor: isDark.value ? "rgba(226,232,240,0.4)" : "rgba(15,23,42,0.22)",
51
- textColor: isDark.value ? "rgba(248,250,252,0.95)" : "rgba(15,23,42,0.75)",
52
- font: "10px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
53
- longLineSize: 8,
54
- shortLineSize: 4,
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 update = () => {
176
+ const updateTheme = () => {
105
177
  isDark.value = root.classList.contains("dark");
106
178
  };
107
- update();
108
- themeObserver = new MutationObserver(update);
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': `${size}px`,
229
+ '--rv-ruler-size': sizePx,
141
230
  zIndex: PANEL_Z_INDEX.ruler,
142
231
  }"
143
232
  >
144
233
  <div
145
- class="absolute left-[var(--rv-ruler-size)] right-[var(--rv-ruler-size)] top-0 h-[var(--rv-ruler-size)] overflow-visible"
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 left-0 top-[var(--rv-ruler-size)] bottom-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] overflow-visible"
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 left-[var(--rv-ruler-size)] right-[var(--rv-ruler-size)] bottom-0 h-[var(--rv-ruler-size)] overflow-visible"
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 right-0 top-[var(--rv-ruler-size)] bottom-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] overflow-visible"
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
- <div
165
- class="absolute left-0 top-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-b border-r border-border bg-background"
166
- />
167
- <div
168
- class="absolute right-0 top-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-b border-l border-border bg-background"
169
- />
170
- <div
171
- class="absolute left-0 bottom-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-r border-t border-border bg-background"
172
- />
173
- <div
174
- class="absolute right-0 bottom-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-l border-t border-border bg-background"
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
- container: props.container,
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-2.5 rounded-lg border border-gray-200/80 bg-white/80 p-2.5">
55
- <div class="flex items-center gap-1.5">
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-2.5">
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-xl border border-gray-200/80 bg-white shadow-sm">
28
- <div class="flex items-center gap-1.5 px-3 py-2">
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-6 w-6 items-center justify-center rounded-md text-xs hover:bg-gray-100"
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-gray-500">{{ title }}</div>
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-3 border-t border-gray-200/60 bg-gray-50/50 px-3 pb-3 pt-2.5"
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>