diff --git a/src/components/graph/widgets/BadgedNumberInput.vue b/src/components/graph/widgets/BadgedNumberInput.vue
new file mode 100644
index 000000000..2d2f85042
--- /dev/null
+++ b/src/components/graph/widgets/BadgedNumberInput.vue
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/widgets/useBadgedNumberInput.ts b/src/composables/widgets/useBadgedNumberInput.ts
new file mode 100644
index 000000000..9fe95866b
--- /dev/null
+++ b/src/composables/widgets/useBadgedNumberInput.ts
@@ -0,0 +1,91 @@
+import type { LGraphNode } from '@comfyorg/litegraph'
+import { ref } from 'vue'
+
+import BadgedNumberInput from '@/components/graph/widgets/BadgedNumberInput.vue'
+import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
+import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
+import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
+
+const PADDING = 8
+
+type BadgeState = 'normal' | 'random' | 'lock' | 'increment' | 'decrement'
+
+interface BadgedNumberInputOptions {
+ defaultValue?: number
+ badgeState?: BadgeState
+ disabled?: boolean
+ minHeight?: number
+ serialize?: boolean
+}
+
+export const useBadgedNumberInput = (
+ options: BadgedNumberInputOptions = {}
+) => {
+ const {
+ defaultValue = 0,
+ badgeState = 'normal',
+ disabled = false,
+ minHeight = 40,
+ serialize = true
+ } = options
+
+ const widgetConstructor: ComfyWidgetConstructorV2 = (
+ node: LGraphNode,
+ inputSpec: InputSpec
+ ) => {
+ // Initialize widget value as string to conform to ComponentWidgetImpl requirements
+ const widgetValue = ref(defaultValue.toString())
+
+ // Create the widget instance
+ const widget = new ComponentWidgetImpl<
+ string | object,
+ Omit<
+ InstanceType['$props'],
+ 'widget' | 'modelValue'
+ >
+ >({
+ node,
+ name: inputSpec.name,
+ component: BadgedNumberInput,
+ inputSpec,
+ props: {
+ badgeState,
+ disabled
+ },
+ options: {
+ // Required: getter for widget value - return as string
+ getValue: () => widgetValue.value as string | object,
+
+ // Required: setter for widget value - accept number, string or object
+ setValue: (value: string | object | number) => {
+ let stringValue: string
+ if (typeof value === 'object') {
+ stringValue = JSON.stringify(value)
+ } else {
+ stringValue = String(value)
+ }
+ const numValue = parseFloat(stringValue)
+ if (!isNaN(numValue)) {
+ widgetValue.value = numValue.toString()
+ }
+ },
+
+ // Optional: minimum height for the widget
+ getMinHeight: () => minHeight + PADDING,
+
+ // Optional: whether to serialize this widget's value
+ serialize
+ }
+ })
+
+ // Register the widget with the node
+ addWidget(node, widget)
+
+ return widget
+ }
+
+ return widgetConstructor
+}
+
+// Export types for use in other modules
+export type { BadgeState, BadgedNumberInputOptions }
diff --git a/src/scripts/widgets.ts b/src/scripts/widgets.ts
index 90f5e54f7..d8a4b2c46 100644
--- a/src/scripts/widgets.ts
+++ b/src/scripts/widgets.ts
@@ -5,6 +5,7 @@ import type {
IStringWidget
} from '@comfyorg/litegraph/dist/types/widgets'
+import { useBadgedNumberInput } from '@/composables/widgets/useBadgedNumberInput'
import { useBooleanWidget } from '@/composables/widgets/useBooleanWidget'
import { useComboWidget } from '@/composables/widgets/useComboWidget'
import { useFloatWidget } from '@/composables/widgets/useFloatWidget'
@@ -289,5 +290,6 @@ export const ComfyWidgets: Record = {
STRING: transformWidgetConstructorV2ToV1(useStringWidget()),
MARKDOWN: transformWidgetConstructorV2ToV1(useMarkdownWidget()),
COMBO: transformWidgetConstructorV2ToV1(useComboWidget()),
- IMAGEUPLOAD: useImageUploadWidget()
+ IMAGEUPLOAD: useImageUploadWidget(),
+ BADGED_NUMBER: transformWidgetConstructorV2ToV1(useBadgedNumberInput())
}
diff --git a/tests-ui/tests/composables/useBadgedNumberInput.test.ts b/tests-ui/tests/composables/useBadgedNumberInput.test.ts
new file mode 100644
index 000000000..b0216e597
--- /dev/null
+++ b/tests-ui/tests/composables/useBadgedNumberInput.test.ts
@@ -0,0 +1,73 @@
+import type { LGraphNode } from '@comfyorg/litegraph'
+import { describe, expect, it, vi } from 'vitest'
+
+import { useBadgedNumberInput } from '@/composables/widgets/useBadgedNumberInput'
+import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
+
+// Mock dependencies
+vi.mock('@/scripts/domWidget', () => ({
+ ComponentWidgetImpl: vi.fn().mockImplementation((config) => ({
+ ...config,
+ value: config.options.getValue(),
+ setValue: config.options.setValue,
+ options: config.options,
+ props: config.props
+ })),
+ addWidget: vi.fn()
+}))
+
+describe('useBadgedNumberInput', () => {
+ const createMockNode = (): LGraphNode =>
+ ({
+ id: 1,
+ title: 'Test Node',
+ widgets: [],
+ addWidget: vi.fn()
+ }) as any
+
+ const createInputSpec = (overrides: Partial = {}): InputSpec => ({
+ name: 'test_input',
+ type: 'number',
+ ...overrides
+ })
+
+ it('creates widget constructor with default options', () => {
+ const constructor = useBadgedNumberInput()
+ expect(constructor).toBeDefined()
+ expect(typeof constructor).toBe('function')
+ })
+
+ it('creates widget with default options', () => {
+ const constructor = useBadgedNumberInput()
+ const node = createMockNode()
+ const inputSpec = createInputSpec()
+
+ const widget = constructor(node, inputSpec)
+
+ expect(widget).toBeDefined()
+ expect(widget.name).toBe(inputSpec.name)
+ })
+
+ it('creates widget with custom badge state', () => {
+ const constructor = useBadgedNumberInput({ badgeState: 'random' })
+ const node = createMockNode()
+ const inputSpec = createInputSpec()
+
+ const widget = constructor(node, inputSpec)
+
+ expect(widget).toBeDefined()
+ // Widget is created with the props, but accessing them requires the mock structure
+ expect((widget as any).props.badgeState).toBe('random')
+ })
+
+ it('creates widget with disabled state', () => {
+ const constructor = useBadgedNumberInput({ disabled: true })
+ const node = createMockNode()
+ const inputSpec = createInputSpec()
+
+ const widget = constructor(node, inputSpec)
+
+ expect(widget).toBeDefined()
+ expect((widget as any).props.disabled).toBe(true)
+ })
+})