Skip to content

feat(essentials): redesign Essentials tab [reference only]#12304

Draft
comfydesigner wants to merge 11 commits into
Comfy-Org:mainfrom
comfydesigner:essentials-update
Draft

feat(essentials): redesign Essentials tab [reference only]#12304
comfydesigner wants to merge 11 commits into
Comfy-Org:mainfrom
comfydesigner:essentials-update

Conversation

@comfydesigner
Copy link
Copy Markdown
Contributor

@comfydesigner comfydesigner commented May 16, 2026

⚠️ DRAFT — REFERENCE ONLY

This PR is not intended for merge. It captures the current state of the Essentials tab redesign prototype for review and handoff. Many tiles are still unmapped placeholders, the dev picker is hidden but retained, and several open design questions remain unresolved.

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

  • What:
    • New EssentialNodesPlaceholderPanel with sticky section headers, subgroups, jump-to navigation, search filter, collapsible sections.
    • Responsive auto-fill tile grid (min 96px, fluid to fill width); tile sizing driven by container queries (text snaps at @[112px], icon fixed at size-7).
    • Brand-logo support via iconUrl + optional tintable flag (CSS-mask render for currentColor SVGs).
    • 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 packages/design-system.
    • Single source of truth for tile content lives in src/constants/essentialsPlaceholders.ts.
  • Breaking: None — prototype scope, behind the existing Essentials tab only.

Review Focus

This PR is not for merge — please treat it as a reference snapshot. Useful angles:

  • Tile/grid responsive behavior and container-query patterns
  • Brand-logo tintable mask rendering approach
  • Popover implementation (inline positioning vs. reusing useNodePreviewAndDrag)
  • Tile data shape in essentialsPlaceholders.ts — pending source-of-truth handoff in Notion

Open Items (not blockers, called out for awareness)

  • Many tiles still need nodeName mappings (tracked in Notion handoff)
  • Dev picker (EssentialsGapPicker.vue) is hidden but file remains
  • Brand-tagged vs generic-verb tile dilemma unresolved
  • Tile picker (Large/Small) is functionally a no-op now
  • Smooth-scroll edge case when spam-clicking Jump To items
  • Placeholder labels still hardcoded English (acceptable for prototype phase)

┆Issue is synchronized with this Notion page by Unito

…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
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This 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.

Changes

Essentials Data Model Refactor

Layer / File(s) Summary
Core path model with section/subgroup hierarchy
src/constants/essentialsNodes.ts
Introduces EssentialsPath, ESSENTIALS_SECTIONS, ESSENTIALS_SUBGROUPS, ESSENTIALS_NODE_PATHS, and derives path/rank lookup maps.
Model validation and tests
src/constants/essentialsNodes.test.ts
Updates tests to validate node-path integrity, ranks, canonicalization, and backend normalization.
Placeholder schemas and data
src/constants/essentialsPlaceholders.ts
Adds TypeScript interfaces for placeholder tiles/subgroups/sections and exports ESSENTIAL_PLACEHOLDER_SECTIONS.
Blueprint path resolution
src/constants/essentialsDisplayNames.ts
Retypes BLUEPRINT_PREFIX_MAP to use EssentialsPath and exports resolveBlueprintEssentialsPath.
Node category types
src/types/nodeOrganizationTypes.ts
Adds 'essentialNodes' to NodeCategoryId and updates NODE_CATEGORY_LABELS mapping.

Essentials UI Components and State

Layer / File(s) Summary
Media-type filter composable
src/composables/useEssentialsFilters.ts
Defines ESSENTIALS_MEDIA_TYPES, EssentialsMediaType, media filter state, control helpers, and effectiveMediaFilters.
Placeholder card and popover
src/components/sidebar/tabs/nodeLibrary/EssentialNodePlaceholderCard.vue, src/components/sidebar/tabs/nodeLibrary/EssentialNodePlaceholderPopover.vue
Renders icon/label tiles with sidebar-aware hover popovers via Teleport and displays NodePreviewCard.
Placeholder panel with search and filtering
src/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vue
Renders ESSENTIAL_PLACEHOLDER_SECTIONS, filters tiles by searchQuery, applies mediaFilters, and updates expandedKeys when searching.

