Merge latest main (v1.29.2) into bl-selective-snapshot-update

This commit is contained in:
bymyself
2025-10-14 11:56:10 -07:00
27 changed files with 340 additions and 21 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -60,7 +60,6 @@ async function getInputLinkDetails(
) )
} }
// Test helpers to reduce repetition across cases
function slotLocator( function slotLocator(
page: Page, page: Page,
nodeId: NodeId, nodeId: NodeId,
@@ -789,6 +788,45 @@ test.describe('Vue Node Link Interaction', () => {
}) })
}) })
test('should batch disconnect all links with ctrl+alt+click on slot', async ({
comfyPage
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(clipNode && samplerNode).toBeTruthy()
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 1 },
() => comfyPage.nextFrame()
)
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 2 },
() => comfyPage.nextFrame()
)
const clipOutput = await clipNode.getOutput(0)
expect(await clipOutput.getLinkCount()).toBe(2)
const clipOutputSlot = slotLocator(comfyPage.page, clipNode.id, 0, false)
await clipOutputSlot.dispatchEvent('pointerdown', {
button: 0,
buttons: 1,
ctrlKey: true,
altKey: true,
shiftKey: false,
bubbles: true,
cancelable: true
})
await comfyPage.nextFrame()
expect(await clipOutput.getLinkCount()).toBe(0)
})
test.describe('Release actions (Shift-drop)', () => { test.describe('Release actions (Shift-drop)', () => {
test('Context menu opens and endpoint is pinned on Shift-drop', async ({ test('Context menu opens and endpoint is pinned on Shift-drop', async ({
comfyPage, comfyPage,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -1,7 +1,7 @@
{ {
"name": "@comfyorg/comfyui-frontend", "name": "@comfyorg/comfyui-frontend",
"private": true, "private": true,
"version": "1.29.1", "version": "1.29.2",
"type": "module", "type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org", "homepage": "https://comfy.org",

View File

@@ -89,6 +89,21 @@
--color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15); --color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15);
--color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1); --color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1);
--color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4); --color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4);
--color-alpha-charcoal-600-30: color-mix(
in srgb,
var(--color-charcoal-600) 30%,
transparent
);
--color-alpha-stone-100-20: color-mix(
in srgb,
var(--color-stone-100) 20%,
transparent
);
--color-alpha-gray-500-50: color-mix(
in srgb,
var(--color-gray-500) 50%,
transparent
);
/* PrimeVue pulled colors */ /* PrimeVue pulled colors */
--color-muted: var(--p-text-muted-color); --color-muted: var(--p-text-muted-color);
@@ -155,6 +170,8 @@
from var(--color-zinc-500) r g b / 10% from var(--color-zinc-500) r g b / 10%
); );
--node-component-widget-skeleton-surface: var(--color-zinc-300); --node-component-widget-skeleton-surface: var(--color-zinc-300);
--node-component-disabled: var(--color-alpha-stone-100-20);
--node-icon-disabled: var(--color-alpha-gray-500-50);
--node-stroke: var(--color-gray-400); --node-stroke: var(--color-gray-400);
--node-stroke-selected: var(--color-accent-primary); --node-stroke-selected: var(--color-accent-primary);
--node-stroke-error: var(--color-error); --node-stroke-error: var(--color-error);
@@ -184,6 +201,8 @@
--node-component-tooltip-border: var(--color-slate-300); --node-component-tooltip-border: var(--color-slate-300);
--node-component-tooltip-surface: var(--color-charcoal-800); --node-component-tooltip-surface: var(--color-charcoal-800);
--node-component-widget-skeleton-surface: var(--color-zinc-800); --node-component-widget-skeleton-surface: var(--color-zinc-800);
--node-component-disabled: var(--color-alpha-charcoal-600-30);
--node-icon-disabled: var(--color-alpha-stone-100-20);
--node-stroke: var(--color-stone-200); --node-stroke: var(--color-stone-200);
--node-stroke-selected: var(--color-pure-white); --node-stroke-selected: var(--color-pure-white);
--node-stroke-error: var(--color-error); --node-stroke-error: var(--color-error);
@@ -224,6 +243,8 @@
--color-node-component-widget-skeleton-surface: var( --color-node-component-widget-skeleton-surface: var(
--node-component-widget-skeleton-surface --node-component-widget-skeleton-surface
); );
--color-node-component-disabled: var(--node-component-disabled);
--color-node-icon-disabled: var(--node-icon-disabled);
--color-node-stroke: var(--node-stroke); --color-node-stroke: var(--node-stroke);
--color-node-stroke-selected: var(--node-stroke-selected); --color-node-stroke-selected: var(--node-stroke-selected);
--color-node-stroke-error: var(--node-stroke-error); --color-node-stroke-error: var(--node-stroke-error);

View File

@@ -178,8 +178,9 @@ app.registerExtension({
audioUIWidget.options.canvasOnly = true audioUIWidget.options.canvasOnly = true
const onAudioWidgetUpdate = () => { const onAudioWidgetUpdate = () => {
if (typeof audioWidget.value !== 'string') return
audioUIWidget.element.src = api.apiURL( audioUIWidget.element.src = api.apiURL(
getResourceURL(...splitFilePath(audioWidget.value as string)) getResourceURL(...splitFilePath(audioWidget.value))
) )
} }
// Initially load default audio file to audioUIWidget. // Initially load default audio file to audioUIWidget.

View File

@@ -832,6 +832,11 @@
"guidance": { "guidance": {
"name": "guidance" "name": "guidance"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"CLIPTextEncodeHiDream": { "CLIPTextEncodeHiDream": {
@@ -871,6 +876,11 @@
"mt5xl": { "mt5xl": {
"name": "mt5xl" "name": "mt5xl"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"CLIPTextEncodeLumina2": { "CLIPTextEncodeLumina2": {
@@ -937,6 +947,11 @@
"empty_padding": { "empty_padding": {
"name": "empty_padding" "name": "empty_padding"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"CLIPTextEncodeSDXL": { "CLIPTextEncodeSDXL": {
@@ -1475,10 +1490,12 @@
}, },
"outputs": { "outputs": {
"0": { "0": {
"name": "positive" "name": "positive",
"tooltip": null
}, },
"1": { "1": {
"name": "negative" "name": "negative",
"tooltip": null
} }
} }
}, },
@@ -1974,6 +1991,11 @@
"batch_size": { "batch_size": {
"name": "batch_size" "name": "batch_size"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"EmptyHunyuanLatentVideo": { "EmptyHunyuanLatentVideo": {
@@ -1991,6 +2013,11 @@
"batch_size": { "batch_size": {
"name": "batch_size" "name": "batch_size"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"EmptyImage": { "EmptyImage": {
@@ -2113,6 +2140,11 @@
"batch_size": { "batch_size": {
"name": "batch_size" "name": "batch_size"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"Epsilon Scaling": { "Epsilon Scaling": {
@@ -2200,6 +2232,11 @@
"conditioning": { "conditioning": {
"name": "conditioning" "name": "conditioning"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"FluxGuidance": { "FluxGuidance": {
@@ -2211,6 +2248,11 @@
"guidance": { "guidance": {
"name": "guidance" "name": "guidance"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"FluxKontextImageScale": { "FluxKontextImageScale": {
@@ -2220,6 +2262,11 @@
"image": { "image": {
"name": "image" "name": "image"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"FluxKontextMaxImageNode": { "FluxKontextMaxImageNode": {
@@ -2272,6 +2319,11 @@
"reference_latents_method": { "reference_latents_method": {
"name": "reference_latents_method" "name": "reference_latents_method"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"FluxKontextProImageNode": { "FluxKontextProImageNode": {
@@ -2629,6 +2681,10 @@
"name": "files", "name": "files",
"tooltip": "Optional file(s) to use as context for the model. Accepts inputs from the Gemini Generate Content Input Files node." "tooltip": "Optional file(s) to use as context for the model. Accepts inputs from the Gemini Generate Content Input Files node."
}, },
"aspect_ratio": {
"name": "aspect_ratio",
"tooltip": "Defaults to matching the output image size to that of your input image, or otherwise generates 1:1 squares."
},
"control_after_generate": { "control_after_generate": {
"name": "control after generate" "name": "control after generate"
} }
@@ -2870,10 +2926,12 @@
}, },
"outputs": { "outputs": {
"0": { "0": {
"name": "positive" "name": "positive",
"tooltip": null
}, },
"1": { "1": {
"name": "latent" "name": "latent",
"tooltip": null
} }
} }
}, },
@@ -2895,13 +2953,16 @@
}, },
"outputs": { "outputs": {
"0": { "0": {
"name": "positive" "name": "positive",
"tooltip": null
}, },
"1": { "1": {
"name": "negative" "name": "negative",
"tooltip": null
}, },
"2": { "2": {
"name": "latent" "name": "latent",
"tooltip": null
} }
} }
}, },
@@ -3478,6 +3539,11 @@
"image": { "image": {
"name": "image" "name": "image"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"ImageYUVToRGB": { "ImageYUVToRGB": {
@@ -3582,6 +3648,11 @@
"alpha": { "alpha": {
"name": "alpha" "name": "alpha"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"KarrasScheduler": { "KarrasScheduler": {
@@ -4209,6 +4280,11 @@
"samples2": { "samples2": {
"name": "samples2" "name": "samples2"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentApplyOperation": { "LatentApplyOperation": {
@@ -4220,6 +4296,11 @@
"operation": { "operation": {
"name": "operation" "name": "operation"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentApplyOperationCFG": { "LatentApplyOperationCFG": {
@@ -4231,6 +4312,11 @@
"operation": { "operation": {
"name": "operation" "name": "operation"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentBatch": { "LatentBatch": {
@@ -4242,6 +4328,11 @@
"samples2": { "samples2": {
"name": "samples2" "name": "samples2"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentBatchSeedBehavior": { "LatentBatchSeedBehavior": {
@@ -4253,6 +4344,11 @@
"seed_behavior": { "seed_behavior": {
"name": "seed_behavior" "name": "seed_behavior"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentBlend": { "LatentBlend": {
@@ -4324,6 +4420,11 @@
"dim": { "dim": {
"name": "dim" "name": "dim"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentCrop": { "LatentCrop": {
@@ -4361,6 +4462,11 @@
"amount": { "amount": {
"name": "amount" "name": "amount"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentFlip": { "LatentFlip": {
@@ -4400,6 +4506,11 @@
"ratio": { "ratio": {
"name": "ratio" "name": "ratio"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentMultiply": { "LatentMultiply": {
@@ -4411,6 +4522,11 @@
"multiplier": { "multiplier": {
"name": "multiplier" "name": "multiplier"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentOperationSharpen": { "LatentOperationSharpen": {
@@ -4425,6 +4541,11 @@
"alpha": { "alpha": {
"name": "alpha" "name": "alpha"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentOperationTonemapReinhard": { "LatentOperationTonemapReinhard": {
@@ -4433,6 +4554,11 @@
"multiplier": { "multiplier": {
"name": "multiplier" "name": "multiplier"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentRotate": { "LatentRotate": {
@@ -4455,6 +4581,11 @@
"samples2": { "samples2": {
"name": "samples2" "name": "samples2"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"LatentUpscale": { "LatentUpscale": {
@@ -7701,6 +7832,10 @@
"name": "video", "name": "video",
"tooltip": "The reference video used to generate the output video. Must be at least 5 seconds long. Videos longer than 5s will be automatically trimmed. Only MP4 format supported." "tooltip": "The reference video used to generate the output video. Must be at least 5 seconds long. Videos longer than 5s will be automatically trimmed. Only MP4 format supported."
}, },
"steps": {
"name": "steps",
"tooltip": "Number of inference steps"
},
"control_type": { "control_type": {
"name": "control_type" "name": "control_type"
}, },
@@ -8106,6 +8241,11 @@
"upscale_method": { "upscale_method": {
"name": "upscale_method" "name": "upscale_method"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"PerpNeg": { "PerpNeg": {
@@ -8407,7 +8547,7 @@
}, },
"mask": { "mask": {
"name": "mask", "name": "mask",
"tooltip": "Use the mask to define areas in the video to replace" "tooltip": "Use the mask to define areas in the video to replace."
}, },
"prompt_text": { "prompt_text": {
"name": "prompt_text" "name": "prompt_text"
@@ -8418,6 +8558,10 @@
"seed": { "seed": {
"name": "seed" "name": "seed"
}, },
"region_to_modify": {
"name": "region_to_modify",
"tooltip": "Plaintext description of the object / region to modify."
},
"control_after_generate": { "control_after_generate": {
"name": "control after generate" "name": "control after generate"
} }
@@ -8635,6 +8779,14 @@
"mode": { "mode": {
"name": "mode" "name": "mode"
} }
},
"outputs": {
"0": {
"tooltip": null
},
"1": {
"tooltip": null
}
} }
}, },
"Preview3D": { "Preview3D": {
@@ -10248,6 +10400,11 @@
"rescaling_scale": { "rescaling_scale": {
"name": "rescaling_scale" "name": "rescaling_scale"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"SkipLayerGuidanceDiTSimple": { "SkipLayerGuidanceDiTSimple": {
@@ -10269,6 +10426,11 @@
"end_percent": { "end_percent": {
"name": "end_percent" "name": "end_percent"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"SkipLayerGuidanceSD3": { "SkipLayerGuidanceSD3": {
@@ -10290,6 +10452,11 @@
"end_percent": { "end_percent": {
"name": "end_percent" "name": "end_percent"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"SolidMask": { "SolidMask": {
@@ -10329,6 +10496,14 @@
"image": { "image": {
"name": "image" "name": "image"
} }
},
"outputs": {
"0": {
"tooltip": null
},
"1": {
"tooltip": null
}
} }
}, },
"SplitSigmas": { "SplitSigmas": {
@@ -11152,6 +11327,11 @@
"name": "image_interleave", "name": "image_interleave",
"tooltip": "How much the image influences things vs the text prompt. Higher number means more influence from the text prompt." "tooltip": "How much the image influences things vs the text prompt. Higher number means more influence from the text prompt."
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"TextEncodeQwenImageEdit": { "TextEncodeQwenImageEdit": {
@@ -11379,6 +11559,11 @@
"clip_name3": { "clip_name3": {
"name": "clip_name3" "name": "clip_name3"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"TripoConversionNode": { "TripoConversionNode": {
@@ -11760,6 +11945,11 @@
"model_name": { "model_name": {
"name": "model_name" "name": "model_name"
} }
},
"outputs": {
"0": {
"tooltip": null
}
} }
}, },
"USOStyleReference": { "USOStyleReference": {

View File

@@ -595,6 +595,13 @@ export function useSlotLinkInteraction({
event.altKey && event.altKey &&
!event.shiftKey !event.shiftKey
const shouldBatchDisconnectOutputLinks =
isOutputSlot &&
hasExistingOutputLink &&
ctrlOrMeta &&
event.altKey &&
!event.shiftKey
const existingInputLink = const existingInputLink =
isInputSlot && inputLinkId != null isInputSlot && inputLinkId != null
? graph.getLink(inputLinkId) ? graph.getLink(inputLinkId)
@@ -604,6 +611,14 @@ export function useSlotLinkInteraction({
resolvedNode.disconnectInput(index, true) resolvedNode.disconnectInput(index, true)
} }
if (shouldBatchDisconnectOutputLinks && resolvedNode) {
resolvedNode.disconnectOutput(index)
app.canvas?.setDirty(true, true)
event.preventDefault()
event.stopPropagation()
return
}
const baseDirection = isInputSlot const baseDirection = isInputSlot
? inputSlot?.dir ?? LinkDirection.LEFT ? inputSlot?.dir ?? LinkDirection.LEFT
: outputSlot?.dir ?? LinkDirection.RIGHT : outputSlot?.dir ?? LinkDirection.RIGHT

View File

@@ -10,6 +10,7 @@ import {
filterWidgetProps filterWidgetProps
} from '@/utils/widgetPropFilter' } from '@/utils/widgetPropFilter'
import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt'
import { WidgetInputBaseClass } from './layout' import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue' import WidgetLayoutField from './layout/WidgetLayoutField.vue'
@@ -65,17 +66,24 @@ const useGrouping = computed(() => {
// Check if increment/decrement buttons should be disabled due to precision limits // Check if increment/decrement buttons should be disabled due to precision limits
const buttonsDisabled = computed(() => { const buttonsDisabled = computed(() => {
const currentValue = localValue.value || 0 const currentValue = localValue.value ?? 0
return !Number.isSafeInteger(currentValue) return (
!Number.isFinite(currentValue) ||
Math.abs(currentValue) > Number.MAX_SAFE_INTEGER
)
}) })
// Tooltip message for disabled buttons
const buttonTooltip = computed(() => { const buttonTooltip = computed(() => {
if (buttonsDisabled.value) { if (buttonsDisabled.value) {
return 'Increment/decrement disabled: value exceeds JavaScript precision limit (±2^53)' return 'Increment/decrement disabled: value exceeds JavaScript precision limit (±2^53)'
} }
return null return null
}) })
const inputNumberPt = useNumberWidgetButtonPt({
roundedLeft: true,
roundedRight: true
})
</script> </script>
<template> <template>
@@ -84,19 +92,14 @@ const buttonTooltip = computed(() => {
<InputNumber <InputNumber
v-model="localValue" v-model="localValue"
v-bind="filteredProps" v-bind="filteredProps"
:show-buttons="!buttonsDisabled"
button-layout="horizontal" button-layout="horizontal"
size="small" size="small"
:step="stepValue" :step="stepValue"
:use-grouping="useGrouping" :use-grouping="useGrouping"
:class="cn(WidgetInputBaseClass, 'w-full text-xs')" :class="cn(WidgetInputBaseClass, 'w-full text-xs')"
:aria-label="widget.name" :aria-label="widget.name"
:pt="{ :show-buttons="!buttonsDisabled"
incrementButton: :pt="inputNumberPt"
'!rounded-r-lg bg-transparent border-none hover:bg-zinc-500/30 active:bg-zinc-500/40',
decrementButton:
'!rounded-l-lg bg-transparent border-none hover:bg-zinc-500/30 active:bg-zinc-500/40'
}"
@update:model-value="onChange" @update:model-value="onChange"
> >
<template #incrementicon> <template #incrementicon>
@@ -120,4 +123,9 @@ const buttonTooltip = computed(() => {
margin: 1px 0; margin: 1px 0;
box-shadow: none; box-shadow: none;
} }
:deep(.p-inputnumber-button.p-disabled .pi),
:deep(.p-inputnumber-button.p-disabled .p-icon) {
color: var(--color-node-icon-disabled) !important;
}
</style> </style>

View File

@@ -24,6 +24,8 @@
size="small" size="small"
pt:pc-input-text:root="min-w-full bg-transparent border-none text-center" pt:pc-input-text:root="min-w-full bg-transparent border-none text-center"
class="w-16" class="w-16"
:show-buttons="!buttonsDisabled"
:pt="sliderNumberPt"
@update:model-value="handleNumberInputUpdate" @update:model-value="handleNumberInputUpdate"
/> />
</div> </div>
@@ -43,6 +45,7 @@ import {
filterWidgetProps filterWidgetProps
} from '@/utils/widgetPropFilter' } from '@/utils/widgetPropFilter'
import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt'
import { WidgetInputBaseClass } from './layout' import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue' import WidgetLayoutField from './layout/WidgetLayoutField.vue'
@@ -103,4 +106,24 @@ const stepValue = computed(() => {
// precision 1 → 0.1, precision 2 → 0.01, etc. // precision 1 → 0.1, precision 2 → 0.01, etc.
return 1 / Math.pow(10, precision.value) return 1 / Math.pow(10, precision.value)
}) })
const buttonsDisabled = computed(() => {
const currentValue = localValue.value ?? 0
return (
!Number.isFinite(currentValue) ||
Math.abs(currentValue) > Number.MAX_SAFE_INTEGER
)
})
const sliderNumberPt = useNumberWidgetButtonPt({
roundedLeft: true,
roundedRight: true
})
</script> </script>
<style scoped>
:deep(.p-inputnumber-button.p-disabled .pi),
:deep(.p-inputnumber-button.p-disabled .p-icon) {
color: var(--color-node-icon-disabled) !important;
}
</style>

View File

@@ -0,0 +1,23 @@
const sharedButtonClasses =
'!inline-flex !items-center !justify-center !border-0 !bg-transparent text-inherit transition-colors duration-150 ease-in-out ' +
'hover:!bg-[var(--color-node-component-surface-hovered)] active:!bg-[var(--color-node-component-surface-selected)] ' +
'disabled:!bg-[var(--color-node-component-disabled)] disabled:!text-[var(--color-node-icon-disabled)] disabled:cursor-not-allowed'
export function useNumberWidgetButtonPt(options?: {
roundedLeft?: boolean
roundedRight?: boolean
}) {
const { roundedLeft = false, roundedRight = false } = options ?? {}
const increment = `${sharedButtonClasses}${roundedRight ? ' !rounded-r-lg' : ''}`
const decrement = `${sharedButtonClasses}${roundedLeft ? ' !rounded-l-lg' : ''}`
return {
incrementButton: {
class: increment.trim()
},
decrementButton: {
class: decrement.trim()
}
}
}