Compare commits

...

2 Commits

Author SHA1 Message Date
Alexander Brown
ff9e6415b5 fix(nodes-2): apply Textarea widget font-size setting in Vue Nodes 2.0 (#12386)
*PR Created by the Glary-Bot Agent*

---

## Summary

`Settings → Appearance → Node Widget → Textarea widget font size`
(`Comfy.TextareaWidget.FontSize`) was wired through the legacy LiteGraph
textarea only. The Vue Nodes 2.0 `WidgetTextarea.vue` hardcoded Tailwind
`text-xs`, so once Vue nodes were enabled the slider had no effect.

`GraphView.vue` already writes the setting value to
`--comfy-textarea-font-size` on `:root` for the legacy
`.comfy-multiline-input` rule. This PR makes `WidgetTextarea` consume
the same variable via Tailwind v4's parenthesized CSS-variable
shorthand, keeping `GraphView` as the single source of truth.

- `text-xs` → `text-(length:--comfy-textarea-font-size) leading-normal`.
The `length:` type hint is required because `text-` is ambiguous between
`font-size` and `color`. `leading-normal` keeps line-height proportional
to font-size across the 8–24 px range so multi-line text doesn't clip at
the high end.
- Initialize `--comfy-textarea-font-size: 10px` on `:root` in the
design-system stylesheet so isolated renders (Storybook, tests) that do
not mount `GraphView` still pick up the documented default.

- Fixes
[FE-799](https://linear.app/comfyorg/issue/FE-799/bug-textarea-widget-font-size-setting-not-working-in-nodes-20)

## Verification

- `pnpm typecheck`, `pnpm lint`, `pnpm exec stylelint`, `pnpm exec oxfmt
--check`, `pnpm knip`, and `WidgetTextarea.test.ts` (20 tests) all pass.
- Manual browser verification with Vue Nodes 2.0 enabled and a
`CLIPTextEncode` node:
  - setting `8` → computed `font-size: 8px`
  - setting `22` → computed `font-size: 22px`
- setting `24` → computed `font-size: 24px`, computed `line-height:
36px` (ratio 1.5, no clipping)
- Confirmed the legacy LiteGraph path still resolves to
`comfy-multiline-input` with `font-size: 22px` when Vue Nodes is
disabled (no regression).
- Confirmed the `:root` default resolves to `10px` when `GraphView`'s
inline override is absent (Storybook-like environments).

## Out of scope (follow-up)

`WidgetMarkdown.vue` (the Vue Nodes 2.0 markdown/tiptap widget) also
hardcodes `text-sm`. The legacy `.comfy-markdown .tiptap` rule reads the
same `--comfy-textarea-font-size` variable, so the setting historically
governed markdown widgets in Nodes 1.0. Bringing that into line with
this PR's approach is a follow-up the design team should weigh in on
before changing.

## Screenshots


![textarea-fontsize-8px](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/6f2a0d3f4193afb5583ebf6c01c558f759abf5059f5f0535c4168e081a68ea47/pr-images/1779307449630-3046bbe9-cb29-41f7-8994-9d251bd0ab5d.png)


![textarea-fontsize-22px](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/6f2a0d3f4193afb5583ebf6c01c558f759abf5059f5f0535c4168e081a68ea47/pr-images/1779307449987-46aed4a1-b09c-4b2e-88cf-e6302944c319.png)


![textarea-fontsize-24px-multiline](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/6f2a0d3f4193afb5583ebf6c01c558f759abf5059f5f0535c4168e081a68ea47/pr-images/1779307450337-164136c9-b1e2-4dac-8390-4d935d416675.png)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12386-fix-nodes-2-apply-Textarea-widget-font-size-setting-in-Vue-Nodes-2-0-3666d73d365081fd8084e84a41ee357b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-06-06 00:26:51 +00:00
Terry Jia
c695aa1ee0 fix(load3d): load Preview3DAdvanced / splat / pointcloud previews from temp/ (#12671)
BE is https://github.com/Comfy-Org/ComfyUI/pull/14294, need FE as well

Pair with backend
[BE-1172](https://github.com/Comfy-Org/ComfyUI/pull/14294). Two changes
bundled because both touch the same Preview3DAdvanced /
PreviewGaussianSplat / PreviewPointCloud

1. Backend now saves the incoming File3D into temp/ instead of output/
2. viewport input renamed from 'image' to 'viewport_state', Backend
renamed IO.Load3D.Input("image") to ("viewport_state") on the three
nodes (the field carries the viewport snapshot).
3. Exsting Load3D / Preview3D keep 'image' for workflow JSON
compatibility.
2026-06-05 20:04:09 -04:00
12 changed files with 57 additions and 16 deletions

View File

@@ -0,0 +1,30 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe(
'Textarea widget font size',
{ tag: ['@widget', '@vue-nodes'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('default')
})
test('applies Comfy.TextareaWidget.FontSize to Vue Nodes 2.0 textarea widget', async ({
comfyPage
}) => {
const textarea = comfyPage.vueNodes.nodes.locator('textarea').first()
await expect(textarea).toBeVisible()
await comfyPage.settings.setSetting('Comfy.TextareaWidget.FontSize', 14)
await expect
.poll(() => textarea.evaluate((el) => getComputedStyle(el).fontSize))
.toBe('14px')
await comfyPage.settings.setSetting('Comfy.TextareaWidget.FontSize', 22)
await expect
.poll(() => textarea.evaluate((el) => getComputedStyle(el).fontSize))
.toBe('22px')
})
}
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -121,6 +121,7 @@
--comfy-topbar-height: 2.5rem;
--workflow-tabs-height: 2.375rem;
--comfy-input-bg: #222;
--comfy-textarea-font-size: 10px;
--input-text: #ddd;
--descrip-text: #999;
--drag-text: #ccc;

View File

@@ -173,7 +173,7 @@ function makePreview3DAdvancedNode(
constructor: { comfyClass: overrides.comfyClass ?? 'Preview3DAdvanced' },
size: [400, 550],
setSize: vi.fn(),
widgets: overrides.widgets ?? [{ name: 'image', value: '' }],
widgets: overrides.widgets ?? [{ name: 'viewport_state', value: '' }],
properties: overrides.properties ?? {}
} as unknown as LGraphNode
}
@@ -783,9 +783,9 @@ describe('Comfy.Preview3DAdvanced.nodeCreated', () => {
expect(load3dInstance.setCameraState).not.toHaveBeenCalled()
})
it('attaches a camera-only serializeValue to the image widget', async () => {
it('attaches a camera-only serializeValue to the viewport_state widget', async () => {
const { preview3DAdvancedExt } = await loadExtensionsFresh()
const widgets: FakeWidget[] = [{ name: 'image', value: '' }]
const widgets: FakeWidget[] = [{ name: 'viewport_state', value: '' }]
const node = makePreview3DAdvancedNode({ widgets })
await preview3DAdvancedExt.nodeCreated(node)
@@ -795,7 +795,7 @@ describe('Comfy.Preview3DAdvanced.nodeCreated', () => {
it('serializeValue returns live camera_info plus empty media fields, omitting model_3d_info when none', async () => {
const { preview3DAdvancedExt } = await loadExtensionsFresh()
const widgets: FakeWidget[] = [{ name: 'image', value: '' }]
const widgets: FakeWidget[] = [{ name: 'viewport_state', value: '' }]
const node = makePreview3DAdvancedNode({ widgets })
const load3d = makeLoad3dMock()
@@ -819,7 +819,7 @@ describe('Comfy.Preview3DAdvanced.nodeCreated', () => {
it('serializeValue wraps a present getModelInfo result in a single-element list', async () => {
const { preview3DAdvancedExt } = await loadExtensionsFresh()
const widgets: FakeWidget[] = [{ name: 'image', value: '' }]
const widgets: FakeWidget[] = [{ name: 'viewport_state', value: '' }]
const node = makePreview3DAdvancedNode({ widgets })
const load3d = makeLoad3dMock()

View File

@@ -270,8 +270,16 @@ useExtensionService().registerExtension({
}
],
getCustomWidgets() {
const VIEWPORT_STATE_NODES = new Set([
'Preview3DAdvanced',
'PreviewGaussianSplat',
'PreviewPointCloud'
])
return {
LOAD_3D(node) {
const inputName = VIEWPORT_STATE_NODES.has(node.constructor.comfyClass)
? 'viewport_state'
: 'image'
const hasModelFileWidget = node.widgets?.some(
(w) => w.name === 'model_file'
)
@@ -316,9 +324,9 @@ useExtensionService().registerExtension({
const widget = new ComponentWidgetImpl({
node: node,
name: 'image',
name: inputName,
component: Load3D,
inputSpec: inputSpecLoad3D,
inputSpec: { ...inputSpecLoad3D, name: inputName },
options: {}
})
@@ -715,7 +723,7 @@ useExtensionService().registerExtension({
})
useLoad3d(node).waitForLoad3d((load3d) => {
const sceneWidget = node.widgets?.find((w) => w.name === 'image')
const sceneWidget = node.widgets?.find((w) => w.name === 'viewport_state')
if (!sceneWidget) return
const resolveLoad3d = () => nodeToLoad3dMap.get(node) ?? load3d

View File

@@ -186,7 +186,7 @@ describe('Comfy.PreviewGaussianSplat.nodeCreated', () => {
expect(node.properties['Last Time Model File']).toBe('scene.ply')
expect(configureForSaveMeshMock).toHaveBeenLastCalledWith(
'output',
'temp',
'scene.ply',
expect.objectContaining({ silentOnNotFound: true })
)
@@ -231,7 +231,7 @@ describe('Comfy.PreviewGaussianSplat.nodeCreated', () => {
const node = makePreviewNode({
widgets: [
{ name: 'model_file', value: '' },
{ name: 'image', value: '' },
{ name: 'viewport_state', value: '' },
widthWidget,
heightWidget
]
@@ -262,7 +262,7 @@ describe('Comfy.PreviewGaussianSplat.nodeCreated', () => {
)
const sceneWidget: FakeWidget & {
serializeValue?: () => Promise<unknown>
} = { name: 'image', value: '' }
} = { name: 'viewport_state', value: '' }
const node = makePreviewNode({
widgets: [{ name: 'model_file', value: '' }, sceneWidget]
})
@@ -318,7 +318,7 @@ describe('Comfy.PreviewPointCloud.nodeCreated', () => {
expect(node.properties['Last Time Model File']).toBe('pointcloud.ply')
expect(configureForSaveMeshMock).toHaveBeenLastCalledWith(
'output',
'temp',
'pointcloud.ply',
expect.objectContaining({ silentOnNotFound: true })
)

View File

@@ -46,7 +46,7 @@ function applyResultToLoad3d(
}
const config = new Load3DConfiguration(load3d, node.properties)
config.configureForSaveMesh('output', normalizedPath, {
config.configureForSaveMesh('temp', normalizedPath, {
silentOnNotFound: true
})
@@ -119,7 +119,7 @@ function createPreview3DExtension(
if (!lastTimeModelFile) return
const config = new Load3DConfiguration(load3d, node.properties)
config.configureForSaveMesh('output', lastTimeModelFile as string, {
config.configureForSaveMesh('temp', lastTimeModelFile as string, {
silentOnNotFound: true
})
@@ -136,7 +136,9 @@ function createPreview3DExtension(
})
waitForLoad3d((load3d) => {
const sceneWidget = node.widgets?.find((w) => w.name === 'image')
const sceneWidget = node.widgets?.find(
(w) => w.name === 'viewport_state'
)
const widthWidget = node.widgets?.find((w) => w.name === 'width')
const heightWidget = node.widgets?.find((w) => w.name === 'height')

View File

@@ -22,7 +22,7 @@
:class="
cn(
WidgetInputBaseClass,
'size-full resize-none text-xs',
'size-full resize-none text-(length:--comfy-textarea-font-size) leading-normal',
!hideLayoutField && 'pt-5',
// Avoid overflow-auto when idle to prevent per-textarea compositing layers.
'overflow-hidden hover:overflow-auto focus:overflow-auto'