Node Organization Service Integration

Layer / File(s) Summary
Essentials path resolution & tree building
src/services/nodeOrganizationService.ts
Adds resolveEssentialsPath (map → blueprint → backend), refactors buildEssentialsTree and organizeEssentials to use resolved paths.
Essentials tree sorting
src/services/nodeOrganizationService.ts
Sorts sections/subgroups using section/subgroup rank maps and orders leaf nodes by ESSENTIALS_NODE_RANK.
Node classification and essentials category
src/services/nodeOrganizationService.ts
Extends classifyNodes to return essentialNodes, splits Core/Essentials into essentialNodes vs comfyNodes, and conditionally adds essentialNodes to output.

Node Library Sidebar Refactor

Layer / File(s) Summary
Sidebar template support
src/components/sidebar/tabs/SidebarTabTemplate.vue
Adds optional hideToolbar prop to conditionally render header toolbar.
Sidebar template and control dropdowns
src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue (template)
Removes blueprints panel, adds sticky search, implements category filter, tab-dependent dropdowns (sort/media), and essentials "jump to" control; renders essentials placeholder + all-tab panels.
Sidebar state management and search
src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue (script)
Persists selectedTab via useLocalStorage, normalizes invalid stored tabs, implements effectiveFilterOptions, simplifies fillNodeInfo, updates handleSearch to skip expansion on essentials, and adds jump-to scrolling helpers.

Supporting Changes

Layer / File(s) Summary
Styling and component updates
packages/design-system/src/css/style.css, src/components/tab/Tab.vue, src/components/common/MarqueeLine.vue, src/components/LiteGraphCanvasSplitterOverlay.vue
Adds Tailwind safelists for essentials icons/gaps, adjusts Tab sizing (h-8/padding), reduces marquee animation duration, and small computed refactor.
Localization and configuration
src/locales/en/main.json, knip.config.ts, src/constants/essentialsDisplayNames.ts
Adds essentialNodes filter labels, essentials.jumpTo and devPicker strings; updates knip.config.ts ignore entries and places a @knipIgnoreUsedByStackedPR directive.
Text utilities and new view
src/utils/textTickerUtils.ts, src/utils/textTickerUtils.test.ts, src/views/HomeView.vue
Add fallback split behavior at first space, update test, and introduce HomeView.vue with navigation, projects list, search, and new-workflow action.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

size:L

Suggested reviewers

  • dante01yoon
  • DrJKL

Poem

🐰 I hop through tiles both bright and neat,
Sections, subgroups—ordered, sweet,
A jump-to click, a popper's glow,
Filters hum and lists now flow,
Essentials landed—chew and eat!

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides comprehensive context including a clear summary, detailed changes, review focus areas, and open items. However, it deviates significantly from the repository's template structure. While content-rich, consider restructuring to match the template: move the draft warning to a note, add a concise one-sentence summary, reorganize changes into the What/Breaking/Dependencies format, and ensure all sections align with the standard template for consistency.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly indicates this is a prototype redesign of the Essentials tab with [reference only] notation, accurately reflecting the draft nature and main focus of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
End-To-End Regression Coverage For Fixes ✅ Passed PR uses feature language (feat), not bug-fix language. End-to-end regression test requirement does not apply to feature PRs, only bug fixes.
Adr Compliance For Entity/Litegraph Changes ✅ Passed ADR compliance check not applicable. No files under src/lib/litegraph/, src/ecs/, or modifying graph entities (LGraphNode, LGraphCanvas, LGraph, Subgraph). PR focuses on UI/essentials redesign only.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 16, 2026

🎨 Storybook: loading Building...

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 16, 2026

🎭 Playwright: ⏳ Running...

@comfydesigner comfydesigner changed the title feat(essentials): redesign Essentials tab — placeholder tiles, responsive grid, hover popovers [REFERENCE ONLY] feat(essentials): redesign Essentials tab [reference only] May 16, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7160a9e and c58762e.

