Compare commits
2 Commits
test/fe-74
...
main
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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    ┆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> |
||
|
|
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. |
30
browser_tests/tests/textareaWidgetFontSize.spec.ts
Normal 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')
|
||||
})
|
||||
}
|
||||
)
|
||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 104 KiB |
|
|
@@ -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;
|
||||
|
|
|
|||
|
|
@@ -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()
|
||||
|
|
|
|||
|
|
@@ -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
|
||||
|
|
|
|||
|
|
@@ -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 })
|
||||
)
|
||||
|
|
|
|||
|
|
@@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@@ -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'
|
||||
|
|
|
|||