feat: per-widget grid override via node.properties (PoC)

Adds a 'grid overrides' concept on node properties keyed by widget input
name. Each entry overrides the CSS grid-template-rows track for that
widget in the node's widget grid (e.g. '200px', 'minmax(150px, 300px)',
'1fr'), letting users size individual textareas/widgets independently of
the node's auto-fill layout.

Storage: node.properties.gridOverrides[widgetName] = '<css-track>'
(persists with the workflow JSON).

Rendering: useProcessedWidgets consults overrides before falling back to
the existing shouldExpand/hasLayoutSize default.

Testing UI: right-click a node -> 'Widget Grid Sizes' submenu lists each
widget; pick one to set or clear a custom track value.
This commit is contained in:
Glary-Bot
2026-05-12 05:37:32 +00:00
committed by Connor Byrne
parent 4e07fe3a43
commit bc966a39b7
4 changed files with 169 additions and 6 deletions

View File

@@ -99,6 +99,13 @@ export interface SafeWidgetData {
promotedLabel?: string
}
/**
* Maps widget name -> CSS grid-template-rows track value
* (e.g. '200px', 'minmax(150px, 300px)', '1fr', 'auto').
* Persisted on `node.properties.gridOverrides`.
*/
export type WidgetGridOverrides = Record<string, string>
export interface VueNodeData {
executing: boolean
id: NodeId
@@ -115,6 +122,7 @@ export interface VueNodeData {
ghost?: boolean
pinned?: boolean
}
gridOverrides?: WidgetGridOverrides
hasErrors?: boolean
inputs?: INodeInputSlot[]
outputs?: INodeOutputSlot[]
@@ -133,6 +141,9 @@ export interface GraphNodeManager {
// Access to original LiteGraph nodes (non-reactive)
getNode(id: string): LGraphNode | undefined
// Re-extract VueNodeData for fields not covered by tracked-property events
refreshNode(id: string): void
// Lifecycle methods
cleanup(): void
}
@@ -513,12 +524,23 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
flags: node.flags ? { ...node.flags } : undefined,
color: node.color || undefined,
bgcolor: node.bgcolor || undefined,
gridOverrides: readGridOverrides(node),
resizable: node.resizable,
shape: node.shape,
showAdvanced: node.showAdvanced
}
}
function readGridOverrides(node: LGraphNode): WidgetGridOverrides | undefined {
const raw = node.properties?.gridOverrides
if (!raw || typeof raw !== 'object') return undefined
const entries = Object.entries(raw as Record<string, unknown>).filter(
(entry): entry is [string, string] => typeof entry[1] === 'string'
)
if (entries.length === 0) return undefined
return Object.fromEntries(entries)
}
export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Get layout mutations composable
const { createNode, deleteNode, setSource } = useLayoutMutations()
@@ -858,9 +880,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
})
}
const refreshNode = (id: string) => {
const nodeRef = nodeRefs.get(id)
if (nodeRef) vueNodeData.set(id, extractVueNodeData(nodeRef))
}
return {
vueNodeData,
getNode,
refreshNode,
cleanup
}
}

View File

@@ -30,6 +30,7 @@ import './slotDefaults'
import './uploadAudio'
import './uploadImage'
import './webcamCapture'
import './widgetGridOverrides'
import './widgetInputs'
// Cloud-only extensions - tree-shaken in OSS builds

View File

@@ -0,0 +1,131 @@
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import { app } from '@/scripts/app'
import { useExtensionService } from '@/services/extensionService'
const PROPERTY_KEY = 'gridOverrides'
const PROMPT_LABEL =
'Grid row size (e.g. 200px, minmax(150px, 300px), 1fr, auto)'
type GridOverrides = Record<string, string>
function readOverrides(node: LGraphNode): GridOverrides {
const raw = node.properties?.[PROPERTY_KEY]
if (!raw || typeof raw !== 'object') return {}
return { ...(raw as GridOverrides) }
}
function writeOverrides(node: LGraphNode, next: GridOverrides): void {
if (Object.keys(next).length === 0) {
delete node.properties[PROPERTY_KEY]
} else {
node.properties[PROPERTY_KEY] = next
}
refreshVueNode(String(node.id))
app.canvas?.setDirty(true, true)
}
function refreshVueNode(nodeId: string): void {
const manager = useVueNodeLifecycle().nodeManager.value
manager?.refreshNode(nodeId)
}
function promptForRowSize(
current: string,
onSubmit: (value: string) => void
): void {
const input = window.prompt(PROMPT_LABEL, current)
if (input == null) return
const trimmed = input.trim()
if (trimmed.length === 0) return
onSubmit(trimmed)
}
function setOverride(
node: LGraphNode,
widgetName: string,
value: string
): void {
const next = readOverrides(node)
next[widgetName] = value
writeOverrides(node, next)
}
function clearOverride(node: LGraphNode, widgetName: string): void {
const next = readOverrides(node)
delete next[widgetName]
writeOverrides(node, next)
}
function buildWidgetMenuItem(
node: LGraphNode,
widgetName: string
): IContextMenuValue {
const overrides = readOverrides(node)
const current = overrides[widgetName]
const label = current
? `${widgetName}${current}`
: `${widgetName} → (auto)`
return {
content: label,
has_submenu: true,
callback: () => {
promptForRowSize(current ?? '200px', (value) =>
setOverride(node, widgetName, value)
)
},
submenu: {
options: [
{
content: 'Set size…',
callback: () => {
promptForRowSize(current ?? '200px', (value) =>
setOverride(node, widgetName, value)
)
}
},
{
content: 'Clear override',
disabled: !current,
callback: () => clearOverride(node, widgetName)
}
]
}
}
}
useExtensionService().registerExtension({
name: 'Comfy.WidgetGridOverrides',
getNodeMenuItems(node: LGraphNode): (IContextMenuValue | null)[] {
const widgets = node.widgets ?? []
if (widgets.length === 0) return []
const overrides = readOverrides(node)
const hasAny = Object.keys(overrides).length > 0
const widgetItems: (IContextMenuValue | null)[] = widgets.map((widget) =>
buildWidgetMenuItem(node, widget.name)
)
if (hasAny) {
widgetItems.push(null, {
content: 'Clear all overrides',
callback: () => writeOverrides(node, {})
})
}
return [
null,
{
content: 'Widget Grid Sizes',
has_submenu: true,
callback: () => {},
submenu: {
options: widgetItems
}
}
]
}
})

View File

@@ -412,13 +412,16 @@ export function useProcessedWidgets(
)
)
const gridTemplateRows = computed((): string =>
visibleWidgets.value
.map((w) =>
shouldExpand(w.type) || w.hasLayoutSize ? 'auto' : 'min-content'
)
const gridTemplateRows = computed((): string => {
const overrides = nodeDataGetter()?.gridOverrides
return visibleWidgets.value
.map((w) => {
const override = overrides?.[w.name]
if (override) return override
return shouldExpand(w.type) || w.hasLayoutSize ? 'auto' : 'min-content'
})
.join(' ')
)
})
return {
canSelectInputs,