⛔ Files ignored due to path filters (89)
  • packages/design-system/src/icons/3d-decomp-1.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/3d-decomp.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/3d-enhance.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/3d-upscale.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/brightness-contrast.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/canny-to-image.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/canny-to-video.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/channels.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/chromatic-aberration.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/depth-to-image.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/depth-to-video.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/dial.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/frames-to-video.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/glow.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/grain.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-batch.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-blur.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-canny.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-captioning.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-collage.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-compare.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-depth.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-edit.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-enhance.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-inpaint.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-invert.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-iterator.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-mask-preview.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-normal-map.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-outpaint.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-pose.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-preview-1.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-preview.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-remove-background.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-rotate.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-select-object-segmentation.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-shader.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-sharpen.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-to-3d.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-to-image.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-to-layers.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-to-video.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-upscale-1.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-upscale.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/image-vectorize.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/layers-to-image.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/load-3d.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/load-audio.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/load-lora-1.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/load-lora.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/load-video.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/mask-preview.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/music-generation.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/pose-to-image.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/pose-to-video.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/save-3d.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/save-audio.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/save-video.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/text-generation.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/text-iterator.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/text-prompt-enhance.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/text-to-3d.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/text-to-audio.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/text-to-image.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/text-to-video.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-canny.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-captioning.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-compare.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-depth.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-edit.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-enhance.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-extract-frame.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-inpaint.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-interpolation.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-normal-map.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-resize.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-rotate.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-select-object-segmentation.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-shader.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-split-screen.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-stitch.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-trim.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/video-upscale.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/voice-clone.svg is excluded by !**/*.svg
  • public/assets/images/brand-logos/bytedance-color.svg is excluded by !**/*.svg
  • public/assets/images/brand-logos/claude-color.svg is excluded by !**/*.svg
  • public/assets/images/brand-logos/gemini-color.svg is excluded by !**/*.svg
  • public/assets/images/brand-logos/grok.svg is excluded by !**/*.svg
  • public/assets/images/brand-logos/kling-color.svg is excluded by !**/*.svg
📒 Files selected for processing (21)
  • knip.config.ts
  • packages/design-system/src/css/style.css
  • src/components/LiteGraphCanvasSplitterOverlay.vue
  • src/components/common/MarqueeLine.vue
  • src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue
  • src/components/sidebar/tabs/SidebarTabTemplate.vue
  • src/components/sidebar/tabs/nodeLibrary/EssentialNodePlaceholderCard.vue
  • src/components/sidebar/tabs/nodeLibrary/EssentialNodePlaceholderPopover.vue
  • src/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vue
  • src/components/sidebar/tabs/nodeLibrary/EssentialsGapPicker.vue
  • src/components/tab/Tab.vue
  • src/composables/useEssentialsFilters.ts
  • src/composables/useEssentialsSubgroupGap.ts
  • src/constants/essentialsDisplayNames.ts
  • src/constants/essentialsPlaceholders.ts
  • src/locales/en/main.json
  • src/services/nodeOrganizationService.ts
  • src/types/nodeOrganizationTypes.ts
  • src/utils/textTickerUtils.test.ts
  • src/utils/textTickerUtils.ts
  • src/views/HomeView.vue

Comment on lines +27 to +28
/* 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}]");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 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:


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.

Comment thread src/components/sidebar/tabs/nodeLibrary/EssentialNodePlaceholderPopover.vue Outdated
Comment thread src/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vue Outdated
Comment on lines +542 to +556
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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +49 to +415
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') }
]
}
]
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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.

Comment thread src/views/HomeView.vue
Comment on lines +153 to +157
<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"
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

comfydesigner and others added 3 commits May 18, 2026 10:29
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>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c4bcece and f9441b0.

📒 Files selected for processing (3)
  • knip.config.ts
  • src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue
  • src/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

Comment on lines +17 to +33
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}`"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 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.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 134


🏁 Script executed:

cat -n src/components/sidebar/tabs/nodeLibrary/EssentialNodesPlaceholderPanel.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 4376


🏁 Script executed:

cat -n src/constants/essentialsPlaceholders.ts | head -80

Repository: 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.

comfydesigner and others added 7 commits May 18, 2026 11:44
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.
- 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)
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant