mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
alias old float/int widgets
This commit is contained in:
53
.claude/commands/create-widget.md
Normal file
53
.claude/commands/create-widget.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Create a Vue Widget for ComfyUI
|
||||
|
||||
Your task is to create a new Vue widget for ComfyUI based on the widget specification: $ARGUMENTS
|
||||
|
||||
## Instructions
|
||||
|
||||
Follow the comprehensive guide in `vue-widget-conversion/vue-widget-guide.md` to create the widget. This guide contains step-by-step instructions, examples from actual PRs, and best practices.
|
||||
|
||||
### Key Steps to Follow:
|
||||
|
||||
1. **Understand the Widget Type**
|
||||
- Analyze what type of widget is needed: $ARGUMENTS
|
||||
- Identify the data type (string, number, array, object, etc.)
|
||||
- Determine if it needs special behaviors (execution state awareness, dynamic management, etc.)
|
||||
|
||||
2. **Component Creation**
|
||||
- Create Vue component in `src/components/graph/widgets/`
|
||||
- REQUIRED: Use PrimeVue components (reference `vue-widget-conversion/primevue-components.md`)
|
||||
- Use Composition API with `<script setup>`
|
||||
- Implement proper v-model binding with `defineModel`
|
||||
|
||||
3. **Composable Pattern**
|
||||
- Always create widget constructor composable in `src/composables/widgets/`
|
||||
- Only create node-level composable in `src/composables/node/` if the widget needs dynamic management
|
||||
- Follow the dual composable pattern explained in the guide
|
||||
|
||||
4. **Registration**
|
||||
- Register in `src/scripts/widgets.ts`
|
||||
- Use appropriate widget type name
|
||||
|
||||
5. **Testing**
|
||||
- Create unit tests for composables
|
||||
- Test with actual nodes that use the widget
|
||||
|
||||
### Important Requirements:
|
||||
|
||||
- **Always use PrimeVue components** - Check `vue-widget-conversion/primevue-components.md` for available components
|
||||
- Use TypeScript with proper types
|
||||
- Follow Vue 3 Composition API patterns
|
||||
- Use Tailwind CSS for styling (no custom CSS unless absolutely necessary)
|
||||
- Implement proper error handling and validation
|
||||
- Consider performance (use v-show vs v-if appropriately)
|
||||
|
||||
### Before Starting:
|
||||
|
||||
1. First read through the entire guide at `vue-widget-conversion/vue-widget-guide.md`
|
||||
2. Check existing widget implementations for similar patterns
|
||||
3. Identify which PrimeVue component(s) best fit the widget requirements
|
||||
|
||||
### Widget Specification to Implement:
|
||||
$ARGUMENTS
|
||||
|
||||
Begin by analyzing the widget requirements and proposing an implementation plan based on the guide.
|
||||
89
copy-widget-resources.sh
Executable file
89
copy-widget-resources.sh
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to copy vue-widget-conversion folder and .claude/commands/create-widget.md
|
||||
# to another local copy of the same repository
|
||||
|
||||
# Check if destination directory was provided
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <destination-repo-path>"
|
||||
echo "Example: $0 /home/c_byrne/projects/comfyui-frontend-testing/ComfyUI_frontend-clone-8"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the destination directory from first argument
|
||||
DEST_DIR="$1"
|
||||
|
||||
# Source files/directories (relative to script location)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE_WIDGET_DIR="$SCRIPT_DIR/vue-widget-conversion"
|
||||
SOURCE_COMMAND_FILE="$SCRIPT_DIR/.claude/commands/create-widget.md"
|
||||
|
||||
# Destination paths
|
||||
DEST_WIDGET_DIR="$DEST_DIR/vue-widget-conversion"
|
||||
DEST_COMMAND_DIR="$DEST_DIR/.claude/commands"
|
||||
DEST_COMMAND_FILE="$DEST_COMMAND_DIR/create-widget.md"
|
||||
|
||||
# Check if destination directory exists
|
||||
if [ ! -d "$DEST_DIR" ]; then
|
||||
echo "Error: Destination directory does not exist: $DEST_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if source vue-widget-conversion directory exists
|
||||
if [ ! -d "$SOURCE_WIDGET_DIR" ]; then
|
||||
echo "Error: Source vue-widget-conversion directory not found: $SOURCE_WIDGET_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if source command file exists
|
||||
if [ ! -f "$SOURCE_COMMAND_FILE" ]; then
|
||||
echo "Error: Source command file not found: $SOURCE_COMMAND_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Copying widget resources to: $DEST_DIR"
|
||||
|
||||
# Copy vue-widget-conversion directory
|
||||
echo "Copying vue-widget-conversion directory..."
|
||||
if [ -d "$DEST_WIDGET_DIR" ]; then
|
||||
echo " Warning: Destination vue-widget-conversion already exists. Overwriting..."
|
||||
rm -rf "$DEST_WIDGET_DIR"
|
||||
fi
|
||||
cp -r "$SOURCE_WIDGET_DIR" "$DEST_WIDGET_DIR"
|
||||
echo " ✓ Copied vue-widget-conversion directory"
|
||||
|
||||
# Create .claude/commands directory if it doesn't exist
|
||||
echo "Creating .claude/commands directory structure..."
|
||||
mkdir -p "$DEST_COMMAND_DIR"
|
||||
echo " ✓ Created .claude/commands directory"
|
||||
|
||||
# Copy create-widget.md command
|
||||
echo "Copying create-widget.md command..."
|
||||
cp "$SOURCE_COMMAND_FILE" "$DEST_COMMAND_FILE"
|
||||
echo " ✓ Copied create-widget.md command"
|
||||
|
||||
# Verify the copy was successful
|
||||
echo ""
|
||||
echo "Verification:"
|
||||
if [ -d "$DEST_WIDGET_DIR" ] && [ -f "$DEST_WIDGET_DIR/vue-widget-guide.md" ] && [ -f "$DEST_WIDGET_DIR/primevue-components.md" ]; then
|
||||
echo " ✓ vue-widget-conversion directory copied successfully"
|
||||
echo " - vue-widget-guide.md exists"
|
||||
echo " - primevue-components.md exists"
|
||||
if [ -f "$DEST_WIDGET_DIR/primevue-components.json" ]; then
|
||||
echo " - primevue-components.json exists"
|
||||
fi
|
||||
else
|
||||
echo " ✗ Error: vue-widget-conversion directory copy may have failed"
|
||||
fi
|
||||
|
||||
if [ -f "$DEST_COMMAND_FILE" ]; then
|
||||
echo " ✓ create-widget.md command copied successfully"
|
||||
else
|
||||
echo " ✗ Error: create-widget.md command copy may have failed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Copy complete! Widget resources are now available in: $DEST_DIR"
|
||||
echo ""
|
||||
echo "You can now use the widget creation command in the destination repo:"
|
||||
echo " /project:create-widget <widget specification>"
|
||||
@@ -2,17 +2,17 @@
|
||||
<div class="badged-number-input relative w-full">
|
||||
<InputGroup class="w-full rounded-lg">
|
||||
<!-- State badge prefix -->
|
||||
<InputGroupAddon v-if="badgeState !== 'normal'" class="!p-1 rounded-l-lg">
|
||||
<Badge
|
||||
:value="badgeIcon"
|
||||
:severity="badgeSeverity"
|
||||
class="text-xs font-medium"
|
||||
<InputGroupAddon v-if="badgeState !== 'normal'" class="rounded-l-lg">
|
||||
<i
|
||||
:class="badgeIcon"
|
||||
:title="badgeTooltip"
|
||||
/>
|
||||
:style="{ color: badgeColor }"
|
||||
></i>
|
||||
</InputGroupAddon>
|
||||
|
||||
<!-- Number input -->
|
||||
<!-- Number input for non-slider mode -->
|
||||
<InputNumber
|
||||
v-if="!isSliderMode"
|
||||
v-model="numericValue"
|
||||
:min="min"
|
||||
:max="max"
|
||||
@@ -29,15 +29,44 @@
|
||||
:increment-button-icon="'pi pi-plus'"
|
||||
:decrement-button-icon="'pi pi-minus'"
|
||||
/>
|
||||
|
||||
<!-- Slider mode -->
|
||||
<div
|
||||
v-else
|
||||
:class="{
|
||||
'rounded-r-lg': badgeState !== 'normal',
|
||||
'rounded-lg': badgeState === 'normal'
|
||||
}"
|
||||
class="flex-1 flex items-center gap-2 px-3 py-2 bg-surface-0 border border-surface-300"
|
||||
>
|
||||
<Slider
|
||||
v-model="numericValue"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
class="flex-1"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model="numericValue"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
class="w-16"
|
||||
:show-buttons="false"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Badge from 'primevue/badge'
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
@@ -71,34 +100,40 @@ const max = (inputSpec as any).max ?? 100
|
||||
const step = (inputSpec as any).step ?? 1
|
||||
const placeholder = (inputSpec as any).placeholder ?? 'Enter number'
|
||||
|
||||
// Check if slider mode should be enabled
|
||||
const isSliderMode = computed(() => {
|
||||
console.log('inputSpec', inputSpec)
|
||||
return (inputSpec as any).slider === true
|
||||
})
|
||||
|
||||
// Badge configuration
|
||||
const badgeIcon = computed(() => {
|
||||
switch (badgeState) {
|
||||
case 'random':
|
||||
return '🎲'
|
||||
return 'pi pi-refresh'
|
||||
case 'lock':
|
||||
return '🔒'
|
||||
return 'pi pi-lock'
|
||||
case 'increment':
|
||||
return '⬆️'
|
||||
return 'pi pi-arrow-up'
|
||||
case 'decrement':
|
||||
return '⬇️'
|
||||
return 'pi pi-arrow-down'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const badgeSeverity = computed(() => {
|
||||
const badgeColor = computed(() => {
|
||||
switch (badgeState) {
|
||||
case 'random':
|
||||
return 'info'
|
||||
return 'var(--p-primary-color)'
|
||||
case 'lock':
|
||||
return 'warn'
|
||||
return 'var(--p-orange-500)'
|
||||
case 'increment':
|
||||
return 'success'
|
||||
return 'var(--p-green-500)'
|
||||
case 'decrement':
|
||||
return 'danger'
|
||||
return 'var(--p-red-500)'
|
||||
default:
|
||||
return 'info'
|
||||
return 'var(--p-text-color)'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,21 +1,40 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { ref } from 'vue'
|
||||
import { reactive, 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'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
|
||||
const PADDING = 8
|
||||
|
||||
type BadgeState = 'normal' | 'random' | 'lock' | 'increment' | 'decrement'
|
||||
|
||||
type NumberWidgetMode = 'int' | 'float'
|
||||
|
||||
interface BadgedNumberInputOptions {
|
||||
defaultValue?: number
|
||||
badgeState?: BadgeState
|
||||
disabled?: boolean
|
||||
minHeight?: number
|
||||
serialize?: boolean
|
||||
mode?: NumberWidgetMode
|
||||
}
|
||||
|
||||
// Helper function to map control widget values to badge states
|
||||
const mapControlValueToBadgeState = (controlValue: string): BadgeState => {
|
||||
switch (controlValue) {
|
||||
case 'fixed':
|
||||
return 'lock'
|
||||
case 'increment':
|
||||
return 'increment'
|
||||
case 'decrement':
|
||||
return 'decrement'
|
||||
case 'randomize':
|
||||
return 'random'
|
||||
default:
|
||||
return 'normal'
|
||||
}
|
||||
}
|
||||
|
||||
export const useBadgedNumberInput = (
|
||||
@@ -23,10 +42,10 @@ export const useBadgedNumberInput = (
|
||||
) => {
|
||||
const {
|
||||
defaultValue = 0,
|
||||
badgeState = 'normal',
|
||||
disabled = false,
|
||||
minHeight = 40,
|
||||
serialize = true
|
||||
serialize = true,
|
||||
mode = 'int'
|
||||
} = options
|
||||
|
||||
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||
@@ -36,7 +55,23 @@ export const useBadgedNumberInput = (
|
||||
// Initialize widget value as string to conform to ComponentWidgetImpl requirements
|
||||
const widgetValue = ref<string>(defaultValue.toString())
|
||||
|
||||
// Create the widget instance
|
||||
// Determine if we should show control widget and badge
|
||||
const shouldShowControlWidget =
|
||||
inputSpec.control_after_generate ??
|
||||
// Legacy compatibility: seed inputs get control widgets
|
||||
['seed', 'noise_seed'].includes(inputSpec.name)
|
||||
|
||||
// Create reactive props object for the component
|
||||
const componentProps = reactive({
|
||||
badgeState:
|
||||
options.badgeState ??
|
||||
(shouldShowControlWidget ? 'random' : ('normal' as BadgeState)),
|
||||
disabled
|
||||
})
|
||||
|
||||
const controlWidget: any = null
|
||||
|
||||
// Create the main widget instance
|
||||
const widget = new ComponentWidgetImpl<
|
||||
string | object,
|
||||
Omit<
|
||||
@@ -48,24 +83,34 @@ export const useBadgedNumberInput = (
|
||||
name: inputSpec.name,
|
||||
component: BadgedNumberInput,
|
||||
inputSpec,
|
||||
props: {
|
||||
badgeState,
|
||||
disabled
|
||||
},
|
||||
props: componentProps,
|
||||
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
|
||||
let numValue: number
|
||||
if (typeof value === 'object') {
|
||||
stringValue = JSON.stringify(value)
|
||||
numValue = parseFloat(JSON.stringify(value))
|
||||
} else {
|
||||
stringValue = String(value)
|
||||
numValue =
|
||||
typeof value === 'number' ? value : parseFloat(String(value))
|
||||
}
|
||||
const numValue = parseFloat(stringValue)
|
||||
|
||||
if (!isNaN(numValue)) {
|
||||
// Apply int/float specific value processing
|
||||
if (mode === 'int') {
|
||||
const step = (inputSpec as any).step ?? 1
|
||||
if (step === 1) {
|
||||
numValue = Math.round(numValue)
|
||||
} else {
|
||||
const min = (inputSpec as any).min ?? 0
|
||||
const offset = min % step
|
||||
numValue =
|
||||
Math.round((numValue - offset) / step) * step + offset
|
||||
}
|
||||
}
|
||||
widgetValue.value = numValue.toString()
|
||||
}
|
||||
},
|
||||
@@ -78,6 +123,41 @@ export const useBadgedNumberInput = (
|
||||
}
|
||||
})
|
||||
|
||||
// Add control widget if needed - temporarily disabled to fix circular dependency
|
||||
if (shouldShowControlWidget) {
|
||||
// TODO: Re-implement control widget functionality without circular dependency
|
||||
console.warn(
|
||||
'Control widget functionality temporarily disabled due to circular dependency'
|
||||
)
|
||||
// controlWidget = addValueControlWidget(
|
||||
// node,
|
||||
// widget as any, // Cast to satisfy the interface
|
||||
// 'randomize',
|
||||
// undefined,
|
||||
// undefined,
|
||||
// transformInputSpecV2ToV1(inputSpec)
|
||||
// )
|
||||
|
||||
// Set up reactivity to update badge state when control widget changes
|
||||
if (controlWidget) {
|
||||
const originalCallback = controlWidget.callback
|
||||
controlWidget.callback = function (value: string) {
|
||||
componentProps.badgeState = mapControlValueToBadgeState(value)
|
||||
if (originalCallback) {
|
||||
originalCallback.call(this, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize badge state
|
||||
componentProps.badgeState = mapControlValueToBadgeState(
|
||||
controlWidget.value || 'randomize'
|
||||
)
|
||||
|
||||
// Link the widgets
|
||||
;(widget as any).linkedWidgets = [controlWidget]
|
||||
}
|
||||
}
|
||||
|
||||
// Register the widget with the node
|
||||
addWidget(node, widget)
|
||||
|
||||
@@ -88,4 +168,4 @@ export const useBadgedNumberInput = (
|
||||
}
|
||||
|
||||
// Export types for use in other modules
|
||||
export type { BadgeState, BadgedNumberInputOptions }
|
||||
export type { BadgeState, BadgedNumberInputOptions, NumberWidgetMode }
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type InputSpec,
|
||||
isBooleanInputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
|
||||
export const useBooleanWidget = () => {
|
||||
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ref } from 'vue'
|
||||
import ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
|
||||
const PADDING = 16
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import {
|
||||
ComboInputSpec,
|
||||
type InputSpec,
|
||||
@@ -14,10 +13,7 @@ import {
|
||||
ComponentWidgetImpl,
|
||||
addWidget
|
||||
} from '@/scripts/domWidget'
|
||||
import {
|
||||
type ComfyWidgetConstructorV2,
|
||||
addValueControlWidgets
|
||||
} from '@/scripts/widgets'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
|
||||
import { useRemoteWidget } from './useRemoteWidget'
|
||||
|
||||
@@ -82,13 +78,17 @@ const addComboWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
|
||||
}
|
||||
|
||||
if (inputSpec.control_after_generate) {
|
||||
widget.linkedWidgets = addValueControlWidgets(
|
||||
node,
|
||||
widget,
|
||||
undefined,
|
||||
undefined,
|
||||
transformInputSpecV2ToV1(inputSpec)
|
||||
// TODO: Re-implement control widget functionality without circular dependency
|
||||
console.warn(
|
||||
'Control widget functionality temporarily disabled for combo widgets due to circular dependency'
|
||||
)
|
||||
// widget.linkedWidgets = addValueControlWidgets(
|
||||
// node,
|
||||
// widget,
|
||||
// undefined,
|
||||
// undefined,
|
||||
// transformInputSpecV2ToV1(inputSpec)
|
||||
// )
|
||||
}
|
||||
|
||||
return widget
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type InputSpec,
|
||||
isFloatInputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
function onFloatValueChange(this: INumericWidget, v: number) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import { calculateImageGrid } from '@/scripts/ui/imagePreview'
|
||||
import { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { is_all_same_aspect_ratio } from '@/utils/imageUtil'
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { INumericWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import {
|
||||
type InputSpec,
|
||||
isIntInputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import {
|
||||
type ComfyWidgetConstructorV2,
|
||||
addValueControlWidget
|
||||
} from '@/scripts/widgets'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
function onValueChange(this: INumericWidget, v: number) {
|
||||
@@ -81,15 +77,19 @@ export const useIntWidget = () => {
|
||||
['seed', 'noise_seed'].includes(inputSpec.name)
|
||||
|
||||
if (controlAfterGenerate) {
|
||||
const seedControl = addValueControlWidget(
|
||||
node,
|
||||
widget,
|
||||
'randomize',
|
||||
undefined,
|
||||
undefined,
|
||||
transformInputSpecV2ToV1(inputSpec)
|
||||
// TODO: Re-implement control widget functionality without circular dependency
|
||||
console.warn(
|
||||
'Control widget functionality temporarily disabled for int widgets due to circular dependency'
|
||||
)
|
||||
widget.linkedWidgets = [seedControl]
|
||||
// const seedControl = addValueControlWidget(
|
||||
// node,
|
||||
// widget,
|
||||
// 'randomize',
|
||||
// undefined,
|
||||
// undefined,
|
||||
// transformInputSpecV2ToV1(inputSpec)
|
||||
// )
|
||||
// widget.linkedWidgets = [seedControl]
|
||||
}
|
||||
|
||||
return widget
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Markdown as TiptapMarkdown } from 'tiptap-markdown'
|
||||
|
||||
import { type InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
|
||||
function addMarkdownWidget(
|
||||
node: LGraphNode,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ref } from 'vue'
|
||||
import TextPreviewWidget from '@/components/graph/widgets/TextPreviewWidget.vue'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
|
||||
const PADDING = 16
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
isStringInputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
function addMultilineWidget(
|
||||
|
||||
12
src/scripts/widgetTypes.ts
Normal file
12
src/scripts/widgetTypes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
/**
|
||||
* Constructor function type for ComfyUI widgets using V2 input specification
|
||||
*/
|
||||
export type ComfyWidgetConstructorV2 = (
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpecV2
|
||||
) => IBaseWidget
|
||||
@@ -8,25 +8,18 @@ import type {
|
||||
import { useBadgedNumberInput } from '@/composables/widgets/useBadgedNumberInput'
|
||||
import { useBooleanWidget } from '@/composables/widgets/useBooleanWidget'
|
||||
import { useComboWidget } from '@/composables/widgets/useComboWidget'
|
||||
import { useFloatWidget } from '@/composables/widgets/useFloatWidget'
|
||||
import { useImageUploadWidget } from '@/composables/widgets/useImageUploadWidget'
|
||||
import { useIntWidget } from '@/composables/widgets/useIntWidget'
|
||||
import { useMarkdownWidget } from '@/composables/widgets/useMarkdownWidget'
|
||||
import { useStringWidget } from '@/composables/widgets/useStringWidget'
|
||||
import { t } from '@/i18n'
|
||||
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import type { ComfyApp } from './app'
|
||||
import './domWidget'
|
||||
import './errorNodeWidgets'
|
||||
|
||||
export type ComfyWidgetConstructorV2 = (
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpecV2
|
||||
) => IBaseWidget
|
||||
import type { ComfyWidgetConstructorV2 } from './widgetTypes'
|
||||
|
||||
export type ComfyWidgetConstructor = (
|
||||
node: LGraphNode,
|
||||
@@ -284,8 +277,10 @@ export function addValueControlWidgets(
|
||||
}
|
||||
|
||||
export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
INT: transformWidgetConstructorV2ToV1(useIntWidget()),
|
||||
FLOAT: transformWidgetConstructorV2ToV1(useFloatWidget()),
|
||||
INT: transformWidgetConstructorV2ToV1(useBadgedNumberInput({ mode: 'int' })),
|
||||
FLOAT: transformWidgetConstructorV2ToV1(
|
||||
useBadgedNumberInput({ mode: 'float' })
|
||||
),
|
||||
BOOLEAN: transformWidgetConstructorV2ToV1(useBooleanWidget()),
|
||||
STRING: transformWidgetConstructorV2ToV1(useStringWidget()),
|
||||
MARKDOWN: transformWidgetConstructorV2ToV1(useMarkdownWidget()),
|
||||
|
||||
919
vue-widget-conversion/primevue-components.json
Normal file
919
vue-widget-conversion/primevue-components.json
Normal file
@@ -0,0 +1,919 @@
|
||||
{
|
||||
"version": "4.2.5",
|
||||
"generatedAt": "2025-06-09T04:50:20.566Z",
|
||||
"totalComponents": 147,
|
||||
"categories": {
|
||||
"Panel": [
|
||||
{
|
||||
"name": "accordion",
|
||||
"description": "Groups a collection of contents in tabs",
|
||||
"documentationUrl": "https://primevue.org/accordion/",
|
||||
"pascalCase": "Accordion"
|
||||
},
|
||||
{
|
||||
"name": "accordioncontent",
|
||||
"description": "Content container for accordion panels",
|
||||
"documentationUrl": "https://primevue.org/accordion/",
|
||||
"pascalCase": "Accordioncontent"
|
||||
},
|
||||
{
|
||||
"name": "accordionheader",
|
||||
"description": "Header section for accordion panels",
|
||||
"documentationUrl": "https://primevue.org/accordion/",
|
||||
"pascalCase": "Accordionheader"
|
||||
},
|
||||
{
|
||||
"name": "accordionpanel",
|
||||
"description": "Individual panel in an accordion",
|
||||
"documentationUrl": "https://primevue.org/accordion/",
|
||||
"pascalCase": "Accordionpanel"
|
||||
},
|
||||
{
|
||||
"name": "accordiontab",
|
||||
"description": "Legacy accordion tab component",
|
||||
"documentationUrl": "https://primevue.org/accordion/",
|
||||
"pascalCase": "Accordiontab"
|
||||
},
|
||||
{
|
||||
"name": "card",
|
||||
"description": "Flexible content container",
|
||||
"documentationUrl": "https://primevue.org/card/",
|
||||
"pascalCase": "Card"
|
||||
},
|
||||
{
|
||||
"name": "deferredcontent",
|
||||
"description": "Loads content on demand",
|
||||
"documentationUrl": "https://primevue.org/deferredcontent/",
|
||||
"pascalCase": "Deferredcontent"
|
||||
},
|
||||
{
|
||||
"name": "divider",
|
||||
"description": "Separator component",
|
||||
"documentationUrl": "https://primevue.org/divider/",
|
||||
"pascalCase": "Divider"
|
||||
},
|
||||
{
|
||||
"name": "fieldset",
|
||||
"description": "Groups related form elements",
|
||||
"documentationUrl": "https://primevue.org/fieldset/",
|
||||
"pascalCase": "Fieldset"
|
||||
},
|
||||
{
|
||||
"name": "panel",
|
||||
"description": "Collapsible content container",
|
||||
"documentationUrl": "https://primevue.org/panel/",
|
||||
"pascalCase": "Panel"
|
||||
},
|
||||
{
|
||||
"name": "scrollpanel",
|
||||
"description": "Scrollable content container",
|
||||
"documentationUrl": "https://primevue.org/scrollpanel/",
|
||||
"pascalCase": "Scrollpanel"
|
||||
},
|
||||
{
|
||||
"name": "splitter",
|
||||
"description": "Resizable split panels",
|
||||
"documentationUrl": "https://primevue.org/splitter/",
|
||||
"pascalCase": "Splitter"
|
||||
},
|
||||
{
|
||||
"name": "splitterpanel",
|
||||
"description": "Panel within splitter",
|
||||
"documentationUrl": "https://primevue.org/splitter/",
|
||||
"pascalCase": "Splitterpanel"
|
||||
},
|
||||
{
|
||||
"name": "tab",
|
||||
"description": "Individual tab component",
|
||||
"documentationUrl": "https://primevue.org/tabs/",
|
||||
"pascalCase": "Tab"
|
||||
},
|
||||
{
|
||||
"name": "tablist",
|
||||
"description": "Container for tabs",
|
||||
"documentationUrl": "https://primevue.org/tabs/",
|
||||
"pascalCase": "Tablist"
|
||||
},
|
||||
{
|
||||
"name": "tabpanel",
|
||||
"description": "Content panel for tabs",
|
||||
"documentationUrl": "https://primevue.org/tabs/",
|
||||
"pascalCase": "Tabpanel"
|
||||
},
|
||||
{
|
||||
"name": "tabpanels",
|
||||
"description": "Container for tab panels",
|
||||
"documentationUrl": "https://primevue.org/tabs/",
|
||||
"pascalCase": "Tabpanels"
|
||||
},
|
||||
{
|
||||
"name": "tabs",
|
||||
"description": "Modern tab container",
|
||||
"documentationUrl": "https://primevue.org/tabs/",
|
||||
"pascalCase": "Tabs"
|
||||
},
|
||||
{
|
||||
"name": "tabview",
|
||||
"description": "Legacy tabbed interface",
|
||||
"documentationUrl": "https://primevue.org/tabview/",
|
||||
"pascalCase": "Tabview"
|
||||
}
|
||||
],
|
||||
"Directives": [
|
||||
{
|
||||
"name": "animateonscroll",
|
||||
"description": "Directive to apply animations when element becomes visible",
|
||||
"documentationUrl": "https://primevue.org/animateonscroll/",
|
||||
"pascalCase": "Animateonscroll"
|
||||
},
|
||||
{
|
||||
"name": "badgedirective",
|
||||
"description": "Directive to add badges to any element",
|
||||
"documentationUrl": "https://primevue.org/badge/",
|
||||
"pascalCase": "Badgedirective"
|
||||
},
|
||||
{
|
||||
"name": "focustrap",
|
||||
"description": "Directive to trap focus within element",
|
||||
"documentationUrl": "https://primevue.org/focustrap/",
|
||||
"pascalCase": "Focustrap"
|
||||
},
|
||||
{
|
||||
"name": "keyfilter",
|
||||
"description": "Directive to filter keyboard input",
|
||||
"documentationUrl": "https://primevue.org/keyfilter/",
|
||||
"pascalCase": "Keyfilter"
|
||||
},
|
||||
{
|
||||
"name": "ripple",
|
||||
"description": "Directive for material design ripple effect",
|
||||
"documentationUrl": "https://primevue.org/ripple/",
|
||||
"pascalCase": "Ripple"
|
||||
},
|
||||
{
|
||||
"name": "styleclass",
|
||||
"description": "Directive for dynamic styling",
|
||||
"documentationUrl": "https://primevue.org/styleclass/",
|
||||
"pascalCase": "Styleclass"
|
||||
}
|
||||
],
|
||||
"Form": [
|
||||
{
|
||||
"name": "autocomplete",
|
||||
"description": "Provides filtered suggestions while typing input, supports multiple selection and custom item templates",
|
||||
"documentationUrl": "https://primevue.org/autocomplete/",
|
||||
"pascalCase": "Autocomplete"
|
||||
},
|
||||
{
|
||||
"name": "calendar",
|
||||
"description": "Input component for date selection (legacy)",
|
||||
"documentationUrl": "https://primevue.org/calendar/",
|
||||
"pascalCase": "Calendar"
|
||||
},
|
||||
{
|
||||
"name": "cascadeselect",
|
||||
"description": "Nested dropdown selection component",
|
||||
"documentationUrl": "https://primevue.org/cascadeselect/",
|
||||
"pascalCase": "Cascadeselect"
|
||||
},
|
||||
{
|
||||
"name": "checkbox",
|
||||
"description": "Binary selection component",
|
||||
"documentationUrl": "https://primevue.org/checkbox/",
|
||||
"pascalCase": "Checkbox"
|
||||
},
|
||||
{
|
||||
"name": "checkboxgroup",
|
||||
"description": "Groups multiple checkboxes",
|
||||
"documentationUrl": "https://primevue.org/checkbox/",
|
||||
"pascalCase": "Checkboxgroup"
|
||||
},
|
||||
{
|
||||
"name": "chips",
|
||||
"description": "Input component for entering multiple values",
|
||||
"documentationUrl": "https://primevue.org/chips/",
|
||||
"pascalCase": "Chips"
|
||||
},
|
||||
{
|
||||
"name": "colorpicker",
|
||||
"description": "Input component for color selection",
|
||||
"documentationUrl": "https://primevue.org/colorpicker/",
|
||||
"pascalCase": "Colorpicker"
|
||||
},
|
||||
{
|
||||
"name": "datepicker",
|
||||
"description": "Input component for date and time selection with calendar popup, supports date ranges and custom formatting",
|
||||
"documentationUrl": "https://primevue.org/datepicker/",
|
||||
"pascalCase": "Datepicker"
|
||||
},
|
||||
{
|
||||
"name": "dropdown",
|
||||
"description": "Single selection dropdown",
|
||||
"documentationUrl": "https://primevue.org/dropdown/",
|
||||
"pascalCase": "Dropdown"
|
||||
},
|
||||
{
|
||||
"name": "editor",
|
||||
"description": "Rich text editor component",
|
||||
"documentationUrl": "https://primevue.org/editor/",
|
||||
"pascalCase": "Editor"
|
||||
},
|
||||
{
|
||||
"name": "floatlabel",
|
||||
"description": "Floating label for input components",
|
||||
"documentationUrl": "https://primevue.org/inputtext/",
|
||||
"pascalCase": "Floatlabel"
|
||||
},
|
||||
{
|
||||
"name": "iconfield",
|
||||
"description": "Input field with icon",
|
||||
"documentationUrl": "https://primevue.org/inputtext/",
|
||||
"pascalCase": "Iconfield"
|
||||
},
|
||||
{
|
||||
"name": "iftalabel",
|
||||
"description": "Input field with top-aligned label",
|
||||
"documentationUrl": "https://primevue.org/inputtext/",
|
||||
"pascalCase": "Iftalabel"
|
||||
},
|
||||
{
|
||||
"name": "inputchips",
|
||||
"description": "Multiple input values as chips",
|
||||
"documentationUrl": "https://primevue.org/chips/",
|
||||
"pascalCase": "Inputchips"
|
||||
},
|
||||
{
|
||||
"name": "inputgroup",
|
||||
"description": "Groups input elements",
|
||||
"documentationUrl": "https://primevue.org/inputtext/",
|
||||
"pascalCase": "Inputgroup"
|
||||
},
|
||||
{
|
||||
"name": "inputgroupaddon",
|
||||
"description": "Addon for input groups",
|
||||
"documentationUrl": "https://primevue.org/inputtext/",
|
||||
"pascalCase": "Inputgroupaddon"
|
||||
},
|
||||
{
|
||||
"name": "inputicon",
|
||||
"description": "Icon for input components",
|
||||
"documentationUrl": "https://primevue.org/inputtext/",
|
||||
"pascalCase": "Inputicon"
|
||||
},
|
||||
{
|
||||
"name": "inputmask",
|
||||
"description": "Input with format masking",
|
||||
"documentationUrl": "https://primevue.org/inputtext/",
|
||||
"pascalCase": "Inputmask"
|
||||
},
|
||||
{
|
||||
"name": "inputnumber",
|
||||
"description": "Numeric input with spinner",
|
||||
"documentationUrl": "https://primevue.org/inputtext/",
|
||||
"pascalCase": "Inputnumber"
|
||||
},
|
||||
{
|
||||
"name": "inputotp",
|
||||
"description": "One-time password input",
|
||||
"documentationUrl": "https://primevue.org/inputtext/",
|
||||
"pascalCase": "Inputotp"
|
||||
},
|
||||
{
|
||||
"name": "inputswitch",
|
||||
"description": "Binary switch component",
|
||||
"documentationUrl": "https://primevue.org/toggleswitch/",
|
||||
"pascalCase": "Inputswitch"
|
||||
},
|
||||
{
|
||||
"name": "inputtext",
|
||||
"description": "Text input component",
|
||||
"documentationUrl": "https://primevue.org/inputtext/",
|
||||
"pascalCase": "Inputtext"
|
||||
},
|
||||
{
|
||||
"name": "knob",
|
||||
"description": "Circular input component",
|
||||
"documentationUrl": "https://primevue.org/knob/",
|
||||
"pascalCase": "Knob"
|
||||
},
|
||||
{
|
||||
"name": "listbox",
|
||||
"description": "Selection component with list interface",
|
||||
"documentationUrl": "https://primevue.org/listbox/",
|
||||
"pascalCase": "Listbox"
|
||||
},
|
||||
{
|
||||
"name": "multiselect",
|
||||
"description": "Multiple selection dropdown",
|
||||
"documentationUrl": "https://primevue.org/multiselect/",
|
||||
"pascalCase": "Multiselect"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"description": "Password input with strength meter",
|
||||
"documentationUrl": "https://primevue.org/password/",
|
||||
"pascalCase": "Password"
|
||||
},
|
||||
{
|
||||
"name": "radiobutton",
|
||||
"description": "Single selection from group",
|
||||
"documentationUrl": "https://primevue.org/radiobutton/",
|
||||
"pascalCase": "Radiobutton"
|
||||
},
|
||||
{
|
||||
"name": "radiobuttongroup",
|
||||
"description": "Groups radio buttons",
|
||||
"documentationUrl": "https://primevue.org/radiobutton/",
|
||||
"pascalCase": "Radiobuttongroup"
|
||||
},
|
||||
{
|
||||
"name": "rating",
|
||||
"description": "Star rating input component",
|
||||
"documentationUrl": "https://primevue.org/rating/",
|
||||
"pascalCase": "Rating"
|
||||
},
|
||||
{
|
||||
"name": "select",
|
||||
"description": "Modern dropdown selection",
|
||||
"documentationUrl": "https://primevue.org/select/",
|
||||
"pascalCase": "Select"
|
||||
},
|
||||
{
|
||||
"name": "selectbutton",
|
||||
"description": "Button-style selection component",
|
||||
"documentationUrl": "https://primevue.org/selectbutton/",
|
||||
"pascalCase": "Selectbutton"
|
||||
},
|
||||
{
|
||||
"name": "slider",
|
||||
"description": "Range selection component",
|
||||
"documentationUrl": "https://primevue.org/slider/",
|
||||
"pascalCase": "Slider"
|
||||
},
|
||||
{
|
||||
"name": "textarea",
|
||||
"description": "Multi-line text input",
|
||||
"documentationUrl": "https://primevue.org/textarea/",
|
||||
"pascalCase": "Textarea"
|
||||
},
|
||||
{
|
||||
"name": "togglebutton",
|
||||
"description": "Two-state button component",
|
||||
"documentationUrl": "https://primevue.org/togglebutton/",
|
||||
"pascalCase": "Togglebutton"
|
||||
},
|
||||
{
|
||||
"name": "toggleswitch",
|
||||
"description": "Switch component for binary state",
|
||||
"documentationUrl": "https://primevue.org/toggleswitch/",
|
||||
"pascalCase": "Toggleswitch"
|
||||
},
|
||||
{
|
||||
"name": "treeselect",
|
||||
"description": "Tree-structured selection",
|
||||
"documentationUrl": "https://primevue.org/treeselect/",
|
||||
"pascalCase": "Treeselect"
|
||||
}
|
||||
],
|
||||
"Misc": [
|
||||
{
|
||||
"name": "avatar",
|
||||
"description": "Represents people using icons, labels and images",
|
||||
"documentationUrl": "https://primevue.org/avatar/",
|
||||
"pascalCase": "Avatar"
|
||||
},
|
||||
{
|
||||
"name": "avatargroup",
|
||||
"description": "Groups multiple avatars together",
|
||||
"documentationUrl": "https://primevue.org/avatar/",
|
||||
"pascalCase": "Avatargroup"
|
||||
},
|
||||
{
|
||||
"name": "badge",
|
||||
"description": "Small numeric indicator for other components",
|
||||
"documentationUrl": "https://primevue.org/badge/",
|
||||
"pascalCase": "Badge"
|
||||
},
|
||||
{
|
||||
"name": "blockui",
|
||||
"description": "Blocks user interaction with page elements",
|
||||
"documentationUrl": "https://primevue.org/blockui/",
|
||||
"pascalCase": "Blockui"
|
||||
},
|
||||
{
|
||||
"name": "chip",
|
||||
"description": "Compact element representing input, attribute or action",
|
||||
"documentationUrl": "https://primevue.org/chip/",
|
||||
"pascalCase": "Chip"
|
||||
},
|
||||
{
|
||||
"name": "inplace",
|
||||
"description": "Editable content in place",
|
||||
"documentationUrl": "https://primevue.org/inplace/",
|
||||
"pascalCase": "Inplace"
|
||||
},
|
||||
{
|
||||
"name": "metergroup",
|
||||
"description": "Displays multiple meter values",
|
||||
"documentationUrl": "https://primevue.org/metergroup/",
|
||||
"pascalCase": "Metergroup"
|
||||
},
|
||||
{
|
||||
"name": "overlaybadge",
|
||||
"description": "Badge overlay for components",
|
||||
"documentationUrl": "https://primevue.org/badge/",
|
||||
"pascalCase": "Overlaybadge"
|
||||
},
|
||||
{
|
||||
"name": "progressbar",
|
||||
"description": "Progress indication component",
|
||||
"documentationUrl": "https://primevue.org/progressbar/",
|
||||
"pascalCase": "Progressbar"
|
||||
},
|
||||
{
|
||||
"name": "progressspinner",
|
||||
"description": "Loading spinner component",
|
||||
"documentationUrl": "https://primevue.org/progressspinner/",
|
||||
"pascalCase": "Progressspinner"
|
||||
},
|
||||
{
|
||||
"name": "skeleton",
|
||||
"description": "Placeholder for loading content",
|
||||
"documentationUrl": "https://primevue.org/skeleton/",
|
||||
"pascalCase": "Skeleton"
|
||||
},
|
||||
{
|
||||
"name": "tag",
|
||||
"description": "Label component for categorization",
|
||||
"documentationUrl": "https://primevue.org/tag/",
|
||||
"pascalCase": "Tag"
|
||||
},
|
||||
{
|
||||
"name": "terminal",
|
||||
"description": "Command line interface",
|
||||
"documentationUrl": "https://primevue.org/terminal/",
|
||||
"pascalCase": "Terminal"
|
||||
}
|
||||
],
|
||||
"Menu": [
|
||||
{
|
||||
"name": "breadcrumb",
|
||||
"description": "Navigation component showing current page location",
|
||||
"documentationUrl": "https://primevue.org/breadcrumb/",
|
||||
"pascalCase": "Breadcrumb"
|
||||
},
|
||||
{
|
||||
"name": "contextmenu",
|
||||
"description": "Right-click context menu",
|
||||
"documentationUrl": "https://primevue.org/contextmenu/",
|
||||
"pascalCase": "Contextmenu"
|
||||
},
|
||||
{
|
||||
"name": "dock",
|
||||
"description": "Dock layout with expandable items",
|
||||
"documentationUrl": "https://primevue.org/dock/",
|
||||
"pascalCase": "Dock"
|
||||
},
|
||||
{
|
||||
"name": "megamenu",
|
||||
"description": "Navigation with grouped menu items",
|
||||
"documentationUrl": "https://primevue.org/megamenu/",
|
||||
"pascalCase": "Megamenu"
|
||||
},
|
||||
{
|
||||
"name": "menu",
|
||||
"description": "Navigation menu component",
|
||||
"documentationUrl": "https://primevue.org/menu/",
|
||||
"pascalCase": "Menu"
|
||||
},
|
||||
{
|
||||
"name": "menubar",
|
||||
"description": "Horizontal navigation menu",
|
||||
"documentationUrl": "https://primevue.org/menubar/",
|
||||
"pascalCase": "Menubar"
|
||||
},
|
||||
{
|
||||
"name": "panelmenu",
|
||||
"description": "Vertical navigation menu",
|
||||
"documentationUrl": "https://primevue.org/panelmenu/",
|
||||
"pascalCase": "Panelmenu"
|
||||
},
|
||||
{
|
||||
"name": "steps",
|
||||
"description": "Step-by-step navigation",
|
||||
"documentationUrl": "https://primevue.org/steps/",
|
||||
"pascalCase": "Steps"
|
||||
},
|
||||
{
|
||||
"name": "tabmenu",
|
||||
"description": "Menu styled as tabs",
|
||||
"documentationUrl": "https://primevue.org/tabmenu/",
|
||||
"pascalCase": "Tabmenu"
|
||||
},
|
||||
{
|
||||
"name": "tieredmenu",
|
||||
"description": "Hierarchical menu component",
|
||||
"documentationUrl": "https://primevue.org/tieredmenu/",
|
||||
"pascalCase": "Tieredmenu"
|
||||
}
|
||||
],
|
||||
"Button": [
|
||||
{
|
||||
"name": "button",
|
||||
"description": "Standard button component with various styles and severity levels, supports icons and loading states",
|
||||
"documentationUrl": "https://primevue.org/button/",
|
||||
"pascalCase": "Button"
|
||||
},
|
||||
{
|
||||
"name": "buttongroup",
|
||||
"description": "Groups multiple buttons together as a cohesive unit with shared styling",
|
||||
"documentationUrl": "https://primevue.org/button/",
|
||||
"pascalCase": "Buttongroup"
|
||||
},
|
||||
{
|
||||
"name": "speeddial",
|
||||
"description": "Floating action button with expandable menu items, supports radial and linear layouts",
|
||||
"documentationUrl": "https://primevue.org/speeddial/",
|
||||
"pascalCase": "Speeddial"
|
||||
},
|
||||
{
|
||||
"name": "splitbutton",
|
||||
"description": "Button with attached dropdown menu for additional actions",
|
||||
"documentationUrl": "https://primevue.org/splitbutton/",
|
||||
"pascalCase": "Splitbutton"
|
||||
}
|
||||
],
|
||||
"Data": [
|
||||
{
|
||||
"name": "carousel",
|
||||
"description": "Displays content in a rotating slideshow",
|
||||
"documentationUrl": "https://primevue.org/carousel/",
|
||||
"pascalCase": "Carousel"
|
||||
},
|
||||
{
|
||||
"name": "datatable",
|
||||
"description": "Advanced data table with sorting, filtering, pagination, row selection, and column resizing",
|
||||
"documentationUrl": "https://primevue.org/datatable/",
|
||||
"pascalCase": "Datatable"
|
||||
},
|
||||
{
|
||||
"name": "dataview",
|
||||
"description": "Displays data in list layout",
|
||||
"documentationUrl": "https://primevue.org/dataview/",
|
||||
"pascalCase": "Dataview"
|
||||
},
|
||||
{
|
||||
"name": "orderlist",
|
||||
"description": "Reorderable list component",
|
||||
"documentationUrl": "https://primevue.org/orderlist/",
|
||||
"pascalCase": "Orderlist"
|
||||
},
|
||||
{
|
||||
"name": "organizationchart",
|
||||
"description": "Hierarchical organization display",
|
||||
"documentationUrl": "https://primevue.org/organizationchart/",
|
||||
"pascalCase": "Organizationchart"
|
||||
},
|
||||
{
|
||||
"name": "paginator",
|
||||
"description": "Navigation for paged data",
|
||||
"documentationUrl": "https://primevue.org/paginator/",
|
||||
"pascalCase": "Paginator"
|
||||
},
|
||||
{
|
||||
"name": "picklist",
|
||||
"description": "Dual list for item transfer",
|
||||
"documentationUrl": "https://primevue.org/picklist/",
|
||||
"pascalCase": "Picklist"
|
||||
},
|
||||
{
|
||||
"name": "timeline",
|
||||
"description": "Chronological event display",
|
||||
"documentationUrl": "https://primevue.org/timeline/",
|
||||
"pascalCase": "Timeline"
|
||||
},
|
||||
{
|
||||
"name": "tree",
|
||||
"description": "Hierarchical tree structure",
|
||||
"documentationUrl": "https://primevue.org/tree/",
|
||||
"pascalCase": "Tree"
|
||||
},
|
||||
{
|
||||
"name": "treetable",
|
||||
"description": "Table with tree structure",
|
||||
"documentationUrl": "https://primevue.org/treetable/",
|
||||
"pascalCase": "Treetable"
|
||||
},
|
||||
{
|
||||
"name": "virtualscroller",
|
||||
"description": "Virtual scrolling for large datasets",
|
||||
"documentationUrl": "https://primevue.org/virtualscroller/",
|
||||
"pascalCase": "Virtualscroller"
|
||||
}
|
||||
],
|
||||
"Chart": [
|
||||
{
|
||||
"name": "chart",
|
||||
"description": "Charts and graphs using Chart.js",
|
||||
"documentationUrl": "https://primevue.org/chart/",
|
||||
"pascalCase": "Chart"
|
||||
}
|
||||
],
|
||||
"Utilities": [
|
||||
{
|
||||
"name": "column",
|
||||
"description": "Table column component for DataTable",
|
||||
"documentationUrl": "https://primevue.org/datatable/",
|
||||
"pascalCase": "Column"
|
||||
},
|
||||
{
|
||||
"name": "columngroup",
|
||||
"description": "Groups table columns",
|
||||
"documentationUrl": "https://primevue.org/datatable/",
|
||||
"pascalCase": "Columngroup"
|
||||
},
|
||||
{
|
||||
"name": "confirmationservice",
|
||||
"description": "Service for programmatically displaying and managing confirmation dialogs",
|
||||
"documentationUrl": "https://primevue.org/confirmdialog/",
|
||||
"pascalCase": "Confirmationservice"
|
||||
},
|
||||
{
|
||||
"name": "dialogservice",
|
||||
"description": "Service for dynamic dialog creation",
|
||||
"documentationUrl": "https://primevue.org/dialogservice/",
|
||||
"pascalCase": "Dialogservice"
|
||||
},
|
||||
{
|
||||
"name": "fluid",
|
||||
"description": "Container with fluid width",
|
||||
"documentationUrl": "https://primevue.org/fluid/",
|
||||
"pascalCase": "Fluid"
|
||||
},
|
||||
{
|
||||
"name": "portal",
|
||||
"description": "Renders content in different DOM location",
|
||||
"documentationUrl": "https://primevue.org/portal/",
|
||||
"pascalCase": "Portal"
|
||||
},
|
||||
{
|
||||
"name": "row",
|
||||
"description": "Table row component",
|
||||
"documentationUrl": "https://primevue.org/datatable/",
|
||||
"pascalCase": "Row"
|
||||
},
|
||||
{
|
||||
"name": "scrolltop",
|
||||
"description": "Button to scroll to top",
|
||||
"documentationUrl": "https://primevue.org/scrolltop/",
|
||||
"pascalCase": "Scrolltop"
|
||||
},
|
||||
{
|
||||
"name": "terminalservice",
|
||||
"description": "Service for terminal component",
|
||||
"documentationUrl": "https://primevue.org/terminal/",
|
||||
"pascalCase": "Terminalservice"
|
||||
},
|
||||
{
|
||||
"name": "useconfirm",
|
||||
"description": "Composable for confirmation dialogs",
|
||||
"documentationUrl": "https://primevue.org/confirmdialog/",
|
||||
"pascalCase": "Useconfirm"
|
||||
},
|
||||
{
|
||||
"name": "usedialog",
|
||||
"description": "Composable for dynamic dialogs",
|
||||
"documentationUrl": "https://primevue.org/dynamicdialog/",
|
||||
"pascalCase": "Usedialog"
|
||||
},
|
||||
{
|
||||
"name": "usestyle",
|
||||
"description": "Composable for dynamic styling",
|
||||
"documentationUrl": "https://primevue.org/usestyle/",
|
||||
"pascalCase": "Usestyle"
|
||||
},
|
||||
{
|
||||
"name": "usetoast",
|
||||
"description": "Composable for toast notifications",
|
||||
"documentationUrl": "https://primevue.org/toast/",
|
||||
"pascalCase": "Usetoast"
|
||||
}
|
||||
],
|
||||
"Uncategorized": [
|
||||
{
|
||||
"name": "config",
|
||||
"description": "Configuration utility for global PrimeVue settings including theming, locale, and component options",
|
||||
"documentationUrl": "https://primevue.org/config/",
|
||||
"pascalCase": "Config"
|
||||
},
|
||||
{
|
||||
"name": "confirmationeventbus",
|
||||
"description": "Internal event bus system for managing confirmation dialog events and communication",
|
||||
"documentationUrl": "https://primevue.org/confirmdialog/",
|
||||
"pascalCase": "Confirmationeventbus"
|
||||
},
|
||||
{
|
||||
"name": "confirmationoptions",
|
||||
"description": "TypeScript interface definitions for confirmation dialog configuration options",
|
||||
"documentationUrl": "https://primevue.org/confirmdialog/",
|
||||
"pascalCase": "Confirmationoptions"
|
||||
},
|
||||
{
|
||||
"name": "dynamicdialogeventbus",
|
||||
"description": "Internal event bus system for managing dynamic dialog creation and communication",
|
||||
"documentationUrl": "https://primevue.org/dynamicdialog/",
|
||||
"pascalCase": "Dynamicdialogeventbus"
|
||||
},
|
||||
{
|
||||
"name": "dynamicdialogoptions",
|
||||
"description": "TypeScript interface definitions for dynamic dialog configuration options",
|
||||
"documentationUrl": "https://primevue.org/dynamicdialog/",
|
||||
"pascalCase": "Dynamicdialogoptions"
|
||||
},
|
||||
{
|
||||
"name": "menuitem",
|
||||
"description": "TypeScript interface definitions for menu item configuration shared across menu components",
|
||||
"documentationUrl": "https://primevue.org/menuitem/",
|
||||
"pascalCase": "Menuitem"
|
||||
},
|
||||
{
|
||||
"name": "overlayeventbus",
|
||||
"description": "Internal event bus system for managing overlay component events and communication",
|
||||
"documentationUrl": "https://primevue.org/overlaypanel/",
|
||||
"pascalCase": "Overlayeventbus"
|
||||
},
|
||||
{
|
||||
"name": "passthrough",
|
||||
"description": "Utility for customizing component styling and attributes through pass-through properties",
|
||||
"documentationUrl": "https://primevue.org/passthrough/",
|
||||
"pascalCase": "Passthrough"
|
||||
},
|
||||
{
|
||||
"name": "toasteventbus",
|
||||
"description": "Internal event bus system for managing toast notification events and communication",
|
||||
"documentationUrl": "https://primevue.org/toast/",
|
||||
"pascalCase": "Toasteventbus"
|
||||
},
|
||||
{
|
||||
"name": "toastservice",
|
||||
"description": "Service for programmatically displaying and managing toast notifications",
|
||||
"documentationUrl": "https://primevue.org/toast/",
|
||||
"pascalCase": "Toastservice"
|
||||
},
|
||||
{
|
||||
"name": "toolbar",
|
||||
"description": "Container for action buttons",
|
||||
"documentationUrl": "https://primevue.org/toolbar/",
|
||||
"pascalCase": "Toolbar"
|
||||
},
|
||||
{
|
||||
"name": "treenode",
|
||||
"description": "Individual node in tree",
|
||||
"documentationUrl": "https://primevue.org/tree/",
|
||||
"pascalCase": "Treenode"
|
||||
}
|
||||
],
|
||||
"Overlay": [
|
||||
{
|
||||
"name": "confirmdialog",
|
||||
"description": "Modal dialog for user confirmation",
|
||||
"documentationUrl": "https://primevue.org/confirmdialog/",
|
||||
"pascalCase": "Confirmdialog"
|
||||
},
|
||||
{
|
||||
"name": "confirmpopup",
|
||||
"description": "Popup for user confirmation",
|
||||
"documentationUrl": "https://primevue.org/confirmdialog/",
|
||||
"pascalCase": "Confirmpopup"
|
||||
},
|
||||
{
|
||||
"name": "dialog",
|
||||
"description": "Modal dialog component",
|
||||
"documentationUrl": "https://primevue.org/dialog/",
|
||||
"pascalCase": "Dialog"
|
||||
},
|
||||
{
|
||||
"name": "drawer",
|
||||
"description": "Sliding panel overlay",
|
||||
"documentationUrl": "https://primevue.org/drawer/",
|
||||
"pascalCase": "Drawer"
|
||||
},
|
||||
{
|
||||
"name": "dynamicdialog",
|
||||
"description": "Programmatically created dialogs",
|
||||
"documentationUrl": "https://primevue.org/dynamicdialog/",
|
||||
"pascalCase": "Dynamicdialog"
|
||||
},
|
||||
{
|
||||
"name": "overlaypanel",
|
||||
"description": "Overlay panel component",
|
||||
"documentationUrl": "https://primevue.org/overlaypanel/",
|
||||
"pascalCase": "Overlaypanel"
|
||||
},
|
||||
{
|
||||
"name": "popover",
|
||||
"description": "Overlay component triggered by user interaction",
|
||||
"documentationUrl": "https://primevue.org/popover/",
|
||||
"pascalCase": "Popover"
|
||||
},
|
||||
{
|
||||
"name": "sidebar",
|
||||
"description": "Side panel overlay",
|
||||
"documentationUrl": "https://primevue.org/sidebar/",
|
||||
"pascalCase": "Sidebar"
|
||||
},
|
||||
{
|
||||
"name": "tooltip",
|
||||
"description": "Informational popup on hover",
|
||||
"documentationUrl": "https://primevue.org/tooltip/",
|
||||
"pascalCase": "Tooltip"
|
||||
}
|
||||
],
|
||||
"File": [
|
||||
{
|
||||
"name": "fileupload",
|
||||
"description": "File upload component with drag-drop",
|
||||
"documentationUrl": "https://primevue.org/fileupload/",
|
||||
"pascalCase": "Fileupload"
|
||||
}
|
||||
],
|
||||
"Media": [
|
||||
{
|
||||
"name": "galleria",
|
||||
"description": "Image gallery with thumbnails",
|
||||
"documentationUrl": "https://primevue.org/galleria/",
|
||||
"pascalCase": "Galleria"
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"description": "Enhanced image component with preview",
|
||||
"documentationUrl": "https://primevue.org/image/",
|
||||
"pascalCase": "Image"
|
||||
},
|
||||
{
|
||||
"name": "imagecompare",
|
||||
"description": "Before/after image comparison slider",
|
||||
"documentationUrl": "https://primevue.org/imagecompare/",
|
||||
"pascalCase": "Imagecompare"
|
||||
}
|
||||
],
|
||||
"Messages": [
|
||||
{
|
||||
"name": "inlinemessage",
|
||||
"description": "Inline message display",
|
||||
"documentationUrl": "https://primevue.org/inlinemessage/",
|
||||
"pascalCase": "Inlinemessage"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"description": "Message component for notifications",
|
||||
"documentationUrl": "https://primevue.org/message/",
|
||||
"pascalCase": "Message"
|
||||
},
|
||||
{
|
||||
"name": "toast",
|
||||
"description": "Temporary message notifications",
|
||||
"documentationUrl": "https://primevue.org/toast/",
|
||||
"pascalCase": "Toast"
|
||||
}
|
||||
],
|
||||
"Stepper": [
|
||||
{
|
||||
"name": "step",
|
||||
"description": "Individual step in stepper",
|
||||
"documentationUrl": "https://primevue.org/stepper/",
|
||||
"pascalCase": "Step"
|
||||
},
|
||||
{
|
||||
"name": "stepitem",
|
||||
"description": "Item within step",
|
||||
"documentationUrl": "https://primevue.org/stepper/",
|
||||
"pascalCase": "Stepitem"
|
||||
},
|
||||
{
|
||||
"name": "steplist",
|
||||
"description": "List of steps",
|
||||
"documentationUrl": "https://primevue.org/stepper/",
|
||||
"pascalCase": "Steplist"
|
||||
},
|
||||
{
|
||||
"name": "steppanel",
|
||||
"description": "Content panel for stepper",
|
||||
"documentationUrl": "https://primevue.org/stepper/",
|
||||
"pascalCase": "Steppanel"
|
||||
},
|
||||
{
|
||||
"name": "steppanels",
|
||||
"description": "Container for step panels",
|
||||
"documentationUrl": "https://primevue.org/stepper/",
|
||||
"pascalCase": "Steppanels"
|
||||
},
|
||||
{
|
||||
"name": "stepper",
|
||||
"description": "Multi-step process navigation",
|
||||
"documentationUrl": "https://primevue.org/stepper/",
|
||||
"pascalCase": "Stepper"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
2261
vue-widget-conversion/primevue-components.md
Normal file
2261
vue-widget-conversion/primevue-components.md
Normal file
File diff suppressed because it is too large
Load Diff
552
vue-widget-conversion/vue-widget-guide.md
Normal file
552
vue-widget-conversion/vue-widget-guide.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# Step-by-Step Guide: Converting Widgets to Vue Components in ComfyUI
|
||||
|
||||
## Overview
|
||||
This guide explains how to convert existing DOM widgets or create new widgets using Vue components in ComfyUI. The Vue widget system provides better reactivity, type safety, and maintainability compared to traditional DOM manipulation.
|
||||
|
||||
## Prerequisites
|
||||
- Understanding of Vue 3 Composition API
|
||||
- Basic knowledge of TypeScript
|
||||
- Familiarity with ComfyUI widget system
|
||||
|
||||
## Step 1: Create the Vue Component
|
||||
|
||||
Create a new Vue component in `src/components/graph/widgets/`:
|
||||
|
||||
```vue
|
||||
<!-- src/components/graph/widgets/YourWidget.vue -->
|
||||
<template>
|
||||
<div class="your-widget-container">
|
||||
<!-- Your widget UI here -->
|
||||
<input
|
||||
v-model="modelValue"
|
||||
type="text"
|
||||
class="w-full px-2 py-1 rounded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineModel, defineProps } from 'vue'
|
||||
import type { ComponentWidget } from '@/types'
|
||||
|
||||
// Define two-way binding for the widget value
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
|
||||
// Receive widget configuration
|
||||
const { widget } = defineProps<{
|
||||
widget: ComponentWidget<string>
|
||||
}>()
|
||||
|
||||
// Access widget properties
|
||||
const inputSpec = widget.inputSpec
|
||||
const options = inputSpec.options || {}
|
||||
|
||||
// Add any logic necessary here to make a functional, feature-rich widget.
|
||||
// You can use the vueuse library for helper functions.
|
||||
// You can take liberty in things to add, as this is just a prototype.
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Use Tailwind classes in template, custom CSS here if needed */
|
||||
</style>
|
||||
```
|
||||
|
||||
## Step 2: Create the Widget Composables (Dual Pattern)
|
||||
|
||||
The Vue widget system uses a **dual composable pattern** for separation of concerns:
|
||||
|
||||
### 2a. Create the Widget Constructor Composable
|
||||
|
||||
Create the core widget constructor in `src/composables/widgets/`:
|
||||
|
||||
```typescript
|
||||
// src/composables/widgets/useYourWidget.ts
|
||||
import { ref } from 'vue'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import YourWidget from '@/components/graph/widgets/YourWidget.vue'
|
||||
|
||||
const PADDING = 8
|
||||
|
||||
export const useYourWidget = (options: { defaultValue?: string } = {}) => {
|
||||
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpec
|
||||
) => {
|
||||
// Initialize widget value
|
||||
const widgetValue = ref<string>(options.defaultValue ?? '')
|
||||
|
||||
// Create the widget instance
|
||||
const widget = new ComponentWidgetImpl<string>({
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: YourWidget,
|
||||
inputSpec,
|
||||
options: {
|
||||
// Required: getter for widget value
|
||||
getValue: () => widgetValue.value,
|
||||
|
||||
// Required: setter for widget value
|
||||
setValue: (value: string) => {
|
||||
widgetValue.value = value
|
||||
},
|
||||
|
||||
// Optional: minimum height for the widget
|
||||
getMinHeight: () => options.minHeight ?? 40 + PADDING,
|
||||
|
||||
// Optional: whether to serialize this widget's value
|
||||
serialize: true,
|
||||
|
||||
// Optional: custom serialization
|
||||
serializeValue: (value: string) => {
|
||||
return { yourWidget: value }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Register the widget with the node
|
||||
addWidget(node, widget)
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
return widgetConstructor
|
||||
}
|
||||
```
|
||||
|
||||
### 2b. Create the Node-Level Logic Composable (When Needed)
|
||||
|
||||
**Only create this if your widget needs dynamic management** (showing/hiding widgets based on events, execution state, etc.). Most standard widgets only need the widget constructor composable.
|
||||
|
||||
For widgets that need node-level operations (like showing/hiding widgets dynamically), create a separate composable in `src/composables/node/`:
|
||||
|
||||
```typescript
|
||||
// src/composables/node/useNodeYourWidget.ts
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { useYourWidget } from '@/composables/widgets/useYourWidget'
|
||||
|
||||
const YOUR_WIDGET_NAME = '$$node-your-widget'
|
||||
|
||||
/**
|
||||
* Composable for handling node-level operations for YourWidget
|
||||
*/
|
||||
export function useNodeYourWidget() {
|
||||
const yourWidget = useYourWidget()
|
||||
|
||||
const findYourWidget = (node: LGraphNode) =>
|
||||
node.widgets?.find((w) => w.name === YOUR_WIDGET_NAME)
|
||||
|
||||
const addYourWidget = (node: LGraphNode) =>
|
||||
yourWidget(node, {
|
||||
name: YOUR_WIDGET_NAME,
|
||||
type: 'yourWidgetType'
|
||||
})
|
||||
|
||||
/**
|
||||
* Shows your widget for a node
|
||||
* @param node The graph node to show the widget for
|
||||
* @param value The value to set
|
||||
*/
|
||||
function showYourWidget(node: LGraphNode, value: string) {
|
||||
const widget = findYourWidget(node) ?? addYourWidget(node)
|
||||
widget.value = value
|
||||
node.setDirtyCanvas?.(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes your widget from a node
|
||||
* @param node The graph node to remove the widget from
|
||||
*/
|
||||
function removeYourWidget(node: LGraphNode) {
|
||||
if (!node.widgets) return
|
||||
|
||||
const widgetIdx = node.widgets.findIndex(
|
||||
(w) => w.name === YOUR_WIDGET_NAME
|
||||
)
|
||||
|
||||
if (widgetIdx > -1) {
|
||||
node.widgets[widgetIdx].onRemove?.()
|
||||
node.widgets.splice(widgetIdx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showYourWidget,
|
||||
removeYourWidget
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Register the Widget
|
||||
|
||||
Add your widget to the global widget registry in `src/scripts/widgets.ts`:
|
||||
|
||||
```typescript
|
||||
// src/scripts/widgets.ts
|
||||
import { useYourWidget } from '@/composables/widgets/useYourWidget'
|
||||
import { transformWidgetConstructorV2ToV1 } from '@/scripts/utils'
|
||||
|
||||
export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
// ... existing widgets ...
|
||||
YOUR_WIDGET: transformWidgetConstructorV2ToV1(useYourWidget()),
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Handle Widget-Specific Logic
|
||||
|
||||
For widgets that need special handling (e.g., listening to execution events):
|
||||
|
||||
```typescript
|
||||
// In your composable or a separate composable
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { watchEffect, onUnmounted } from 'vue'
|
||||
|
||||
export const useYourWidgetLogic = (nodeId: string) => {
|
||||
const executionStore = useExecutionStore()
|
||||
|
||||
// Watch for execution state changes
|
||||
const stopWatcher = watchEffect(() => {
|
||||
if (executionStore.isNodeExecuting(nodeId)) {
|
||||
// Handle execution start
|
||||
}
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
onUnmounted(() => {
|
||||
stopWatcher()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Handle Complex Widget Types
|
||||
|
||||
For widgets with complex data types or special requirements:
|
||||
|
||||
```typescript
|
||||
// Multi-value widget example
|
||||
const widget = new ComponentWidgetImpl<string[]>({
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: MultiSelectWidget,
|
||||
inputSpec,
|
||||
options: {
|
||||
getValue: () => widgetValue.value,
|
||||
setValue: (value: string[]) => {
|
||||
widgetValue.value = Array.isArray(value) ? value : []
|
||||
},
|
||||
getMinHeight: () => 40 + PADDING,
|
||||
|
||||
// Custom validation
|
||||
isValid: (value: string[]) => {
|
||||
return Array.isArray(value) && value.length > 0
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Step 6: Add Widget Props (Optional)
|
||||
|
||||
Pass additional props to your Vue component:
|
||||
|
||||
```typescript
|
||||
const widget = new ComponentWidgetImpl<string, { placeholder: string }>({
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: YourWidget,
|
||||
inputSpec,
|
||||
props: {
|
||||
placeholder: 'Enter value...'
|
||||
},
|
||||
options: {
|
||||
// ... options
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Step 7: Handle Widget Lifecycle
|
||||
|
||||
For widgets that need cleanup or special lifecycle handling:
|
||||
|
||||
```typescript
|
||||
// In your widget component
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize resources
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Cleanup resources
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Step 8: Test Your Widget
|
||||
|
||||
1. Create a test node that uses your widget:
|
||||
```python
|
||||
class TestYourWidget:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"value": ("YOUR_WIDGET", {"default": "test"})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Write unit tests for your composable:
|
||||
```typescript
|
||||
// tests-ui/composables/useYourWidget.test.ts
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { useYourWidget } from '@/composables/widgets/useYourWidget'
|
||||
|
||||
describe('useYourWidget', () => {
|
||||
it('creates widget with correct default value', () => {
|
||||
const constructor = useYourWidget({ defaultValue: 'test' })
|
||||
// ... test implementation
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Common Patterns and Best Practices
|
||||
|
||||
### 1. Use PrimeVue Components (REQUIRED)
|
||||
|
||||
**Always use PrimeVue components** for UI elements to maintain consistency across the application. ComfyUI includes PrimeVue 4.2.5 with 147 available components.
|
||||
|
||||
**Reference Documentation**:
|
||||
- See `primevue-components.md` in the project root directory for a complete list of all available PrimeVue components with descriptions and documentation links
|
||||
- Alternative location: `vue-widget-conversion/primevue-components.md` (if working in a conversion branch)
|
||||
- This reference includes all 147 components organized by category (Form, Button, Data, Panel, etc.) with enhanced descriptions
|
||||
|
||||
**Important**: When deciding how to create a widget, always consult the PrimeVue components reference first to find the most appropriate component for your use case.
|
||||
|
||||
Common widget components include:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- Text input -->
|
||||
<InputText v-model="modelValue" class="w-full" />
|
||||
|
||||
<!-- Number input -->
|
||||
<InputNumber v-model="modelValue" class="w-full" />
|
||||
|
||||
<!-- Dropdown selection -->
|
||||
<Dropdown v-model="modelValue" :options="options" class="w-full" />
|
||||
|
||||
<!-- Multi-selection -->
|
||||
<MultiSelect v-model="modelValue" :options="options" class="w-full" />
|
||||
|
||||
<!-- Toggle switch -->
|
||||
<ToggleSwitch v-model="modelValue" />
|
||||
|
||||
<!-- Slider -->
|
||||
<Slider v-model="modelValue" class="w-full" />
|
||||
|
||||
<!-- Text area -->
|
||||
<Textarea v-model="modelValue" class="w-full" />
|
||||
|
||||
<!-- File upload -->
|
||||
<FileUpload mode="basic" />
|
||||
|
||||
<!-- Color picker -->
|
||||
<ColorPicker v-model="modelValue" />
|
||||
|
||||
<!-- Rating -->
|
||||
<Rating v-model="modelValue" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import InputText from 'primevue/inputtext'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
import MultiSelect from 'primevue/multiselect'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import Slider from 'primevue/slider'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import FileUpload from 'primevue/fileupload'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import Rating from 'primevue/rating'
|
||||
</script>
|
||||
```
|
||||
|
||||
**Important**: Always import PrimeVue components individually as shown above, not from the main primevue package.
|
||||
|
||||
### 2. Handle Type Conversions
|
||||
Ensure proper type handling:
|
||||
```typescript
|
||||
setValue: (value: string | number) => {
|
||||
widgetValue.value = String(value)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Responsive Design
|
||||
Use Tailwind classes for responsive widgets:
|
||||
```vue
|
||||
<div class="w-full min-h-[40px] max-h-[200px] overflow-y-auto">
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
Add validation and error states:
|
||||
```vue
|
||||
<template>
|
||||
<div :class="{ 'border-red-500': hasError }">
|
||||
<!-- widget content -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 5. Performance
|
||||
Use `v-show` instead of `v-if` for frequently toggled content:
|
||||
```vue
|
||||
<div v-show="isExpanded">...</div>
|
||||
```
|
||||
|
||||
## File References
|
||||
|
||||
- **Widget components**: `src/components/graph/widgets/`
|
||||
- **Widget composables**: `src/composables/widgets/`
|
||||
- **Widget registration**: `src/scripts/widgets.ts`
|
||||
- **DOM widget implementation**: `src/scripts/domWidget.ts`
|
||||
- **Widget store**: `src/stores/domWidgetStore.ts`
|
||||
- **Widget container**: `src/components/graph/DomWidgets.vue`
|
||||
- **Widget wrapper**: `src/components/graph/widgets/DomWidget.vue`
|
||||
|
||||
## Real Examples from PRs
|
||||
|
||||
### Example 1: Text Progress Widget (PR #3824)
|
||||
|
||||
**Component** (`src/components/graph/widgets/TextPreviewWidget.vue`):
|
||||
```vue
|
||||
<template>
|
||||
<div class="relative w-full text-xs min-h-[28px] max-h-[200px] rounded-lg px-4 py-2 overflow-y-auto">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 break-all flex items-center gap-2">
|
||||
<span v-html="formattedText"></span>
|
||||
<Skeleton v-if="isParentNodeExecuting" class="!flex-1 !h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useNodeProgressText } from '@/composables/node/useNodeProgressText'
|
||||
import { formatMarkdownValue } from '@/utils/formatUtil'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
const { widget } = defineProps<{ widget?: object }>()
|
||||
|
||||
const { isParentNodeExecuting } = useNodeProgressText(widget?.node)
|
||||
const formattedText = computed(() => formatMarkdownValue(modelValue.value || ''))
|
||||
</script>
|
||||
```
|
||||
|
||||
### Example 2: Multi-Select Widget (PR #2987)
|
||||
|
||||
**Component** (`src/components/graph/widgets/MultiSelectWidget.vue`):
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<MultiSelect
|
||||
v-model="selectedItems"
|
||||
:options="options"
|
||||
filter
|
||||
:placeholder="placeholder"
|
||||
:max-selected-labels="3"
|
||||
:display="display"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MultiSelect from 'primevue/multiselect'
|
||||
import type { ComponentWidget } from '@/types'
|
||||
import type { ComboInputSpec } from '@/types/apiTypes'
|
||||
|
||||
const selectedItems = defineModel<string[]>({ required: true })
|
||||
const { widget } = defineProps<{ widget: ComponentWidget<string[]> }>()
|
||||
|
||||
const inputSpec = widget.inputSpec as ComboInputSpec
|
||||
const options = inputSpec.options ?? []
|
||||
const placeholder = inputSpec.multi_select?.placeholder ?? 'Select items'
|
||||
const display = inputSpec.multi_select?.chip ? 'chip' : 'comma'
|
||||
</script>
|
||||
```
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
When converting an existing widget:
|
||||
|
||||
- [ ] Identify the widget type and its current implementation
|
||||
- [ ] Create Vue component with proper v-model binding using PrimeVue components
|
||||
- [ ] Create widget constructor composable in `src/composables/widgets/`
|
||||
- [ ] Create node-level composable in `src/composables/node/` (only if widget needs dynamic management)
|
||||
- [ ] Implement getValue/setValue logic with Vue reactivity
|
||||
- [ ] Handle any special widget behavior (events, validation, execution state)
|
||||
- [ ] Register widget in ComfyWidgets registry
|
||||
- [ ] Test with actual nodes that use the widget type
|
||||
- [ ] Add unit tests for both composables
|
||||
- [ ] Update documentation if needed
|
||||
|
||||
## Key Implementation Patterns
|
||||
|
||||
### 1. Vue Component Definition
|
||||
- Use Composition API with `<script setup>`
|
||||
- Use `defineModel` for two-way binding
|
||||
- Accept `widget` prop to access configuration
|
||||
- Use Tailwind CSS for styling
|
||||
|
||||
### 2. Dual Composable Pattern
|
||||
- **Widget Composable** (`src/composables/widgets/`): Always required - creates widget constructor, handles component instantiation and value management
|
||||
- **Node Composable** (`src/composables/node/`): Only needed for dynamic widget management (showing/hiding based on events/state)
|
||||
- Return `ComfyWidgetConstructorV2` from widget composable
|
||||
- Use `ComponentWidgetImpl` class as bridge between Vue and LiteGraph
|
||||
- Handle value initialization and updates with Vue reactivity
|
||||
|
||||
### 3. Widget Registration
|
||||
- Use `ComponentWidgetImpl` as bridge between Vue and LiteGraph
|
||||
- Register in `domWidgetStore` for state management
|
||||
- Add to `ComfyWidgets` registry
|
||||
|
||||
### 4. Integration Points
|
||||
- **DomWidgets.vue**: Main container for all widgets
|
||||
- **DomWidget.vue**: Wrapper handling positioning and rendering
|
||||
- **domWidgetStore**: Centralized widget state management
|
||||
- **executionStore**: For widgets reacting to execution state
|
||||
|
||||
This guide provides a complete pathway for creating Vue-based widgets in ComfyUI, following the patterns established in PRs #3824 and #2987.
|
||||
|
||||
The system uses:
|
||||
- Cloud Scheduler → HTTP POST → Cloud Run API endpoints
|
||||
- Pub/Sub is only for event-driven tasks (file uploads, deletions)
|
||||
- Direct HTTP approach for scheduled tasks with OIDC authentication
|
||||
|
||||
Existing Scheduled Tasks
|
||||
|
||||
1. Node Reindexing - Daily at midnight
|
||||
2. Security Scanning - Hourly
|
||||
3. Comfy Node Pack Backfill - Every 6 hours
|
||||
|
||||
Standard Approach for GitHub Stars Update
|
||||
|
||||
Following the established pattern, you would:
|
||||
|
||||
1. Add API endpoint to openapi.yml
|
||||
2. Implement handler in server/implementation/registry.go
|
||||
3. Add Cloud Scheduler job in infrastructure/modules/compute/cloud_scheduler.tf
|
||||
4. Update authentication rules
|
||||
|
||||
The scheduler would be configured like:
|
||||
schedule = "0 2 */2 * *" # Every 2 days at 2 AM PST
|
||||
uri = "${var.registry_backend_url}/packs/update-github-stars?max_packs=100"
|
||||
|
||||
This follows the exact same HTTP-based pattern as the existing reindex-nodes, security-scan, and
|
||||
comfy-node-pack-backfill jobs. The infrastructure is designed around direct HTTP calls rather than
|
||||
pub/sub orchestration for scheduled tasks.
|
||||
Reference in New Issue
Block a user