@tldiagram/core-ui 2.0.2 → 2.0.4
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/components/FloatingEdge.d.ts +1 -1
- package/dist/demo/viewEditor.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8198 -7926
- package/dist/pages/ViewEditor/index.d.ts +6 -1
- package/package.json +1 -1
- package/src/components/CrossBranchControls.tsx +178 -61
- package/src/components/FloatingEdge.tsx +145 -39
- package/src/components/ViewFloatingMenu.tsx +158 -68
- package/src/demo/viewEditor.ts +2 -0
- package/src/index.ts +1 -1
- package/src/pages/InfiniteZoom.tsx +4 -4
- package/src/pages/ViewEditor/index.tsx +34 -10
|
@@ -2,7 +2,7 @@ import React, { memo } from 'react'
|
|
|
2
2
|
import type { ViewFloatingMenuSlots } from '../slots'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
HStack, Tooltip, Button, Box, Text, Popover, PopoverTrigger, Portal, PopoverContent, PopoverBody, IconButton, Slider, SliderTrack, SliderFilledTrack, SliderThumb, useDisclosure
|
|
5
|
+
HStack, Tooltip, Button, Box, Text, Popover, PopoverTrigger, Portal, PopoverContent, PopoverBody, IconButton, Slider, SliderTrack, SliderFilledTrack, SliderThumb, Switch, VStack, useDisclosure
|
|
6
6
|
} from '@chakra-ui/react'
|
|
7
7
|
import { DownloadIcon } from '@chakra-ui/icons'
|
|
8
8
|
import {
|
|
@@ -15,12 +15,21 @@ import {
|
|
|
15
15
|
CollapseExtrasIcon as CollapseExtrasSvg,
|
|
16
16
|
FocusIcon as FocusSvg,
|
|
17
17
|
TagsIcon,
|
|
18
|
+
ChevronDownIcon,
|
|
18
19
|
} from './Icons'
|
|
19
20
|
import { KbdHint } from './PanelUI'
|
|
20
21
|
import { RedoSvg, UndoSvg } from './ViewDrawMenu'
|
|
21
22
|
import { useViewEditorContext } from '../pages/ViewEditor/context'
|
|
22
23
|
import type { Tag, ViewLayer } from '../types'
|
|
23
24
|
|
|
25
|
+
const DENSITY_STOPS = [
|
|
26
|
+
{ value: -2, label: 'Quiet' },
|
|
27
|
+
{ value: -1, label: 'Lean' },
|
|
28
|
+
{ value: 0, label: 'Normal' },
|
|
29
|
+
{ value: 1, label: 'Rich' },
|
|
30
|
+
{ value: 2, label: 'Full' },
|
|
31
|
+
] as const
|
|
32
|
+
|
|
24
33
|
export interface ViewFloatingMenuProps extends ViewFloatingMenuSlots {
|
|
25
34
|
handleAddElementAtCenter: () => void
|
|
26
35
|
drawingMode: boolean
|
|
@@ -105,7 +114,11 @@ function ViewFloatingMenu({
|
|
|
105
114
|
}: ViewFloatingMenuProps) {
|
|
106
115
|
const { canEdit } = useViewEditorContext()
|
|
107
116
|
const { isOpen: isTagsOpen, onClose: onTagsClose, onToggle: onTagsToggle } = useDisclosure()
|
|
117
|
+
const { isOpen: isFiltersOpen, onClose: onFiltersClose, onToggle: onFiltersToggle } = useDisclosure()
|
|
108
118
|
const [draftDensityLevel, setDraftDensityLevel] = React.useState(densityLevel)
|
|
119
|
+
const activeDensityLabel = DENSITY_STOPS.find((stop) => stop.value === draftDensityLevel)?.label ?? 'Normal'
|
|
120
|
+
const showFilters = !hideFocusView || !!onDensityLevelChange
|
|
121
|
+
const hasActiveFilters = (!hideFocusView && focusMode) || (!!onDensityLevelChange && densityLevel !== 0)
|
|
109
122
|
|
|
110
123
|
React.useEffect(() => {
|
|
111
124
|
setDraftDensityLevel(densityLevel)
|
|
@@ -191,26 +204,152 @@ function ViewFloatingMenu({
|
|
|
191
204
|
</>
|
|
192
205
|
)}
|
|
193
206
|
|
|
194
|
-
{
|
|
207
|
+
{showFilters && (
|
|
195
208
|
<>
|
|
196
209
|
<Box w="1px" h="16px" bg="whiteAlpha.100" flexShrink={0} mx={0.5} />
|
|
197
|
-
<
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
<Popover isOpen={isFiltersOpen} onClose={onFiltersClose} placement="top" isLazy closeOnBlur>
|
|
211
|
+
<PopoverTrigger>
|
|
212
|
+
<Button
|
|
213
|
+
variant="ghost"
|
|
214
|
+
h="28px"
|
|
215
|
+
px={2.5}
|
|
216
|
+
color={isFiltersOpen || hasActiveFilters ? 'var(--accent)' : 'gray.300'}
|
|
217
|
+
bg={hasActiveFilters ? 'rgba(var(--accent-rgb), 0.12)' : 'transparent'}
|
|
218
|
+
_hover={{ bg: 'rgba(var(--accent-rgb), 0.12)', color: 'var(--accent)' }}
|
|
219
|
+
onClick={onFiltersToggle}
|
|
220
|
+
aria-label="Open filters"
|
|
221
|
+
>
|
|
222
|
+
<HStack spacing={1.5}>
|
|
223
|
+
<FocusSvg />
|
|
224
|
+
<Text fontSize="11px" fontWeight={hasActiveFilters ? 'semibold' : 'normal'}>Filters</Text>
|
|
225
|
+
{hasActiveFilters && <Box w="6px" h="6px" rounded="full" bg="var(--accent)" />}
|
|
226
|
+
<ChevronDownIcon size={10} strokeWidth={3.5} />
|
|
227
|
+
</HStack>
|
|
228
|
+
</Button>
|
|
229
|
+
</PopoverTrigger>
|
|
230
|
+
<Portal>
|
|
231
|
+
<PopoverContent
|
|
232
|
+
bg="linear-gradient(180deg, rgba(var(--bg-main-rgb), 0.98) 0%, rgba(var(--bg-main-rgb), 0.92) 100%)"
|
|
233
|
+
backdropFilter="blur(22px)"
|
|
234
|
+
borderColor="whiteAlpha.100"
|
|
235
|
+
boxShadow="0 18px 48px rgba(0,0,0,0.46), inset 0 1px 0 rgba(255,255,255,0.04)"
|
|
236
|
+
borderRadius="lg"
|
|
237
|
+
width="280px"
|
|
238
|
+
_focus={{ boxShadow: 'none' }}
|
|
239
|
+
>
|
|
240
|
+
<PopoverBody p={3}>
|
|
241
|
+
<VStack align="stretch" spacing={3}>
|
|
242
|
+
{!hideFocusView && (
|
|
243
|
+
<HStack
|
|
244
|
+
justify="space-between"
|
|
245
|
+
spacing={3}
|
|
246
|
+
px={2.5}
|
|
247
|
+
py={2}
|
|
248
|
+
rounded="md"
|
|
249
|
+
bg={focusMode ? 'rgba(var(--accent-rgb), 0.10)' : 'whiteAlpha.50'}
|
|
250
|
+
border="1px solid"
|
|
251
|
+
borderColor={focusMode ? 'rgba(var(--accent-rgb), 0.22)' : 'whiteAlpha.100'}
|
|
252
|
+
>
|
|
253
|
+
<HStack spacing={2.5} minW={0}>
|
|
254
|
+
<Box color={focusMode ? 'var(--accent)' : 'gray.400'} flexShrink={0}>
|
|
255
|
+
<FocusSvg />
|
|
256
|
+
</Box>
|
|
257
|
+
<Box minW={0}>
|
|
258
|
+
<Text fontSize="xs" fontWeight="semibold" color="whiteAlpha.900">Hide External</Text>
|
|
259
|
+
</Box>
|
|
260
|
+
</HStack>
|
|
261
|
+
<Switch
|
|
262
|
+
size="sm"
|
|
263
|
+
isChecked={focusMode}
|
|
264
|
+
onChange={(event) => onFocusModeChange(event.target.checked)}
|
|
265
|
+
colorScheme="teal"
|
|
266
|
+
flexShrink={0}
|
|
267
|
+
aria-label="Toggle external view"
|
|
268
|
+
/>
|
|
269
|
+
</HStack>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{onDensityLevelChange && (
|
|
273
|
+
<Box
|
|
274
|
+
px={2.5}
|
|
275
|
+
py={2.5}
|
|
276
|
+
rounded="md"
|
|
277
|
+
bg="whiteAlpha.50"
|
|
278
|
+
border="1px solid"
|
|
279
|
+
borderColor="whiteAlpha.100"
|
|
280
|
+
>
|
|
281
|
+
<HStack justify="space-between" mb={2.5}>
|
|
282
|
+
<Box>
|
|
283
|
+
<Text fontSize="xs" fontWeight="semibold" color="whiteAlpha.900">Density</Text>
|
|
284
|
+
<Text fontSize="10px" color="whiteAlpha.600">Control how much detail is shown</Text>
|
|
285
|
+
</Box>
|
|
286
|
+
<Text
|
|
287
|
+
fontSize="10px"
|
|
288
|
+
fontWeight="bold"
|
|
289
|
+
color="var(--accent)"
|
|
290
|
+
bg="rgba(var(--accent-rgb), 0.10)"
|
|
291
|
+
border="1px solid"
|
|
292
|
+
borderColor="rgba(var(--accent-rgb), 0.18)"
|
|
293
|
+
rounded="full"
|
|
294
|
+
px={2}
|
|
295
|
+
py={0.5}
|
|
296
|
+
>
|
|
297
|
+
{activeDensityLabel}
|
|
298
|
+
</Text>
|
|
299
|
+
</HStack>
|
|
300
|
+
<Box px={1} pt={1} pb={0.5}>
|
|
301
|
+
<Slider
|
|
302
|
+
aria-label="Density"
|
|
303
|
+
min={-2}
|
|
304
|
+
max={2}
|
|
305
|
+
step={1}
|
|
306
|
+
value={draftDensityLevel}
|
|
307
|
+
onChange={setDraftDensityLevel}
|
|
308
|
+
onChangeEnd={(value) => {
|
|
309
|
+
setDraftDensityLevel(value)
|
|
310
|
+
onDensityLevelChange(value)
|
|
311
|
+
}}
|
|
312
|
+
focusThumbOnChange={false}
|
|
313
|
+
>
|
|
314
|
+
<SliderTrack h="4px" bg="whiteAlpha.200">
|
|
315
|
+
<SliderFilledTrack bg="var(--accent)" />
|
|
316
|
+
</SliderTrack>
|
|
317
|
+
{DENSITY_STOPS.map((stop) => (
|
|
318
|
+
<Box
|
|
319
|
+
key={stop.value}
|
|
320
|
+
position="absolute"
|
|
321
|
+
left={`${((stop.value + 2) / 4) * 100}%`}
|
|
322
|
+
top="50%"
|
|
323
|
+
transform="translate(-50%, -50%)"
|
|
324
|
+
w={stop.value === draftDensityLevel ? '6px' : '2px'}
|
|
325
|
+
h={stop.value === draftDensityLevel ? '6px' : '10px'}
|
|
326
|
+
rounded="full"
|
|
327
|
+
bg={draftDensityLevel >= stop.value ? 'var(--accent)' : 'whiteAlpha.500'}
|
|
328
|
+
pointerEvents="none"
|
|
329
|
+
/>
|
|
330
|
+
))}
|
|
331
|
+
<SliderThumb boxSize="14px" bg="white" border="2px solid" borderColor="var(--accent)" />
|
|
332
|
+
</Slider>
|
|
333
|
+
<HStack justify="space-between" mt={2} px={0.5}>
|
|
334
|
+
{DENSITY_STOPS.map((stop) => (
|
|
335
|
+
<Text
|
|
336
|
+
key={stop.value}
|
|
337
|
+
fontSize="9px"
|
|
338
|
+
fontWeight={stop.value === draftDensityLevel ? 'bold' : 'medium'}
|
|
339
|
+
color={stop.value === draftDensityLevel ? 'whiteAlpha.900' : 'whiteAlpha.500'}
|
|
340
|
+
>
|
|
341
|
+
{stop.label}
|
|
342
|
+
</Text>
|
|
343
|
+
))}
|
|
344
|
+
</HStack>
|
|
345
|
+
</Box>
|
|
346
|
+
</Box>
|
|
347
|
+
)}
|
|
348
|
+
</VStack>
|
|
349
|
+
</PopoverBody>
|
|
350
|
+
</PopoverContent>
|
|
351
|
+
</Portal>
|
|
352
|
+
</Popover>
|
|
214
353
|
</>
|
|
215
354
|
)}
|
|
216
355
|
<Box w="1px" h="16px" bg="whiteAlpha.100" flexShrink={0} mx={0.5} />
|
|
@@ -325,55 +464,6 @@ function ViewFloatingMenu({
|
|
|
325
464
|
</>
|
|
326
465
|
)}
|
|
327
466
|
|
|
328
|
-
{onDensityLevelChange && (
|
|
329
|
-
<>
|
|
330
|
-
<Box w="1px" h="16px" bg="whiteAlpha.100" flexShrink={0} mx={0.5} />
|
|
331
|
-
<Tooltip label={`Density ${draftDensityLevel}`} placement="top" openDelay={200}>
|
|
332
|
-
<Box
|
|
333
|
-
w="92px"
|
|
334
|
-
h="28px"
|
|
335
|
-
px={2.5}
|
|
336
|
-
display="flex"
|
|
337
|
-
alignItems="center"
|
|
338
|
-
bg="whiteAlpha.50"
|
|
339
|
-
rounded="md"
|
|
340
|
-
>
|
|
341
|
-
<Slider
|
|
342
|
-
aria-label="Density"
|
|
343
|
-
min={-2}
|
|
344
|
-
max={2}
|
|
345
|
-
step={1}
|
|
346
|
-
value={draftDensityLevel}
|
|
347
|
-
onChange={setDraftDensityLevel}
|
|
348
|
-
onChangeEnd={(value) => {
|
|
349
|
-
setDraftDensityLevel(value)
|
|
350
|
-
onDensityLevelChange(value)
|
|
351
|
-
}}
|
|
352
|
-
focusThumbOnChange={false}
|
|
353
|
-
>
|
|
354
|
-
<SliderTrack h="3px" bg="whiteAlpha.200">
|
|
355
|
-
<SliderFilledTrack bg="var(--accent)" />
|
|
356
|
-
</SliderTrack>
|
|
357
|
-
{[-2, -1, 0, 1, 2].map((value) => (
|
|
358
|
-
<Box
|
|
359
|
-
key={value}
|
|
360
|
-
position="absolute"
|
|
361
|
-
left={`${((value + 2) / 4) * 100}%`}
|
|
362
|
-
top="50%"
|
|
363
|
-
transform="translate(-50%, -50%)"
|
|
364
|
-
w="1px"
|
|
365
|
-
h="9px"
|
|
366
|
-
bg={draftDensityLevel >= value ? 'var(--accent)' : 'whiteAlpha.400'}
|
|
367
|
-
pointerEvents="none"
|
|
368
|
-
/>
|
|
369
|
-
))}
|
|
370
|
-
<SliderThumb boxSize="12px" bg="white" border="2px solid" borderColor="var(--accent)" />
|
|
371
|
-
</Slider>
|
|
372
|
-
</Box>
|
|
373
|
-
</Tooltip>
|
|
374
|
-
</>
|
|
375
|
-
)}
|
|
376
|
-
|
|
377
467
|
{/* Draw mode toggle */}
|
|
378
468
|
<Tooltip
|
|
379
469
|
label={drawingMode ? 'Exit drawing mode' : 'Draw on diagram'}
|
package/src/demo/viewEditor.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface ViewEditorDemoOptions {
|
|
|
8
8
|
disableOnboarding?: boolean
|
|
9
9
|
hideFocusView?: boolean
|
|
10
10
|
hideExpandExtras?: boolean
|
|
11
|
+
defaultHiddenLayerTags?: string[]
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export const DEMO_VIEW_EDITOR_OPTIONS: Omit<ViewEditorDemoOptions, 'revealProgress'> = {
|
|
@@ -16,6 +17,7 @@ export const DEMO_VIEW_EDITOR_OPTIONS: Omit<ViewEditorDemoOptions, 'revealProgre
|
|
|
16
17
|
disableOnboarding: true,
|
|
17
18
|
hideFocusView: true,
|
|
18
19
|
hideExpandExtras: true,
|
|
20
|
+
defaultHiddenLayerTags: ['view_layers:admin', 'view_layers:ops'],
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
// ─── Pages ───────────────────────────────────────────────────────────────────
|
|
11
|
-
export { default as ViewEditor } from './pages/ViewEditor'
|
|
11
|
+
export { default as ViewEditor, type ViewEditorPermissions } from './pages/ViewEditor'
|
|
12
12
|
export { default as ViewsPage } from './pages/Views'
|
|
13
13
|
export { default as ViewsGrid } from './pages/ViewsGrid'
|
|
14
14
|
export { default as Dependencies } from './pages/Dependencies'
|
|
@@ -257,12 +257,12 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
257
257
|
{noDiagrams ? 'No diagrams to explore yet' : 'Your diagrams are empty'}
|
|
258
258
|
</Text>
|
|
259
259
|
<Text color="gray.500" fontSize="sm" maxW="400px">
|
|
260
|
-
{noDiagrams
|
|
261
|
-
? 'Start by creating your first diagram in the workspace.'
|
|
260
|
+
{noDiagrams
|
|
261
|
+
? 'Start by creating your first diagram in the workspace.'
|
|
262
262
|
: 'Add elements to your diagrams in the editor to see them rendered on this infinite canvas.'}
|
|
263
263
|
</Text>
|
|
264
264
|
</VStack>
|
|
265
|
-
|
|
265
|
+
|
|
266
266
|
{!sharedToken && (
|
|
267
267
|
<Button size="sm" colorScheme="blue" onClick={() => navigate('/views')} borderRadius="full" px={6}>
|
|
268
268
|
{noDiagrams ? 'Create First Diagram' : 'Go to Editor'}
|
|
@@ -349,7 +349,7 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
349
349
|
onEnabledChange={setCrossBranchEnabled}
|
|
350
350
|
onBudgetChange={setCrossBranchConnectorBudget}
|
|
351
351
|
onPriorityChange={setCrossBranchConnectorPriority}
|
|
352
|
-
label="
|
|
352
|
+
label="Filters"
|
|
353
353
|
/>
|
|
354
354
|
|
|
355
355
|
{(allTags.length > 0 || layers.length > 0) && (
|
|
@@ -100,6 +100,8 @@ const nodeTypes = {
|
|
|
100
100
|
const edgeTypes = { default: ViewBezierConnector, contextStraightConnector: ContextStraightConnector, proxyConnectorEdge: ProxyConnectorEdge }
|
|
101
101
|
const EMPTY_LINKS: ViewConnector[] = []
|
|
102
102
|
const VIEW_EDITOR_MIN_ZOOM_FLOOR = 0.12
|
|
103
|
+
const VIEW_EDITOR_INITIAL_FIT_PADDING = 0.25
|
|
104
|
+
const VIEW_EDITOR_FOCUS_FIT_PADDING = 0.35
|
|
103
105
|
const VIEW_EDITOR_EMPTY_EXTENT_RATIO = 0.75
|
|
104
106
|
const VIEW_EDITOR_PAN_MARGIN_RATIO = 0.25
|
|
105
107
|
const VIEW_EDITOR_PAN_MARGIN_MIN = 180
|
|
@@ -159,6 +161,19 @@ function viewSnapshotsEqual(left: ViewMetadataSnapshot, right: ViewMetadataSnaps
|
|
|
159
161
|
return left.id === right.id && left.name === right.name && (left.level_label ?? '') === (right.level_label ?? '')
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
function nodesMatchCurrentView(nodes: RFNode[], elements: PlacedElement[], viewId: number | null) {
|
|
165
|
+
if (viewId === null || elements.length === 0) return false
|
|
166
|
+
if (!elements.every((element) => element.view_id === viewId)) return false
|
|
167
|
+
|
|
168
|
+
const nodesById = new Map(nodes.map((node) => [node.id, node]))
|
|
169
|
+
return elements.every((element) => {
|
|
170
|
+
const node = nodesById.get(String(element.element_id))
|
|
171
|
+
return node !== undefined &&
|
|
172
|
+
Math.abs(node.position.x - (element.position_x ?? 0)) < 0.5 &&
|
|
173
|
+
Math.abs(node.position.y - (element.position_y ?? 0)) < 0.5
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
162
177
|
function alphaColor(color: string, opacity: number): string {
|
|
163
178
|
if (opacity >= 1) return color
|
|
164
179
|
return `color-mix(in srgb, ${color} ${Math.round(opacity * 100)}%, transparent)`
|
|
@@ -193,12 +208,21 @@ function canonicalNodePairKey(leftId: string, rightId: string) {
|
|
|
193
208
|
|
|
194
209
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
195
210
|
|
|
196
|
-
interface
|
|
211
|
+
export interface ViewEditorPermissions {
|
|
212
|
+
canEdit?: boolean
|
|
213
|
+
isOwner?: boolean
|
|
214
|
+
isFreePlan?: boolean
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface Props extends CoreUISlots, ViewEditorPermissions {
|
|
197
218
|
demoOptions?: ViewEditorDemoOptions
|
|
198
219
|
}
|
|
199
220
|
|
|
200
221
|
function ViewEditorInner({
|
|
201
222
|
demoOptions,
|
|
223
|
+
canEdit = true,
|
|
224
|
+
isOwner = true,
|
|
225
|
+
isFreePlan = false,
|
|
202
226
|
canvasOverlaySlot,
|
|
203
227
|
toolbarSlot,
|
|
204
228
|
shareSlot,
|
|
@@ -225,10 +249,6 @@ function ViewEditorInner({
|
|
|
225
249
|
undo: undoViewEdit,
|
|
226
250
|
redo: redoViewEdit,
|
|
227
251
|
} = useViewEditHistory()
|
|
228
|
-
const canEdit = true
|
|
229
|
-
const isOwner = true
|
|
230
|
-
const isFreePlan = false
|
|
231
|
-
|
|
232
252
|
const setHeader = useSetHeader()
|
|
233
253
|
const isMobileLayout = useBreakpointValue({ base: true, md: false }) ?? false
|
|
234
254
|
const [densityLevel, setDensityLevel] = useState(0)
|
|
@@ -375,7 +395,7 @@ function ViewEditorInner({
|
|
|
375
395
|
}, [])
|
|
376
396
|
|
|
377
397
|
const [layers, setLayers] = useState<import('../../types').ViewLayer[]>([])
|
|
378
|
-
const [hiddenLayerTags, setHiddenLayerTags] = useState<string[]>([])
|
|
398
|
+
const [hiddenLayerTags, setHiddenLayerTags] = useState<string[]>(() => demoOptions?.defaultHiddenLayerTags ?? [])
|
|
379
399
|
const hiddenLayerTagsRef = useRef<string[]>([])
|
|
380
400
|
hiddenLayerTagsRef.current = hiddenLayerTags
|
|
381
401
|
const [hoveredLayerTags, setHoveredLayerTags] = useState<string[] | null>(null)
|
|
@@ -669,7 +689,7 @@ function ViewEditorInner({
|
|
|
669
689
|
useEffect(() => {
|
|
670
690
|
const unsub = vscodeBridge.onMessage(async (msg: ExtensionToWebviewMessage) => {
|
|
671
691
|
if (msg.type === 'focus-element') {
|
|
672
|
-
fitView({ nodes: [{ id: String(msg.elementId) }], duration: 800, padding:
|
|
692
|
+
fitView({ nodes: [{ id: String(msg.elementId) }], duration: 800, padding: VIEW_EDITOR_FOCUS_FIT_PADDING })
|
|
673
693
|
} else if (msg.type === 'element-placed') {
|
|
674
694
|
if (viewId === null) return
|
|
675
695
|
try {
|
|
@@ -1274,6 +1294,7 @@ function ViewEditorInner({
|
|
|
1274
1294
|
if (!rfReadyRef.current || !needsFitView.current) return
|
|
1275
1295
|
const nodes = rfNodesRef.current
|
|
1276
1296
|
if (nodes.length === 0) return
|
|
1297
|
+
if (!nodesMatchCurrentView(nodes, viewElementsRef.current, viewIdRef.current)) return
|
|
1277
1298
|
if (!nodes.every((n) => typeof n.width === 'number' && n.width > 0 && typeof n.height === 'number' && n.height > 0)) return
|
|
1278
1299
|
|
|
1279
1300
|
if (clampedRevealProgress !== null) {
|
|
@@ -1283,13 +1304,17 @@ function ViewEditorInner({
|
|
|
1283
1304
|
return
|
|
1284
1305
|
}
|
|
1285
1306
|
|
|
1286
|
-
const ok = safeFitView({ duration: 0, padding:
|
|
1307
|
+
const ok = safeFitView({ duration: 0, padding: VIEW_EDITOR_INITIAL_FIT_PADDING, minZoom: computedMinZoom, maxZoom: 4 })
|
|
1287
1308
|
if (ok) needsFitView.current = false
|
|
1288
1309
|
else setTimeout(() => { if (needsFitView.current) maybeFitView() }, 50)
|
|
1289
|
-
}, [applyDemoRevealViewport, clampedRevealProgress, safeFitView, rfNodesRef])
|
|
1310
|
+
}, [applyDemoRevealViewport, clampedRevealProgress, computedMinZoom, safeFitView, rfNodesRef, viewElementsRef, viewIdRef])
|
|
1290
1311
|
|
|
1291
1312
|
const onRFInit = useCallback(() => { rfReadyRef.current = true; maybeFitView() }, [maybeFitView])
|
|
1292
1313
|
|
|
1314
|
+
useEffect(() => {
|
|
1315
|
+
needsFitView.current = true
|
|
1316
|
+
}, [viewId])
|
|
1317
|
+
|
|
1293
1318
|
useEffect(() => { maybeFitView() }, [rfNodes, maybeFitView])
|
|
1294
1319
|
|
|
1295
1320
|
useEffect(() => {
|
|
@@ -1310,7 +1335,6 @@ function ViewEditorInner({
|
|
|
1310
1335
|
closeElementPanelRef.current()
|
|
1311
1336
|
closeConnectorPanelRef.current()
|
|
1312
1337
|
closeProxyConnectorPanelRef.current()
|
|
1313
|
-
needsFitView.current = true
|
|
1314
1338
|
}, [clearEditHistory, viewId])
|
|
1315
1339
|
|
|
1316
1340
|
// ── Dynamic viewport bounds ────────────────────────────────────────────────
|