feat: node resize less than min content

This commit is contained in:
Simula_r
2025-11-16 23:38:00 -08:00
parent 4dab27a84e
commit 333ba0ee1a
11 changed files with 90 additions and 41 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div class="editable-text">
<span v-if="!isEditing">
<div class="editable-text min-w-0">
<span v-if="!isEditing" class="block truncate">
{{ modelValue }}
</span>
<!-- Avoid double triggering finishEditing event when keyup.enter is triggered -->

View File

@@ -88,7 +88,7 @@
<!-- Node Body - rendered based on LOD level and collapsed state -->
<div
class="flex min-h-0 flex-1 flex-col gap-4 pb-4"
class="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden pb-4"
:data-testid="`node-body-${nodeData.id}`"
>
<!-- Slots only rendered at full detail -->
@@ -266,14 +266,15 @@ const handleContextMenu = (event: MouseEvent) => {
}
onMounted(() => {
// Set initial DOM size from layout store, but respect intrinsic content minimum
// Set initial DOM size from layout store, respecting intrinsic minimum for initial render
// Note: Once manually resized, users can go smaller than intrinsic size for responsive behavior
if (size.value && nodeContainerRef.value && transformState) {
const intrinsicMin = calculateIntrinsicSize(
nodeContainerRef.value,
transformState.camera.z
)
// Use the larger of stored size or intrinsic minimum
// Use the larger of stored size or intrinsic minimum for initial render
const finalWidth = Math.max(size.value.width, intrinsicMin.width)
const finalHeight = Math.max(size.value.height, intrinsicMin.height)
@@ -405,3 +406,10 @@ const nodeMedia = computed(() => {
const nodeContainerRef = ref<HTMLDivElement>()
</script>
<style scoped>
.lg-node {
/* Minimum width to ensure widgets have enough space for responsive truncation */
min-width: 200px;
}
</style>

View File

@@ -14,9 +14,9 @@
:data-testid="`node-header-${nodeData?.id || ''}`"
@dblclick="handleDoubleClick"
>
<div class="flex items-center justify-between gap-2.5">
<div class="flex min-w-0 items-center justify-between gap-2.5">
<!-- Collapse/Expand Button -->
<div class="relative flex items-center gap-2.5">
<div class="relative flex min-w-0 flex-1 items-center gap-2.5">
<div class="lod-toggle flex shrink-0 items-center px-0.5">
<IconButton
size="fit-content"
@@ -40,7 +40,7 @@
<!-- Node Title -->
<div
v-tooltip.top="tooltipConfig"
class="lod-toggle flex flex-1 items-center gap-2 truncate text-sm font-bold"
class="lod-toggle flex min-w-0 flex-1 items-center gap-2 truncate text-sm font-bold"
data-testid="node-title"
>
<EditableText

View File

@@ -6,7 +6,7 @@
v-else
:class="
cn(
'lg-node-widgets flex flex-col gap-2 pr-3',
'lg-node-widgets flex flex-col gap-2 pr-3 min-w-0',
shouldHandleNodePointerEvents
? 'pointer-events-auto'
: 'pointer-events-none'
@@ -19,7 +19,7 @@
<div
v-for="(widget, index) in processedWidgets"
:key="`widget-${index}-${widget.name}`"
class="lg-widget-container group flex items-center"
class="lg-widget-container group flex min-w-0 items-center"
>
<!-- Widget Input Slot Dot -->
@@ -44,7 +44,7 @@
:widget="widget.simplified"
:model-value="widget.value"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
class="flex-1"
class="min-w-0 flex-1"
@update:model-value="widget.updateHandler"
/>
</div>

View File

@@ -36,7 +36,7 @@ export function useNodeResize(
const isResizing = ref(false)
const resizeStartPos = ref<Position | null>(null)
const resizeStartSize = ref<Size | null>(null)
const intrinsicMinSize = ref<Size | null>(null)
const intrinsicMinHeight = ref<number | null>(null)
// Snap-to-grid functionality
const { shouldSnap, applySnapToSize } = useNodeSnap()
@@ -60,7 +60,7 @@ export function useNodeResize(
isResizing.value = true
resizeStartPos.value = { x: event.clientX, y: event.clientY }
// Get current node size from the DOM and calculate intrinsic min size
// Get current node size from the DOM
const nodeElement = target.closest('[data-node-id]')
if (!(nodeElement instanceof HTMLElement)) return
@@ -73,15 +73,16 @@ export function useNodeResize(
height: rect.height / scale
}
// Calculate intrinsic content size (minimum based on content)
intrinsicMinSize.value = calculateIntrinsicSize(nodeElement, scale)
// Calculate intrinsic height to prevent resizing shorter than content
const intrinsicSize = calculateIntrinsicSize(nodeElement, scale)
intrinsicMinHeight.value = intrinsicSize.height
const handlePointerMove = (moveEvent: PointerEvent) => {
if (
!isResizing.value ||
!resizeStartPos.value ||
!resizeStartSize.value ||
!intrinsicMinSize.value
intrinsicMinHeight.value === null
)
return
@@ -93,16 +94,16 @@ export function useNodeResize(
const scaledDx = dx / scale
const scaledDy = dy / scale
// Apply constraints: only minimum size based on content, no maximum
// Calculate new size
const newWidth = resizeStartSize.value.width + scaledDx
const newHeight = resizeStartSize.value.height + scaledDy
// Apply constraints:
// - Width: No constraint - let CSS handle minimum via widget min-width
// - Height: Respect intrinsic height to keep all content visible
const constrainedSize = {
width: Math.max(
intrinsicMinSize.value.width,
resizeStartSize.value.width + scaledDx
),
height: Math.max(
intrinsicMinSize.value.height,
resizeStartSize.value.height + scaledDy
)
width: newWidth,
height: Math.max(intrinsicMinHeight.value, newHeight)
}
// Apply snap-to-grid if shift is held or always snap is enabled
@@ -122,7 +123,7 @@ export function useNodeResize(
isResizing.value = false
resizeStartPos.value = null
resizeStartSize.value = null
intrinsicMinSize.value = null
intrinsicMinHeight.value = null
// Stop tracking shift key state
stopShiftSync()

View File

@@ -16,9 +16,12 @@
}"
@update:model-value="onPickerUpdate"
/>
<span class="text-xs" data-testid="widget-color-text">{{
toHexFromFormat(localValue, format)
}}</span>
<span
class="min-w-0 truncate text-xs"
style="min-width: 3ch"
data-testid="widget-color-text"
>{{ toHexFromFormat(localValue, format) }}</span
>
</label>
</WidgetLayoutField>
</template>

View File

@@ -114,6 +114,10 @@ const inputNumberPt = useNumberWidgetButtonPt({
</template>
<style scoped>
:deep(.p-inputnumber) {
min-width: 0;
}
:deep(.p-inputnumber-input) {
background-color: transparent;
border: 1px solid var(--node-stroke);
@@ -122,6 +126,8 @@ const inputNumberPt = useNumberWidgetButtonPt({
height: 1.625rem;
margin: 1px 0;
box-shadow: none;
min-width: 3ch;
text-overflow: ellipsis;
}
:deep(.p-inputnumber-button.p-disabled .pi),

View File

@@ -3,9 +3,10 @@
<InputText
v-model="localValue"
v-bind="filteredProps"
:class="cn(WidgetInputBaseClass, 'w-full text-xs py-2 px-4')"
:class="cn(WidgetInputBaseClass, 'w-full text-xs py-2 px-4 min-w-0')"
:aria-label="widget.name"
size="small"
style="min-width: 3ch"
@update:model-value="onChange"
/>
</WidgetLayoutField>

View File

@@ -4,13 +4,16 @@
v-model="localValue"
:options="multiSelectOptions"
v-bind="combinedProps"
class="w-full text-xs"
class="w-full min-w-0 text-xs"
:aria-label="widget.name"
size="small"
display="chip"
:pt="{
option: 'text-xs'
option: 'text-xs',
label: 'truncate min-w-0',
root: 'min-w-0'
}"
style="min-width: 3ch"
@update:model-value="onChange"
/>
</WidgetLayoutField>
@@ -72,3 +75,16 @@ const multiSelectOptions = computed((): T[] => {
return []
})
</script>
<style scoped>
:deep(.p-multiselect) {
min-width: 3ch !important;
}
:deep(.p-multiselect-label) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
</style>

View File

@@ -4,12 +4,15 @@
v-model="localValue"
:options="selectOptions"
v-bind="combinedProps"
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
:class="cn(WidgetInputBaseClass, 'w-full text-xs truncate min-w-0')"
:aria-label="widget.name"
size="small"
:pt="{
option: 'text-xs'
option: 'text-xs',
label: 'truncate min-w-0',
root: 'min-w-0'
}"
style="min-width: 3ch"
data-capture-wheel="true"
@update:model-value="onChange"
/>
@@ -68,3 +71,16 @@ const selectOptions = computed(() => {
return []
})
</script>
<style scoped>
:deep(.p-select) {
min-width: 3ch !important;
}
:deep(.p-select-label) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
</style>

View File

@@ -11,21 +11,19 @@ defineProps<{
</script>
<template>
<div
class="flex h-[30px] items-center justify-between gap-2 overscroll-contain"
>
<div class="relative flex h-6 items-center">
<div class="flex h-[30px] min-w-0 items-center justify-between gap-2">
<div class="relative flex h-6 min-w-0 items-center" style="min-width: 3ch">
<p
v-if="widget.name"
class="lod-toggle w-28 flex-1 truncate text-sm font-normal text-node-component-slot-text"
class="lod-toggle w-16 min-w-0 truncate text-sm font-normal text-node-component-slot-text"
>
{{ widget.label || widget.name }}
</p>
<LODFallback />
</div>
<div class="relative">
<div class="relative min-w-0 flex-1" style="min-width: 3ch">
<div
class="lod-toggle w-75 cursor-default"
class="lod-toggle min-w-0 cursor-default"
@pointerdown.stop="noop"
@pointermove.stop="noop"
@pointerup.stop="noop"