Compare commits

...

77 Commits

Author SHA1 Message Date
Marwan Ahmed
5fb48a8718 feat(website): interactive node-workflow hero with simulated runs 2026-07-08 14:52:19 +03:00
imick-io
010389903d feat(website): sitewide announcement banner (#13481)
## Summary

Adds a **sitewide announcement banner** to the website, rendered above
the navbar on every page. It has a two-layer visibility model:

- **Build-time gate** — a pure, unit-tested `evaluateBannerVisibility()`
decides whether the banner mounts at all (active flag + optional date
window + locale/section targeting), driven by a typed config
(`src/config/banner.ts`). No CMS; copy resolves through i18n.
- **Client-side dismissal** — persisted in `localStorage`, keyed by a
**content hash** so editing the copy re-shows the banner (per-locale, so
an en edit doesn't re-show it for zh-CN).

Current content points the CTA at **Comfy MCP** (`/mcp`).

## Highlights

- **Full-width branded bar** above a now-`sticky` navbar; reuses the
design system (`Button`, gradient tokens, new reusable `IconButton`).
- **Flash-free** on load: an inline pre-hydration script hides an
already-dismissed banner before paint (no pop-in, no layout shift);
`close()` sets the same signal after the leave animation to stay
flash-free across ClientRouter navigations.
- **Open/close transition**: grid-rows height collapse + fade,
respecting `prefers-reduced-motion`.
- **i18n**: copy in `en` + `zh-CN`.

## Where to edit later

- **Copy**: `apps/website/src/i18n/translations.ts` →
`launches.banner.text` / `launches.banner.cta`
- **Link / on-off / dates / targeting**:
`apps/website/src/config/banner.ts` (`bannerConfig`)

## Notes

- On a static site the `startsAt`/`endsAt` window is evaluated at
**build time** (documented in `banner.ts`).
- Changing the copy or link changes the content hash, so
previously-dismissed visitors will see the banner again — by design.

## Test plan

- `pnpm test:unit` — evaluator + version-hash unit tests pass.
- `pnpm typecheck` + lint clean.
- On the preview: banner shows on `/`, `/launches`, and a `zh-CN` page;
CTA -> `/mcp`; dismiss animates and stays dismissed on reload (no
flash); reduced-motion disables the animation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 01:56:48 +00:00
Benjamin Lu
684b0b08b0 feat(telemetry): register client + deployment platform axes on PostHog (#13469)
## Problem

Filtering PostHog for the three product surfaces — **desktop local**,
**desktop cloud**, **web cloud** — currently requires a different hack
per pipe. For this repo's cloud build, the desktop-embedded frontend and
a plain browser are indistinguishable except by sniffing `Electron` in
`$raw_user_agent` (~124K desktop-cloud vs ~844K web-cloud execution
events/week get separated that way today).

## Change

Register the two standardized platform axes as PostHog super properties
at SDK init in `PostHogTelemetryProvider`:

- **`client`** — which surface emitted the event: `'desktop'` when the
desktop preload bridge (`window.__comfyDesktop2`) is present, else
`'web'`. The bridge is injected by Electron before any page script runs,
so detection is deterministic — unlike the existing utm-based
`source_app` attribution, which only covers sessions that *entered* via
a desktop link.
- **`deployment`** — which backend runs the work: pinned to `'cloud'`.

The register happens before the pre-init event queue flushes, so events
captured during the posthog-js dynamic-import window carry the axes too.

## Why pinning `deployment: 'cloud'` is safe (including
embedded-in-desktop)

The cloud bundle also runs **embedded in Comfy Desktop** — a cloud
install loads this same bundle in Electron, where `isCloud` and the host
bridge are both true. Two things happen there:

1. `main.ts` runs `initHostTelemetry()` *after* `initTelemetry()`, and
(when remote config `enable_telemetry` is on) it **replaces** the
registry with `HostTelemetrySink` — so tracked events
(`execution_start`, …) route through the desktop main process, bypassing
this provider. Those are tagged the same `client`/`deployment` values
main-side from the install's source category
([Comfy-Desktop#1229](https://github.com/Comfy-Org/Comfy-Desktop/pull/1229)).
2. posthog-js keeps capturing independently of the registry (pageviews,
web vitals, identify) — those are what these super properties cover in
the embedded case, and `deployment: 'cloud'` is correct for them because
the cloud bundle always talks to the cloud backend regardless of
embedding; the embedding itself is what `client: 'desktop'` captures.

The only way a cloud build runs against a non-cloud backend is a dev
setup, where `window.__CONFIG__.posthog_project_token` is absent
(injected by the cloud server) and the provider disables itself before
registering anything.

The locally-served frontend (desktop/localhost builds) never runs this
provider: `__DISTRIBUTION__` is a compile-time define, so the
`initTelemetry()` call folds away, with a runtime `IS_CLOUD_BUILD` guard
as backstop.

With both PRs, the three platforms become clean property filters:

| Surface | Filter |
|---|---|
| Desktop local | `client=desktop, deployment=local` |
| Desktop cloud | `client=desktop, deployment=cloud` |
| Web cloud | `client=web, deployment=cloud` |

## Testing

- `vitest run` on `PostHogTelemetryProvider.test.ts` — 45 passing,
including new coverage: web default, bridge-present → `client=desktop`,
and register-before-queue-flush ordering. The two desktop-entry tests
that asserted `register` is never called were narrowed to assert no
`source_app` register call.
- `pnpm typecheck` + eslint/oxlint on touched files — clean.

Ref
[MAR-51](https://linear.app/comfyorg/issue/MAR-51/foundation-desktop-sdk-dual-send-to-posthog-alongside-mixpanel)

---------

Co-authored-by: AustinMroz <austin@comfy.org>
2026-07-08 01:11:01 +00:00
Benjamin Lu
95b121bed9 fix(website): unbreak website-e2e on main (card count, CTA overflow, stale goldens) (#13496)
## Summary

Fix the website-e2e job, red on main since 2026-07-07 15:19 UTC, by
updating the cloud model-card count test and regenerating stale visual
screenshot goldens.

## Context

website-e2e runs Playwright tests against the marketing site
(`apps/website`). It broke on main in two independent ways, so every PR
since — however unrelated — has shown a red website-e2e check:

1. **Model-card count.** #13431 added a sixth model card (GPT Image 2)
to the /cloud "AI models" section but didn't update the test that pins
the card count at 5. CI: `locator resolved to 6 elements`.
2. **Stale screenshot goldens.** #13431 also swapped the ProductCard CTA
to the shared `Button` (`whitespace-nowrap`, so e.g. "SEE ENTERPRISE
FEATURES" renders on one line instead of wrapping) and committed
matching `home-product-cards-*` goldens. Minutes later #13445 — a branch
cut from main *before* #13431 — ran the screenshot-regen workflow, which
checks out the raw PR branch, not the branch merged with main. Its
regenerated lg/xl goldens therefore depict the **old** pre-#13431 card
(wrapped label; pixel-identical layout to the pre-#13431 golden), and
overwrote #13431's correct ones at merge. #13445's own website-e2e was
red at merge time for exactly this reason. The sm/md goldens (last
captured by #13431, without #13445's `ppformula-text-center`
0.19em→0.1em nudge) went stale by ~900 px the moment #13445 landed.

## Changes

- **What**: `cloud.spec.ts` — model-card count assertion and test title
5 → 6 (verified against the 6 entries in `AIModelsSection.vue`; passes
locally).
- **What**: `ProductCard.vue` — `h-auto whitespace-normal` on the CTA
`Button`. The shared Button's `whitespace-nowrap` made long labels ("SEE
ENTERPRISE FEATURES") overflow past the card edge at lg/xl (live on prod
since #13431); labels now wrap inside the card as they did before
#13431.
- **What**: regenerated `home-product-cards-*` goldens via the `Update
Website Screenshots` workflow, run on this branch, so they capture the
fixed rendering rather than enshrining the overflow.

## Review Focus

- At lg, "SEE DESKTOP/CLOUD FEATURES" now also wrap to two lines: #13431
raised the CTA font from `text-xs` to `md:text-sm`, so those labels no
longer fit one line in the 200px content box either (pre-fix they
silently consumed the card padding). If design prefers one-liners,
shrinking the CTA font is a follow-up.
- Process gaps this incident exposed (follow-ups, not in this PR): the
regen workflow captures against the raw branch instead of the
main-merged result, and website-e2e was red on #13445 at merge without
blocking it.

---

*Six cards where five once stood,*
*a button's text sat where it should —*
*but pixels pinned in amber lied,*
*so Linux looked, and rectified.*

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-07-07 17:59:21 -07:00
Wei Hai
baeb6df662 Gate signup submit on Turnstile being enabled, not enforced (#13463)
## Summary
- Shadow-mode Turnstile never blocked the signup Submit button on the
async Cloudflare challenge resolving, so most real submits raced ahead
of the widget and reached the backend with an empty token. This defeated
the point of shadow mode, which needs real tokens to measure the
false-positive rate before flipping to enforce.
- Submit is now blocked while the widget is enabled (shadow or enforce)
and has no token yet, in both modes.
- To keep a broken or slow Cloudflare load (network issue, ad-blocker,
CDN outage) from permanently blocking a legitimate signup,
`TurnstileWidget` now reports itself "unavailable" on a script-load
failure, a challenge error, or a 9s load timeout, and the form treats
that the same as shadow previously did: proceed without a token.

## Test plan
- [x] `vitest run` on `TurnstileWidget.test.ts` + `SignUpForm.test.ts`
(unit tests updated/added, all passing)
- [x] `pnpm typecheck` / eslint / oxlint / stylelint / oxfmt via
pre-commit hooks
- [ ] Manual click-through on staging to confirm no perceptible UX
regression during normal-latency challenge solves
- [ ] Confirm the 9s fallback timeout against real p95 Turnstile
challenge-solve latency

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-08 00:39:58 +00:00
CodeJuggernaut
e58b231664 fix: suppress the stray focus ring on the sidebar splitter panel (#13482)
Clicking empty space in a workspace panel focuses the whole PrimeVue
SplitterPanel (tabindex=-1 makes it click-focusable), and any following
non-chord keypress (e.g. Shift) trips the browser focus-visible
heuristic, painting the default blue ring around the entire panel.

The wrappers are not Tab-reachable (Tab lands on the controls inside,
never the panel box) and nothing focuses them programmatically, so the
ring conveys nothing. `focus-visible:outline-hidden` suppresses it while
keeping a forced-colors (High Contrast) indicator. Confirmed as noise
with design (Alex Tov).

Covers every click-focusable panel in
`LiteGraphCanvasSplitterOverlay.vue`: the sidebar panel (both
locations), the properties-side panel (both branches), and the bottom
panel. The center and graph-canvas panels are left untouched - they
inherit `pointer-events-none`, so a click can never focus them.

## Repro / QA

Ring trigger: click an empty, non-interactive spot inside the panel,
then press solo Shift. On main the browser paints a blue ring around the
whole panel; on this PR nothing appears. (Ctrl+Shift only triggers when
Shift lands first, hence the original "sometimes".)

| Panel | How to open | Fixed |
| --- | --- | --- |
| Sidebar (left, default) | Any rail icon, e.g. Assets | yes |
| Sidebar (right) | Settings > Sidebar Location > right | yes |
| Properties-side panel | Toggle properties panel / builder mode | yes |
| Bottom panel | Toggle Logs/Terminal | yes |
| Canvas / center | n/a | untouched - pointer-events-none, cannot be
click-focused |

- [ ] Each fixed panel: click empty spot, press Shift, no ring
- [ ] Same steps on main/prod show the ring (before-state)
- [ ] Tab still reaches controls inside each panel and their own focus
rings still show
- [ ] Media Assets shortcuts unchanged (Ctrl/Cmd+A, marquee modifiers) -
PR is CSS-only

- Surfaced during design review of #13323
2026-07-08 00:33:41 +00:00
Comfy Org PR Bot
545b48ee5b [chore] Update Ingest API types from cloud@421de6d (#12777)
## Automated Ingest API Type Update

This PR updates the Ingest API TypeScript types and Zod schemas from the
latest cloud OpenAPI specification.

- Cloud commit: 421de6d
- Generated using @hey-api/openapi-ts with Zod plugin

These types cover cloud-only endpoints (workspaces, billing, secrets,
assets, tasks, etc.).
Overlapping endpoints shared with the local ComfyUI Python backend are
excluded.

---------

Co-authored-by: mattmillerai <7741082+mattmillerai@users.noreply.github.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
2026-07-08 00:29:59 +00:00
Benjamin Lu
22ea53fb56 fix(ci): remove out-of-rootDir config files from package tsconfig includes (#13485)
## Summary

Type-aware oxlint rejects `packages/ingest-types/tsconfig.json` and
`packages/object-info-parser/tsconfig.json` as invalid (TS6059): their
`include` lists a root-level config file (`openapi-ts.config.ts` /
`vitest.config.ts`) that sits outside `rootDir: "src"`. This fails the
lint-and-format job with "Invalid tsconfig" on any PR that touches those
packages' src files — currently blocking every auto-generated
ingest-types sync (e.g. #12777).

## Changes

- **What**: Drop the out-of-`rootDir` config-file entries from the two
package tsconfig `include` arrays, matching the other workspace
packages. Repro: `pnpm exec oxlint --type-aware
packages/ingest-types/src/index.ts` fails before, passes after; the
config files themselves still lint clean.

## Review Focus

Neither package emits a build, so `rootDir`/`outDir` are
editor/lint-only; excluding the config files from the project has no
runtime effect (vitest/openapi-ts load their configs directly).
2026-07-07 17:03:12 -07:00
cloud-code-bot[bot]
9fe5dd51b8 ci: bump cursor-review to github-workflows@df507e6 (#13493)
Automatic SHA bump — `cursor-review.yml` was updated in
`Comfy-Org/github-workflows` at
[`df507e6`](df507e6bae).
_Opened by the `bump-cursor-review-callers` workflow._

Co-authored-by: cloud-code-bot[bot] <234529496+cloud-code-bot[bot]@users.noreply.github.com>
2026-07-07 23:06:59 +00:00
Wei Hai
747f76db76 ci: auto-merge approved, green backport PRs (#13390)
## Problem
`pr-backport.yaml` opens each backport PR (labelled `backport`) and
calls `gh pr merge --auto --squash`. GitHub's `--auto` only takes effect
when the repository's **"Allow auto-merge"** setting is enabled — it's
currently off, so that call is a silent no-op (swallowed by its `|| echo
"::warning::…"`). The result: every backport PR sits unmerged until
someone manually clicks merge, even when it's already approved with
green checks.

## What this does
Adds `.github/workflows/backport-auto-merge.yaml`, which completes the
merge directly — a plain `gh pr merge --squash` (which does **not**
depend on the "Allow auto-merge" setting) — once GitHub reports the PR
ready to merge.

Ready = `reviewDecision == APPROVED` **and** `mergeStateStatus` is
`CLEAN` or `UNSTABLE`. `UNSTABLE` means the required checks passed but a
*non-required* check is still pending/failing — GitHub still permits
that merge, and gating on `CLEAN` alone would leave backports stuck
behind slow/flaky non-required checks (Socket, codecov, perf, storybook,
etc.).

**Branch protection stays the real gate.** The `core/**` / `cloud/**`
ruleset unconditionally requires an approval + the required checks and
can't be bypassed, and GitHub's merge API re-enforces it at merge time —
so this workflow can only ever finish a merge that already satisfies
those rules. The eligibility check just avoids pointless attempts.

## Design notes
- **Merges with `PR_GH_TOKEN`, not the default token**, on purpose: a
merge by the default `GITHUB_TOKEN` does not emit the `pull_request:
closed` event, which would silently starve `cloud-backport-tag.yaml` (it
creates the `cloud/vX.Y.Z` tag on that event).
- **Triggers:** review submission + check-suite completion (low
latency), plus a 30-min sweep as a backstop for cases the events miss.
- **Never checks out PR code** (no untrusted-code path); only reads PR
metadata via the API. `permissions` on the default token are read-only.
- **Idempotent, bounded merge loop:** treats an already-merged PR (e.g.
a concurrent run or a human) as success, so it won't post a false
failure comment.
- Leaves the existing conflict path in `pr-backport.yaml` untouched
(conflicts never create a PR, so there's nothing here to act on).

## Validation
YAML parses; `actionlint` (with shellcheck) and `zizmor` both clean (0
findings).

## Before relying on it
- Confirm the org allows this workflow to run/merge (Actions policy) —
the merge uses a PAT so it shouldn't depend on the "Actions can approve
PRs" toggle, but worth verifying.
- First real backport: confirm it merges on ready and that
`cloud-backport-tag.yaml` then fires and creates the tag.
2026-07-07 16:50:23 +00:00
Mobeen Abdullah
386460afef fix(website): center button labels by tuning ppformula-text-center (#13445)
## Summary

Vertically center button/badge/nav labels by tuning the shared
`ppformula-text-center` utility from `top: 0.19em` to `top: 0.1em`.

## The alignment issue

PP Formula (our brand font) has asymmetric vertical metrics: its caps
sit high in the line box, so a naively centered label looks too high.
`ppformula-text-center` compensates by nudging the label down with
`position: relative; top: <em>` (a purely visual shift, it does not
change the element's box, so button/badge sizes are unaffected).

The value was `0.19em`, which **over-corrected**: the glyph ink ended up
~1.4px **below** center on every button, so labels read slightly low.
Measuring the actual glyph ink (canvas `measureText`
`actualBoundingBox*`) showed ~**0.09-0.10em** centers uppercase labels;
`0.1em` lands the ink within ~0.1px of center.

## Why it's safe (verified)

This utility is used site-wide (Button, Badge, ButtonPill, ButtonMask,
BrandButton, nav triggers, section labels). Because it's a
`position:relative` nudge, there is **no layout/box-size change**
anywhere. I measured glyph-ink centering across **12 pages** (home,
cloud, cloud/pricing, download, careers, customers, demos, enterprise,
api, mcp, gallery, learning):

- Buttons/badges/pills went from ~1.37px low to **~0.11px** (centered).
- **Nothing regressed** (no element pushed too high).
- The handful of numeric "outliers" were `text-transform: uppercase`
measurement artifacts (source text with descenders that don't render);
confirmed visually as centered.

## Changes

- **What**: `apps/website/src/styles/global.css` —
`ppformula-text-center` `top: 0.19em` → `0.1em` (one line).

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-07-07 20:37:32 +05:00
Mobeen Abdullah
5cf647d183 feat(website): add GPT Image 2 to cloud page model list (#13431)
## Summary

Add GPT Image 2 to the `/cloud` "AI models" section, and apply the
design review polish from Bert and June on the same section and the
neighbouring cloud-page cards.

## Changes

- **GPT Image 2 card**: 6th card in `AIModelsSection` (workflow video on
`media.comfy.org`, OpenAI badge reused from `packages/design-system`),
new `cloud.aiModels.card.gptImage2` i18n key (en + zh-CN), and GPT Image
2 added to the `cloud.reason.2.description` partner-model list.
- **AI models layout**: the six cards are now equal 1:1 squares in one
shared grey container (was a per-card treatment, then simplified to a
single container per design), with a corner arrow affordance on each.
- **Audience cards** (`AudienceSection`): the creators / teams cards now
link to `cloud.comfy.org` with the same corner arrow (highlights on card
hover).
- **Reusable `CardArrow`**: extracted the corner arrow into a shared,
decorative (`aria-hidden`) component used by both sections;
`hover="group"` (card hover) for audience, self-hover for the model
cards so it doesn't double up with the provider badge.
- **`ProductCard` CTA**: swapped the hand-rolled pill `<span>` for the
shared `Button` (`as="span"`) so the label is vertically centered (fixes
Bert's off-centre text) without nesting an anchor inside the card link.

## Split out of this PR

- **Button label centering** (the global `ppformula-text-center` tweak)
→ separate PR #13445, so the site-wide change is reviewed in isolation.
- **Pricing banner frame fix** reverted here; it belongs in Michael's
upcoming pricing PR (team tier, edu billing, FAQ). `PricingSection.vue`
shows only an automatic Tailwind class-order reformat from the
pre-commit hook, no behaviour change.

## Review focus

- The single-container AI models layout and the `CardArrow` hover
behaviour (group vs self).
- `Button as="span"` inside the `ProductCard` link (avoids nested
`<a>`).

Linear: FE-423

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-07-07 20:19:54 +05:00
Mobeen Abdullah
fe1fc8baa6 fix(website): standardize favicon to square brand icon (#13467)
## Summary

Standardize the marketing-site favicons to the square, full-bleed brand
mark (ink background, yellow C) so each platform applies its own corner
mask instead of double-rounding a pre-rounded asset.

## Changes

- **What**: Replace three files in `apps/website/public/`:
- `favicon.svg` — was a 57 KB RealFaviconGenerator wrapper around an
embedded PNG; now a 1.2 KB vector of the square mark.
- `favicon-96x96.png` and `apple-touch-icon.png` — regenerated square.
The apple-touch icon previously had transparent rounded corners, which
iOS composites onto a white tile; it is now full-bleed.
- `favicon.ico` and the `web-app-manifest-*.png` files were already the
square mark, so they are left unchanged.

## Review Focus

- Favicons cache aggressively (browser + CDN, and these paths are marked
`immutable` in `vercel.json`), so verify the preview with a hard refresh
or a fresh profile.
- Part of the org-wide favicon standardization (FE-705). Companion PRs
update the workflows hub, docs, and registry favicons to the same mark.
Design direction (square, not rounded) confirmed by Bert.

## Screenshots

The before/after for each binary is visible inline in the Files changed
tab. Square ink + yellow C, no transparency, sharp corners.
2026-07-07 20:18:12 +05:00
Benjamin Lu
3e4dd59e5f feat(navigation): add opt-in strip-on-capture to preserved-query tracker (#13465)
## What

- `installPreservedQueryTracker` definitions accept an opt-in
`stripAfterCapture` flag: the marked keys are captured into the
sessionStorage stash and removed from the URL before the navigation
completes (single guard redirect at the decoded query-object level;
push/replace semantics are inherited from the original navigation, and
vue-router force-replaces the initial one).
- `preservedQueryManager` now captures the first non-empty string
element of repeated (array-valued) params instead of silently dropping
them.
- New real-router test suite for the tracker (createRouter +
createMemoryHistory, no router mocks), including a history-depth test
pinning the push/replace inheritance; manager tests extended for the
array/junk-value cases.

## Why

One-time secrets in query params (first consumer: desktop login codes,
GTM-93) must not linger in the visible URL, browser history,
`previousFullPath` redirects, or telemetry. Stripping after navigation —
what each loader does ad hoc today — leaves a window and forces
per-feature URL scrubbing; #13418 originally needed a hand-rolled
encoding-aware string parser in three places. Stripping at capture time,
at the decoded query-object level, makes the stash the only carrier and
lets vue-router round-trip the surviving params' encoding itself.

Capability only — no existing namespace opts in; behavior is unchanged
for all current definitions. `stripAfterCapture`'s contract is
documented on the option: strip-marked keys must never be read from
`route.query` by later guards or views; the stash is the only
post-capture source.

## Landing order

Independent of everything else; #13418 stacks on this branch.
2026-07-07 07:08:11 +00:00
Benjamin Lu
e25e0f2e16 refactor: simplify slot link drop finalization (#13471)
## Summary

Small cleanup in `useSlotLinkInteraction.ts`, no behavior change:

- Removed a duplicated `raf.flush()` in `finishInteraction` (it was
called twice back-to-back).
- Collapsed four single-line `attempt*` alias closures in
`connectByPriority` into a direct short-circuit chain, preserving the
same evaluation order:

  ```ts
  return (
    tryConnectToCandidate(snappedCandidate) ||
    tryConnectToCandidate(domSlotCandidate) ||
    tryConnectToCandidate(nodeSurfaceSlotCandidate) ||
    tryConnectViaRerouteAtPointer()
  )
  ```

The closures added no behavior beyond renaming the calls (AGENTS.md rule
26), and `||` gives the same first-truthy-wins semantics as the previous
`if (attempt()) return true` ladder.

Verification not rerun after rebasing onto `Comfy-Org/main`; original
branch reported `pnpm typecheck`, `pnpm lint`, `pnpm format:check`,
`pnpm knip`, and the existing `useSlotLinkInteraction` unit tests
passing.

Link to Devin session:
https://app.devin.ai/sessions/1351ff5174494106a7a688777554f387
Requested by: @benceruleanlu

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-07 07:05:36 +00:00
Benjamin Lu
2ee91c30ee fix: report CLA check in merge queue (#13477)
Report the CLA Assistant required check for merge queue commits so
queued PRs do not wait indefinitely on a check that ran on the PR head.
2026-07-07 07:57:02 +00:00
Alexis Rolland
854770d305 ci: Update CLA workflow to build author-only allowlist (#13378)
## Summary

Update CLA workflow to build a dynamic `allowlist` that includes
everyone except the author of the PR. This relaxes the CLA signature
requirement so that it is limited to the PR author only. By signing, the
author confirms he gots approval from other contributors.

## Changes

- **What**: `cla.yml`

## Screenshots (if applicable)

<img width="1831" height="756"
alt="{B6F6C23D-EC2E-4BB3-A288-99B6087F4CAC}"
src="https://github.com/user-attachments/assets/62c04465-d1a3-4ddb-bfe7-950a29a802c4"
/>
2026-07-06 19:29:59 -07:00
jaeone94
2e4c9c6fdc style: use neutral credits info button (#13461)
## Summary

Replaces the Additional credits tooltip trigger with the shared Button
component so the icon renders with the neutral muted treatment used by
the rest of the UI.

## Changes

- **What**: Uses the shared Button component for the Additional credits
info action while preserving the existing tooltip and aria label.
- **Dependencies**: None.

## Review Focus

Confirm this remains a visual-only change scoped to the Plan & Credits
credits tile.

## Screenshots (if applicable)

Before
<img width="397" height="368" alt="스크린샷 2026-07-06 오후 11 30 38"
src="https://github.com/user-attachments/assets/9cda1aee-4dc2-4ce1-b001-29d0e09fc07d"
/>

After
<img width="374" height="355" alt="스크린샷 2026-07-06 오후 11 31 00"
src="https://github.com/user-attachments/assets/64d0b726-6031-42d4-84d7-35405150698c"
/>


## Testing

- `pnpm format`
- `pnpm lint`
- pre-commit hook: stylelint, oxfmt, oxlint, eslint, typecheck
- `pnpm test:unit
src/platform/cloud/subscription/components/CreditsTile.test.ts`
2026-07-06 14:42:49 +00:00
Mobeen Abdullah
7d2858e74b feat(website): add Supported Models link to nav and footer (#13432)
## Summary

Surface the supported-models catalog (`/p/supported-models`) from the
home page by adding a "Supported Models" link to the Products dropdown
and the footer.

## Changes

- **What**: Add a `nav.supportedModels` i18n entry (en `Supported
Models`, zh-CN `支持的模型`) and link it to the existing `routes.models`
constant in two places: the Products mega-menu Features column (between
Launches and Docs) and the footer Products column (after Comfy MCP).
Locale handling comes from `getRoutes`, so zh-CN resolves to
`/zh-CN/p/supported-models` automatically.

## Review Focus

- Placement is intentionally in **both** the nav and the footer (per
FE-1190). Fine to drop either, happy to adjust after review.
- Reuses the existing nav/footer link pattern, no new components or
styles, and no `new` badge (the page is not newly launched).
- Consumes `routes.models`, which was already defined but previously
unused.

FE-1190

## Screenshots

**Nav — Products dropdown, Features column**

_Desktop:_

_Mobile:_

**Footer — Products column**

_Desktop:_

_Mobile:_

Preview: https://comfy-website-preview-pr-13432.vercel.app
2026-07-04 09:33:26 +05:00
Robin Huang
b9a0ac0fed fix(manager): drop embed=true so the Cloud survey renders its dark theme (#13438)
Remove `embed=true`, so PostHog applies the survey's own dark appearance
and it renders correctly.

Verified live on testcloud that removing the flag fixes the styling;
`distinct_id` user linkage is unaffected.
2026-07-04 01:56:29 +00:00
AustinMroz
b61e54db3b Allow forcing icon display as mask or image (#13414)
- Adds support for forcing an icon to display as a mask or image with
`icon-mask` and `icon-image`.
- Updated the logic so that svg of a solid color (like the claude logo)
display as an image by default
- Update many svg to consistently use `currentColor` so that they still
function as masks by default
2026-07-04 00:04:56 +00:00
jaeone94
b51ea29074 test: clean up TemplateHelper route mocks (#13019)
## Summary

Follow-up draft PR for the CodeRabbit issues created from the #12999
review. This keeps the original stabilization PR merged as-is and moves
the non-functional TemplateHelper cleanup into its own small branch.

## Changes

- Extracted TemplateHelper route patterns into named module-scope
constants.
- Normalized the TemplateHelper route patterns to anchored regexes with
optional query-string handling.
- Extracted `mockCustomTemplates()` from `mockIndex()` and made `mock()`
register custom templates, core index, and thumbnails together.
- Added a private `registerRoute()` helper so every mocked route is
registered for teardown consistently.
- Simplified the fixed empty custom-template response to `body: '{}'`.
- Updated the cloud template filtering spec to use `templateApi.mock()`
instead of manually combining thumbnail and index mocks.

## Issues

- Closes #13014
- Closes #13016
- Closes #13017
- Closes #13018
- Related #13015: this PR normalizes the TemplateHelper route patterns
only. The broader fixture-wide route pattern convention cleanup remains
intentionally separate.

## Validation

- `pnpm exec oxfmt --check
browser_tests/fixtures/helpers/TemplateHelper.ts
browser_tests/tests/templateFilteringCount.spec.ts`
- `pnpm exec eslint browser_tests/fixtures/helpers/TemplateHelper.ts
browser_tests/tests/templateFilteringCount.spec.ts`
- `pnpm typecheck:browser`
- Pre-commit hook also ran `oxfmt`, `oxlint`, `eslint`, `pnpm
typecheck`, and `pnpm typecheck:browser` successfully.

Note: I attempted the targeted cloud Playwright spec locally with
`PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm exec
playwright test browser_tests/tests/templateFilteringCount.spec.ts
--project=cloud`, but the local 5173 app was not running with the cloud
distribution configuration, so the distribution-filter assertions failed
in the expected local/cloud mismatch way. This should be verified by
CI's cloud project.
2026-07-03 23:55:52 +00:00
Robin Huang
d85ce2bf9e feat: add custom nodes waitlist survey to Manager button on Cloud (#13135)
## Summary

On Comfy Cloud, show the Manager button and open a hosted survey in the
manager modal (in place of the local node manager) so we can gauge
demand for custom nodes on Cloud.

## Changes

- **What**: `TopMenuSection` shows the Manager button when `isCloud`;
clicking it opens `ManagerSurveyDialog`, which embeds a PostHog hosted
survey via iframe. The survey URL comes per-environment from cloud
config (`manager_survey_url`), with the logged-in user's `distinct_id`
appended so responses link to the user. Includes loading/error states
and PostHog's `posthog:survey:height` iframe auto-resize.
- **Dependencies**: none

## Review Focus

- Survey URL is sourced from `remoteConfig.manager_survey_url` (must be
set per environment in cloud config); falls back to an error state when
unset or malformed.
- iframe embedding requires the PostHog survey to be `external_survey`
type with embedding enabled.
2026-07-03 18:12:34 +00:00
Benjamin Lu
fa87c46f90 chore: remove PreToolUse pnpm-enforcement hooks (#13422)
## Summary

Removes the Claude Code PreToolUse hooks added in #11201. Their `if`
patterns blocked any bash command the static pattern parser could not
fully resolve — loop variables, `$(...)`/backtick substitution,
heredocs, `${...}` expansions — with a misleading error naming a random
unrelated tool. Transcript analysis across a month of sessions found 37
hook firings: 1 true positive, 36 false positives (~97%), including
blocking `pnpm typecheck ... | tail` itself.

## Changes

- **What**: Delete `.claude/settings.json` (it contained only the hook
config) and the script-based replacement from earlier revisions of this
PR.
- Earlier revisions replaced the hooks with a stdin-inspecting matcher
script, but the hooks' original rationale — protecting Nx task
orchestration back when `test:unit` was `nx run test` — disappeared when
Nx was removed in #12355 and the pnpm scripts became direct tool
invocations. The remaining value (nudging agents toward pnpm scripts)
does not justify maintaining a bash-parsing matcher with its own edge
cases.

## Review Focus

- Agents can now run `npx tsc` / `npx vitest` etc. without being
redirected; the pnpm-script convention remains documented in AGENTS.md,
which is what agents follow in practice.
2026-07-03 06:57:05 +00:00
Benjamin Lu
941f151520 fix: avoid duplicate unit coverage execution (#13423)
## Summary

Stop running the full Vitest suite twice in unit CI. The critical
coverage gate is now a glob-keyed `coverage.thresholds` entry enforced
during the single `pnpm test:coverage` run, instead of a second
`COVERAGE_CRITICAL=true vitest run --coverage` pass.

## Changes

- **What**: All critical directories form one brace-expanded glob key in
`coverage.thresholds`; Vitest aggregates the matching files into a
single bucket and checks the existing thresholds (69/60/67/70) against
it during the normal coverage run. Untested files matching
`coverage.include` are counted at 0%, preserving the previous gate's
semantics.
- **What**: Narrows the litegraph coverage exclusion from a blanket
`src/lib/litegraph/**` to the non-critical subfolders, so the critical
litegraph folders (`node`, `subgraph`, `utils`) are present in the
coverage report the thresholds read.
- **What**: Removes the `test:coverage:critical` script, the
`COVERAGE_CRITICAL` env branch in `vite.config.mts`, and the separate CI
gate step.

## Notes

- The normal coverage report now includes the critical litegraph
folders, so the Codecov `unit` flag and the coverage Slack baseline will
show a one-time shift.
- Filtered local runs (`pnpm test:coverage <file>`) fail the gate since
most critical files are uncovered; a full `pnpm test:coverage`
reproduces CI exactly.

Validation:
- `pnpm typecheck`, `pnpm exec eslint vite.config.mts`, `pnpm
format:check`, `pnpm knip` (pre-push)
- Smoke: `pnpm vitest run --coverage src/utils/colorUtil.test.ts` —
tests pass, then the gate fails all four metrics against the critical
bucket and exits 1, confirming enforcement happens inside the single run
- Full-suite gate numbers should be confirmed in CI
2026-07-03 05:43:05 +00:00
Benjamin Lu
df5f5b3367 feat: identify auth users to Syft (#13311)
## Summary

Identifies Cloud auth users to Syft via the required `identify(email, {
source })` handoff so Syft enrichment reliably attaches to signup/login
users instead of relying only on GTM page-load capture.

## Changes

- **What**: Adds a cloud-only Syft telemetry provider that reads
`syftdata_source_id` from `remoteConfig`, lazy-loads the Syft SDK
(reusing an already-loaded GTM Syft client when present), and calls
`identify` with `source: 'signup'` or `source: 'login'` on auth and on
session restore. `trackUserLoggedIn()` dedupes against the email already
handled by `trackAuth()` so a fresh login is not identified twice.
- **Dependencies**: None.

## Review Focus

- Preserves the FE-945 startup-blocker fix: the constructor reads only
the `remoteConfig` ref (a plain reactive ref, not Pinia) and never
touches current-user state; the user-email lookup happens only in
`trackUserLoggedIn()`, after app/auth setup. The source id is present at
construction because `main.ts` awaits the anonymous
`refreshRemoteConfig` before `initTelemetry`, and a later authenticated
refresh is picked up reactively on the next `ensureSyftClient()` call.
- SDK loader is idempotent with the current GTM Syft tag during rollout
(one script per `SYFT_SRC`). On load failure it clears its own stub —
guarded by an identity check so it never evicts a real client another
loader installed — letting a subsequent call retry. Long-term cleanup is
to keep one loader path per surface.
- Acceptance should include staging Network verification for
`https://e2.sy-d.io/events` payloads containing an `identify` event for
Google, GitHub, and email auth.

Linear: GTM-168
2026-07-03 05:10:52 +00:00
imick-io
156f2f59b7 feat(website): swap nav featured card to Comfy MCP (#13388)
## Summary

Repurpose the Products dropdown featured card to promote Comfy MCP.

## Changes

- **What**: Update the nav featured card title ("NEW: COMFY MCP"), alt
text, image asset (`mcp-card.webp`), and CTA ("GET STARTED") in
`mainNavigation.ts`; route the CTA to the localized `/mcp` page via
`routes.mcp`. All copy is i18n'd (en + zh-CN) in `translations.ts`,
adding a reusable `cta.getStarted` key.

## Review Focus

- CTA uses a new reusable `cta.getStarted` key rather than the
section-scoped `mcp.setup.label`, and routes to the internal
`routes.mcp` so non-en locales resolve to `/{locale}/mcp`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 05:01:55 +00:00
nav-tej
d855466fdf fix(website): cap contact intro text width and space it from the form (#13420)
*PR Created by the Glary-Bot Agent*

---

## Summary

On `comfy.org/contact` the intro copy ("Create powerful workflows, scale
without limits." + description) ran right up against the HubSpot form
fields on desktop. The two `lg:w-1/2` columns had no gap between them
and the left column had no max-width, so long description text extended
almost to the form's edge.

- Add `lg:gap-16` between the two columns in `FormSection.vue`, matching
the pattern already used by `common/ContentSection.vue` and
`legal/LegalContentSection.vue`.
- Wrap the intro text block (badge + heading + description) in an
`lg:max-w-xl` container so the copy no longer stretches into the gap.
The illustration below keeps its full-column bleed via the existing
`lg:-ml-20`.
- Mobile (`<lg`) layout is unchanged — all new classes are
`lg:`-prefixed.

## Verification

Screenshots taken via Playwright against the local `apps/website` dev
server:

- Desktop (1512×900) — intro text now caps at a comfortable line length
with a clear gap to the form.
- 1024px — still works at the `lg:` breakpoint.
- 375px mobile — visually identical to before.

Also ran `pnpm format` and `pnpm --filter @comfyorg/website typecheck`
(0 errors). Three pre-existing
`better-tailwindcss/enforce-consistent-class-order` lint warnings on
this file exist on `main` and were left untouched.

- Fixes contact-page layout complaint from #website-and-docs (July 2)

## Screenshots

![Contact page after fix at 1512px — intro text capped with clear gap to
form](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/5f8bf9cb18d1cd3fea28126c5aa832b0c655c1ecef2398b16fa50d81520df3fd/pr-images/1783049233475-b5932d36-9087-4689-a7ea-925bad2f09ff.png)

![Contact page after fix at 1024px
viewport](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/5f8bf9cb18d1cd3fea28126c5aa832b0c655c1ecef2398b16fa50d81520df3fd/pr-images/1783049234208-8ef2cdac-b929-4c4d-b60a-e794f989fd76.png)

![Contact page after fix at 375px mobile viewport — layout
unchanged](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/5f8bf9cb18d1cd3fea28126c5aa832b0c655c1ecef2398b16fa50d81520df3fd/pr-images/1783049234909-c502d978-2586-4ce7-b54a-d96fc759d306.png)

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
2026-07-03 03:51:31 +00:00
AustinMroz
9d5719871a Compact vue nodes (#12886)
Updates vue nodes to be compact. 

This PR does modify the sizing of the asset dropdown (as used on nodes
like "Load Image"). There are outstanding concerns about the visibility
of the upload button and ongoing work to address this.
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/5c866d6f-d83e-40e1-9d87-17b990d94e04"/>
| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/2a809e90-13aa-4f95-8b73-3f20b02fd9a1"
/>|

Subsumes #12678

---------

Co-authored-by: Alex <alex@Alexs-MacBook-Pro.local>
Co-authored-by: github-actions <github-actions@github.com>
2026-07-03 02:31:41 +00:00
ShihChi Huang
7610a61250 test: cover queue display formatting (#13089)
## Summary

Add direct tests for queue job display formatting.

Base: `main`

## Changes

- Covers state icons, pending/initializing labels, running progress,
completed local/cloud output, fallback completed titles, and failed
display.

## Test Results

| | before | after |
| -- | -- | -- |
| `pnpm test:unit src/utils/queueDisplay.test.ts --run` | no direct
queue display test file |  13 passed |

## Coverage

Superseded by #13332. Historical pre-#13313 branch coverage:
`src/utils/queueDisplay.ts` 22.72% -> 79.54% (+56.82%); overall branches
52.95% -> 53.03% (+0.08%).

Codecov project coverage is intentionally omitted here because it is not
the branch-ratchet metric.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Test-only change; no runtime or production code modified.
> 
> **Overview**
> Adds **`src/utils/queueDisplay.test.ts`**, a Vitest suite that
exercises **`iconForJobState`** and **`buildJobDisplay`** from
`queueDisplay.ts` without touching UI or production logic.
> 
> Tests use small **`createJob` / `createTask` / `createCtx`** helpers
with a stub **`t`** and clock formatter so expectations assert i18n keys
and formatted values. Coverage includes pending “added to queue” hint,
queued/initializing labels, active vs inactive running progress,
completed local preview vs cloud duration, completed title fallback, and
failed rows with **`showClear`** behavior.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
6260c101e5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: huang47 <157390+huang47@users.noreply.github.com>
2026-07-02 22:39:24 +00:00
ShihChi Huang
47c8b09ebf test: 2/x cover fuse search ranking (#13087)
## Summary

Add direct tests for `fuseUtil` search ranking and filter behavior.


## Changes

- Covers ranking tiers, deprecated penalties, post-processing, empty
queries, auxiliary score comparison, and filter wildcard/comma matching.

## Test Results

- `pnpm test:unit src/utils/fuseUtil.test.ts`: 7 passed.
- `pnpm typecheck`: passed.
- `pnpm test:coverage`: 876 test files passed; 11,759 passed / 8
skipped.

## Coverage

Superseded by #13332. Historical pre-#13313 branch coverage:
`src/utils/fuseUtil.ts` 81.48% -> 92.59% (+11.11%); overall branches
52.93% -> 52.95% (+0.02%).

Codecov project coverage is intentionally omitted here because it is not
the branch-ratchet metric.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a
summary for commit 8bf748d1a4. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: huang47 <157390+huang47@users.noreply.github.com>
Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com>
2026-07-02 22:30:50 +00:00
ShihChi Huang
65b4c53bcb ci: skip website report deploy for fork PRs (#13344)
## Summary

Skip the website e2e report/deploy step for fork PRs, which lack the
deploy secrets and otherwise fail the job.

## Changes

- **What**: Guard the report/deploy step's `if:` in
`ci-website-e2e.yaml` so it runs only when the event is not a fork pull
request.
- **Breaking**: none. CI-config only.

## Review Focus

CI-config only — no test or coverage change. Confirms fork PRs no longer
fail on the deploy step.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> CI workflow condition only; no application or test logic changes.
> 
> **Overview**
> **Website E2E CI** no longer runs the **Deploy report to Cloudflare**
step on pull requests from forks.
> 
> The step’s `if:` still requires `always()` and `!cancelled()`, and now
also requires either a non–pull-request event or a PR whose head repo is
**not** a fork. Playwright tests and artifact upload are unchanged; only
the wrangler deploy (which needs `CLOUDFLARE_*` secrets) is skipped for
fork PRs so those runs don’t fail when secrets aren’t available.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
02a4ab0769. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: huang47 <157390+huang47@users.noreply.github.com>
2026-07-02 22:30:03 +00:00
ShihChi Huang
15b31d69ea ci: skip secret-backed CI deploys for fork PRs (#13291)
## Summary

Skip secret-backed CI deploy and dispatch work for fork PRs so missing
repo secrets do not fail otherwise valid checks.

## Changes

- **What**: Guard Website E2E report deploy, Vercel website preview
deploy, cloud build dispatch, cloud cleanup dispatch, and Storybook
Chromatic deploy so PR paths only run for same-repo PRs.
- **Dependencies**: None

## Why

Fork `pull_request` runs do not receive repository secrets. Several CI
jobs already separated normal validation from privileged follow-up work,
but some deploy or dispatch steps could still run on fork PRs and fail
only because their secret-backed integration token was empty.

The existing Website E2E fork guard only protected the PR comment job.
It did not protect the earlier Cloudflare report deploy step inside
`website-e2e`, which uses `CLOUDFLARE_API_TOKEN` and
`CLOUDFLARE_ACCOUNT_ID`.

The same failure mode existed in these CI jobs:

- `ci-vercel-website-preview.yaml`: preview deploy uses Vercel and
website API secrets.
- `cloud-dispatch-build.yaml`: preview dispatch uses
`CLOUD_DISPATCH_TOKEN` to call `Comfy-Org/cloud`.
- `cloud-dispatch-cleanup.yaml`: preview cleanup dispatch uses
`CLOUD_DISPATCH_TOKEN`.
- `ci-tests-storybook.yaml`: Chromatic deploy uses
`CHROMATIC_PROJECT_TOKEN`.

`ci-website-build.yaml` was left unchanged. Its Ashby and Cloud nodes
integrations intentionally fall back to committed snapshots when secrets
are missing for preview/local builds, so it is not the same class of
fork-secret failure.

## Review Focus

Confirm fork PRs still run the unprivileged validation/build paths,
while same-repo PRs and non-PR events keep the existing deploy or
dispatch behavior.

## Validation PRs

Both validation PRs compare against `main`.

- Fork PR from `shihchi`:
[#13309](https://github.com/Comfy-Org/ComfyUI_frontend/pull/13309)
- Same-repo PR from `origin`:
[#13310](https://github.com/Comfy-Org/ComfyUI_frontend/pull/13310)

| Workflow | Guarded job or step | Fork #13309 | Same-repo #13310 |
| --- | --- | --- | --- |
| CI: Website E2E | `Upload test report` | success  | success  |
| CI: Website E2E | `Deploy report to Cloudflare` | skipped  | success
 |
| CI: Vercel Website Preview | `deploy-preview` | skipped  | success 
|
| Cloud Frontend Build Dispatch | `dispatch` | skipped  | success  |
| CI: Tests Storybook | `chromatic-deployment` | skipped  | success  |

Expected result: fork PRs still keep the useful validation artifact
path, but skip secret-backed deploy and dispatch work. Same-repo PRs
keep the privileged behavior.

## Screenshots (if applicable)

N/A, CI-only.

Created by Codex

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Workflow `if` condition changes only; no application code. Same-repo
PR behavior is unchanged when secrets are available.
> 
> **Overview**
> Adds **`github.event.pull_request.head.repo.fork == false`** guards so
fork PRs no longer run steps that need repo secrets or trigger external
deploys.
> 
> **Website E2E** — the Cloudflare Playwright report deploy step now
runs only on non-PR events or same-repo PRs, so fork runs can still pass
tests and upload artifacts without failing on missing `CLOUDFLARE_*`
secrets.
> 
> **Vercel website preview** — the preview deploy job is skipped
entirely for fork PRs (Vercel tokens).
> 
> **Storybook Chromatic** — Chromatic deployment on `version-bump-*` PRs
is limited to non-fork PRs (`CHROMATIC_PROJECT_TOKEN`).
> 
> **Cloud dispatch** — build and cleanup dispatches to the cloud repo
for preview labels no longer run for fork PRs, aligning with the
existing fork-guard comment in those workflows.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
027aabc9e3. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: huang47 <157390+huang47@users.noreply.github.com>
2026-07-02 22:29:47 +00:00
Benjamin Lu
471236e08d feat: track subscription cancellation intent and resubscribe clicks (#13368)
## Summary

Instruments the churn funnel: cancellation intent, attempt, abandonment,
and request failure, plus resubscribe clicks — all client-observed from
existing request/response flows, no watchers or polling added. Covers
both billing paths: the mainline (`/customers/*` + Stripe portal) path
via the "Manage subscription" click, and the workspace path via its
in-app cancel dialog.

## Changes

- **What**:
- New events: `app:subscription_cancel_flow_opened` / `_confirmed` /
`_abandoned` / `_failed` and `app:resubscribe_button_clicked`, via
`trackSubscriptionCancellation(stage, metadata)` and
`trackResubscribeClicked` (registry, PostHog, host sink)
  - All cancellation events carry a `source` discriminator:
- `manage_subscription_button` — the mainline path. Legacy users can
only cancel inside the Stripe billing portal, and in-app UI already
covers plan changes, so this click is the closest observable
cancel-intent signal for ~all production users. Only `flow_opened` fires
here (everything past the click happens in Stripe's UI). Probable, not
certain, intent — the portal also serves card updates/invoices.
- `cancel_plan_menu` — the workspace in-app dialog (allowlist-gated
pilot): `flow_opened` on mount, `confirmed` before the API call (failed
attempts still register), `failed` with the error message, `abandoned`
on "Keep subscription"/close. Successful cancels close via a different
path and never emit `abandoned`.
- Metadata carries `current_tier`, billing `cycle`, and (dialog path)
the `end_date` shown to the user
- Resubscribe clicks tracked at both call sites with `source`:
`pricing_dialog` (`useSubscriptionCheckout`, also carrying the dialog's
`payment_intent_source` from #13363) and `settings_billing_panel`
(`useResubscribe`)
- Not instrumented on purpose: the workspace "Manage billing" button and
the "Invoice history" footer link (portal opens without cancel
connotation)

## Review Focus

- Deliberately **no** client-side "cancel succeeded" event: outcome
truth is server-side. Mainline already has it
(`billing:subscription_deleted` from the Stripe webhook in comfy-api);
the workspace path needs a `subscription_cancelled` billing event type
(separate cloud-repo change). The legacy
`useSubscriptionCancellationWatcher` poller emits an undercounted
`app:monthly_subscription_cancelled`; analysis should prefer the server
event.
- `confirmed` fires before the request; growth can join
`flow_opened`/`confirmed` → server-side cancelled events by user +
timestamp.
2026-07-02 14:51:12 -07:00
Mobeen Abdullah
4cc0402325 revert(website): remove Creative Campus customer stories (#13370) (#13407)
## Summary

Reverts #13370 (the five Creative Campus customer stories) from `main`.
These are education-tied stories, and the "Education Program is live"
CTA links to the education page, which is not live yet, so they should
not be public before the education launch.

This is a clean `git revert` of the squash commit `49a90d4e2` (no
history rewrite, no force-push). No work is lost: the story branch
(`feat/website-customer-stories-education`) is intact, and the stories
will relaunch together with pricing and the education page via #13406.

## Changes

- **What**: Reverts the 5 new story MDX files, the new article block
components, and the related changes to `CustomerArticle.astro`,
`global.css`, `Figure`/`Quote`/`Contributors`, the content test, and the
e2e spec. The existing five stories and the customers pages are
unaffected.
- **Breaking**: none.

## Review Focus

- Pure inverse of #13370; the diff is `-858/+11` mirroring the original
merge.
- Files touched by #13370 are disjoint from the education-page work in
#13406, so this does not conflict with that branch.

## Verification

- Build: 497 pages (down 5 en story pages). Unit: 156/156. Typecheck: 0
errors. format:check and knip clean.

## Next steps

- Stories move into the education bundle (#13406) via a separate PR.
- When the education page and its auth (FE-1174) are ready, pricing +
customer stories + education launch together.
2026-07-03 01:49:47 +05:00
Wei Hai
a2adfe5124 fix(ci): drop unsupported 'range' genhtml ignore-errors category (#13396)
## Summary
- `CI: E2E Coverage`'s `Generate HTML coverage report` step fails on
every run with `genhtml: ERROR: unknown argument for --ignore-errors:
'range'`
- The runner's `apt-get install lcov` resolves to lcov 2.0-4ubuntu2
(Ubuntu 24.04/noble), but the `range` ignore-errors category was only
added in lcov 2.1
- lcov 2.0 already reports the out-of-range-line condition under the
`source` category, which is already in the ignore list, so `range` was
both unsupported and redundant on this runner

## Test plan
- [x] Confirmed lcov 2.0-4ubuntu2 is what `apt-get install lcov`
resolves to on `ubuntu-latest`
- [x] Confirmed via lcov's `lcovutil.pm` source that `range`
(`$ERROR_RANGE`) is only registered as of v2.1, and in v2.0 the
equivalent out-of-range case falls under `$ERROR_SOURCE`
- [ ] CI: E2E Coverage run on this branch's merge should pass the
"Generate HTML coverage report" step
2026-07-02 20:08:47 +00:00
Mobeen Abdullah
49a90d4e2e feat(website): add five Creative Campus customer stories (#13370)
## Summary

Add the five new Comfy Education Initiative (Creative Campus) customer
stories to `/customers`, each with its own detail page, reusing the
existing Astro content-collection pattern. Brings the listing to ten
stories. Linear: FE-1161.

## Changes

- **What**: Five new English MDX stories (Xindi Zhang, Ina Conradi,
Golan Levin, Kathy Smith, and the UAL CCI partnership) added to the
customers collection, ordered after the existing five. Adds a small set
of reusable article blocks these stories need: `Embed` (Vimeo), `Video`
(wraps the existing `VideoPlayer`), `Download` (workflow JSON),
`AuthorBio`, `EducationCta`, `AtAGlance`, a styled inline `Link`, and
`Heading4`. `Quote`'s `name` is now optional for unattributed
pull-quotes; `Figure` gained an optional rich-caption slot (for captions
that contain links); `AuthorBio` supports a single-author bio via slot.
- **Breaking**: none. All additions are backward compatible; the
existing five stories and their pages are untouched.
- **Dependencies**: none.

## Review Focus

- The logic to review is small and isolated: the new block components in
`components/customers/content/` and their registration in
`CustomerArticle.astro`. The rest of the diff is MDX content.
- **Story copy is transcribed verbatim from the source docs**;
punctuation (em/en dashes, curly quotes) is preserved as written and is
intentional, not a formatting slip.
- **Downloads (cross-origin):** the workflow JSON files are on
media.comfy.org, so the HTML `download` attribute is ignored by
browsers. The real download is forced server-side with
`Content-Disposition: attachment` on the storage objects. Xindi's two
workflow files are served from a cache-fresh `.../workflows/` path (with
an explicit `filename=`) so the CDN serves the attachment header
immediately.
- **Embed hardening:** the Vimeo `Embed` iframe carries
`referrerpolicy="strict-origin-when-cross-origin"` and a scoped
`sandbox` (`allow-scripts allow-same-origin allow-presentation
allow-popups`); the player was verified to still load and play.
- All media (card covers, inline images, one video with a poster frame,
workflow JSON/PNG downloads) is hosted on media.comfy.org. No local
assets are committed. Golan's workflow files are re-hosted there; his
lesson-plan and demo-project links intentionally stay on GitHub/p5.js as
view-only.
- English-first: Chinese versions will be added later through a separate
translation service. The listing and detail pages already handle a
locale that only has English entries, so no page-code changes were
needed.
- Tags: "Creative Campus Showcase" for the four teaching stories, and
"Creative Campus Partnership" for the UAL announcement.

## Verification

- Unit `176/176`, typecheck (astro check) `0 errors`, build `502 pages`,
`format:check`, `knip`, and `eslint` all pass.
- e2e customer specs `6/6` pass (includes a new test asserting the
Creative Campus education blocks render).
- Visual pass on all ten stories at desktop (1440) and mobile (390): no
horizontal overflow, the Vimeo player plays, and all downloads resolve
to media.comfy.org.

## Screenshots (if applicable)

Easiest way to review is the Vercel preview:
https://comfy-website-preview-pr-13370.vercel.app/customers then open
the five new stories. Verified on desktop (1440) and mobile (390).
2026-07-03 00:34:20 +05:00
Hunter
d6c582c399 feat(billing): gate consolidated billing behind consolidated_billing_enabled flag (#13359)
## Summary

Shields personal-workspace billing code paths behind the new
`consolidated_billing_enabled` feature flag so they fall back to the
**legacy** billing flow while the flag is `false`. Team workspaces are
unaffected and continue to use the workspace-scoped billing flow.

## Changes

- Add `consolidatedBillingEnabled` to `useFeatureFlags` (reads the
`consolidated_billing_enabled` server flag / remote config, defaults to
`false`) and to the `RemoteConfig` type.
- New `useBillingRouting` composable — a single source of truth for
whether the active workspace uses the workspace vs. legacy billing flow:
  - team workspaces disabled → legacy
  - personal workspace + consolidated billing off/missing → legacy
  - personal workspace + consolidated billing on → workspace
  - team workspace → workspace
  - workspace not loaded yet → legacy
- Route `useBillingContext` and the affected UI sites
(`SubscriptionPanel`, `useSubscriptionDialog`, `UsageLogsTable`,
`TopUpCreditsDialogContentLegacy`) through `useBillingRouting` instead
of keying on `teamWorkspacesEnabled` directly.
- Update the storybook `useFeatureFlags` mock to stay in sync.

## Testing

- `pnpm test:unit` for `useBillingRouting`, `useBillingContext`,
`useSubscriptionDialog`, and `UsageLogsTable` (new + updated coverage
for the routing matrix). Remaining quality gates (`typecheck`, `lint`)
are being verified in CI.

## Related

Requires the backend PR that adds the `consolidated_billing_enabled`
flag to `/api/features`.

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-07-02 18:34:39 +00:00
imick-io
a6db1ab3d6 fix(website): restore node-link.svg intrinsic sizing (#13384)
## Summary

Restore the original `node-link.svg` asset, which PR #13095 accidentally
overwrote with a stretch-to-fill Figma export, breaking the node
connector across the marketing site.

## Changes

- **What**: Revert `apps/website/public/icons/node-link.svg` to its
intrinsic **20×32** form (`fill="#F2FF59"`). PR #13283 had replaced it
with a raw Figma export (`preserveAspectRatio="none"`, `width="100%"
height="100%"`, `fill="var(--fill-0, …)"`). Every consumer loads it as a
bare `<img src>` and relies on the intrinsic size plus
`scale-*`/`rotate` classes — with no intrinsic dimensions the connector
expanded to fill its container and distorted.

## Review Focus

- The overwrite originated in the first commit of #13283's stack and
rode through the squash merge; nothing in that PR actually referenced
this file (the MCP page uses the separate `NodeUnionIcon.vue`), so
restoring the shared asset fixes all consumers (`BuildWhatSection`,
`ProductShowcaseSection`, `OurValuesSection`, `GalleryDetailModal`)
without touching the MCP page.
- `apps/website/dist/icons/node-link.svg` is stale build output and
regenerates on the next `pnpm build`.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-07-02 13:07:00 +00:00
Benjamin Lu
2ec2a0e091 feat: attribute payment intent through paywall, checkout, and top-up telemetry (#13363)
## Summary

Answers "why did this user want to pay?" by capturing the triggering
product moment at every paywall/upsell entry point and carrying it
through checkout and success telemetry.

## Changes

- **What**:
- Widen `SubscriptionDialogReason` from 4 coarse values to 13 grounded
intent sources (`subscribe_to_run`, `upgrade_to_add_credits`,
`invite_member_upsell`, `settings_billing_panel`, etc.)
- Fire `app:subscription_required_modal_opened` from
`useSubscriptionDialog` (the choke point all dialog variants pass
through) — the workspace/unified path previously emitted nothing; remove
the now-duplicate emitters in `useSubscription` and
`usePricingTableUrlLoader`
- Add `payment_intent_source` to
`BeginCheckoutMetadata`/`SubscriptionSuccessMetadata`, threaded via the
existing `reason` prop: dialog → `PricingTable` →
`performSubscriptionCheckout` → pending-attempt record, so legacy
`app:monthly_subscription_succeeded` carries intent alongside
`checkout_attempt_id`
- Fire `begin_checkout` on the workspace checkout path
(`useSubscriptionCheckout`, personal + team confirm) and the team
deep-link util — both previously emitted nothing; `tier` widened to
`TierKey | 'team'`
- Implement `trackBeginCheckout` in `PostHogTelemetryProvider` (was
GTM/host-only, so `begin_checkout` never reached PostHog)
- Thread `showSubscriptionDialog(options)` through the billing-context
adapters and pass a reason at ~14 call sites; add `source` to
`app:add_api_credit_button_clicked`

## Review Focus

- `modal_opened` now fires once per dialog actually shown, so a
free-tier user clicking Upgrade emits two events (free-tier dialog, then
pricing table) where the legacy path emitted one
- Intent is threaded explicitly via props/params rather than shared
state; `useSubscriptionCheckout` gained an optional second parameter
2026-07-02 03:11:21 +00:00
Mobeen Abdullah
9cf5c9a93f refactor(website): tidy customer story review nits (#13324)
## Summary

Small follow-up to #13289 applying two non-blocking review nits from
Alex's review.

## Changes

- **What**: drop the redundant `before:content-['']` on the
customer-story list bullet (Tailwind emits the empty `content`
automatically once another `before:` utility is present), and rename
`HEADER_OFFSET` to `HEADER_OFFSET_PX` in `ArticleNav` so the scroll
constants use consistent unit suffixes.

## Review Focus

Both changes are cosmetic with no behavior change. Confirmed in the
browser that the list bullet still renders identically (6px yellow dot)
without the explicit `content` utility.

## Notes from the #13289 review (left as-is here, open to discussion)

Three other comments from the review are intentionally not changed in
this PR; reasoning below so the decisions are on record:

- **`Category` type in `ArticleNav`**: kept the `ComponentProps<typeof
CategoryNav>` derivation. AGENTS.md says to derive component types via
`vue-component-type-helpers` rather than redefining them, so the current
form follows the styleguide. Happy to switch to a plain named type if
preferred.
- **Section ids in frontmatter vs the body `<Section>`**: kept the
`customers.content.test.ts` parity test. The short TOC labels live only
in frontmatter and Astro can't introspect the rendered MDX body to build
the nav, so the frontmatter `sections` list and the body anchor ids
can't be trivially deduplicated. A real fix would need a remark plugin
(larger, separate change). The test guards against silent drift in the
meantime.
- **`nextStory` throw**: left as a fail-loud, build-time invariant. The
slug always comes from the same `getStaticPaths` collection, so the
throw is effectively unreachable; it surfaces a future-refactor bug
loudly instead of linking to the wrong story.
2026-07-01 12:45:24 +00:00
jaeone94
9e5fb67b76 Show app mode run validation warning (#12557)
## Summary
Adds an app mode validation warning so users can see when a workflow has
errors before running and jump directly back to graph mode to review
them.

## Changes
- **What**: Adds a reusable app mode warning banner above the Run button
when the execution error store reports workflow errors, including
validation and missing asset states.
- **What**: Reuses the existing graph-error navigation flow so the
warning action switches out of app mode and opens the Errors panel in
graph mode.
- **What**: Updates the app mode Run button icon and accessible label in
the warning state while keeping the Run action non-blocking.
- **What**: Adds unit coverage for the warning render/accessibility
state and an E2E flow that triggers a validation failure, dismisses the
overlay, and opens graph errors from the app mode warning.
- **Breaking**: None.
- **Dependencies**: None.

## Review Focus
The warning intentionally mirrors graph mode behavior: it surfaces the
error state but does not prevent the user from clicking Run. This avoids
turning display-level validation signals into hard execution blockers.

The warning is driven by the existing `hasAnyError` aggregate, so
missing nodes, missing models, and missing media are included alongside
prompt/node/execution errors.

## Tests
- `pnpm format`
- `pnpm lint`
- `pnpm typecheck`
- `pnpm test:unit`
- `pnpm knip`
- `pnpm test:browser:local
browser_tests/tests/appModeValidationWarning.spec.ts`

## Screenshots

<img width="461" height="994" alt="스크린샷 2026-06-25 오후 7 00 55"
src="https://github.com/user-attachments/assets/f8fc20bf-d572-46b5-9fa4-312e7c4c8076"
/>
2026-07-01 15:24:45 +09:00
ShihChi Huang
690e0e3590 test: add critical unit coverage gate (#13313)
## Summary

Add a `COVERAGE_CRITICAL` unit-coverage gate over folder-based critical
runtime areas and wire it into the unit CI job. First PR of a stacked
series that ratchets the gate upward as tests land.

## Changes

- **What**: `vite.config.mts` gains `CRITICAL_COVERAGE_INCLUDE` folder
globs for core runtime areas: `src/base`, `src/composables`, `src/core`,
`src/schemas`, `src/scripts`, `src/services`, `src/stores`, `src/utils`,
selected `src/platform` logic slices, selected
`src/lib/litegraph/src/{node,subgraph,utils}` primitives, and selected
`src/workbench` manager logic; `package.json` gains
`test:coverage:critical` (`COVERAGE_CRITICAL=true vitest run
--coverage`); `ci-tests-unit.yaml` runs the gate. The thresholds are
env-gated, so the normal `test:coverage` run is unaffected.
- **Breaking**: none.

## Review Focus

Establishes the measurement substrate, no tests added yet. Thresholds
are locked to the current baseline over the folder-based critical scope
so CI is green:

| metric | baseline | threshold |
|---|---|---|
| statements | 69.53% (24287/34930) | 69 |
| branches | 60.7% (11497/18940) | 60 |
| functions | 67.34% (4980/7395) | 67 |
| lines | 70.83% (22619/31930) | 70 |

The scope is intentionally not whole `src/platform`, `src/lib`, or
`src/workbench`: UI-heavy and specialized lanes like platform
components, telemetry/surveys, litegraph
canvas/widgets/infrastructure/types, and manager components/types stay
outside this gate for now.

Subsequent stacked PRs add tests and bump these thresholds; a later
refactor series ratchets branches to 90.

Created by Codex

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Changes are limited to test/coverage configuration and CI; no
application runtime behavior is modified.
> 
> **Overview**
> Introduces a **critical-path unit coverage gate** that only runs when
`COVERAGE_CRITICAL=true`, leaving the existing `pnpm test:coverage`
behavior unchanged.
> 
> **Vitest** (`vite.config.mts`): when the flag is set, coverage is
limited to folder globs for core runtime areas (base, composables, core,
services, stores, utils, selected platform/workspace/auth slices,
litegraph node/subgraph/utils, workbench manager logic, etc.) and
**Vitest thresholds** are enforced (statements 69%, branches 60%,
functions 67%, lines 70%). In that mode, litegraph is no longer
blanket-excluded from coverage the way the full `src` run still excludes
`src/lib/litegraph/**`.
> 
> **Tooling & CI**: adds `test:coverage:critical` in `package.json` and
a new unit CI step after Codecov upload that runs the gate so
regressions in those areas fail the job.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
25e73f3844. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: huang47 <157390+huang47@users.noreply.github.com>
2026-06-30 21:54:56 +00:00
Mobeen Abdullah
01738b7b19 feat(website): move customer stories to Astro content collections (#13289)
## Summary

Move the five customer stories on `/customers` out of the old
`customerStories.ts` array and the shared i18n file into an Astro
content collection (MDX, English and Chinese). The pages look and read
exactly the same; this just changes where the content lives so it is
easier to edit, and it sets the pattern we will reuse to migrate the
rest of the marketing content.

Linear: FE-1158

## Changes

- **What**:
- Added a `customers` content collection (`src/content.config.ts` +
`src/content/customers.schema.ts`), with one MDX file per story per
locale under `src/content/customers/{en,zh-CN}/`.
- Rebuilt the article rendering as small static components (`Section`,
`Figure`, `Quote`, `Contributors`, `Steps`, plus styled
paragraph/heading/list). The article body is now static HTML; only the
scroll-spy sidebar (`ArticleNav.vue`) ships JavaScript.
- Repointed the `/customers` listing and detail pages (both locales) to
read from the collection.
- Removed the old `customerStories.ts` array and the customer-story keys
from `translations.ts` (about 1,300 lines).
- Dropped the two "Read more" links that just redirected back to the
same page; kept the two that point to the real Substack articles.
- Switched the "Read more" button to the design-system `Button`, which
also fixes its vertical alignment.
- Added a short pattern doc at `apps/website/src/content/README.md` for
reuse.
- **Dependencies**: `@astrojs/mdx` (renders the MDX content).

## Review Focus

This is meant to be a no-visual-change migration. I checked content and
layout against the live site for all five stories in both languages, on
desktop and mobile. The only intended differences are the two removed
self-referential "Read more" links and the read-more button now using
the shared `Button`.

A few small setup changes explain part of the diff:

- `src/env.d.ts` now references `.astro/types.d.ts` so the collection
types resolve (this is the repo's first content collection).
- `astro.config.ts` sets `markdown.smartypants: false` so quotes stay
straight (MDX would otherwise curl them). This option is deprecated in
Astro 7 and moves onto the markdown processor; that belongs with the
eventual Astro 7 upgrade, not here.
- ESLint ignores the `astro:` virtual modules for `apps/website` files
(they are real at build time, but the resolver cannot see them).
- Content MDX is excluded from `oxfmt` in `.oxfmtrc.json`: the formatter
rewraps component slots and changes the rendered output (it broke the
blockquotes), so content files are kept out of it like generated files
and fixtures.
- `components/common/ContentSection.vue` and `config/contentSections.ts`
are untouched; they still power the legal and privacy pages.

The diff is large, but most of it is MDX content, the lockfile, and the
removed i18n keys. The logic to review is small: the collection config
and schema, the components, and the page wiring.

## Screenshots

No visual change is intended, so before and after of the article pages
are identical (verified across both locales and on desktop and mobile).
The one deliberate tweak is the "Read more" button, which now uses the
design-system `Button` for better vertical alignment. Before/after
captures are available if needed.
2026-06-30 21:43:54 +00:00
Alexander Brown
be9de941c9 refactor: brand link slot and reroute ids (#13296)
## Summary

Brand link, reroute, and slot identifiers through LiteGraph, subgraph,
and layout flows so raw numeric workflow data is converted at boundaries
while runtime APIs keep branded IDs.

## Changes

- **What**: Add canonical `LinkId`, `RerouteId`, and `SlotId` types plus
minting helpers, then re-export litegraph/layout ID types from those
modules.
- **What**: Keep `LinkId`, `RerouteId`, and `SlotId` references branded
across graph links, reroutes, node slots, subgraph slots, link
deduplication, link drop handling, layout storage, and tests.
- **What**: Convert raw numeric IDs only at periphery points: serialized
workflow DTOs, legacy graph link proxy access, copied/pasted graph data,
Yjs/string layout keys, and test fixtures.
- **What**: Move slot layout identity onto branded `SlotId` values using
stable `node:direction:index` ordering, while keeping DOM dataset values
stringified at the boundary.
- **What**: Avoid slot-key scans during link drops by carrying the link
segment identity directly through the drop path.

## Review Focus

- Branded IDs should not be widened back to `LinkId | number` /
`RerouteId | number` in runtime APIs.
- Serialized workflow shapes intentionally remain numeric for
compatibility.
- `_subgraphSlot.linkIds` remains `LinkId[]`; call sites should not
treat it as raw `number[]`.
- `MapProxyHandler` is the compatibility boundary for deprecated indexed
`graph.links[id]` access.

## Validation

- `pnpm typecheck`
- `pnpm test:unit src/lib/litegraph/src/LLink.test.ts
src/lib/litegraph/src/LGraph.test.ts
src/lib/litegraph/src/LGraphNode.test.ts
src/lib/litegraph/src/canvas/LinkConnector.core.test.ts
src/lib/litegraph/src/canvas/LinkConnector.integration.test.ts
src/lib/litegraph/src/canvas/LinkConnectorSubgraphInputValidation.test.ts
src/lib/litegraph/src/LGraphCanvas.drawConnections.test.ts
src/lib/litegraph/src/node/slotUtils.test.ts
src/lib/litegraph/src/subgraph/ExecutableNodeDTO.test.ts
src/core/graph/subgraph/promotionUtils.test.ts
src/core/graph/subgraph/migration/proxyWidgetMigration.test.ts
src/renderer/core/layout/store/layoutStore.test.ts
src/renderer/core/layout/utils/layoutUtils.test.ts
src/renderer/extensions/minimap/minimapCanvasRenderer.test.ts
src/scripts/promotedWidgetControl.test.ts`
- Commit hook: `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`
- Push hook: `knip --cache`

---------

Co-authored-by: AustinMroz <austin@comfy.org>
2026-06-30 17:29:33 +00:00
Rizumu Ayaka
f4e0430072 fix: disable global keybindings while a modal dialog is open (#12184)
## Summary

Block background keybindings from firing while a modal dialog (e.g.
Templates) is open, so typing `w` no longer toggles the workflow sidebar
behind the modal.

## Changes

- **What**: In `keybindingService.keybindHandler`, gate command
execution on `dialogStore.dialogStack`. When a dialog is open, only
keybindings whose event target is inside the dialog (`[role="dialog"]`)
fire; all other matches are dropped.

## Review Focus

- The dialog scope check uses `target.closest('[role="dialog"]')` so
dialog-internal shortcuts still work — confirm PrimeVue/Reka dialogs
render with `role="dialog"` on the wrapper (they do; this is the
WAI-ARIA standard the libraries follow).
- Updated `keybindingService.escape.test.ts` "modifiers regardless of
dialog state" case to the new contract (modifiers also blocked),
matching the team consensus in FE-642 that all keybindings should be
disabled when a modal is open.
- New `keybindingService.dialog.test.ts` covers: no-dialog → fires;
dialog open + target outside → blocked; dialog open + target inside →
fires.

Fixes FE-642

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12184-fix-disable-global-keybindings-while-a-modal-dialog-is-open-35e6d73d3650812fbc5dd5490ccde24f)
by [Unito](https://www.unito.io)

Co-authored-by: Dante <bunggl@naver.com>
2026-06-30 06:33:33 +00:00
Rizumu Ayaka
c78592c1ec feat: add upload button to dropdown menu filter bar (#12507)
## Summary

Add an Upload button to the dropdown popover's filter bar so users can
pick a file without closing the dropdown to reach the small upload icon
next to the input.

The upload button in the dropdown menu includes text and uses the same
icon as the external quick upload button. This design ensures that after
using it, users will understand that the icon on the external button
means upload. Even if users didn't understand it before, they will
correctly interpret it next time.

related linear FE-581

## Changes

- **What**:
- Expose `showPicker()` from `FormDropdownInput`; it calls
`HTMLInputElement.showPicker()` on the single existing hidden `<input
type="file">` (falls back to `input.click()` on browsers without
showPicker).
- Add an Upload button in `FormDropdownMenuFilter` that emits
`show-picker`, bubbled up through `FormDropdownMenu` to `FormDropdown`,
which then calls `triggerRef.showPicker()`. The whole chain runs in the
click event's synchronous stack to satisfy the browser's transient
activation requirement, so no extra `<input type="file">` is added to
the DOM.
- Style the button with the project's standard inverted-button tokens
(`bg-base-foreground` / `text-base-background`) so it tracks theme
changes.

## Review Focus

- The `triggerRef!.showPicker()` non-null assertion in
`FormDropdown.vue` is intentional: by the time `show-picker` is emitted
the trigger is guaranteed to be mounted; a null here would indicate a
real bug we want to surface, not swallow.
- Verify the new button reuses the same upload path as the inline icon
button (single `<input type="file">`, single `handleFileChange`).

## Screenshots

<img width="1304" height="1442" alt="CleanShot 2026-06-02 at 14 39
33@2x"
src="https://github.com/user-attachments/assets/b2d1cdd8-e28a-467d-8142-afd707264d0e"
/>


<details><summary>Old Versions</summary>
<p>


https://github.com/user-attachments/assets/2d64873b-6bec-4eca-aa89-a72dd11aa809

</p>
</details>
2026-06-30 06:25:24 +00:00
Rizumu Ayaka
00b0c6b434 fix: close widget dropdown on outside pointerdown and canvas viewport moves (#12812)
## Summary

Model/widget dropdowns stayed open until mouseup, detached from their
node when the canvas moved while open, and needed two clicks to dismiss
after the inner scrollbar took focus.

## Changes

- **What**:
- Dismiss the dropdown on `pointerdown` outside the menu/trigger
(capture phase) instead of PrimeVue's `click` (mouseup) dismissal. The
dropdown now closes the instant a press lands, before a drag or
box-select can start, and a focused inner scrollbar no longer swallows
the first outside click.
- Close the dropdown whenever the canvas viewport moves, by watching the
reactive `useTransformState().camera`. This reacts to the canvas
abstraction layer rather than guessing input intent, so it covers
pan/zoom from any device — mouse drag, trackpad pan, wheel scroll/zoom —
where no `pointerdown` ever fires. The popover is teleported to the
document body and cannot follow the viewport, so closing is the correct
behavior.

## Review Focus

- Box-select and node-drag both begin with a `pointerdown` outside the
popover, so they are covered by the immediate dismissal path; the camera
watch handles pointer-less viewport motion.
- `closeOnEscape` and in-menu interactions are unaffected; presses
inside the menu or on the trigger are excluded via `composedPath()`.

Fixes FE-808

---------

Co-authored-by: Dante <bunggl@naver.com>
2026-06-30 05:58:55 +00:00
Christian Byrne
da34fa3944 docs(website): update ToS payment terms for Free Tier overages (#13315)
*PR Created by the Glary-Bot Agent*

---

Updates two sections on https://comfy.org/terms-of-service per legal
copy provided in [the website-and-docs Slack
thread](https://comfy-org.slack.com/archives/C098QHJ8YDR/p1782775899132369).

## Changes

Edits `apps/website/src/i18n/translations.ts` (the source of truth for
the ToS page rendered by
`apps/website/src/pages/terms-of-service.astro`):

- **`tos.payment.block.1` — Plans; Fees; Free Tier.** Adds language
clarifying that a Free Tier user who provides a payment method expressly
authorizes Comfy to charge it for overages (intentional use, third-party
use, or technical factors), and that approach-to-cap notifications are
best-effort, not a precondition to charging.
- **`tos.payment.block.3` — Self-Serve Credit Card Billing.** Clarifies
that the billing authorization applies to paid Plan and Free Tier
overages alike, and that retry rights for failed charges extend to Free
Tier overage charges.

`en` and `zh-CN` values are kept in sync per the existing convention for
these keys (the `/zh-CN/terms-of-service` page is a redirect to the
English page).

## Open question for legal / requester

`tos.effectiveDate` is currently `May 13, 2026` and was **not** bumped
in this PR — the original request did not mention it. If legal wants
this revision to carry a new effective date, that should be a follow-up
commit on this branch before merge.

## Verification

- `pnpm typecheck` (apps/website): 0 errors, 0 warnings.
- `pnpm build` (apps/website): 497 pages built; the rendered
`/terms-of-service` HTML contains both new sentences.
- `pnpm exec eslint` / `oxfmt --check` on the changed file: clean.
- Husky pre-commit (`lint-staged` + `check-unused-i18n-keys`): clean.
- Manual: served the built `dist/` via local HTTP and verified the
rendered Payment section in a real browser (screenshot below).

## Screenshots

![Rendered /terms-of-service Payment section showing the updated Plans;
Fees; Free Tier and Self-Serve Credit Card Billing
copy](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/3d085431f019603d3250f274fdae4f9186eaaecbdaee4cbc6b924e2b84854661/pr-images/1782796953351-4deec91c-ac02-4bc5-b8cd-cd0a3413613e.png)

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
2026-06-30 05:29:13 +00:00
Alexander Piskun
c8ed15da31 feat: follow --comfy-api-base for staging and preview backends (#13054)
## Summary

Let the running ComfyUI server decide which backend the web UI talks to
(and which Firebase project it signs you into), so launching with
`--comfy-api-base` just works with the regular bundled frontend.

## Changes

- **What**: At startup the frontend reads `/api/features` on every build
(not just cloud) and treats the server's `comfy_api_base_url` /
`comfy_platform_base_url` as authoritative, falling back to the
build-time defaults.
When that api base is a staging-tier host (staging, or a
`*.testenvs.comfy.org` preview env) and the server hasn't supplied its
own Firebase config, the frontend picks the dev Firebase project,
derived from the api base.
Production is left exactly as it is today.
- `main.ts`: load remote config first thing, before Firebase
initializes, so every module sees the right values from the first render
- `config/comfyApi.ts`: the api/platform getters now read the server's
values on all distributions
- `config/firebase.ts`: `getFirebaseConfig()` resolves in order: a
server-provided config first (cloud), then the dev project for a
staging-tier api base, then the build-time default
- `platform/remoteConfig/refreshRemoteConfig.ts`: the startup fetch now
has a 5s timeout, so a slow or wedged `/features` can never keep the app
from mounting; on failure we fall back to the build-time defaults
- **Breaking**: None. With no `/features` overrides (production and
ordinary self-hosting), behavior is unchanged

## Review Focus

- The precedence in `getFirebaseConfig()` (`config/firebase.ts`): server
config first, then the staging-tier dev project, then the build-time
default. The staging-tier check matches `stagingapi.comfy.org` and any
`*.testenvs.comfy.org` host, and falls back to build-time for anything
it can't parse.
- Running `refreshRemoteConfig()` unconditionally and first in
`main.ts`, with the new fetch timeout as the safety net.

## Testing

I tested every case by hand, locally, on top of the automated checks.
Tested both with `pnpm run build` and `USE_PROD_CONFIG=true pnpm build`
and running Comfy from that folder.

Pointed a local ComfyUI at each backend with `--comfy-api-base` and
signed in with Google each time:

- **Production** (default / `https://api.comfy.org`): stays on
production and signs into the production Firebase project, identical to
today.
- **Staging** (`https://stagingapi.comfy.org`): follows it and signs
into the dev project.
- **Ephemeral preview env** (`https://pr-<n>.testenvs.comfy.org`): the
friendly host is accepted as-is, the frontend follows it, lands in the
dev project, and Google sign-in completes.

The only exception where fronted does not respect the `--comfy-api-base`
is when Comfy runs against `prod` and frontend runs with the `pnpm run
dev` - due to overridden config(this is expected behavior).

Supersedes: https://github.com/Comfy-Org/ComfyUI_frontend/pull/12560
Companion Core PR: https://github.com/Comfy-Org/ComfyUI/pull/14569

## Screenshots (if applicable)

<!-- Add screenshots or video recording to help explain your changes -->
2026-06-30 05:18:24 +00:00
CodeJuggernaut
b132abc64a fix: center video asset in the Load Video node preview (#13172)
## Summary

Center the Load Video node preview and keep the node from auto-resizing
on clip load, so the video letterboxes in place like the Load Image node
(FE-1092).

## Changes

- **What**: Makes the video stay centered horizontally and vertically
- Playwright browser test expectations updated at run
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/28267324092

## Review Focus

- Ensure that patterns and idioms were followed

## Screenshots (if applicable)
Horizontally centered
<img width="902" height="392" alt="image"
src="https://github.com/user-attachments/assets/a9fbec56-1613-44b4-a423-9f709a246c63"
/>

Vertically centered
<img width="220" height="1124" alt="image"
src="https://github.com/user-attachments/assets/5497f39b-2ea2-4247-a087-a7d89768b4ce"
/>

Full aspect ratio
<img width="433" height="672" alt="image"
src="https://github.com/user-attachments/assets/d579fb14-34c6-4963-abc9-034611232d3d"
/>

Minimum size
<img width="217" height="376" alt="image"
src="https://github.com/user-attachments/assets/80df0411-3ff1-4050-ac8e-761b7b8a7c40"
/>


Preview centering is asserted in
`browser_tests/tests/vueNodes/videoPreview.spec.ts`.

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-06-30 02:59:11 +00:00
steven-comfy
55c52a730a Enable cloud PostHog pageviews (#13286)
## Summary

This PR enables native PostHog `$pageview` capture for `cloud.comfy.org`
by setting cloud PostHog `capture_pageview` to `history_change`.

This keeps `autocapture` disabled, preserves the existing custom
`app:page_view` event, and lets the PostHog SDK capture the initial
pageview plus SPA history navigation pageviews. The goal is to make
cross-domain funnel tracking cleaner between `comfy.org` and
`cloud.comfy.org`, since `comfy.org` already emits native `$pageview`
events.

## Why

We want to measure the visitor funnel more accurately across:

- `comfy.org` visits
- `cloud.comfy.org` visits
- signup clicks / signup opened
- signup completion
- first cloud workflow run
- first subscription
- first credit purchase

Using native `$pageview` on both website and cloud should make PostHog
and downstream warehouse/Hex analysis cleaner for trackable users, while
leaving custom app pageview telemetry intact for existing consumers.

## Validation

- `pnpm test:unit
src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts`
- `pnpm typecheck`
- `pnpm lint:unstaged`
- pre-commit hook: `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`
- pre-push hook: `knip --cache`

Note: local validation printed an engine warning because the Codex
runtime has Node `v24.14.0` while this repo declares `>=25 <26`; the
commands above still passed.
2026-06-30 00:27:13 +00:00
Denis
fbe462143a fix: re-export GroupNodeHandler for custom node compat (#13299)
Fixes #13175

#12931 slimmed groupNode.ts down to migration-only and dropped the
export on GroupNodeHandler.

ComfyUI-Manager still imports it (import { GroupNodeConfig,
GroupNodeHandler } from "../../extensions/core/groupNode.js" in
components-manager.js), so the legacy shim no longer providing that
export throws "does not provide an export named 'GroupNodeHandler'" at
module load. That kills the whole Manager extension before setup() runs
— which is why the Manager button vanished from the toolbar since 1.47.3
(backend loads fine, frontend JS dies).

Just re-adds the export (class is still there, only the keyword was
lost) plus the existing @knipIgnoreUnusedButUsedByCustomNodes tag since
nothing in src imports it.

Tested by loading with ComfyUI-Manager installed: the groupNode.js
import error is gone and the Manager button shows again.
typecheck/knip/lint pass.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:04:59 +00:00
nav-tej
61cb1bcde0 fix(website): point launches Comfy MCP CTA to /mcp (#13287)
*PR Created by the Glary-Bot Agent*

---

## Summary

Update the `EXPLORE` CTA on the Comfy MCP card on
[/launches](https://comfy.org/launches) to link to
[/mcp](https://comfy.org/mcp) instead of the docs
(`docs.comfy.org/agent-tools/cloud`).

## Change

Single line in `apps/website/src/data/drops.ts`:

```diff
     cta: {
       label: EXPLORE,
-      href: { en: externalLinks.docsMcp, 'zh-CN': externalLinks.docsMcp }
+      href: { en: '/mcp', 'zh-CN': '/zh-CN/mcp' }
     }
```

Matches the locale-aware pattern used by sibling cards (`/download` /
`/zh-CN/download`, `/api` / `/zh-CN/api`). Both `src/pages/mcp.astro`
and `src/pages/zh-CN/mcp.astro` already exist, so neither link is dead.
The `externalLinks.docsMcp` constant is retained because the MCP page
itself still uses it.

## Verification

- `pnpm typecheck` / `pnpm typecheck:website` clean.
- `oxfmt`, `oxlint`, `eslint` clean (all ran via lint-staged on commit).
- Manually loaded `/launches` and `/zh-CN/launches` in the dev server
and confirmed the Comfy MCP card now points to `/mcp` and `/zh-CN/mcp`
respectively.
- Loaded `/mcp` and confirmed the destination page renders ("Comfy MCP —
Drive ComfyUI from any AI agent").
- Code review by Oracle: no issues.

Screenshot shows the updated MCP card on /launches.

## Screenshots

![Comfy MCP card on /launches with EXPLORE button now linking to
/mcp](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/9f315ccc2692129159ae31efab9464684ff2f6db3e144feae6dd52fd314c0b47/pr-images/1782765166237-5e83667b-8dc3-4182-9891-609385a1dae5.png)

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
2026-06-29 22:29:38 +00:00
AustinMroz
9dcab4ee96 Essentials Cleanup (#13183)
Address several followup comments from #12744
2026-06-29 22:15:54 +00:00
Benjamin Lu
dc29f30b02 Track theme setting changes via telemetry (#13142)
Fix Color Palette changes not getting tracked, requested by design team.

Capture theme changes as `app:setting_changed` telemetry. The only
existing hook lived in `SettingItem.vue`, which renders *visible*
settings; `Comfy.ColorPalette` is hidden and changed through bespoke
theme UI, so it was never tracked.

Open to opinions here, we can also remove the hook in SettingItem.vue,
and just make everything that was visible opt in.

Linear:
https://linear.app/comfyorg/issue/GTM-158/track-theme-usage-with-posthog-events
2026-06-29 22:05:05 +00:00
imick-io
fb3350ee0e feat(website): redesign Comfy MCP setup steps and add button variant (#13285)
## Summary

Reworks the Comfy MCP page's **"Set up Comfy MCP in three steps"**
section to match the new design, and adds a per-action button `variant`
option to `FeatureGrid01`.

The three steps are now:

| Step | Title | Action |
| --- | --- | --- |
| 1 | Copy the MCP URL | Copy field showing
`https://cloud.comfy.org/mcp` |
| 2 | Add the connector | Filled button **"COMFY CLOUD MCP DOCS" ↗** →
MCP docs |
| 3 | Connect and sign in | Filled button **"COMFY CLOUD SKILLS" ↗** →
comfy-skills repo |

## Changes

- **`FeatureGrid01.vue`** — add `variant?: 'default' | 'outline'` to the
link card action; button now uses `card.action.variant ?? 'outline'`
instead of a hardcoded outline, so callers can opt into the filled
style.
- **`config/routes.ts`** — add `mcpSkills` external link
(`https://github.com/Comfy-Org/comfy-skills`).
- **`i18n/translations.ts`** — refresh the `mcp.setup.*` copy (en +
zh-CN): new subtitle, reworded steps, new `step2.cta` / `step3.cta`,
drop the now-unused `step1.cta`.
- **`SetupSection.vue`** — re-map cards: step 1 → copy field, steps 2 &
3 → filled link buttons.

## Test plan

- [x] `pnpm typecheck` — 0 errors
- [x] Pre-commit hooks (stylelint, oxfmt, oxlint, eslint, typecheck)
pass
- [ ] Visual check on `/mcp` and `/zh-CN/mcp` (copy field on step 1; two
filled yellow CTAs with up-right arrows on steps 2 & 3)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:08:03 +00:00
imick-io
be8e0010ee feat(website): rebuild Comfy MCP page on the design system (+ zh-CN) (#13283)
Rebuilds the **Comfy MCP** marketing page on the website design-system
stack and adds the missing zh-CN page.

## What's here
- Replaces the bespoke `components/product/mcp/` section silo with thin
`templates/mcp/*` wrappers over reusable `blocks/` + `common/`
components.
- Adds `src/pages/zh-CN/mcp.astro` and threads `locale` through every
section (was English-only).
- New/extended design-system blocks:
- `FeatureGrid01` — setup steps, with a reusable `ui/CopyableField`
(uses `@vueuse/core` `useClipboard`).
- `FeatureGrid02` — how-it-works steps with `NodeUnionIcon` connectors +
a CTA pair via `ui/button`.
- `FeatureRows01` — alternating media rows; `ReasonsSplit01` — "why"
list.
- `HeroSplit01` gained `subtitle`, a `media` slot, and a `class`
passthrough; `SectionHeader` gained `align`.
- Standardized block section spacing on `px-6 py-16 lg:py-24`.
- Refreshed all 8 MCP FAQ answers (en + zh-CN) and hydrated the FAQ
section so the accordion is interactive.

## Notes
- Stacked on the original MCP landing-page commits (previously PR
#13095); those ride along here.
- `typecheck` and `build` are green; `/mcp` and `/zh-CN/mcp` both render
in both locales.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Balpreet Brar <balpreet.brar@growthnatives.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-06-29 10:04:24 -07:00
imick-io
d0e97d6933 fix(website): move launches nav item and add cleanplate workflow link (#13282)
## Summary
- Move the `/launches` nav item from **Company → More** to **Products →
Features** in the main navbar
- Add the workflow link to the **Cleanplate Walkthrough** learning
tutorial (`https://comfy.org/workflows/8f2cf0df5da6-8f2cf0df5da6/`)

## Changes
- `apps/website/src/data/mainNavigation.ts`
- `apps/website/src/data/learningTutorials.ts`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:24:10 +00:00
Maanil Verma
3377b8e07e feat(publish): prefill prior metadata and update published workflows in place (#13139)
> **Goal: make updating a published Hub workflow painless in admin
panel.** Today a admin who needs to change a
> published workflow has to reject + reshare + republish, which breaks
the share link, resets stats,
> and blanks the thumbnail — and the rejected backlog can't be cleared.
This is one of 3 PRs that fix it:
>
> | PR | Repo | Fixes |
> |----|------|-------|
> | [#4505](https://github.com/Comfy-Org/cloud/pull/4505) | `cloud` |
Re-publish stops losing data; the editor can read its own published
metadata; reviewer search + admin delete |
> | #13139 | `ComfyUI_frontend` | The publish dialog prefills prior
metadata + thumbnail and updates in place |
> | [#10 ](https://github.com/Comfy-Org/comfy-admin-panels/pull/10)|
`comfy-admin-panels` | Admins can delete rejected/stale workflows |
>
> **Merge order:** `cloud` first (the others depend on its endpoints),
then `ComfyUI_frontend` and
> `comfy-admin-panels` in either order.

## Summary

Editor side of the same effort. Re-opening the publish dialog for an
already-published workflow now
prefills its description, tags, and thumbnail, restores the thumbnail
across step/type changes, keeps
the local workflow name in sync, and labels the action "Update" instead
of "Publish". Depends on the
`cloud` PR — that's what finally returns the `share_id` the dialog reads
from.

## Changes

- **What**:
- **Prefill the thumbnail** — `extractPrefill` dropped `thumbnail_url` /
`thumbnail_comparison_url`,
so only the thumbnail *type* was remembered, never the image. Thread the
URLs into `PublishPrefill`
and restore them; on submit, send the existing URL when no new file is
attached (reuses the
`sampleImageUrls` precedent — an existing URL, not a `File`), so
re-publishing doesn't blank it.
- **Uploads survive navigation** — the thumbnail step kept its `File` in
local component state, so
leaving the step and coming back blanked a fresh upload. The step is now
controlled — files live in
    the form data, the single source of truth that survives the remount.
- **Type-gate the prefilled image** — a restored image must only show on
the tab it belongs to;
`existingThumbnailType` keeps an image off the video tab (which was
hiding the upload prompt) while
    still restoring it when you toggle back.
- **Refetch on rename** — the dialog is a reused singleton, so
`onMounted` fires once; a watch on the
active workflow path refetches prefill when a rename changes it (the
description was going stale).
- **Name sync** — editing the name field published a new Hub display
name but never renamed the local
workflow, so the editor tab (and a reload) kept the old name. Publish
now renames the local file
    when the chosen name differs.
- **"Update" CTA** — the intro panel and footer read "Update" (not
"Publish") when the workflow is
already published, and note that the share link + stats are preserved.

## Review Focus

- `existingThumbnailType` is the load-bearing bit for both the preview
and submit gating — confirm an
image prefill never submits as a video after a type toggle, and that
toggling back restores it.
- Name sync renames *after* a successful publish and is non-fatal on
failure (toast + keep the publish).
The Hub record is keyed by workflow ID, so the rename doesn't orphan it
— worth a sanity check.

## Screenshots



https://github.com/user-attachments/assets/99dd9eff-987f-4ddb-9cf1-e9b40f61e7dc
2026-06-27 05:14:17 +00:00
Christian Byrne
4a2393be48 chore: drop unnecessary exports on file-local types to satisfy knip (#13204)
Current `main` **fails a fresh `knip` run** with 13 unused exported
types (exit 1). They're invisible on main because lint/knip only runs on
`pull_request`/`merge_group`, never on push to main — so merge skew (one
PR adds an export used by file X; a later PR removes X's usage)
accumulates latent failures that ambush backport branches (e.g. #13163,
#13162).

Each of the 13 is `export`ed but referenced only within its own file
(verified 0 importers; ≥2 in-file uses, so not dead code). Fix: drop the
redundant `export`.

Types cleaned: `VideoSource`, `ObjectInfoResponse`,
`PromotedMissingModelWorkflow`, `PixelReadout`, `ResizeDirection`,
`ResizeHandle`, `RunButtonTelemetryOptions`, `ResolvedModelNode`,
`AccountPreconditionContext`, `SubscriptionDialogOptions`,
`MonthlyCreditsUsage`, `MissingMediaReference`, `ResolvedHostWidget`.

Reviewer note: `ResolvedHostWidget` and `ResolvedModelNode` sit under
`renderer/extensions`/`platform/assets`; no in-repo importers, but if
either is intended as published/extension-facing API, prefer a knip
`entry`/`ignore` over un-exporting — flag in review and I'll adjust.

After fresh `knip`: **0 unused exported types**.

Supersedes #13179 (fixed only `AccountPreconditionContext`). Pairs with
the push-gate workflow #13203 — merge this first so that gate is green
on main.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 03:01:59 +00:00
Terry Jia
a451a90868 FE-1150 feat(rightSidePanel): hide 3D viewport widgets from panel (#13206)
## Summary
Add a hideInPanel widget option so a widget still renders on the node
body but is omitted from the right side panel. Apply it to the Three.js
viewport widgets (Load3D, Preview3D, Load3DAdvanced, SaveGLB), whose
non-syncable scene state would diverge if a second instance rendered in
the panel.

App mode and the subgraph editor are unaffected (they filter on
canvasOnly independently).

Discussed with @alexisrolland and @PabloWiedemann 

## Screenshots
before
<img width="2206" height="1181" alt="image"
src="https://github.com/user-attachments/assets/e536871f-65e6-4d6e-aa61-dc981362214f"
/>

after
<img width="2743" height="1295" alt="image"
src="https://github.com/user-attachments/assets/6cc6d252-57ac-464a-a2b7-1ada5ab9e705"
/>
2026-06-26 22:48:47 -04:00
Comfy Org PR Bot
be102899d7 1.47.6 (#13194)
Patch version increment to 1.47.6

**Base branch:** `main`

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-06-27 01:06:26 +00:00
Wei Hai
abd1a6f10a fix: use error colour for runtime execution error node outline (#13184)
## Summary

A node that fails at runtime is now outlined in the same red as a node
that fails validation, instead of magenta.

## Changes

- **What**: The `executionError` stroke in `litegraphService.ts` was
hardcoded to magenta (`#f0f`); validation errors use
`LiteGraph.NODE_ERROR_COLOUR` (red). Reuse that constant so both error
states render consistently. One-line change; `LiteGraph` is already
imported.

## Review Focus

No test added: asserting a hardcoded stroke colour would be a
change-detector test. The two error paths (validation via `has_errors` /
`NODE_ERROR_COLOUR`, runtime via `lastExecutionError`) now share the
same colour source.
2026-06-27 00:00:08 +00:00
AustinMroz
c16f10b49e Long workflow name cleanup (#13180)
When loading a workflow by dragging and dropping an output from the
assets sidebar, the very long and unhelpful url would be used as the
workflow name. This is fixed by instead using the asset display name
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/5c68ae48-1fa6-40e1-b2fb-6188ccd60391"/>
| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/29770c35-da48-4be9-943e-8ee69eb25e6a"
/>|


Additionally, a max width is added to the breadcrumb items to avoid
extremely long names.
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/508155ec-81d7-4ca5-8910-f42a70c9cb4b"/>
| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/d335ceb7-bfeb-481f-a132-c700e017ee0c"
/>|
2026-06-26 23:11:40 +00:00
ShihChi Huang
64253de713 fix: e2e coverage html report asset failures (#13127)
## Summary

Harden E2E coverage HTML generation against non-renderable LCOV source
entries so public assets and stale sourcemap paths no longer abort the
report.

## Changes

- **What**: Removes `assets/images/*` entries from merged E2E LCOV
before upload/report generation.
- **What**: Lets `genhtml` ignore range warnings and synthesize missing
source files when LCOV references stale paths.
- **Dependencies**: None.

## Review Focus

Root cause: Playwright/Monocart can emit LCOV `SF:` records that
`genhtml` cannot read from the checkout. The failed run stopped first on
public assets like `assets/images/hf-logo.svg`; replaying the same
artifact also exposed stale source paths after those assets were
removed.

The filter is intentionally `assets/images/*`, not `assets/*`, because
real `lcov` matching would also remove legitimate source coverage under
`src/platform/assets/...`.

## Validation

- `yamllint --config-file .yamllint
.github/workflows/ci-tests-e2e-coverage.yaml`
- Replayed failed run `28138018468` merged LCOV:
  - `assets/images/*` strip leaves `0` `SF:assets/...` entries
  - preserves `68` `SF:src/platform/assets/...` entries
- `genhtml` exits `0` with `--ignore-errors source,unmapped,range
--synthesize-missing`
- Commit hook: `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`
- Push hook: `knip --cache`

## Screenshots (if applicable)

N/A, CI workflow-only.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Changes are limited to the E2E coverage GitHub Actions workflow; no
application runtime or security paths are touched.
> 
> **Overview**
> Fixes E2E coverage HTML generation failing when merged LCOV references
paths **genhtml** cannot read (public static assets and stale sourcemap
paths from Playwright/Monocart).
> 
> The **Strip non-source entries** step now also drops `assets/images/*`
via `lcov --remove`, scoped narrowly so real source under
`src/platform/assets/...` stays in the report. **Generate HTML coverage
report** passes `--ignore-errors source,unmapped,range` and
`--synthesize-missing` so remaining unmapped or missing sources do not
abort the job.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ede5556644. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: huang47 <157390+huang47@users.noreply.github.com>
2026-06-26 23:05:13 +00:00
Alexander Brown
b4ae6344d7 Brand local node IDs (#13085)
## Summary

Adds a branded local `NodeId` helper and starts separating local node
identity from serialized workflow IDs.

## Changes

- **What**: Adds central `NodeId` parsing/branding helpers, migrates
nearby widget identity types, keeps queue results at the serialized
boundary, and removes misleading workflow `NodeId` usage from execution
error maps.

## Review Focus

Check that the first migration slice keeps serialized/API IDs as raw
`number | string` while local UI/store IDs use the branded string type.

## Caveat

`SUBGRAPH_INPUT_ID` and `SUBGRAPH_OUTPUT_ID` are now branded local
`NodeId` string values internally instead of numeric sentinels.
Reviewers should double-check extension compatibility for callers that
import `Constants` and compare those values numerically.

## Screenshots (if applicable)

N/A

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 22:54:04 +00:00
Christian Byrne
feaa4ce82f refactor: localize system stats headers and fix PyTorch casing (#12253)
## Summary
Localizes the 12 system stats column headers via vue-i18n and fixes the
`Pytorch` → `PyTorch` casing typo.

## Changes
- **src/locales/en/main.json**: Add 11 i18n keys under `g.systemStats*`
namespace
- **src/components/common/SystemStatsPanel.vue**: 
  - Import `useI18n` and use `t()` for column headers
  - Change `header` field to `headerKey` in ColumnDef type
  - Fix PyTorch casing (was `Pytorch Version`)

## Context
Follow-up to PR #11816 per review comment.

## Test plan
- [x] Column headers render correctly
- [x] Copy System Info includes localized headers
- [ ] Verify other locales can override (out of scope - EN only for now)

Closes #11870

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12253-refactor-localize-system-stats-headers-and-fix-PyTorch-casing-3606d73d36508134af99f7ca4f9c6593)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2026-06-26 15:30:28 -07:00
Yosef Chai
f5d059b720 feat(i18n): add Hebrew (he) language support (#12721)
## Summary

Adds full Hebrew (`he` / עברית) localization for the ComfyUI frontend
UI, registered the same additive way as the existing RTL locales (`ar`,
`fa`).

## Changes

- **What**:
- New `src/locales/he/main.json`, `commands.json`, and `settings.json` —
complete, human-reviewed Hebrew translations.
- Exact key parity with `en` (3524 / 125 / 235 keys), verified
programmatically.
- All `{interpolation}` placeholders and `|`-separated plural forms are
preserved (same segment counts as `en`).
- Established technical terms are kept in English (API, GPU, VAE, CLIP,
LoRA, ControlNet, Civitai, Hugging Face, flux, Nodes 2.0, …).
- `dataTypes` and `nodeCategories` are kept as verbatim English
identifiers; option **keys** in
`settings.json`/`menuLabels`/`contextMenu` are left untouched (only
their display values are translated).
- `src/locales/he/nodeDefs.json` is an empty object on purpose, so node
definitions fall back to English and get auto-populated by the release
i18n workflow (per the locale CONTRIBUTING guide).
- Registered `he: { text: 'עברית', loaders: loadersFor('he') }` in
`src/locales/localeConfig.ts` (which automatically adds it to the
Settings → Language dropdown and `SUPPORTED_LOCALE_OPTIONS`).
- Added `he` to `outputLocales` in `.i18nrc.cjs`, plus a Hebrew glossary
block for the CI translator.

## Review Focus

- This follows the approach of #7876 (Persian/Farsi). Like the existing
`ar`/`fa` locales, it translates UI text only and does **not** introduce
RTL layout (`dir="rtl"`) — the app does not currently apply RTL layout
for any locale. I'm happy to follow up with proper RTL layout support in
a separate PR if that's wanted.
- Recurring-term glossary used for consistency: node = צומת, workflow =
תהליך עבודה, queue = תור, widget = פקד, subgraph = תת-גרף, canvas =
קנבס, bypass = עקיפה, prompt = פרומפט.
- Native-speaker review is very welcome. cc translation maintainers
@Yorha4D @KarryCharon @DorotaLuna @shinshin86

## Screenshots

Text-only locale addition — no UI/layout changes. After this change,
**Settings → Language** lists **"עברית"**, and selecting it renders the
UI in Hebrew (untranslated node definitions fall back to English).

---------

Co-authored-by: Yosef Chai <192742853+yosef-chai@users.noreply.github.com>
Co-authored-by: christian-byrne <abolkonsky.rem@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 21:58:18 +00:00
balpreetgrowthnatives
e2c670bbc3 fix: prevent payment routes from being indexed (#12706)
## Summary

Allow crawler access to `/payment/` routes in robots.txt so search
engines can read the `noindex` tag, and forcefully inject the
`x-robots-tag: noindex` header via `vercel.json`.

## Changes

- **What**: Removed `Disallow: /payment/` from `robots.txt` and added
rules to `vercel.json` applying `x-robots-tag: noindex` to
`/payment/(.*)` and `/zh-CN/payment/(.*)` routes.

## Review Focus

- The configurations in `vercel.json` apply to both English and
localized payment routes (`/zh-CN/payment/(.*)`).

---------

Co-authored-by: nav-tej <36310614+nav-tej@users.noreply.github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-06-26 15:11:12 -07:00
Christian Byrne
dfcb336499 fix: give backport push a token that can update workflow files (#13038)
## Summary

The `PR Backport` workflow silently fails for any PR that also modifies
a file under `.github/workflows/**`.

## Root cause

The `backport` job checks out with the default `GITHUB_TOKEN` and reuses
those persisted credentials for `git push`. GitHub refuses to let that
token create or update workflow files:

```
! [remote rejected]  backport-12804-to-core-1.45 -> backport-12804-to-core-1.45
  (refusing to allow a GitHub App to create or update workflow
   `.github/workflows/ci-tests-e2e.yaml` without `workflows` permission)
error: failed to push some refs
```

The cherry-pick itself succeeds — only the push is rejected. And because
the `run:` step inherits `set -e`, the loop aborts before writing the
`failed=` output, so the "Comment on failures" step (`if: failure() &&
steps.backport.outputs.failed`) posts nothing. The result is a red job
with no explanation on the PR.

## History

PR #12804 touched `.github/workflows/ci-tests-e2e.yaml` and
`.github/actions/setup-frontend/action.yaml`. Its backport run
([27788259837](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27788259837/job/82230406910))
failed exactly this way: cherry-pick clean on every target, push
rejected on the workflow file. All four backports (#12966, #12967,
#12968, #12969) had to be created manually.

## Changes

Check out with `PR_GH_TOKEN` (already used by the Create-PR step) so the
push carries `workflow` scope.

> [!IMPORTANT]
> `PR_GH_TOKEN` must have **workflow** write permission for this to take
effect. If it does not, the secret needs that scope added.

## Follow-up (not in this PR)

The push failure aborts the whole job under `set -e` with no PR comment.
Even with the token fixed, a push rejected for another reason (branch
protection, etc.) would still fail silently. Wrapping the push so a
single-target failure is recorded as a `push-failed` reason and reported
via the existing failure-comment step would make the workflow degrade
gracefully.

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-06-26 20:37:02 +00:00
jaeone94
caabebe145 Redesign missing model detection contract for promoted subgraph widgets (#13059)
## Summary

Redesign missing-model detection for ADR 0009 promoted subgraph widgets
so candidates are created from the widget value the user can actually
edit, while still using the concrete interior widget as the
schema/options source.

## Why This PR Exists

This PR comes from the follow-up missing-model detection work for the
ADR 0009 / 1.46 subgraph widget changes introduced by
[#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197).

[#12197](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12197)
intentionally changed promoted subgraph widgets to be represented
through subgraph input links. After that change, the promoted widget on
the host `SubgraphNode` is the editable value owner, and linked interior
widgets are no longer guaranteed to mirror that value.

The old missing-model contract still treated the concrete interior node
widget as the effective source of truth in subgraph cases:

- recursive scans entered the subgraph and scanned the interior widget
value;
- candidates were keyed by the interior node/widget identity;
- the parent subgraph host mostly received propagated
highlight/navigation behavior;
- subgraph container widgets were not treated as first-class candidate
sources.

That contract breaks after ADR 0009. A user can resolve a missing model
by changing the promoted host widget to an installed model, while the
linked interior widget can still hold the old stale value. If detection
keeps scanning the linked interior value, entering the subgraph or
reloading the workflow can re-create a false missing-model error that no
longer corresponds to the value the user can edit.

ADR 0009 also means the same subgraph definition can be reused by
multiple `SubgraphNode` hosts. Once missing-model detection moves from
the interior definition widget to the promoted host widget, the selected
value is no longer a property of the shared definition alone. It is a
property of a specific host instance. That makes the old interior-node
identity insufficient for mode changes, removal handling, and re-scan
behavior: a single interior leaf definition can be reachable through
multiple host execution paths, and only the affected host path should
add, remove, or restore a candidate.

This PR also builds on
[#12990](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12990),
which narrowed workflow-level `models[]` and embedded model data to
metadata enrichment only. Together, the intended boundary is:

- live widgets create missing-model candidates;
- workflow/root-level `models[]` and node metadata only enrich
candidates that already came from a live widget;
- promoted widget values are read from the editable host widget, not
inferred from stale interior `widgets_values`.

## Changes

- **What**:
  - Introduces promoted-widget scan targets that split:
    - host promoted widget value and candidate identity;
    - concrete leaf widget/node definition data.
- Scans the outermost unlinked promoted widget on a `SubgraphNode` host
as the selected value owner.
- Skips linked interior widgets as candidate sources, preventing stale
linked widget values from producing duplicate or false missing-model
candidates.
- Resolves the concrete leaf widget for combo options, asset-widget
support, node type, directory lookup, and embedded metadata enrichment.
- Keys promoted missing-model candidates by the host execution id and
host promoted widget name.
- Adds `sourceExecutionId` to promoted candidates so liveness still
follows the concrete source execution path, including nested inactive
subgraph containers.
- Uses source-scope activity for pipeline filtering and async
verification, while keeping highlight/store/clearing identity
host-keyed.
- Removes host-keyed promoted candidates when their source execution
scope is removed or bypassed.
- Re-scans ancestor subgraph hosts when an interior source path is
un-bypassed, so host-keyed promoted errors can reappear correctly.
- Handles shared subgraph definitions by deriving promoted source paths
from the concrete host instance path, rather than treating the shared
definition node id as globally unique.
- Shares promoted source resolution between Vue node processing and the
right-side panel to avoid drift.
- Aligns missing-model clearing across Vue node widgets, legacy canvas
widgets, and right-side panel Parameters/Nodes section widgets.
- Adds unit coverage for scan identity, source-scope liveness, dynamic
mode changes, source-scope removal, shared-definition host isolation,
and right-side panel clearing.
- Adds nested promoted-widget E2E coverage for OSS and Cloud flows
across Vue, Parameters tab, and legacy widget surfaces.
- **Breaking**: None expected.
- **Dependencies**: None.

## New Detection Contract

A missing-model candidate is created from an unlinked final editable
value owner.

That value owner can be:

- a normal node model widget; or
- the outermost promoted model widget displayed on a `SubgraphNode`
host.

For promoted widgets:

- the host promoted widget supplies the selected value;
- the host execution id and host widget name are the candidate identity;
- the concrete leaf widget supplies definition data such as combo
options and asset-browser support;
- the concrete source execution path is retained as `sourceExecutionId`
for liveness only;
- linked interior widgets are skipped as candidate sources because their
values are not authoritative when driven by a promoted input.

For a nested chain:

`Outer promoted widget A -> inner promoted widget B -> concrete widget
C`

only `A` creates the candidate. `B` and `C` are linked along the
promoted-input path and are skipped as selected-value sources, while `C`
still provides the concrete widget definition used to evaluate `A`.

## Shared Definition And Source-Scope Liveness

ADR 0009 promoted widgets make subgraphs behave more like reusable
definitions with host-owned inputs. Two host `SubgraphNode`s can point
at the same interior subgraph definition while carrying different
promoted widget values. In that shape, the missing-model candidate must
be keyed to the editable host surface, but the activity check cannot use
the host id alone.

For example, if two outer hosts share the same nested subgraph
definition, one host can select a valid model while the other still
selects a missing model. The result should be one missing-model
reference, not a single definition-level error and not two errors after
one host is fixed. Likewise, bypassing or un-bypassing an interior
nested container should affect only the host execution paths that
actually pass through that container.

This PR therefore separates two concepts:

- **candidate identity**: host execution id + host promoted widget name,
used for storage, highlight, navigation, and clearing;
- **candidate liveness**: concrete source execution path, used for
scan-time activity checks, pipeline filtering, async verification,
source-scope removal, and re-exposure after mode changes.

That separation is the reason this PR updates more than the scan itself.
Moving the detection target to the subgraph host also requires the
mode-change and removal paths to understand that a host-keyed candidate
can be invalidated by a descendant source path, and can need to be
restored by re-scanning an ancestor host when an interior source path
becomes active again.

## Review Focus

Please review the identity split carefully:

- candidate/store/highlight/clearing identity should remain host-keyed
for promoted widgets;
- liveness should use `sourceExecutionId` when present, falling back to
`nodeId` for normal candidates;
- scan-time activity checks should account for the source node itself
and all ancestor subgraph containers;
- source-scope removal should remove host-keyed candidates whose
concrete source path was removed or bypassed;
- un-bypassing an interior source path should re-scan affected ancestor
subgraph hosts so host-keyed candidates can reappear;
- shared subgraph definitions should not merge errors across different
host instances;
- linked interior widgets should not produce their own missing-model
candidates;
- asset-browser eligibility should be resolved from the concrete leaf
node type and widget name, not the synthetic subgraph host type;
- right-side panel edits should clear host missing-model errors and
source validation errors consistently.

The E2E matrix intentionally keeps nested promoted workflows only.
Nested promoted widgets cover the same editable host path as single
promoted widgets while also exercising the `A -> B -> C` chain that can
break source-scope liveness and re-scan behavior. The nested fixture
also includes multiple host instances that share the same subgraph
definition, so it verifies that fixing one host does not accidentally
clear or suppress another host's missing-model candidate. Direct/single
promoted behavior is still covered at the unit level.

## Non-Goals

- This PR does not reintroduce workflow-level `models[]` candidate
creation.
- This PR does not infer selected model values from `widgets_values`.
- This PR does not synchronize linked interior widget values back from
promoted host widgets.
- This PR does not redesign missing-media scanning; missing media still
skips subgraph containers and remains keyed by concrete interior paths.
The shared async post-verification active-scope filter is intentionally
stricter, so a pending missing-media candidate is no longer surfaced if
its own node is bypassed or removed while verification is in flight.

## Validation

- `pnpm exec vitest run
src/components/rightSidePanel/parameters/SectionWidgets.test.ts
src/platform/missingModel/missingModelScan.test.ts
src/composables/graph/useErrorClearingHooks.test.ts
src/platform/missingModel/missingModelPipeline.test.ts
src/platform/missingModel/missingModelStore.test.ts
src/utils/graphTraversalUtil.test.ts
src/composables/graph/useGraphNodeManager.test.ts
src/renderer/extensions/vueNodes/composables/useProcessedWidgets.test.ts
--reporter=dot`
  - 8 files passed, 294 tests passed.
- `pnpm exec vitest run
src/platform/missingModel/missingModelScan.test.ts
src/core/graph/subgraph/resolveConcretePromotedWidget.test.ts
src/components/rightSidePanel/parameters/SectionWidgets.test.ts`
  - 3 files passed, 71 tests passed.
- `pnpm typecheck`
- `pnpm typecheck:browser`
- `pnpm format:check`
- targeted ESLint for changed production/unit/E2E files
- `git diff --check`
- `pnpm build`
- `pnpm build:cloud`
- OSS affected E2E on the 8188 build:
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 pnpm
exec playwright test
browser_tests/tests/propertiesPanel/errorsTabModeAware.spec.ts
--project=chromium --grep "Changing an OSS .*promoted|Refreshing a
resolved promoted|Reloading a resolved nested"`
  - 5 passed.
- Cloud affected E2E on the 8188 cloud build:
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 pnpm
exec playwright test
browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts
--project=cloud --grep "Changing a Cloud .*promoted"`
- 2 passed; the Cloud legacy promoted asset-modal case still fails until
[#13075](https://github.com/Comfy-Org/ComfyUI_frontend/pull/13075) is
merged.
- Full OSS `errorsTabModeAware.spec.ts` on the 8188 build:
- 23 passed; 3 existing paste/clipboard cases failed before the promoted
subgraph section with node count remaining at 1 after
`clipboard.paste()`.
- Commit hooks ran `oxfmt`, `oxlint`, `eslint`, `pnpm typecheck`, and
browser typecheck where applicable.
- Pre-push hook ran `pnpm knip --cache`.

## Screenshots

Before


https://github.com/user-attachments/assets/6380c1da-1d92-4b70-888e-3ade572c4b5b

After


https://github.com/user-attachments/assets/4cfc24d6-3dc3-4e36-9b31-72fea6b3d9d5
2026-06-26 19:38:12 +00:00
imick-io
7d3d8ce63f feat(website): add /drops landing page (livestream hero + drops grid) (#13053)
## Summary

Adds the `/drops` landing page in English and Simplified Chinese, ahead
of the 2026-06-24 livestream. The page is composed of a livestream-gated
hero, a 10-card drops grid, a subscribe banner, and a closing CTA.
Production media for the hero and several cards is wired in; the rest
fall back to placeholders.

## Changes

- **New `/drops` page** (`pages/drops.astro`, `pages/zh-CN/drops.astro`)
plus en/zh-CN translations.
- **`HeroLivestream01` block** — renders the rotating-logo video before
the stream window, swaps to a YouTube nocookie iframe between
`startDateTime` and `endDateTime`. SSR stays deterministic on the logo
and only flips to the embed after client hydration. Takes
`youtubeVideoId` directly (no URL parsing).
- **Drops grid** — `data/drops.ts` defines the 10 cards with `imageFor`
/ `videoFor` media helpers, badges, and per-locale
title/description/href. `DropCard.vue` renders an image or autoplaying
muted video inside the new `Card` primitive set (`Card`, `CardHeader`,
`CardTitle`, `CardDescription`, `CardContent`, `CardFooter`).
- **Supporting blocks** — `SubscribeBanner` uses the Button `link`
variant with an animated hover underline; closing `CtaCenter01` is
extended with a terms link. `resolveRel` is shared from `utils/cta.ts`
instead of duplicated per block.
- **Visual extension to `HeroLivestream01.visual`** — discriminated
`image | video` union so the hero can render a looping video.
- **E2E** — `e2e/drops.spec.ts` covers both locales (hero + grid render,
locale-correct links).

## Review Focus

- `youtubeVideoId` is currently a `nlLZfNIqF8M` placeholder;
`startDateTime`/`endDateTime` are placeholders too — see TODO in
`HeroSection.vue`. These need to be swapped to the production stream +
window before launch.
- Several drop cards still point at placeholder destinations (e.g. Comfy
MCP at `/mcp`) — TODO noted in `drops.ts`.
- Drop card media is a mix of images and short MP4s served from
`media.comfy.org`. The videos autoplay muted/looped with
`preload="metadata"`.

## Test plan

- [ ] `pnpm --filter website dev` → visit `/drops` and `/zh-CN/drops`;
confirm hero video plays, grid renders all 10 cards, video cards loop,
subscribe banner link styled correctly
- [ ] Temporarily set `startDateTime` to the past and `endDateTime` to
the future; confirm hero swaps to the YouTube iframe after hydration
- [ ] `pnpm --filter website test:e2e drops.spec.ts`
- [ ] `pnpm typecheck:website`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-26 19:09:04 +00:00
Alexis Rolland
506e33e7dd ci: Update CLA allowlist for GitHub Actions (#13169)
## Summary

- Update CLA check `allowlist` to add `GitHub Action` ,
`action@github.com`, `Glary-Bot`
- Reorder list alphabetically
2026-06-26 11:19:31 -07:00
AustinMroz
7376402fc6 Essentials tab redesign (#12744)
Subsumes #12304 

Redesigns the Essentials tab to be frontend designed with more
accessible icons and tighter organization.
<img width="381" height="1345" alt="image"
src="https://github.com/user-attachments/assets/193f7f5f-20c8-4bf0-8304-ec2c990186d0"
/>

---------

Co-authored-by: comfydesigner <comfydesigner@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-26 17:57:58 +00:00
Alexander Brown
be38f14619 docs: retarget ECS architecture docs to dedicated stores over a single World (#13039)
## Summary

Realign the ECS architecture docs and ADR 0008 with the shipped
direction (PR #12617): entity data lives in dedicated Pinia stores keyed
by string IDs, rather than one unified \"World\" registry addressed by
branded entity IDs.

## Changes

- **What**: Docs-only. Retarget the `docs/architecture/` set + ADR 0008
+ agent guidance from the single-World/branded-`*EntityId` model to the
dedicated-store model that PR #12617 actually shipped
(`widgetValueStore` keyed by `WidgetId`, `layoutStore`,
`nodeOutputStore`, `domWidgetStore`, `subgraphNavigationStore`,
`previewExposureStore`).
- `AGENTS.md` + `.agents/checks/adr-compliance.md`: point entity-data
guidance at dedicated stores; fix the inverted `world.getComponent`
compliance check (it flagged correct store-based code).
- `ADR 0008`: dated amendment note (stays Proposed); rewrite the World
section → dedicated stores, Branded ID design (`WidgetId` composite
string), migration strategy, render-loop, consequences, notes.
- `proto-ecs-stores.md`: flip the \"Unified World / branded IDs\"
framing from gap-to-close → target; replace the deleted
`PromotedWidgetViewManager` with the `input.widgetId` store-backed
model; fix key formats and store count.
- `ecs-target-architecture.md` / `ecs-lifecycle-scenarios.md` /
`ecs-migration-plan.md`: reframe all `world.*` APIs and `*EntityId`
brands to per-store APIs + string keys; mark already-shipped migration
phases done.
- `subgraph-boundaries-and-promotion.md` / `entity-interactions.md` /
`entity-problems.md`: scope-tagged store entries; swap removed
`PromotionStore` for `previewExposureStore`.
- `appendix-critical-analysis.md`: post-pivot status banner + resolution
notes on the critiques the pivot vindicated; still-open gaps (extension
callbacks, atomicity, Y.js↔ECS) left live.
- `appendix-ecs-pattern-survey.md`: supersede banner; keep the external
library survey (§1).
- Delete obsolete `ecs-world-command-api.md` (its command-pattern
argument folded into ADR 0008).
- **Breaking**: None (documentation only).

## Review Focus

- ADR 0008 stays **Proposed** with an amendment note rather than a new
superseding ADR — confirm that's the preferred mechanism vs. a fresh
ADR.
- Numeric per-kind brands (`NodeEntityId`, `LinkEntityId`, …) are
retained in ADR 0008 but explicitly marked aspirational/unshipped; only
`WidgetId` (composite string) reflects shipped code.
- `appendix-ecs-pattern-survey.md` §2–§4 are kept under a supersede
banner as historical record (they describe the deleted `src/world/`
substrate) rather than rewritten — confirm that's preferred over
deletion.
- Net −384 lines; no code or test changes.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:22:36 +00:00
AustinMroz
13b42d9b59 Ensure dynamic combo children cleanup state (#13073)
#12617 introduced a regression in Dynamic Combos. If two options have
child widgets of the same name (such as `bit_depth` on `Save Image
(Advanced)`), then widget state would be incorrectly shared between the
two widgets.

This is resolved by having removed widgets also delete their state.

There was previous interest in having widgets of this type keep state
when valid. This interest remains, but will require a more controlled
intentional implementation in the future.

Since the bit depth options on `Save Image (Advanced)` could potentially
be expanded in the future, this PR specifically adds a new devtools node
for testing with.

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-06-26 01:08:06 +00:00
952 changed files with 55027 additions and 8998 deletions

View File

@@ -33,15 +33,15 @@ Flag:
- **New circular entity dependencies** — New circular imports between `LGraph``Subgraph`, `LGraphNode``LGraphCanvas`, or similar entity classes.
- **Direct `graph._version++`** — Mutating the private version counter directly instead of through a public API. Extensions already depend on this side-channel; it must become a proper API.
### Centralized Registries and ECS-Style Access
### Dedicated Stores and Data/Behavior Separation
All entity data access should move toward centralized query patterns, not instance property access.
Entity data lives in dedicated Pinia stores keyed by string IDs (`widgetValueStore`, `domWidgetStore`, `layoutStore`, `nodeOutputStore`, `subgraphNavigationStore`, `previewExposureStore`), not on entity instances.
Flag:
- **New instance method/property patterns** — Adding `node.someProperty` or `node.someMethod()` for data that should be a component in the World, queried via `world.getComponent(entityId, ComponentType)`.
- **New instance method/property patterns** — Adding `node.someProperty` or `node.someMethod()` for data that belongs in a dedicated store (e.g. widget values → `widgetValueStore` keyed by `WidgetId`).
- **OOP inheritance for entity modeling** — Extending entity classes with new subclasses instead of composing behavior through components and systems.
- **Scattered state** — New entity state stored in multiple locations (class properties, stores, local variables) instead of being consolidated in the World or in a single store.
- **Duplicated authority** — Storing the same entity state in both a class property and a store, or across two stores, so ownership becomes ambiguous. Each piece of state should have one owning store.
### Extension Ecosystem Impact

View File

@@ -1,86 +0,0 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running tsc directly.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(vue-tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running vue-tsc directly.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running tsc via npx.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running tsc via pnpx.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpm exec tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of `pnpm exec tsc`.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx vitest *)",
"command": "echo 'Use `pnpm test:unit` (or `pnpm test:unit <path>`) instead of npx vitest.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx vitest *)",
"command": "echo 'Use `pnpm test:unit` (or `pnpm test:unit <path>`) instead of pnpx vitest.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx eslint *)",
"command": "echo 'Use `pnpm lint` or `pnpm lint:fix` instead of npx eslint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx eslint *)",
"command": "echo 'Use `pnpm lint` or `pnpm lint:fix` instead of pnpx eslint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx prettier *)",
"command": "echo 'This project uses oxfmt, not prettier. Use `pnpm format` or `pnpm format:check`.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx prettier *)",
"command": "echo 'This project uses oxfmt, not prettier. Use `pnpm format` or `pnpm format:check`.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx oxlint *)",
"command": "echo 'Use `pnpm oxlint` instead of npx oxlint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx stylelint *)",
"command": "echo 'Use `pnpm stylelint` instead of npx stylelint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx knip *)",
"command": "echo 'Use `pnpm knip` instead of npx knip.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx knip *)",
"command": "echo 'Use `pnpm knip` instead of pnpx knip.' >&2 && exit 2"
}
]
}
]
}
}

View File

@@ -0,0 +1,197 @@
---
name: Backport Auto-Merge
# Completes the merge of backport PRs once they are approved and their required
# checks pass.
#
# Background: pr-backport.yaml opens each backport PR (labelled `backport`) and
# calls `gh pr merge --auto`, which relies on the repo-level "Allow auto-merge"
# setting. That setting is off, so `--auto` is a silent no-op and backport PRs
# sit unmerged until a human clicks merge. This workflow performs the merge
# directly (a plain `gh pr merge --squash`, which does not depend on that
# setting) once GitHub itself reports the PR as ready to merge.
#
# Safety: branch protection on core/** and cloud/** is the hard gate — it
# unconditionally requires an approval + the required status checks and cannot
# be bypassed, and GitHub's merge API re-enforces it at merge time. This
# workflow can only ever complete a merge that already satisfies those rules;
# the eligibility check below only avoids pointless merge attempts.
#
# The merge uses PR_GH_TOKEN (not the default GITHUB_TOKEN) on purpose: a merge
# performed by the default token does not emit events that trigger other
# workflows, which would silently starve cloud-backport-tag.yaml (it runs on the
# backport PR's `pull_request: closed` event to create the release tag).
on:
# Fires when someone approves — if the required checks are already green, the
# PR merges immediately.
pull_request_review:
types: [submitted]
# Primary catch for the "approved first, checks went green later" case, plus a
# general backstop. A `check_suite`/`workflow_run` trigger would react faster to
# checks completing, but GitHub suppresses `check_suite` events for its own
# Actions suites (so it wouldn't fire for this repo's CI), and `workflow_run` is
# a secrets-bearing "dangerous" trigger we don't want on a public repo for a
# non-latency-critical task. Backports wait hours today, so a short sweep is a
# large improvement and needs neither.
schedule:
- cron: '*/15 * * * *'
# Only constrains the default github.token (used for read-only PR lookups below).
# It does NOT constrain PR_GH_TOKEN, whose authority is fixed by its own scopes.
permissions:
contents: read # read-only; required for gh api / gh pr list to resolve candidates
pull-requests: read # read-only; required for gh pr view eligibility checks
# Serialize runs that act on the same PR (review events keyed by PR number; all
# scheduled sweeps share one key). Cross-key overlaps are still possible but
# harmless: the merge loop treats an already-merged PR as success (idempotent).
concurrency:
group: backport-auto-merge-${{ github.event.pull_request.number || 'sweep' }}
cancel-in-progress: false
jobs:
merge:
name: Merge eligible backport PRs
# Skip review events that can't possibly make a PR mergeable — non-approval
# reviews, or reviews on non-backport PRs (most reviews in the repo) — before
# spending any API call. Schedule sweeps always proceed. The per-PR
# eligibility checks in the job still re-verify the label and decision from
# live state.
if: github.event_name != 'pull_request_review' || (github.event.review.state == 'approved' && contains(github.event.pull_request.labels.*.name, 'backport'))
runs-on: ubuntu-latest
permissions:
contents: read # read-only PR/commit lookups via the default token
pull-requests: read # read-only PR metadata via the default token
steps:
- name: Collect candidate backport PRs
id: candidates
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
EVENT_NAME: ${{ github.event_name }}
PR_FROM_REVIEW: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
numbers=""
case "$EVENT_NAME" in
pull_request_review)
numbers="$PR_FROM_REVIEW"
;;
schedule)
# Sweep every open backport PR.
numbers=$(gh pr list --repo "$GH_REPO" --state open --label backport \
--limit 100 --json number --jq '.[].number')
;;
esac
# De-duplicate and emit space-separated, digit-only tokens.
numbers=$(echo "$numbers" | tr ' ' '\n' | grep -E '^[0-9]+$' | sort -u | tr '\n' ' ' || true)
echo "numbers=${numbers}" >> "$GITHUB_OUTPUT"
echo "Candidate PRs: '${numbers:-<none>}'"
- name: Merge eligible backport PRs
if: steps.candidates.outputs.numbers != ''
env:
GH_REPO: ${{ github.repository }}
# Read with the default token; merge with PR_GH_TOKEN so the merge emits
# the events that downstream workflows (cloud-backport-tag.yaml) rely on.
READ_TOKEN: ${{ github.token }}
MERGE_TOKEN: ${{ secrets.PR_GH_TOKEN }}
CANDIDATES: ${{ steps.candidates.outputs.numbers }}
run: |
set -euo pipefail
is_merged() {
[ "$(GH_TOKEN="$READ_TOKEN" gh pr view "$1" --repo "$GH_REPO" --json merged --jq '.merged' 2>/dev/null || echo false)" = "true" ]
}
for pr in $CANDIDATES; do
echo "::group::PR #${pr}"
info=$(GH_TOKEN="$READ_TOKEN" gh pr view "$pr" --repo "$GH_REPO" \
--json number,state,isDraft,labels,baseRefName,reviewDecision,mergeStateStatus 2>/dev/null || echo '')
if [ -z "$info" ]; then
echo "Could not read PR #${pr} — skipping."; echo "::endgroup::"; continue
fi
state=$(echo "$info" | jq -r '.state')
is_draft=$(echo "$info" | jq -r '.isDraft')
is_backport=$(echo "$info" | jq -r '[.labels[].name] | any(. == "backport")')
base=$(echo "$info" | jq -r '.baseRefName')
review=$(echo "$info" | jq -r '.reviewDecision')
merge_state=$(echo "$info" | jq -r '.mergeStateStatus')
# Only ever act on open, non-draft, backport-labelled PRs targeting a
# protected release branch.
if [ "$state" != "OPEN" ] || [ "$is_draft" != "false" ] || [ "$is_backport" != "true" ]; then
echo "Not an actionable backport PR (state=$state draft=$is_draft backport=$is_backport) — skipping."
echo "::endgroup::"; continue
fi
case "$base" in
cloud/*|core/*) : ;;
*) echo "Base '$base' is not a release branch — skipping."; echo "::endgroup::"; continue ;;
esac
# Ready = approved AND GitHub says it's mergeable with required checks green.
# CLEAN = approved, all required checks green, mergeable, no conflict.
# UNSTABLE = same, but a NON-required check is pending/failing — GitHub
# still allows the merge, so we do too (matches what a human
# clicking "Squash and merge" can do; required checks are the
# only merge gate per the ruleset). Requiring CLEAN alone would
# stick forever behind flaky/slow non-required checks.
# Any other state (BLOCKED/DIRTY/BEHIND/UNKNOWN/...) => not ready; re-checked
# by a later event or the next sweep.
if [ "$review" != "APPROVED" ] || { [ "$merge_state" != "CLEAN" ] && [ "$merge_state" != "UNSTABLE" ]; }; then
echo "Not yet ready (reviewDecision=$review mergeStateStatus=$merge_state) — will re-check later."
echo "::endgroup::"; continue
fi
echo "PR #${pr} is ready — attempting squash merge."
attempt=0
max=3
merged=false
while [ "$attempt" -lt "$max" ]; do
attempt=$((attempt + 1))
# A concurrent run (or a human) may have merged it already.
if is_merged "$pr"; then merged=true; break; fi
if out=$(GH_TOKEN="$MERGE_TOKEN" gh pr merge "$pr" --repo "$GH_REPO" --squash 2>&1); then
merged=true; break
fi
echo "Merge attempt ${attempt}/${max} failed: ${out}"
# No sleep after the final attempt.
[ "$attempt" -lt "$max" ] && sleep $((attempt * 15))
done
# Final reconciliation: a failed merge command may just mean a concurrent
# run won the race — don't post a false failure if the PR is in fact merged.
if [ "$merged" != "true" ] && is_merged "$pr"; then merged=true; fi
if [ "$merged" = "true" ]; then
echo "PR #${pr} merged."
else
echo "::warning::PR #${pr} looked ready but did not merge after ${max} attempts."
# Avoid spamming a persistently-stuck PR: only re-warn if the last
# warning (identified by its marker) is more than an hour old.
marker='<!-- backport-auto-merge:merge-failed -->'
# `gh api --paginate` emits one JSON array per page; `--jq` would run
# per page (missing the true latest across pages), so slurp all pages
# into one array first and filter with a separate jq pass.
last_warned=$(GH_TOKEN="$READ_TOKEN" gh api "repos/${GH_REPO}/issues/${pr}/comments" --paginate 2>/dev/null \
| jq -s "[.[][] | select(.body | contains(\"${marker}\"))] | sort_by(.created_at) | last | .created_at // empty") || last_warned=''
stale=true
if [ -n "$last_warned" ]; then
last_epoch=$(date -d "$last_warned" +%s 2>/dev/null || echo 0)
now_epoch=$(date -u +%s)
[ $((now_epoch - last_epoch)) -lt 3600 ] && stale=false
fi
if [ "$stale" = "true" ]; then
body=$(printf '%s\n\n%s' \
"This backport PR is approved and its required checks are green, but automatic merge failed after ${max} attempts. Please merge manually or investigate (possible branch-protection mismatch)." \
"$marker")
GH_TOKEN="$MERGE_TOKEN" gh pr comment "$pr" --repo "$GH_REPO" --body "$body" || true
else
echo "Already warned within the last hour — skipping duplicate comment."
fi
fi
echo "::endgroup::"
done

View File

@@ -134,6 +134,27 @@ jobs:
fi
echo '✅ No Customer.io references found'
- name: Scan dist for Syft telemetry references
run: |
set -euo pipefail
echo '🔍 Scanning for Syft references...'
if rg --no-ignore -n \
-g '*.html' \
-g '*.js' \
-e '(?i)syft' \
-e '(?i)sy-d\.io' \
dist; then
echo '❌ ERROR: Syft references found in dist assets!'
echo 'Syft must be properly tree-shaken from OSS builds.'
echo ''
echo 'To fix this:'
echo '1. Use the TelemetryProvider pattern (see src/platform/telemetry/)'
echo '2. Call telemetry via useTelemetry() hook'
echo '3. Use conditional dynamic imports behind isCloud checks'
exit 1
fi
echo '✅ No Syft references found'
- name: Scan dist for Cloudflare Turnstile sitekey references
run: |
set -euo pipefail

View File

@@ -88,9 +88,9 @@ jobs:
- name: Strip non-source entries from coverage
if: steps.coverage-shards.outputs.has-coverage == 'true'
run: |
# Drop served bundle scripts (localhost-8188/assets/*.js) that V8 records but have no source file on disk, which would abort genhtml.
lcov --remove coverage/playwright/coverage.lcov \
'*localhost-8188*' \
'assets/images/*' \
-o coverage/playwright/coverage.lcov \
--ignore-errors unused
wc -l coverage/playwright/coverage.lcov
@@ -121,7 +121,8 @@ jobs:
--title "ComfyUI E2E Coverage" \
--no-function-coverage \
--precision 1 \
--ignore-errors source,unmapped
--ignore-errors source,unmapped \
--synthesize-missing
- name: Upload HTML report artifact
if: steps.coverage-shards.outputs.has-coverage == 'true'

View File

@@ -95,6 +95,7 @@ jobs:
if: |
github.event_name == 'workflow_dispatch'
|| (github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.fork == false
&& startsWith(github.head_ref, 'version-bump-')
&& (needs.changes.outputs.storybook-changes == 'true'
|| needs.changes.outputs.app-frontend-changes == 'true'

View File

@@ -30,7 +30,7 @@ concurrency:
jobs:
deploy-preview:
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
runs-on: ubuntu-latest
permissions:
contents: read

View File

@@ -67,7 +67,15 @@ jobs:
- name: Deploy report to Cloudflare
id: deploy
if: always() && !cancelled()
if: >-
${{
always() &&
!cancelled() &&
(
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.fork == false
)
}}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

View File

@@ -6,6 +6,7 @@ on:
pull_request_target:
types: [opened, synchronize, closed]
merge_group:
types: [checks_requested]
permissions:
actions: write
@@ -17,13 +18,45 @@ jobs:
cla-assistant:
runs-on: ubuntu-latest
steps:
- name: CLA already verified before merge queue
if: github.event_name == 'merge_group'
run: echo "CLA is checked on the pull request before it enters merge queue."
# The CLA action normally requires every commit author in a PR to sign.
# We only want the PR author to sign, so we allowlist all other committers
# by computing them from the PR's commits and excluding the PR author.
- name: Build author-only allowlist
id: allowlist
if: >
github.event_name == 'pull_request_target' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && (
github.event.comment.body == 'recheck' ||
github.event.comment.body == 'I have read and agree to the Contributor License Agreement'
))
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
PR_AUTHOR: ${{ github.event.pull_request.user.login || github.event.issue.user.login }}
BASE_ALLOWLIST: action@github.com,actions-user,ampagent,claude,comfy-pr-bot,GitHub Action,github-actions,github-actions[bot],Glary Bot,Glary-Bot,*[bot]
run: |
others=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}/commits" --paginate \
--jq '.[] | (.author.login // empty), (.committer.login // empty)' \
| sort -u | grep -vix "${PR_AUTHOR}" | paste -sd, -)
if [ -n "$others" ]; then
echo "allowlist=${BASE_ALLOWLIST},${others}" >> "$GITHUB_OUTPUT"
else
echo "allowlist=${BASE_ALLOWLIST}" >> "$GITHUB_OUTPUT"
fi
- name: CLA Assistant
# Run on PR events, on "recheck" comment, or when someone posts the exact signing phrase.
# IMPORTANT: this phrase must match `custom-pr-sign-comment` below.
if: >
github.event_name == 'pull_request_target' ||
github.event.comment.body == 'recheck' ||
github.event.comment.body == 'I have read and agree to the Contributor License Agreement'
(github.event_name == 'issue_comment' && github.event.issue.pull_request && (
github.event.comment.body == 'recheck' ||
github.event.comment.body == 'I have read and agree to the Contributor License Agreement'
))
uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -39,9 +72,10 @@ jobs:
path-to-signatures: signatures/cla.json
branch: main
# Allowlist bots so they don't need to sign (optional, comma-separated).
# Only the PR author must sign: bots plus every non-author committer
# are allowlisted via the "Build author-only allowlist" step above.
# *[bot] is a catch-all for any GitHub App bot account.
allowlist: actions-user,ampagent,claude,comfy-pr-bot,github-actions,*[bot],Glary Bot
allowlist: ${{ steps.allowlist.outputs.allowlist }}
# Custom PR comment messages
custom-notsigned-prcomment: |

View File

@@ -32,12 +32,13 @@ jobs:
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
(github.event_name != 'pull_request' ||
(github.event.action == 'labeled' &&
contains(fromJSON('["preview","preview-cpu","preview-gpu"]'), github.event.label.name)) ||
(github.event.action == 'synchronize' &&
(contains(github.event.pull_request.labels.*.name, 'preview') ||
contains(github.event.pull_request.labels.*.name, 'preview-cpu') ||
contains(github.event.pull_request.labels.*.name, 'preview-gpu'))))
(github.event.pull_request.head.repo.fork == false &&
((github.event.action == 'labeled' &&
contains(fromJSON('["preview","preview-cpu","preview-gpu"]'), github.event.label.name)) ||
(github.event.action == 'synchronize' &&
(contains(github.event.pull_request.labels.*.name, 'preview') ||
contains(github.event.pull_request.labels.*.name, 'preview-cpu') ||
contains(github.event.pull_request.labels.*.name, 'preview-gpu'))))))
runs-on: ubuntu-latest
steps:
- name: Build client payload

View File

@@ -21,6 +21,7 @@ jobs:
# - Preview label specifically removed
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.event.pull_request.head.repo.fork == false &&
((github.event.action == 'closed' &&
(contains(github.event.pull_request.labels.*.name, 'preview') ||
contains(github.event.pull_request.labels.*.name, 'preview-cpu') ||

View File

@@ -67,6 +67,11 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
# Persist a token with `workflow` scope so the backport push can
# include changes to .github/workflows/**. The default GITHUB_TOKEN
# is refused by GitHub when a push creates/updates workflow files,
# which silently aborted the whole job (see PR #12804 backport).
token: ${{ secrets.PR_GH_TOKEN }}
- name: Configure git
run: |

View File

@@ -29,7 +29,7 @@ jobs:
# SHA-pinned per zizmor `unpinned-uses: hash-pin`. Bump this SHA to pick up
# upstream changes; keep `workflows_ref` matching so prompts/scripts load
# from the same commit as the workflow definition.
uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@047ca48febe3a6647608ed2e0c4331b491cb9d6a # github-workflows#9
uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@df507e6bae179c567ad3849370f99dae588985dc # github-workflows main (df507e6)
with:
# Overriding diff_excludes replaces the reusable default wholesale, so
# this restates the generated/vendored defaults and adds this repo's heavy
@@ -48,7 +48,7 @@ jobs:
:!**/*-snapshots/**
:!src/workbench/extensions/manager/types/generatedManagerTypes.ts
# Load the prompts/scripts from the same ref as `uses:`.
workflows_ref: 047ca48febe3a6647608ed2e0c4331b491cb9d6a
workflows_ref: df507e6bae179c567ad3849370f99dae588985dc
secrets:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
# Optional — enables start/complete Slack DMs to the triggerer.

View File

@@ -21,7 +21,8 @@ module.exports = defineConfig({
'ar',
'tr',
'pt-BR',
'fa'
'fa',
'he'
],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face.
'latent' is the short form of 'latent space'.
@@ -37,5 +38,11 @@ module.exports = defineConfig({
- Keep commonly used technical terms in English when they are standard in Persian software (e.g., node, workflow).
- Use Arabic-Indic numerals (۰-۹) for numbers where appropriate.
- Maintain consistency with terminology used in Persian software and design applications.
IMPORTANT Hebrew Translation Guidelines:
- For 'he' locale: Use modern, formal Hebrew (עברית תקנית) for a professional tone throughout the UI.
- Hebrew is a right-to-left (RTL) language. Keep all interpolation placeholders ({name}, {count}), pipe-separated plural forms, and English technical terms intact and in their original positions.
- Preferred glossary: node = צומת (plural צמתים), workflow = תהליך עבודה, queue = תור, canvas = קנבס, widget = פקד, subgraph = תת-גרף, prompt = פרומפט/הנחיה (per context), bypass = עקיפה, mute = השתקה.
- Keep widely-recognized technical terms in English (Latin script): API, GPU, CUDA, VAE, CLIP, LoRA, ControlNet, Civitai, Hugging Face, Nodes 2.0, etc.
`
})

View File

@@ -9,6 +9,7 @@
"packages/registry-types/src/comfyRegistryTypes.ts",
"public/materialdesignicons.min.css",
"src/types/generatedManagerTypes.ts",
"**/__fixtures__/**/*.json"
"**/__fixtures__/**/*.json",
"apps/website/src/content/**/*.mdx"
]
}

View File

@@ -179,6 +179,9 @@ This project uses **pnpm**. Always prefer scripts defined in `package.json` (e.g
23. Favor pure functions (especially testable ones)
24. Do not use function expressions if it's possible to use function declarations instead
25. Watch out for [Code Smells](https://wiki.c2.com/?CodeSmell) and refactor to avoid them
26. Do not add alias helpers whose implementation is just a single-line call to another function
- Bad: `function id(value) { return nodeId(value) }`
- Use the real function directly, or introduce a named helper only when it adds validation, branching, domain meaning, or shared behavior beyond renaming
## Design Standards
@@ -246,7 +249,7 @@ All architectural decisions are documented in `docs/adr/`. Code changes must be
### Entity Architecture Constraints (ADR 0003 + ADR 0008)
1. **Command pattern for all mutations**: Every entity state change must be a serializable, idempotent, deterministic command — replayable, undoable, and transmittable over CRDT. No imperative fire-and-forget mutation APIs. Systems produce command batches, not direct side effects.
2. **Centralized registries and ECS-style access**: Entity data lives in the World (centralized registry), queried via `world.getComponent(entityId, ComponentType)`. Do not add new instance properties/methods to entity classes. Do not use OOP inheritance for entity modeling.
2. **Dedicated stores over instance state**: Entity data lives in dedicated Pinia stores keyed by string IDs — widget values in `widgetValueStore` keyed by `WidgetId` (`graphId:nodeId:name`, see `src/types/widgetId.ts`), plus `domWidgetStore`, `layoutStore`, `nodeOutputStore`, `subgraphNavigationStore`, and `previewExposureStore`. Prefer a focused store to a single unified registry. Do not add new instance properties/methods to entity classes for data that belongs in a store. Do not use OOP inheritance for entity modeling.
3. **No god-object growth**: Do not add methods to `LGraphNode`, `LGraphCanvas`, `LGraph`, or `Subgraph`. Extract to systems, stores, or composables.
4. **Plain data components**: ECS components are plain data objects — no methods, no back-references to parent entities. Behavior belongs in systems (pure functions).
5. **Extension ecosystem impact**: Changes to entity callbacks (`onConnectionsChange`, `onRemoved`, `onAdded`, `onConnectInput/Output`, `onConfigure`, `onWidgetChanged`), `node.widgets` access, `node.serialize`, or `graph._version++` affect 40+ custom node repos and require migration guidance.

View File

@@ -1,4 +1,5 @@
import { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx'
import sitemap from '@astrojs/sitemap'
import vue from '@astrojs/vue'
import tailwindcss from '@tailwindcss/vite'
@@ -24,6 +25,9 @@ export default defineConfig({
site: 'https://comfy.org',
output: 'static',
prefetch: { prefetchAll: true },
// Keep MDX punctuation verbatim; SmartyPants would turn the source's straight
// quotes into curly ones and drift from the rest of the site's copy.
markdown: { smartypants: false },
redirects: {
'/cloud/enterprise-case-studies/comfyui-at-architectural-scale-how-moment-factory-reimagined-3d-projection-mapping':
'/customers/moment-factory/',
@@ -37,6 +41,7 @@ export default defineConfig({
devToolbar: { enabled: !process.env.NO_TOOLBAR },
integrations: [
vue(),
mdx(),
sitemap({
filter: (page) => !isExcludedFromSitemap(page)
})

View File

@@ -40,7 +40,7 @@ test.describe('Cloud page @smoke', () => {
}
})
test('AIModelsSection heading and 5 model cards are visible', async ({
test('AIModelsSection heading and 6 model cards are visible', async ({
page
}) => {
const heading = page.getByRole('heading', { name: /leading AI models/i })
@@ -49,7 +49,7 @@ test.describe('Cloud page @smoke', () => {
const section = heading.locator('xpath=ancestor::section')
const grid = section.locator('.grid')
const modelCards = grid.locator('a[href="https://comfy.org/workflows"]')
await expect(modelCards).toHaveCount(5)
await expect(modelCards).toHaveCount(6)
})
test('AIModelsSection CTA links to workflows', async ({ page }) => {

View File

@@ -0,0 +1,73 @@
import { expect } from '@playwright/test'
import { test } from './fixtures/blockExternalMedia'
test.describe('Customer story detail @smoke', () => {
test('renders the migrated article: hero, section nav, and body', async ({
page
}) => {
await page.goto('/customers/series-entertainment')
await expect(
page.getByRole('heading', {
level: 1,
name: /Series Entertainment Rebuilt Game and Video Production/i
})
).toBeVisible()
const nav = page.getByRole('navigation', { name: 'Category filter' })
await expect(nav.getByRole('button', { name: 'INTRO' })).toBeVisible()
await expect(nav.getByRole('button', { name: 'CONCLUSION' })).toBeVisible()
// Section title rendered from the MDX <Section title> wrapper.
await expect(
page.getByRole('heading', {
name: 'The Output Series Achieved Using ComfyUI'
})
).toBeVisible()
})
test('section nav highlights the section the reader selects', async ({
page
}) => {
await page.goto('/customers/series-entertainment')
const nav = page.getByRole('navigation', { name: 'Category filter' })
const intro = nav.getByRole('button', { name: 'INTRO' })
const problem = nav.getByRole('button', { name: 'THE PROBLEM' })
await expect(intro).toHaveAttribute('aria-pressed', 'true')
await problem.click()
await expect(problem).toHaveAttribute('aria-pressed', 'true')
})
test('shows the read-more link only when an external source exists', async ({
page
}) => {
await page.goto('/customers/open-story-movement')
await expect(
page.getByRole('link', { name: /read more on this topic/i })
).toBeVisible()
// series-entertainment only redirected back to itself, so the link is gone.
await page.goto('/customers/series-entertainment')
await expect(
page.getByRole('link', { name: /read more on this topic/i })
).toHaveCount(0)
})
test('links to the next story in the what-is-next section', async ({
page
}) => {
await page.goto('/customers/series-entertainment')
const nextLink = page.getByRole('link', { name: /view article/i })
await expect(nextLink).toBeVisible()
// Links to another customer story, without coupling the test to the
// specific slug or sort order.
await expect(nextLink).toHaveAttribute('href', /^\/customers\/[a-z0-9-]+$/)
await expect(nextLink).not.toHaveAttribute(
'href',
'/customers/series-entertainment'
)
})
})

View File

@@ -0,0 +1,231 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { externalLinks } from '../src/config/routes'
import { drops } from '../src/data/drops'
import type { Locale } from '../src/i18n/translations'
import { t } from '../src/i18n/translations'
import { test } from './fixtures/blockExternalMedia'
const PATH_EN = '/launches'
const PATH_ZH = '/zh-CN/launches'
const CLOUD_URL = 'https://cloud.comfy.org'
const LOCALES: ReadonlyArray<readonly [string, Locale]> = [
[PATH_EN, 'en'],
[PATH_ZH, 'zh-CN']
]
function heroSection(page: Page, locale: Locale) {
return page.locator('section').filter({
has: page.getByRole('heading', {
level: 1,
name: t('launches.hero.title', locale)
})
})
}
function ctaSection(page: Page, locale: Locale) {
return page.locator('section').filter({
has: page.getByRole('heading', {
level: 2,
name: t('launches.cta.heading', locale)
})
})
}
function dropsSection(page: Page, locale: Locale) {
return page.locator('section').filter({
has: page.getByRole('heading', {
level: 2,
name: t('launches.section.title', locale)
})
})
}
test.describe('Launches landing — desktop @smoke', () => {
test('renders the configured title at /launches', async ({ page }) => {
await page.goto(PATH_EN)
await expect(page).toHaveTitle(t('launches.page.title', 'en'))
})
test('renders the localized title at /zh-CN/launches', async ({ page }) => {
await page.goto(PATH_ZH)
await expect(page).toHaveTitle(t('launches.page.title', 'zh-CN'))
})
test('is indexable at both locales', async ({ page }) => {
await page.goto(PATH_EN)
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
await page.goto(PATH_ZH)
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
})
test('hero h1 renders the localized title in both locales', async ({
page
}) => {
await page.goto(PATH_EN)
await expect(
page.getByRole('heading', {
level: 1,
name: t('launches.hero.title', 'en')
})
).toBeVisible()
await page.goto(PATH_ZH)
await expect(
page.getByRole('heading', {
level: 1,
name: t('launches.hero.title', 'zh-CN')
})
).toBeVisible()
})
test('hero primary CTA links to /download per locale', async ({ page }) => {
for (const [path, locale, expectedHref] of [
[PATH_EN, 'en', '/download'],
[PATH_ZH, 'zh-CN', '/zh-CN/download']
] as const) {
await page.goto(path)
const primary = heroSection(page, locale).getByRole('link', {
name: t('launches.hero.primary', locale)
})
await expect(primary).toBeVisible()
await expect(primary).toHaveAttribute('href', expectedHref)
}
})
test('hero secondary CTA opens external Cloud in a new tab on both locales', async ({
page
}) => {
for (const [path, locale] of LOCALES) {
await page.goto(path)
const secondary = heroSection(page, locale).getByRole('link', {
name: t('launches.hero.secondary', locale)
})
await expect(secondary).toBeVisible()
await expect(secondary).toHaveAttribute('href', CLOUD_URL)
await expect(secondary).toHaveAttribute('target', '_blank')
await expect(secondary).toHaveAttribute('rel', 'noopener noreferrer')
}
})
test('closing CTA shows heading and both action buttons in both locales', async ({
page
}) => {
for (const [path, locale] of LOCALES) {
await page.goto(path)
const section = ctaSection(page, locale)
await expect(
section.getByRole('heading', {
level: 2,
name: t('launches.cta.heading', locale)
})
).toBeVisible()
const primary = section.getByRole('link', {
name: t('launches.cta.primary', locale)
})
await expect(primary).toBeVisible()
await expect(primary).toHaveAttribute('href', externalLinks.cloud)
await expect(primary).toHaveAttribute('target', '_blank')
await expect(primary).toHaveAttribute('rel', 'noopener noreferrer')
const secondary = section.getByRole('link', {
name: t('launches.cta.secondary', locale)
})
await expect(secondary).toBeVisible()
await expect(secondary).toHaveAttribute('href', externalLinks.workflows)
await expect(secondary).toHaveAttribute('target', '_blank')
await expect(secondary).toHaveAttribute('rel', 'noopener noreferrer')
}
})
test('drops section renders one card per data entry with the correct localized href in both locales', async ({
page
}) => {
for (const [path, locale] of LOCALES) {
await page.goto(path)
const section = dropsSection(page, locale)
await expect(
section.getByRole('heading', {
level: 2,
name: t('launches.section.title', locale)
})
).toBeVisible()
const cards = section.locator('[data-slot="card"]')
await expect(cards).toHaveCount(drops.length)
for (const [i, drop] of drops.entries()) {
const card = cards.nth(i)
await expect(card).toContainText(drop.title[locale])
const explore = card.getByRole('link', {
name: drop.cta.label[locale]
})
await expect(explore).toBeVisible()
await expect(explore).toHaveAttribute('href', drop.cta.href[locale])
}
}
})
test('desktop: first 4 drop cards are wider than cards 5+', async ({
page
}) => {
await page.goto(PATH_EN)
const cards = dropsSection(page, 'en').locator('[data-slot="card"]')
await expect(cards).toHaveCount(drops.length)
await expect
.poll(async () => {
const firstWidth = (await cards.nth(0).boundingBox())?.width ?? 0
const fifthWidth = (await cards.nth(4).boundingBox())?.width ?? 0
return firstWidth - fifthWidth
})
.toBeGreaterThan(0)
})
})
test.describe('Launches landing — mobile @mobile', () => {
test('drops grid stacks in a single column at mobile width', async ({
page
}) => {
await page.goto(PATH_EN)
const cards = dropsSection(page, 'en').locator('[data-slot="card"]')
await expect(cards).toHaveCount(drops.length)
const viewport = page.viewportSize()
expect(viewport, 'viewport size').not.toBeNull()
await expect
.poll(async () => (await cards.nth(0).boundingBox())?.width ?? 0)
.toBeGreaterThanOrEqual(viewport!.width * 0.7)
await expect
.poll(async () => {
const firstBox = await cards.nth(0).boundingBox()
const secondBox = await cards.nth(1).boundingBox()
if (!firstBox || !secondBox) return false
return secondBox.y >= firstBox.y + firstBox.height
})
.toBe(true)
})
test('closing CTA heading stays within viewport width', async ({ page }) => {
await page.goto(PATH_EN)
const heading = page.getByRole('heading', {
level: 2,
name: t('launches.cta.heading', 'en')
})
await heading.scrollIntoViewIfNeeded()
await expect(heading).toBeVisible()
const box = await heading.boundingBox()
expect(box, 'CTA heading bounding box').not.toBeNull()
const viewport = page.viewportSize()
expect(viewport, 'viewport size').not.toBeNull()
expect(box!.x + box!.width).toBeLessThanOrEqual(viewport!.width + 1)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -40,6 +40,7 @@
},
"devDependencies": {
"@astrojs/check": "catalog:",
"@astrojs/mdx": "catalog:",
"@astrojs/vue": "catalog:",
"@playwright/test": "catalog:",
"@tailwindcss/vite": "catalog:",
@@ -48,6 +49,7 @@
"tsx": "catalog:",
"tw-animate-css": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
"vitest": "catalog:",
"vue-component-type-helpers": "catalog:"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 938 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,10 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1483_15836)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M196.373 184.704V136.491C196.373 132.437 197.909 129.387 201.451 127.36L298.368 71.552C311.573 63.936 327.296 60.3947 343.531 60.3947C404.416 60.3947 442.987 107.584 442.987 157.803C442.987 161.365 442.987 165.419 442.475 169.472L341.995 110.613C339.266 108.876 336.099 107.954 332.864 107.954C329.629 107.954 326.462 108.876 323.733 110.613L196.373 184.704ZM422.699 372.437V257.28C422.699 250.176 419.648 245.12 413.547 241.557L286.187 167.467L327.787 143.616C329.294 142.624 331.059 142.095 332.864 142.095C334.669 142.095 336.434 142.624 337.941 143.616L434.859 199.445C462.784 215.659 481.557 250.176 481.557 283.669C481.557 322.24 458.731 357.76 422.677 372.48L422.699 372.437ZM166.443 270.997L124.843 246.635C121.28 244.608 119.744 241.557 119.744 237.504V125.845C119.744 71.552 161.344 30.4427 217.685 30.4427C239.019 30.4427 258.795 37.5467 275.541 50.24L175.573 108.096C169.493 111.637 166.443 116.715 166.443 123.819V270.976V270.997ZM256 322.731L196.373 289.237V218.197L256 184.704L315.627 218.197V289.237L256 322.731ZM294.315 476.971C272.981 476.971 253.205 469.888 236.459 457.195L336.427 399.339C342.507 395.797 345.557 390.72 345.557 383.616V236.459L387.669 260.821C391.232 262.848 392.747 265.899 392.747 269.952V381.589C392.747 435.883 350.635 476.971 294.315 476.971ZM174.059 363.84L77.12 308.011C49.216 291.776 30.4427 257.28 30.4427 223.787C30.3769 204.756 35.9917 186.138 46.5684 170.317C57.1451 154.495 72.2025 142.19 89.8133 134.976V250.667C89.8133 257.771 92.864 262.848 98.944 266.411L225.813 339.989L184.213 363.84C182.707 364.835 180.941 365.365 179.136 365.365C177.331 365.365 175.565 364.835 174.059 363.84ZM168.469 447.04C111.125 447.04 69.0133 403.925 69.0133 350.635C69.0133 346.581 69.5253 342.528 70.016 338.475L169.984 396.288C176.085 399.851 182.165 399.851 188.245 396.288L315.605 322.731V370.944C315.605 374.997 314.112 378.048 310.549 380.075L213.632 435.883C200.427 443.499 184.704 447.04 168.469 447.04ZM294.315 507.413C323.553 507.416 351.895 497.319 374.547 478.831C397.198 460.343 412.768 434.598 418.624 405.952C475.456 391.232 512 337.92 512 283.648C512 248.128 496.789 213.632 469.376 188.757C471.915 178.091 473.429 167.445 473.429 156.8C473.429 84.2453 414.571 29.9307 346.581 29.9307C332.885 29.9307 319.701 31.9573 306.475 36.544C282.795 13.2354 250.933 0.118797 217.707 1.37049e-07C188.465 -0.00135846 160.121 10.0985 137.469 28.5908C114.817 47.0831 99.2486 72.8325 93.3973 101.483C36.544 116.203 0 169.493 0 223.787C0 259.328 15.2107 293.824 42.624 318.677C40.0853 329.344 38.5707 340.011 38.5707 350.656C38.5707 423.211 97.4293 477.504 165.419 477.504C179.115 477.504 192.299 475.477 205.525 470.912C229.208 494.23 261.08 507.347 294.315 507.456V507.413Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1483_15836">
<rect width="512" height="512" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -29,6 +29,5 @@ Allow: /
Disallow: /_astro/
Disallow: /_website/
Disallow: /_vercel/
Disallow: /payment/
Sitemap: https://comfy.org/sitemap-index.xml

View File

@@ -1,10 +1,14 @@
<script setup lang="ts">
import BrandButton from '../common/BrandButton.vue'
import type { AnchorHTMLAttributes } from 'vue'
import Button from '../ui/button/Button.vue'
import { resolveRel } from '../../utils/cta'
type Cta = {
label: string
href: string
target?: '_blank' | '_self' | '_parent' | '_top'
target?: AnchorHTMLAttributes['target']
rel?: AnchorHTMLAttributes['rel']
}
type TermsLink = {
@@ -12,10 +16,11 @@ type TermsLink = {
href: string
}
defineProps<{
const { heading, primaryCta, secondaryCta, termsLink } = defineProps<{
heading: string
primaryCta: Cta
termsLink: TermsLink
secondaryCta?: Cta
termsLink?: TermsLink
}>()
</script>
@@ -24,23 +29,37 @@ defineProps<{
class="max-w-9xl mx-auto flex flex-col items-center px-6 py-16 text-center lg:py-24"
>
<h2
class="text-4xl font-light tracking-tight text-primary-comfy-canvas lg:text-6xl"
class="max-w-3xl text-4xl/snug font-light tracking-tight text-pretty text-primary-comfy-canvas lg:text-6xl/snug"
>
{{ heading }}
</h2>
<BrandButton
:href="primaryCta.href"
:target="primaryCta.target"
:rel="primaryCta.target === '_blank' ? 'noopener noreferrer' : undefined"
variant="outline"
size="lg"
class="mt-10 px-20 py-4 text-base uppercase lg:mt-12"
>
{{ primaryCta.label }}
</BrandButton>
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
<Button
as="a"
:href="primaryCta.href"
:target="primaryCta.target"
:rel="resolveRel(primaryCta)"
variant="default"
size="lg"
>
{{ primaryCta.label }}
</Button>
<Button
v-if="secondaryCta"
as="a"
:href="secondaryCta.href"
:target="secondaryCta.target"
:rel="resolveRel(secondaryCta)"
variant="outline"
size="lg"
>
{{ secondaryCta.label }}
</Button>
</div>
<a
v-if="termsLink"
:href="termsLink.href"
class="mt-8 text-sm text-primary-comfy-canvas/70 underline underline-offset-4 transition-colors hover:text-primary-comfy-canvas"
>

View File

@@ -26,7 +26,7 @@ function toggle(index: number) {
</script>
<template>
<section class="max-w-9xl mx-auto px-4 py-24 md:px-20 md:py-40">
<section class="max-w-9xl mx-auto px-6 py-16 lg:py-24">
<div class="flex flex-col gap-6 md:flex-row md:gap-16">
<!-- Left heading -->
<div

View File

@@ -0,0 +1,120 @@
<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import type { Component } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import CopyableField from '@/components/ui/copyable-field/CopyableField.vue'
import SectionHeader from '../common/SectionHeader.vue'
type CardAction =
| {
type: 'link'
label: string
href: string
target?: '_blank'
icon?: Component
variant?: 'default' | 'outline'
}
| { type: 'code'; value: string }
export interface FeatureCard {
id: string
label?: string
title: string
description: string
action?: CardAction
}
type ColumnCount = 2 | 3 | 4
const {
cards,
columns = 3,
copiedLabel,
copyLabel,
eyebrow,
heading,
subtitle
} = defineProps<{
cards: readonly FeatureCard[]
columns?: ColumnCount
copiedLabel?: string
copyLabel?: string
eyebrow?: string
heading: string
subtitle?: string
}>()
const columnClass: Record<ColumnCount, string> = {
2: 'lg:grid-cols-2',
3: 'lg:grid-cols-3',
4: 'lg:grid-cols-4'
}
</script>
<template>
<section class="max-w-9xl mx-auto px-6 py-16 lg:py-24">
<SectionHeader max-width="xl" :label="eyebrow" align="start">
{{ heading }}
<template v-if="subtitle" #subtitle>
<p class="mt-4 max-w-xl text-sm text-smoke-700 lg:text-base">
{{ subtitle }}
</p>
</template>
</SectionHeader>
<div :class="cn('mt-16 grid grid-cols-1 gap-6', columnClass[columns])">
<div
v-for="card in cards"
:key="card.id"
class="bg-transparency-white-t4 flex flex-col rounded-3xl p-6 lg:p-8"
>
<p
v-if="card.label"
class="text-primary-comfy-yellow text-xs font-bold tracking-widest uppercase"
>
{{ card.label }}
</p>
<h3
:class="
cn(
'text-xl font-light text-primary-comfy-canvas lg:text-2xl',
card.label && 'mt-3'
)
"
>
{{ card.title }}
</h3>
<p class="mt-3 text-sm text-smoke-700">
{{ card.description }}
</p>
<div v-if="card.action" class="mt-6">
<Button
v-if="card.action.type === 'link'"
as="a"
:href="card.action.href"
:target="card.action.target"
:rel="
card.action.target === '_blank'
? 'noopener noreferrer'
: undefined
"
:variant="card.action.variant ?? 'outline'"
:append-icon="card.action.icon"
>
{{ card.action.label }}
</Button>
<CopyableField
v-else
:value="card.action.value"
:copy-label="copyLabel"
:copied-label="copiedLabel"
/>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue'
import SectionHeader from '../common/SectionHeader.vue'
import NodeUnionIcon from '../icons/NodeUnionIcon.vue'
type Cta = { label: string; href: string; target?: '_blank' }
export interface FeatureStep {
id: string
number: string
title: string
description: string
}
defineProps<{
heading: string
steps: readonly FeatureStep[]
primaryCta?: Cta
secondaryCta?: Cta
}>()
</script>
<template>
<section class="max-w-9xl mx-auto px-6 py-16 lg:py-24">
<SectionHeader>{{ heading }}</SectionHeader>
<!-- Step cards in a row, joined by node-union connectors on desktop -->
<div
class="mt-12 flex flex-col gap-4 lg:flex-row lg:items-stretch lg:gap-0"
>
<template v-for="(step, i) in steps" :key="step.id">
<div
v-if="i > 0"
class="relative z-10 -mx-px hidden shrink-0 items-center justify-center self-stretch lg:flex"
aria-hidden="true"
>
<NodeUnionIcon
class="text-primary-comfy-yellow size-4 scale-x-150 rotate-90"
/>
</div>
<div
class="border-primary-comfy-yellow flex flex-1 flex-col rounded-[40px] border-2 bg-primary-comfy-ink p-2"
>
<div class="flex flex-1 flex-col gap-4 p-8">
<div>
<p
class="text-primary-comfy-yellow text-xs font-bold tracking-widest uppercase"
>
{{ step.number }}
</p>
<h3
class="mt-1 text-2xl font-medium tracking-widest text-primary-comfy-canvas uppercase"
>
{{ step.title }}
</h3>
</div>
<p class="text-primary-comfy-canvas">
{{ step.description }}
</p>
</div>
</div>
</template>
</div>
<div
v-if="primaryCta || secondaryCta"
class="mt-12 flex flex-col items-center gap-4 lg:flex-row lg:justify-center"
>
<Button
v-if="primaryCta"
as="a"
:href="primaryCta.href"
:target="primaryCta.target"
:rel="
primaryCta.target === '_blank' ? 'noopener noreferrer' : undefined
"
size="lg"
class="w-full lg:w-auto lg:min-w-48"
>
{{ primaryCta.label }}
</Button>
<Button
v-if="secondaryCta"
as="a"
:href="secondaryCta.href"
:target="secondaryCta.target"
:rel="
secondaryCta.target === '_blank' ? 'noopener noreferrer' : undefined
"
variant="outline"
size="lg"
class="w-full lg:w-auto lg:min-w-48"
>
{{ secondaryCta.label }}
</Button>
</div>
</section>
</template>

View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import type { Locale } from '../../i18n/translations'
import GlassCard from '../common/GlassCard.vue'
import SectionHeader from '../common/SectionHeader.vue'
import VideoPlayer from '../common/VideoPlayer.vue'
import type { VideoTrack } from '../common/VideoPlayer.vue'
type RowMedia =
| { type: 'image'; src: string; alt?: string }
| {
type: 'video'
src: string
// <video> has no native alt; used as the player's accessible label.
alt?: string
poster?: string
tracks?: readonly VideoTrack[]
autoplay?: boolean
loop?: boolean
minimal?: boolean
hideControls?: boolean
}
export interface FeatureRow {
id: string
title: string
description: string
media: RowMedia
}
const {
heading,
eyebrow,
locale = 'en',
rows
} = defineProps<{
heading: string
eyebrow?: string
locale?: Locale
rows: readonly FeatureRow[]
}>()
</script>
<template>
<section class="max-w-9xl mx-auto px-6 py-16 lg:py-24">
<SectionHeader :label="eyebrow" max-width="xl">
{{ heading }}
</SectionHeader>
<div class="mt-16 flex flex-col gap-4 lg:gap-6">
<GlassCard
v-for="(row, i) in rows"
:key="row.id"
class="flex flex-col gap-8 lg:flex-row lg:items-stretch lg:gap-0"
>
<!-- Text -->
<div
:class="
cn(
'order-2 flex flex-col justify-center gap-4 p-6 lg:w-1/2 lg:p-12',
i % 2 === 0 ? 'lg:order-1' : 'lg:order-2'
)
"
>
<h3 class="text-2xl font-light text-primary-comfy-canvas lg:text-3xl">
{{ row.title }}
</h3>
<p class="text-sm text-smoke-700 lg:text-base">
{{ row.description }}
</p>
</div>
<!-- Media: image or video -->
<div
:class="
cn(
'order-1 flex lg:w-1/2',
i % 2 === 0 ? 'lg:order-2' : 'lg:order-1'
)
"
>
<img
v-if="row.media.type === 'image'"
:src="row.media.src"
:alt="row.media.alt ?? row.title"
loading="lazy"
decoding="async"
class="aspect-4/3 w-full rounded-4xl object-cover"
/>
<VideoPlayer
v-else
:locale="locale"
:aria-label="row.media.alt ?? row.title"
:src="row.media.src"
:poster="row.media.poster"
:tracks="row.media.tracks"
:autoplay="row.media.autoplay"
:loop="row.media.loop"
:minimal="row.media.minimal"
:hide-controls="row.media.hideControls"
class="w-full"
/>
</div>
</GlassCard>
</div>
</section>
</template>

View File

@@ -0,0 +1,166 @@
<script setup lang="ts">
import type { AnchorHTMLAttributes } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useNow } from '@vueuse/core'
import Button from '../ui/button/Button.vue'
import { resolveRel } from '../../utils/cta'
type Cta = {
label: string
href: string
target?: AnchorHTMLAttributes['target']
rel?: AnchorHTMLAttributes['rel']
}
type Visual =
| {
type: 'image'
src: string
alt: string
width?: number
height?: number
}
| {
type: 'video'
src: string
alt: string
poster?: string
width?: number
height?: number
}
const {
visual,
eyebrow,
title,
subtitle,
primaryCta,
secondaryCta,
youtubeVideoId,
startDateTime,
endDateTime
} = defineProps<{
visual?: Visual
eyebrow?: string
title: string
subtitle?: string
primaryCta: Cta
secondaryCta?: Cta
youtubeVideoId: string
startDateTime: string
endDateTime: string
}>()
const embedUrl = computed(
() =>
`https://www.youtube-nocookie.com/embed/${youtubeVideoId}?autoplay=1&mute=1&rel=0`
)
// Keep SSR/initial paint deterministic on the logo and only flip to the embed
// after client hydration — avoids a build-time `now` leaking into the markup.
const mounted = ref(false)
onMounted(() => {
mounted.value = true
})
const now = useNow({ interval: 30_000 })
const startMs = computed(() => new Date(startDateTime).getTime())
const endMs = computed(() => new Date(endDateTime).getTime())
const isLive = computed(
() =>
mounted.value &&
now.value.getTime() >= startMs.value &&
now.value.getTime() < endMs.value
)
</script>
<template>
<section
class="max-w-9xl mx-auto flex flex-col items-center px-6 py-16 text-center lg:py-24"
>
<div
v-if="isLive"
class="mb-10 aspect-video w-full overflow-hidden rounded-2xl lg:mb-12"
>
<iframe
:src="embedUrl"
:title="title"
class="size-full"
loading="lazy"
allow="autoplay; encrypted-media; picture-in-picture"
allowfullscreen
/>
</div>
<slot v-else name="visual">
<img
v-if="visual?.type === 'image'"
:src="visual.src"
:alt="visual.alt"
:width="visual.width"
:height="visual.height"
fetchpriority="high"
decoding="async"
class="mb-10 h-auto w-full max-w-md lg:mb-12 lg:max-w-lg"
/>
<video
v-else-if="visual?.type === 'video'"
:src="visual.src"
:poster="visual.poster"
:aria-label="visual.alt"
:width="visual.width"
:height="visual.height"
autoplay
loop
muted
playsinline
preload="metadata"
class="mb-10 h-auto w-full max-w-md lg:mb-12 lg:max-w-2xl"
/>
</slot>
<p
v-if="eyebrow"
class="mb-4 text-sm font-medium tracking-wide text-primary-comfy-canvas/70 uppercase"
>
{{ eyebrow }}
</p>
<h1
class="max-w-3xl text-4xl/snug font-light tracking-tight text-pretty text-primary-comfy-canvas lg:text-6xl/snug"
>
{{ title }}
</h1>
<p
v-if="subtitle"
class="mt-6 max-w-2xl text-base text-primary-comfy-canvas/70 lg:text-lg"
>
{{ subtitle }}
</p>
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
<Button
as="a"
:href="primaryCta.href"
:target="primaryCta.target"
:rel="resolveRel(primaryCta)"
size="lg"
>
{{ primaryCta.label }}
</Button>
<Button
v-if="secondaryCta"
as="a"
:href="secondaryCta.href"
:target="secondaryCta.target"
:rel="resolveRel(secondaryCta)"
variant="outline"
size="lg"
>
{{ secondaryCta.label }}
</Button>
</div>
</section>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import type { HTMLAttributes } from 'vue'
import type { Locale } from '../../i18n/translations'
import BrandButton from '../common/BrandButton.vue'
import ProductHeroBadge from '../common/ProductHeroBadge.vue'
@@ -27,6 +29,7 @@ const {
badgeLogoAlt,
title,
titleHighlight,
subtitle,
features = [],
primaryCta,
secondaryCta,
@@ -41,14 +44,17 @@ const {
videoAutoplay = false,
videoLoop = false,
videoMinimal = false,
videoHideControls = false
videoHideControls = false,
class: className
} = defineProps<{
locale?: Locale
class?: HTMLAttributes['class']
badgeText: string
badgeLogoSrc?: string
badgeLogoAlt?: string
title: string
titleHighlight?: string
subtitle?: string
features?: string[]
primaryCta: Cta
secondaryCta?: Cta
@@ -72,7 +78,8 @@ const {
:class="
cn(
'max-w-9xl relative mx-auto flex flex-col items-center gap-12 px-6 pt-20 pb-16 md:pt-28 md:pb-24 lg:items-center lg:gap-16 lg:px-16',
imagePosition === 'right' ? 'lg:flex-row' : 'lg:flex-row-reverse'
imagePosition === 'right' ? 'lg:flex-row' : 'lg:flex-row-reverse',
className
)
"
>
@@ -84,7 +91,7 @@ const {
/>
<h1
class="mt-8 text-2xl leading-[125%] font-light tracking-[-1.44px] text-primary-comfy-canvas md:text-4xl lg:text-5xl"
class="mt-8 text-2xl leading-[125%] font-light tracking-[-1.44px] whitespace-pre-line text-primary-comfy-canvas md:text-4xl lg:text-5xl"
>
<template v-if="titleHighlight">
<span class="text-primary-warm-white">{{ titleHighlight }}</span>
@@ -93,6 +100,13 @@ const {
<template v-else>{{ title }}</template>
</h1>
<p
v-if="subtitle"
class="mt-6 max-w-xl text-base text-primary-comfy-canvas/80"
>
{{ subtitle }}
</p>
<ul v-if="features.length" class="mt-8 space-y-3">
<li
v-for="feature in features"
@@ -127,27 +141,29 @@ const {
</div>
<div class="order-first w-full lg:order-last lg:flex-1">
<VideoPlayer
v-if="videoSrc"
:locale
:src="videoSrc"
:poster="videoPoster"
:tracks="videoTracks"
:autoplay="videoAutoplay"
:loop="videoLoop"
:minimal="videoMinimal"
:hide-controls="videoHideControls"
/>
<img
v-else-if="imageSrc"
:src="imageSrc"
:alt="imageAlt"
:width="imageWidth"
:height="imageHeight"
fetchpriority="high"
decoding="async"
class="aspect-4/3 w-full rounded-3xl object-cover"
/>
<slot name="media">
<VideoPlayer
v-if="videoSrc"
:locale
:src="videoSrc"
:poster="videoPoster"
:tracks="videoTracks"
:autoplay="videoAutoplay"
:loop="videoLoop"
:minimal="videoMinimal"
:hide-controls="videoHideControls"
/>
<img
v-else-if="imageSrc"
:src="imageSrc"
:alt="imageAlt"
:width="imageWidth"
:height="imageHeight"
fetchpriority="high"
decoding="async"
class="aspect-4/3 w-full rounded-3xl object-cover"
/>
</slot>
</div>
</section>
</template>

Some files were not shown because too many files have changed in this diff Show More