feat(essentials): redesign Essentials tab [reference only]#12304
feat(essentials): redesign Essentials tab [reference only]#12304comfydesigner wants to merge 11 commits into
Conversation
…sive grid, hover popovers DRAFT / REFERENCE ONLY — not for merge. Prototype of the redesigned Essentials tab in the Node Library sidebar. Single source of truth for tile content lives in src/constants/essentialsPlaceholders.ts; many tiles are still placeholders pending node-name mapping (tracked in Notion handoff page). Highlights: - New placeholder panel with sticky section headers, subgroups, jump-to navigation, search filter, and collapsible sections - Responsive auto-fill tile grid (min 96px, fluid to fill width) - Container-query driven tile sizing — text snaps at @[112px], icon fixed at size-7 - Brand-logo support: full-color SVGs in public/assets/images/brand-logos/, optional tintable flag for currentColor SVGs that should follow theme - Hover popover renders the real NodePreviewCard via nodeDefStore lookup by tile.nodeName - Filters menu defaults to exclusive "All" mode - MarqueeLine cycle bumped to 2s - Many new placeholder icons in design-system Open items: many tiles still need node-name mappings; design picker hidden but file remains; brand vs generic-verb tile dilemma unresolved. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019e2e11-bac3-77cb-b451-2bc3ac9e64a5
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR refactors the Essentials model from categories to section/subgroup/path structures, adds placeholder UI components and a media-type filter composable, integrates essentials into node organization and the Node Library sidebar (including jump-to and per-tab controls), and includes supporting styling, locale, utility, config, and test updates. ChangesEssentials Data Model Refactor
Essentials UI Components and State
Node Organization Service Integration
Node Library Sidebar Refactor
Supporting Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🎭 Playwright: ⏳ Running... |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/design-system/src/css/style.css`:
- Around line 27-28: Update the safelist pattern in the `@source` inline
declaration by removing the non-standard size prefix "icon-s1.5-" and use the
standard Iconify pattern "icon-[lucide--{...}]" instead; specifically edit the
`@source` inline("icon-s1.5-[lucide--{...}]") entry to "`@source`
inline(\"icon-[lucide--{image-up,image-down,text,mic-vocal,speech,crop,sliders-horizontal,chart-spline}]\")"
and ensure sizing is handled via Tailwind utilities (size-*, w-*, h-*, or
font-size) on the elements that render the icons rather than in the safelist.
In `@src/components/sidebar/tabs/nodeLibrary/EssentialNodePlaceholderPopover.vue`:
- Around line 22-27: The computed selection currently falls back to
nodeDefStore.visibleNodeDefs[0] when tile.nodeName is missing or not found,
which causes an unrelated node preview to display; change the fallback to return
undefined (or null) instead of the first visible node so the popover can detect
"no mapped node" and render an empty/placeholder state. Update the computed
property that reads tile.nodeName and nodeDefStore.nodeDefsByName to return
undefined when no match is found (remove the nodeDefStore.visibleNodeDefs[0]
return) and ensure the popover template/consumer checks for a falsy selection
before rendering any node preview.
In `@src/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vue`:
- Around line 159-164: The toggleSection function currently appends key to
expandedKeys.value unconditionally which can create duplicates; update
toggleSection (referencing function toggleSection and expandedKeys.value) so
that when open is true it first checks whether key is already present and only
adds it if absent (e.g., use includes or indexOf to guard the push/spread), and
when open is false keep the existing filter logic to remove the key.
In `@src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue`:
- Around line 542-556: findScrollableAncestor may return
document.scrollingElement cast to HTMLElement which can be null in some
environments; update findScrollableAncestor to return a guaranteed HTMLElement
by using a safe fallback (e.g., document.scrollingElement ||
document.documentElement || document.body) and ensure the return value is
non-nullable. Locate the function findScrollableAncestor and replace the final
return with a fallback chain that picks document.scrollingElement if present,
otherwise document.documentElement, otherwise document.body, so callers like
smoothScrollTo never receive null.
In `@src/constants/essentialsPlaceholders.ts`:
- Around line 49-415: Labels in essentialsPlaceholders.ts are hardcoded English
strings and must be moved to i18n keys; update each label value
(section/subgroup and tile label strings like the objects under keys 'generate',
'control-guidance', 'editing-utilities' and individual tile entries such as
'Load Image', 'Nano Banana', 'Text to Image', etc.) to use the composition API
i18n translator (use useI18n().t or t('...') in the component context) and
reference new keys (e.g. essentials.sections.inputs_outputs,
essentials.generate.image.nano_banana, essentials.control.image.extract_pose,
etc.), then add corresponding entries under src/locales/en/main.json (grouped
under an essentials namespace) with the original English text; ensure any
icon/title properties that must remain strings are unchanged and only replace
user-facing label strings with t(...) references.
In `@src/views/HomeView.vue`:
- Around line 153-157: The search input in HomeView.vue lacks an accessible
label; update the input element (the <input ...
:placeholder="t('homeTab.search')">) to provide an explicit accessible name by
adding an aria-label (e.g. :aria-label="t('homeTab.search')") or by adding an
associated <label> with a unique id/for attribute and binding the translation
there; ensure the chosen approach uses the same t('homeTab.search') translation
so screen readers receive the same localized label.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 72918a1d-548c-4078-80e3-4af287814ee4
⛔ Files ignored due to path filters (89)
packages/design-system/src/icons/3d-decomp-1.svgis excluded by!**/*.svgpackages/design-system/src/icons/3d-decomp.svgis excluded by!**/*.svgpackages/design-system/src/icons/3d-enhance.svgis excluded by!**/*.svgpackages/design-system/src/icons/3d-upscale.svgis excluded by!**/*.svgpackages/design-system/src/icons/brightness-contrast.svgis excluded by!**/*.svgpackages/design-system/src/icons/canny-to-image.svgis excluded by!**/*.svgpackages/design-system/src/icons/canny-to-video.svgis excluded by!**/*.svgpackages/design-system/src/icons/channels.svgis excluded by!**/*.svgpackages/design-system/src/icons/chromatic-aberration.svgis excluded by!**/*.svgpackages/design-system/src/icons/depth-to-image.svgis excluded by!**/*.svgpackages/design-system/src/icons/depth-to-video.svgis excluded by!**/*.svgpackages/design-system/src/icons/dial.svgis excluded by!**/*.svgpackages/design-system/src/icons/frames-to-video.svgis excluded by!**/*.svgpackages/design-system/src/icons/glow.svgis excluded by!**/*.svgpackages/design-system/src/icons/grain.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-batch.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-blur.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-canny.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-captioning.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-collage.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-compare.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-depth.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-edit.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-enhance.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-inpaint.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-invert.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-iterator.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-mask-preview.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-normal-map.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-outpaint.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-pose.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-preview-1.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-preview.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-remove-background.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-rotate.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-select-object-segmentation.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-shader.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-sharpen.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-to-3d.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-to-image.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-to-layers.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-to-video.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-upscale-1.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-upscale.svgis excluded by!**/*.svgpackages/design-system/src/icons/image-vectorize.svgis excluded by!**/*.svgpackages/design-system/src/icons/layers-to-image.svgis excluded by!**/*.svgpackages/design-system/src/icons/load-3d.svgis excluded by!**/*.svgpackages/design-system/src/icons/load-audio.svgis excluded by!**/*.svgpackages/design-system/src/icons/load-lora-1.svgis excluded by!**/*.svgpackages/design-system/src/icons/load-lora.svgis excluded by!**/*.svgpackages/design-system/src/icons/load-video.svgis excluded by!**/*.svgpackages/design-system/src/icons/mask-preview.svgis excluded by!**/*.svgpackages/design-system/src/icons/music-generation.svgis excluded by!**/*.svgpackages/design-system/src/icons/pose-to-image.svgis excluded by!**/*.svgpackages/design-system/src/icons/pose-to-video.svgis excluded by!**/*.svgpackages/design-system/src/icons/save-3d.svgis excluded by!**/*.svgpackages/design-system/src/icons/save-audio.svgis excluded by!**/*.svgpackages/design-system/src/icons/save-video.svgis excluded by!**/*.svgpackages/design-system/src/icons/text-generation.svgis excluded by!**/*.svgpackages/design-system/src/icons/text-iterator.svgis excluded by!**/*.svgpackages/design-system/src/icons/text-prompt-enhance.svgis excluded by!**/*.svgpackages/design-system/src/icons/text-to-3d.svgis excluded by!**/*.svgpackages/design-system/src/icons/text-to-audio.svgis excluded by!**/*.svgpackages/design-system/src/icons/text-to-image.svgis excluded by!**/*.svgpackages/design-system/src/icons/text-to-video.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-canny.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-captioning.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-compare.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-depth.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-edit.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-enhance.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-extract-frame.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-inpaint.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-interpolation.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-normal-map.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-resize.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-rotate.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-select-object-segmentation.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-shader.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-split-screen.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-stitch.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-trim.svgis excluded by!**/*.svgpackages/design-system/src/icons/video-upscale.svgis excluded by!**/*.svgpackages/design-system/src/icons/voice-clone.svgis excluded by!**/*.svgpublic/assets/images/brand-logos/bytedance-color.svgis excluded by!**/*.svgpublic/assets/images/brand-logos/claude-color.svgis excluded by!**/*.svgpublic/assets/images/brand-logos/gemini-color.svgis excluded by!**/*.svgpublic/assets/images/brand-logos/grok.svgis excluded by!**/*.svgpublic/assets/images/brand-logos/kling-color.svgis excluded by!**/*.svg
📒 Files selected for processing (21)
knip.config.tspackages/design-system/src/css/style.csssrc/components/LiteGraphCanvasSplitterOverlay.vuesrc/components/common/MarqueeLine.vuesrc/components/sidebar/tabs/NodeLibrarySidebarTabV2.vuesrc/components/sidebar/tabs/SidebarTabTemplate.vuesrc/components/sidebar/tabs/nodeLibrary/EssentialNodePlaceholderCard.vuesrc/components/sidebar/tabs/nodeLibrary/EssentialNodePlaceholderPopover.vuesrc/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vuesrc/components/sidebar/tabs/nodeLibrary/EssentialsGapPicker.vuesrc/components/tab/Tab.vuesrc/composables/useEssentialsFilters.tssrc/composables/useEssentialsSubgroupGap.tssrc/constants/essentialsDisplayNames.tssrc/constants/essentialsPlaceholders.tssrc/locales/en/main.jsonsrc/services/nodeOrganizationService.tssrc/types/nodeOrganizationTypes.tssrc/utils/textTickerUtils.test.tssrc/utils/textTickerUtils.tssrc/views/HomeView.vue
| /* Safelist dynamic lucide icons used by essentials placeholder tiles */ | ||
| @source inline("icon-s1.5-[lucide--{image-up,image-down,text,mic-vocal,speech,crop,sliders-horizontal,chart-spline}]"); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does the @iconify/tailwind4 plugin support inline size syntax like icon-s1.5-[lucide--icon-name] or should sizing be applied via separate Tailwind size-* utilities?
💡 Result:
The @iconify/tailwind4 plugin does not support inline size syntax like icon-s1.5-[...]. Sizing for icons used with this plugin should be applied using standard Tailwind CSS sizing utilities (such as size-, w-, or h-) or by adjusting the font-size utility [1][2]. By default, icons rendered by the plugin are set to 1em x 1em [1][3]. Because these icons are treated as Tailwind components, you can resize them using standard utility classes [1][2]. For example, applying size-6 or text-2xl to the element containing the icon class will effectively resize it [1]. Additionally, you can configure a global default scale for all icons via the plugin options in your CSS [1][3]: @plugin "@iconify/tailwind4" { scale: 1.2; } There is no native support for a custom icon-s syntax within the plugin itself [1][3].
Citations:
- 1: https://iconify.design/docs/usage/css/tailwind/tailwind4/size-color.html
- 2: https://iconify.design/docs/usage/css/tailwind/iconify/size-color.html
- 3: https://iconify.design/docs/usage/css/tailwind/tailwind4/
Remove the non-standard icon-s1.5-[...] prefix from the lucide icon safelist pattern.
The icon-s1.5-[lucide--...] syntax is not supported by the @iconify/tailwind4 plugin. According to the official iconify documentation, icon sizing must be applied using standard Tailwind utilities (size-, w-, h-*, or font-size), not inline size prefixes. The safelist should use the standard iconify pattern icon-[lucide--...] and control sizing separately via Tailwind utilities on the element containing the icon.
Current pattern (incorrect)
`@source` inline("icon-s1.5-[lucide--{image-up,image-down,text,mic-vocal,speech,crop,sliders-horizontal,chart-spline}]");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/design-system/src/css/style.css` around lines 27 - 28, Update the
safelist pattern in the `@source` inline declaration by removing the non-standard
size prefix "icon-s1.5-" and use the standard Iconify pattern
"icon-[lucide--{...}]" instead; specifically edit the `@source`
inline("icon-s1.5-[lucide--{...}]") entry to "`@source`
inline(\"icon-[lucide--{image-up,image-down,text,mic-vocal,speech,crop,sliders-horizontal,chart-spline}]\")"
and ensure sizing is handled via Tailwind utilities (size-*, w-*, h-*, or
font-size) on the elements that render the icons rather than in the safelist.
| function findScrollableAncestor(el: HTMLElement): HTMLElement { | ||
| let node: HTMLElement | null = el.parentElement | ||
| while (node) { | ||
| const style = getComputedStyle(node) | ||
| const overflowY = style.overflowY | ||
| if ( | ||
| (overflowY === 'auto' || overflowY === 'scroll') && | ||
| node.scrollHeight > node.clientHeight | ||
| ) { | ||
| return node | ||
| } | ||
| node = node.parentElement | ||
| } | ||
| return document.scrollingElement as HTMLElement | ||
| } |
There was a problem hiding this comment.
document.scrollingElement can be null — add a fallback.
document.scrollingElement returns null in quirks mode or certain edge cases. The cast to HTMLElement will cause a null pointer error when smoothScrollTo tries to access scrollTop.
🛡️ Proposed fix
function findScrollableAncestor(el: HTMLElement): HTMLElement {
let node: HTMLElement | null = el.parentElement
while (node) {
const style = getComputedStyle(node)
const overflowY = style.overflowY
if (
(overflowY === 'auto' || overflowY === 'scroll') &&
node.scrollHeight > node.clientHeight
) {
return node
}
node = node.parentElement
}
- return document.scrollingElement as HTMLElement
+ return (document.scrollingElement ?? document.documentElement) as HTMLElement
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function findScrollableAncestor(el: HTMLElement): HTMLElement { | |
| let node: HTMLElement | null = el.parentElement | |
| while (node) { | |
| const style = getComputedStyle(node) | |
| const overflowY = style.overflowY | |
| if ( | |
| (overflowY === 'auto' || overflowY === 'scroll') && | |
| node.scrollHeight > node.clientHeight | |
| ) { | |
| return node | |
| } | |
| node = node.parentElement | |
| } | |
| return document.scrollingElement as HTMLElement | |
| } | |
| function findScrollableAncestor(el: HTMLElement): HTMLElement { | |
| let node: HTMLElement | null = el.parentElement | |
| while (node) { | |
| const style = getComputedStyle(node) | |
| const overflowY = style.overflowY | |
| if ( | |
| (overflowY === 'auto' || overflowY === 'scroll') && | |
| node.scrollHeight > node.clientHeight | |
| ) { | |
| return node | |
| } | |
| node = node.parentElement | |
| } | |
| return (document.scrollingElement ?? document.documentElement) as HTMLElement | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue` around lines 542 -
556, findScrollableAncestor may return document.scrollingElement cast to
HTMLElement which can be null in some environments; update
findScrollableAncestor to return a guaranteed HTMLElement by using a safe
fallback (e.g., document.scrollingElement || document.documentElement ||
document.body) and ensure the return value is non-nullable. Locate the function
findScrollableAncestor and replace the final return with a fallback chain that
picks document.scrollingElement if present, otherwise document.documentElement,
otherwise document.body, so callers like smoothScrollTo never receive null.
| label: 'Inputs & Outputs', | ||
| tiles: [ | ||
| { | ||
| label: 'Load Image', | ||
| icon: lucideIcon('image-up'), | ||
| media: 'image', | ||
| nodeName: 'LoadImage' | ||
| }, | ||
| { | ||
| label: 'Save Image', | ||
| icon: lucideIcon('image-down'), | ||
| media: 'image', | ||
| nodeName: 'SaveImage' | ||
| }, | ||
| { | ||
| label: 'Load Video', | ||
| icon: comfyIcon('load-video'), | ||
| media: 'video', | ||
| nodeName: 'LoadVideo' | ||
| }, | ||
| { | ||
| label: 'Save Video', | ||
| icon: comfyIcon('save-video'), | ||
| media: 'video', | ||
| nodeName: 'SaveVideo' | ||
| }, | ||
| { | ||
| label: 'Load 3D Model', | ||
| icon: comfyIcon('load-3d'), | ||
| media: '3d', | ||
| nodeName: 'Load3D' | ||
| }, | ||
| { | ||
| label: 'Save 3D Model', | ||
| icon: comfyIcon('save-3d'), | ||
| media: '3d', | ||
| nodeName: 'SaveGLB' | ||
| }, | ||
| { | ||
| label: 'Load Audio', | ||
| icon: comfyIcon('load-audio'), | ||
| media: 'audio', | ||
| nodeName: 'LoadAudio' | ||
| }, | ||
| { | ||
| label: 'Save Audio', | ||
| icon: comfyIcon('save-audio'), | ||
| media: 'audio', | ||
| nodeName: 'SaveAudio' | ||
| }, | ||
| { | ||
| label: 'Input Text', | ||
| icon: lucideIcon('text'), | ||
| media: 'text', | ||
| nodeName: 'PrimitiveStringMultiline' | ||
| }, | ||
| { label: 'Preview Text', icon: lucideIcon('text'), media: 'text' } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'generate', | ||
| label: 'Generate', | ||
| subgroups: [ | ||
| { | ||
| key: 'generate-image', | ||
| label: 'Image', | ||
| media: 'image', | ||
| tiles: [ | ||
| { | ||
| label: 'Nano Banana', | ||
| iconUrl: '/assets/images/brand-logos/gemini-color.svg' | ||
| }, | ||
| { | ||
| label: 'Grok Image Edit', | ||
| iconUrl: '/assets/images/brand-logos/grok.svg', | ||
| tintable: true | ||
| }, | ||
| { | ||
| label: 'Bytedance Seedream', | ||
| iconUrl: '/assets/images/brand-logos/bytedance-color.svg' | ||
| }, | ||
| { label: 'Text to Image', icon: comfyIcon('text-to-image') }, | ||
| { label: 'Edit Image', icon: comfyIcon('image-edit') }, | ||
| { label: 'Inpaint Image', icon: comfyIcon('image-inpaint') }, | ||
| { label: 'Outpaint Image', icon: comfyIcon('image-outpaint') }, | ||
| { label: 'Image to Layers', icon: comfyIcon('image-to-layers') }, | ||
| { label: 'Vectorize', icon: comfyIcon('image-vectorize') }, | ||
| { label: 'Pose to Image', icon: comfyIcon('pose-to-image') }, | ||
| { label: 'Canny to Image', icon: comfyIcon('canny-to-image') }, | ||
| { label: 'Depth to Image', icon: comfyIcon('depth-to-image') } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'generate-video', | ||
| label: 'Video', | ||
| media: 'video', | ||
| tiles: [ | ||
| { | ||
| label: 'Grok Video', | ||
| iconUrl: '/assets/images/brand-logos/grok.svg', | ||
| tintable: true | ||
| }, | ||
| { | ||
| label: 'Kling Video', | ||
| iconUrl: '/assets/images/brand-logos/kling-color.svg' | ||
| }, | ||
| { | ||
| label: 'Bytedance Seedance', | ||
| iconUrl: '/assets/images/brand-logos/bytedance-color.svg' | ||
| }, | ||
| { label: 'Text to Video', icon: comfyIcon('text-to-video') }, | ||
| { label: 'Image to Video', icon: comfyIcon('image-to-video') }, | ||
| { | ||
| label: 'First-Last Frame Video', | ||
| icon: comfyIcon('image-to-video') | ||
| }, | ||
| { label: 'Edit Video', icon: comfyIcon('video-edit') }, | ||
| { | ||
| label: 'Lipsync Video', | ||
| icon: lucideIcon('mic-vocal'), | ||
| nodeName: 'KlingLipSyncAudioToVideoNode' | ||
| }, | ||
| { label: 'Inpaint Video', icon: comfyIcon('video-inpaint') }, | ||
| { label: 'Pose to Video', icon: comfyIcon('pose-to-video') }, | ||
| { label: 'Canny to Video', icon: comfyIcon('canny-to-video') }, | ||
| { label: 'Depth to Video', icon: comfyIcon('depth-to-video') } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'generate-text', | ||
| label: 'Text', | ||
| media: 'text', | ||
| tiles: [ | ||
| { | ||
| label: 'Google Gemini', | ||
| iconUrl: '/assets/images/brand-logos/gemini-color.svg' | ||
| }, | ||
| { | ||
| label: 'Anthropic Claude', | ||
| iconUrl: '/assets/images/brand-logos/claude-color.svg' | ||
| }, | ||
| { | ||
| label: 'Generate Text', | ||
| icon: comfyIcon('text-generation'), | ||
| nodeName: 'OpenAIChatNode' | ||
| }, | ||
| { label: 'Text Enhancer', icon: comfyIcon('text-prompt-enhance') }, | ||
| { label: 'Image Captioner', icon: comfyIcon('image-captioning') }, | ||
| { label: 'Video Captioner', icon: comfyIcon('video-captioning') } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'generate-audio', | ||
| label: 'Audio', | ||
| media: 'audio', | ||
| tiles: [ | ||
| { | ||
| label: 'Text to Audio', | ||
| icon: comfyIcon('text-to-audio'), | ||
| nodeName: 'StabilityTextToAudio' | ||
| }, | ||
| { label: 'Text to Speech', icon: lucideIcon('speech') }, | ||
| { label: 'Voice Clone', icon: comfyIcon('voice-clone') } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'generate-3d', | ||
| label: '3D', | ||
| media: '3d', | ||
| tiles: [ | ||
| { | ||
| label: 'Text to Model', | ||
| icon: comfyIcon('text-to-3d'), | ||
| nodeName: 'TencentTextToModelNode' | ||
| }, | ||
| { | ||
| label: 'Image to Model', | ||
| icon: comfyIcon('image-to-3d'), | ||
| nodeName: 'TencentImageToModelNode' | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'control-guidance', | ||
| label: 'Control & Guidance', | ||
| subgroups: [ | ||
| { | ||
| key: 'control-image', | ||
| label: 'Image', | ||
| media: 'image', | ||
| tiles: [ | ||
| { label: 'Extract Pose', icon: comfyIcon('image-pose') }, | ||
| { | ||
| label: 'Extract Canny Edge', | ||
| icon: comfyIcon('image-canny'), | ||
| nodeName: 'Canny' | ||
| }, | ||
| { label: 'Extract Depth Map', icon: comfyIcon('image-depth') }, | ||
| { label: 'Extract Normal Map', icon: comfyIcon('image-normal-map') } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'control-video', | ||
| label: 'Video', | ||
| media: 'video', | ||
| tiles: [ | ||
| { label: 'Extract Pose', icon: comfyIcon('image-pose') }, | ||
| { label: 'Extract Canny Edge', icon: comfyIcon('video-canny') }, | ||
| { label: 'Extract Depth Map', icon: comfyIcon('video-depth') }, | ||
| { label: 'Extract Normal Map', icon: comfyIcon('video-normal-map') } | ||
| ] | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'editing-utilities', | ||
| label: 'Editing & Utilities', | ||
| subgroups: [ | ||
| { | ||
| key: 'image-transform', | ||
| label: 'Image Transform', | ||
| media: 'image', | ||
| tiles: [ | ||
| { | ||
| label: 'Crop Image', | ||
| icon: lucideIcon('crop'), | ||
| nodeName: 'ImageCrop' | ||
| }, | ||
| { label: 'Crop Image 2x2', icon: lucideIcon('crop') }, | ||
| { label: 'Crop Image 3x3', icon: lucideIcon('crop') }, | ||
| { | ||
| label: 'Resize Image', | ||
| icon: comfyIcon('image-upscale'), | ||
| nodeName: 'ImageScale' | ||
| }, | ||
| { label: 'Upscale Image', icon: comfyIcon('image-upscale') }, | ||
| { | ||
| label: 'Rotate Image', | ||
| icon: comfyIcon('image-rotate'), | ||
| nodeName: 'ImageRotate' | ||
| }, | ||
| { label: 'Image Collage', icon: comfyIcon('image-collage') } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'image-utilities', | ||
| label: 'Image Utilities', | ||
| media: 'image', | ||
| tiles: [ | ||
| { | ||
| label: 'Batch Image', | ||
| icon: comfyIcon('image-batch'), | ||
| nodeName: 'ImageBatch' | ||
| }, | ||
| { label: 'Compare Image', icon: comfyIcon('image-compare') }, | ||
| { | ||
| label: 'Image Frames to Video', | ||
| icon: comfyIcon('frames-to-video') | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'image-filters-effects', | ||
| label: 'Image Filters & Effects', | ||
| media: 'image', | ||
| tiles: [ | ||
| { | ||
| label: 'Invert Image', | ||
| icon: comfyIcon('image-invert'), | ||
| nodeName: 'ImageInvert' | ||
| }, | ||
| { | ||
| label: 'Chromatic Aberration', | ||
| icon: comfyIcon('chromatic-aberration') | ||
| }, | ||
| { label: 'Film Grain', icon: comfyIcon('grain') }, | ||
| { label: 'Glow', icon: comfyIcon('glow') }, | ||
| { label: 'Sharpen Image', icon: comfyIcon('image-sharpen') }, | ||
| { | ||
| label: 'Blur Image', | ||
| icon: comfyIcon('image-blur'), | ||
| nodeName: 'ImageBlur' | ||
| }, | ||
| { label: 'Shader', icon: comfyIcon('image-shader') } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'image-color', | ||
| label: 'Image Color', | ||
| media: 'image', | ||
| tiles: [ | ||
| { | ||
| label: 'Brightness & Contrast', | ||
| icon: comfyIcon('brightness-contrast') | ||
| }, | ||
| { label: 'Hue & Saturation', icon: comfyIcon('dial') }, | ||
| { label: 'Color Balance', icon: lucideIcon('sliders-horizontal') }, | ||
| { label: 'Color Curves', icon: lucideIcon('chart-spline') }, | ||
| { label: 'Levels', icon: lucideIcon('sliders-horizontal') }, | ||
| { label: 'Channels', icon: comfyIcon('channels') }, | ||
| { label: 'Color Adjust', icon: lucideIcon('sliders-horizontal') } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'image-selection-masking', | ||
| label: 'Image Selection & Masking', | ||
| media: 'image', | ||
| tiles: [ | ||
| { | ||
| label: 'Select Image Object', | ||
| icon: comfyIcon('image-select-object-segmentation') | ||
| }, | ||
| { | ||
| label: 'Remove Background', | ||
| icon: comfyIcon('image-remove-background'), | ||
| nodeName: 'RecraftRemoveBackgroundNode' | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'video-transform', | ||
| label: 'Video Transform', | ||
| media: 'video', | ||
| tiles: [{ label: 'Upscale Video', icon: comfyIcon('video-upscale') }] | ||
| }, | ||
| { | ||
| key: 'video-compose', | ||
| label: 'Video Compose', | ||
| media: 'video', | ||
| tiles: [ | ||
| { label: 'Merge Videos', icon: comfyIcon('video-stitch') }, | ||
| { label: 'Split-Screen', icon: comfyIcon('video-split-screen') }, | ||
| { | ||
| label: 'Extract Frame', | ||
| icon: comfyIcon('video-extract-frame'), | ||
| nodeName: 'VideoSlice' | ||
| }, | ||
| { | ||
| label: 'Frame Interpolation', | ||
| icon: comfyIcon('video-interpolation') | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| key: 'video-selection-masking', | ||
| label: 'Video Selection & Masking', | ||
| media: 'video', | ||
| tiles: [ | ||
| { | ||
| label: 'Select Video Object', | ||
| icon: comfyIcon('video-select-object-segmentation') | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| key: '3d-transform', | ||
| label: '3D Transform', | ||
| media: '3d', | ||
| tiles: [ | ||
| { label: 'Upscale 3D Model', icon: comfyIcon('3d-upscale') }, | ||
| { label: 'Decompose 3D Model', icon: comfyIcon('3d-decomp') } | ||
| ] | ||
| } | ||
| ] | ||
| } |
There was a problem hiding this comment.
Move Essentials taxonomy labels to i18n keys before merge.
Section/subgroup/tile labels are hardcoded English strings, so this tab won’t localize correctly once exposed beyond prototype usage.
As per coding guidelines: "Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/constants/essentialsPlaceholders.ts` around lines 49 - 415, Labels in
essentialsPlaceholders.ts are hardcoded English strings and must be moved to
i18n keys; update each label value (section/subgroup and tile label strings like
the objects under keys 'generate', 'control-guidance', 'editing-utilities' and
individual tile entries such as 'Load Image', 'Nano Banana', 'Text to Image',
etc.) to use the composition API i18n translator (use useI18n().t or t('...') in
the component context) and reference new keys (e.g.
essentials.sections.inputs_outputs, essentials.generate.image.nano_banana,
essentials.control.image.extract_pose, etc.), then add corresponding entries
under src/locales/en/main.json (grouped under an essentials namespace) with the
original English text; ensure any icon/title properties that must remain strings
are unchanged and only replace user-facing label strings with t(...) references.
| <input | ||
| type="text" | ||
| :placeholder="t('homeTab.search')" | ||
| class="min-w-0 flex-1 bg-transparent text-sm text-base-foreground outline-none placeholder:text-muted-foreground" | ||
| /> |
There was a problem hiding this comment.
Add accessible label for search input.
The search input uses only a placeholder for identification, which is insufficient for screen readers. Screen reader users may not be aware of the input's purpose without a proper label.
♿ Proposed fix to add aria-label
<input
type="text"
:placeholder="t('homeTab.search')"
+ :aria-label="t('homeTab.search')"
class="min-w-0 flex-1 bg-transparent text-sm text-base-foreground outline-none placeholder:text-muted-foreground"
/>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/views/HomeView.vue` around lines 153 - 157, The search input in
HomeView.vue lacks an accessible label; update the input element (the <input ...
:placeholder="t('homeTab.search')">) to provide an explicit accessible name by
adding an aria-label (e.g. :aria-label="t('homeTab.search')") or by adding an
associated <label> with a unique id/for attribute and binding the translation
there; ensure the chosen approach uses the same t('homeTab.search') translation
so screen readers receive the same localized label.
Restructures the "Essential nodes" section of the All Nodes tab to a
two-level hierarchy matching the Essentials tab Jump-To menu:
Inputs & Outputs
Generate / {Image, Video, Text, Audio, 3D}
Control & Guidance / {Image, Video}
Editing & Utilities / {Image Transform, Image Utilities, ...}
essentialsNodes.ts replaces the flat ESSENTIALS_NODES record with a
path-based ESSENTIALS_NODE_PATHS list — each node has a
{ section, subgroup? } path. Display order is preserved per bucket via
ESSENTIALS_NODE_RANK, ESSENTIALS_SECTION_RANK, ESSENTIALS_SUBGROUP_RANK.
Legacy backend `essentials_category` strings ("basics",
"image generation", etc.) are mapped to a section-only path as a
fallback so unmapped nodes still slot under a sensible parent.
Blueprint resolver renamed
resolveBlueprintEssentialsCategory → resolveBlueprintEssentialsPath.
Amp-Thread-ID: https://ampcode.com/threads/T-019e2e11-bac3-77cb-b451-2bc3ac9e64a5
Co-authored-by: Amp <amp@ampcode.com>
…l Nodes filter In the All Nodes tab toolbar, place the Filter dropdown immediately to the right of the search input and push the Sort dropdown to the end. Mirrors the Filter-first layout of the Essentials tab. The Filter menu now leads with an "All" checkbox that follows the same exclusive-All behavior used by the Essentials media filter: - Defaults to "All" checked, every individual category unchecked - Selecting any category snaps "All" off and that category on - Selecting "All" (or checking every category, or unchecking the last one) snaps back to "All" - effectiveFilterOptions exposes all-true while in "All" mode so the All Nodes panel renders unchanged in the default state Amp-Thread-ID: https://ampcode.com/threads/T-019e2e11-bac3-77cb-b451-2bc3ac9e64a5 Co-authored-by: Amp <amp@ampcode.com>
Removes EssentialsGapPicker.vue and the useEssentialsSubgroupGap composable entirely. The picker defaults are baked into the panel: - Section header: text-sm text-muted-foreground (uppercase bold) - Subsection header: text-sm text-foreground - Subgroup gap: gap-8 - Tile grid: grid-cols-[repeat(auto-fill,minmax(96px,1fr))] - Jump-to dropdown: submenu variant - Collapsible sections: off The collapsible plumbing (CollapsibleRoot/Trigger/Content) in EssentialNodesPlaceholderPanel is removed since it was always off. Knip ignore entry for the picker is dropped. Amp-Thread-ID: https://ampcode.com/threads/T-019e2e11-bac3-77cb-b451-2bc3ac9e64a5 Co-authored-by: Amp <amp@ampcode.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vue`:
- Around line 17-33: The section and subgroup containers render before media
filtering, causing empty headers; fix by filtering tiles/subgroups first and
using those filtered arrays to decide rendering. In the
EssentialNodesPlaceholderPanel.vue template, compute local filtered arrays for
section.tiles (e.g., filteredTiles = section.tiles?.filter(t => !t.media ||
mediaFilters[t.media])) and for section.subgroups (e.g., filteredSubgroups =
section.subgroups?.filter(s => mediaFilters[s.media])), then change the v-if and
v-for to use filteredTiles.length and filteredSubgroups respectively and pass
filteredTiles into EssentialNodePlaceholderCard rendering so no empty
section/subgroup blocks are rendered.
- Line 24: The v-for loops in EssentialNodesPlaceholderPanel.vue use
:key="index", causing Vue to reuse component instances when the filtered/search
results change and leaking hover/popover state; update both v-for keys (the ones
currently using :key="index") to use a stable unique identifier from the
iterated item (e.g., item.id, node.id, or a stable string like
`${item.type}-${item.name}`) so each tile has a persistent key across
filters/queries and components are recreated correctly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f2b8cb4c-2f9f-4b56-8f52-b3af22b7f37a
📒 Files selected for processing (3)
knip.config.tssrc/components/sidebar/tabs/NodeLibrarySidebarTabV2.vuesrc/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vue
✅ Files skipped from review due to trivial changes (1)
- knip.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue
| v-if="section.tiles?.length" | ||
| class="grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-2 pb-4" | ||
| > | ||
| <EssentialNodePlaceholderCard | ||
| v-for="(tile, index) in section.tiles.filter( | ||
| (t) => !t.media || mediaFilters[t.media] | ||
| )" | ||
| :key="index" | ||
| :tile="tile" | ||
| /> | ||
| </div> | ||
| <div v-else class="flex flex-col gap-8"> | ||
| <div | ||
| v-for="subgroup in section.subgroups?.filter( | ||
| (s) => mediaFilters[s.media] | ||
| )" | ||
| :id="`essentials-subgroup-${subgroup.key}`" |
There was a problem hiding this comment.
Apply media filtering before section/subgroup render to avoid empty blocks.
On Line 17 and Line 30, the section/subgroup containers are rendered before media filtering is fully resolved, so a section header can appear with no visible tiles.
Proposed fix
- <div
- v-for="section in filteredSections"
+ <div
+ v-for="section in visibleSections"
:id="`essentials-section-${section.key}`"
:key="section.key"
class="scroll-mt-[65px] border-b border-border-default last:border-b-0"
@@
- <EssentialNodePlaceholderCard
- v-for="(tile, index) in section.tiles.filter(
- (t) => !t.media || mediaFilters[t.media]
- )"
+ <EssentialNodePlaceholderCard
+ v-for="(tile, index) in section.tiles"
:key="index"
:tile="tile"
/>
@@
- v-for="subgroup in section.subgroups?.filter(
- (s) => mediaFilters[s.media]
- )"
+ v-for="subgroup in section.subgroups"
:id="`essentials-subgroup-${subgroup.key}`"
:key="subgroup.key"
class="scroll-mt-[121px] last:pb-4"
> const filteredSections = computed<EssentialPlaceholderSection[]>(() => {
@@
})
+
+const visibleSections = computed<EssentialPlaceholderSection[]>(() =>
+ filteredSections.value.flatMap((section) => {
+ if (section.tiles?.length) {
+ const tiles = section.tiles.filter(
+ (t) => !t.media || mediaFilters.value[t.media]
+ )
+ return tiles.length ? [{ ...section, tiles }] : []
+ }
+
+ const subgroups = section.subgroups?.filter((s) => mediaFilters.value[s.media])
+ return subgroups?.length ? [{ ...section, subgroups }] : []
+ })
+)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vue`
around lines 17 - 33, The section and subgroup containers render before media
filtering, causing empty headers; fix by filtering tiles/subgroups first and
using those filtered arrays to decide rendering. In the
EssentialNodesPlaceholderPanel.vue template, compute local filtered arrays for
section.tiles (e.g., filteredTiles = section.tiles?.filter(t => !t.media ||
mediaFilters[t.media])) and for section.subgroups (e.g., filteredSubgroups =
section.subgroups?.filter(s => mediaFilters[s.media])), then change the v-if and
v-for to use filteredTiles.length and filteredSubgroups respectively and pass
filteredTiles into EssentialNodePlaceholderCard rendering so no empty
section/subgroup blocks are rendered.
| v-for="(tile, index) in section.tiles.filter( | ||
| (t) => !t.media || mediaFilters[t.media] | ||
| )" | ||
| :key="index" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify there are no index-based keys left in this component.
rg -n ':\s*key="index"' src/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vueRepository: Comfy-Org/ComfyUI_frontend
Length of output: 134
🏁 Script executed:
cat -n src/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vueRepository: Comfy-Org/ComfyUI_frontend
Length of output: 4376
🏁 Script executed:
cat -n src/constants/essentialsPlaceholders.ts | head -80Repository: Comfy-Org/ComfyUI_frontend
Length of output: 2789
Use stable keys for tiles instead of array indexes.
Lines 24 and 48 use :key="index" with filtered v-for loops. When filtering or searching changes the array, Vue will reuse the wrong component instance and leak hover/popover state between tiles.
Proposed fix
- <EssentialNodePlaceholderCard
- v-for="(tile, index) in section.tiles"
- :key="index"
+ <EssentialNodePlaceholderCard
+ v-for="tile in section.tiles"
+ :key="tile.nodeName ?? `${section.key}-${tile.label}`"
:tile="tile"
/>
@@
- <EssentialNodePlaceholderCard
- v-for="(tile, index) in subgroup.tiles"
- :key="index"
+ <EssentialNodePlaceholderCard
+ v-for="tile in subgroup.tiles"
+ :key="tile.nodeName ?? `${subgroup.key}-${tile.label}`"
:tile="tile"
/>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vue`
at line 24, The v-for loops in EssentialNodesPlaceholderPanel.vue use
:key="index", causing Vue to reuse component instances when the filtered/search
results change and leaking hover/popover state; update both v-for keys (the ones
currently using :key="index") to use a stable unique identifier from the
iterated item (e.g., item.id, node.id, or a stable string like
`${item.type}-${item.name}`) so each tile has a persistent key across
filters/queries and components are recreated correctly.
Wraps the Nodes title, tabs, and search row in a single sticky block. The block's `top` value is animated between 0 (header revealed) and -titleTabsHeight (header collapsed; only the search row visible at the top) based on scroll direction with a small threshold to avoid jitter. Scroll listener attached via VueUse useEventListener on the scroll container; the title+tabs height is measured live so different tab labels do not need hard-coded offsets.
Amp-Thread-ID: https://ampcode.com/threads/T-019e2e11-bac3-77cb-b451-2bc3ac9e64a5 Co-authored-by: Amp <amp@ampcode.com>
- Drop the horizontal divider next to subgroup labels; the gap between subgroups now carries the rhythm - Bump inter-subgroup gap from gap-8 (32px) to gap-12 (48px) - Control & Guidance: remove Extract Pose, Extract Depth Map, Extract Normal Map from Image; remove the entire Video subgroup. None of the blueprints these were meant to link to are live yet - Replace "Upscale 3D Model" tile with "UV Unwrapping" using the lucide package-open icon (safelisted in style.css)
Amp-Thread-ID: https://ampcode.com/threads/T-019e3d28-6bbf-771a-a809-dce8879b91dc Co-authored-by: Amp <amp@ampcode.com>
Map every Essentials placeholder tile to a registered node or blueprint, and rework the hover popover to resolve them. - Resolve via allNodeDefsByName (which merges in subgraphStore.subgraphDefCache) so blueprints register under their hash-prefixed name; fall back to scanning nodeDefs by display_name after stripping the SubgraphBlueprint. prefix. - Use ByteDance (capital D), Kling Image(First Frame) (no space), and Hunyuan3D: Text to Model (colon) to match the backend's exact display_name strings. - Add Load LoRA to Inputs & Outputs; drop Merge Videos from Video Compose. Amp-Thread-ID: https://ampcode.com/threads/T-019e3d28-6bbf-771a-a809-dce8879b91dc Co-authored-by: Amp <amp@ampcode.com>
Match the registered class name or current (non-DEPRECATED) display_name returned by /api/object_info: - Trim Video VideoSlice → Video Slice - Image Collage StitchImages → ImageStitch - Input Text String (Multiline) → PrimitiveStringMultiline - Compare Image image compare → ImageCompare - Crop Image ImageCrop → ImageCropV2 - Batch Image ImageBatch → BatchImagesNode Amp-Thread-ID: https://ampcode.com/threads/T-019e3d28-6bbf-771a-a809-dce8879b91dc Co-authored-by: Amp <amp@ampcode.com>
Resolve each tile to a real ComfyNodeDefImpl via a new shared composable and wire the same drag/click handlers the All Nodes tab uses: - useEssentialTileNodeDef centralises the nodeDef lookup (name first, then display_name fallback after stripping the SubgraphBlueprint. prefix) for both the popover and the card. - The card now hosts dragstart/dragend/click handlers that delegate to useNodeDragToCanvas. Dragstart sets application/x-comfy-node and an empty native drag image so NodeDragPreview (already mounted in the V2 sidebar) renders the ghost. Click enters click-to-place mode. - Tiles whose nodeName does not resolve (e.g. Anthropic Claude on a backend that has not registered it) stay non-draggable with the default cursor. - Popover hides while dragging. Amp-Thread-ID: https://ampcode.com/threads/T-019e3d28-6bbf-771a-a809-dce8879b91dc Co-authored-by: Amp <amp@ampcode.com>

Summary
Prototype of the redesigned Essentials tab in the Node Library sidebar. Replaces the existing Essentials view with a placeholder-driven panel of categorized tiles, sticky section headers, hover popovers, and responsive container-query sizing.
Changes
EssentialNodesPlaceholderPanelwith sticky section headers, subgroups, jump-to navigation, search filter, collapsible sections.@[112px], icon fixed atsize-7).iconUrl+ optionaltintableflag (CSS-mask render forcurrentColorSVGs).NodePreviewCardvianodeDefStorelookup bytile.nodeName.MarqueeLinecycle bumped to 2s.packages/design-system.src/constants/essentialsPlaceholders.ts.Review Focus
This PR is not for merge — please treat it as a reference snapshot. Useful angles:
tintablemask rendering approachuseNodePreviewAndDrag)essentialsPlaceholders.ts— pending source-of-truth handoff in NotionOpen Items (not blockers, called out for awareness)
nodeNamemappings (tracked in Notion handoff)EssentialsGapPicker.vue) is hidden but file remains┆Issue is synchronized with this Notion page by Unito