Compare commits

..

24 Commits

Author SHA1 Message Date
pythongosssss
a6c3ff1a54 fix: load3d used wrong i18n key, add test (#11546)
## Summary

Toast for Load3D initialization failure was using the wrong key and so
showed an untranslated key to the user.

## Changes

- **What**: 
- Update to use correct existing key
- Add test that forces init failure

## Screenshots (if applicable)
Fixed
<img width="482" height="121" alt="image"
src="https://github.com/user-attachments/assets/f89eef99-c1a6-463a-a711-7e9c16d0e89a"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11546-fix-load3d-used-wrong-i18n-key-add-test-34a6d73d36508159aab9f042d3e9c4f0)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-23 11:52:42 -04:00
Dante
9599a4e00a fix cloud frontend runtime guard regressions (#11180)
## Summary
- harden cloud frontend runtime paths that were throwing on
`cloud.comfy.org`
- guard widget value propagation when the source widget is missing
- treat nullish executed outputs as empty output during flatten/parsing
- ignore stale autogrow disconnect callbacks after an autogrow group is
removed

## Root cause
This PR bundles three small runtime guard fixes from
`cloud-frontend-staging` issues that reproduce on
`https://cloud.comfy.org/`:

- `CLOUD-FRONTEND-STAGING-429`: widget propagation assumed
`this.widgets[0]` always existed and crashed during group-node/widget
lifecycle transitions
- `CLOUD-FRONTEND-STAGING-3QA` and sibling `3QB`: executed-event parsing
assumed `detail.output` was always an object and crashed on nullish
output payloads
- `CLOUD-FRONTEND-STAGING-42B`: `autogrowInputDisconnected()` could run
from a stale `requestAnimationFrame()` callback after its autogrow group
had already been removed

## User impact
- prevents unhandled frontend exceptions on `cloud.comfy.org`
- keeps node output rendering and linear-mode flattening resilient to
sparse executed payloads
- avoids autogrow disconnect crashes during graph/widget churn

## Changes
- extracted shared widget propagation logic into
`widgetValuePropagation.ts`
- added source-widget guards in custom widget / primitive widget
propagation paths
- added null guards in result parsing and linear-mode output flattening
- added an autogrow-group existence guard in `dynamicWidgets.ts`
- added focused regression tests for all three bug shapes

## Red / Green Verification
### Red
I ran the new targeted regression suite in a temporary pre-fix worktree
with the runtime guards reverted while keeping the new tests.

Failing tests in that state:
- `src/extensions/core/widgetValuePropagation.test.ts`
  - `returns early when the source widget is missing`
- `src/stores/resultItemParsing.test.ts`
  - `returns empty array for nullish node output`
  - `ignores nullish node outputs`
- `src/renderer/extensions/linearMode/flattenNodeOutput.test.ts`
  - `returns empty array for nullish node output`

Representative pre-fix errors:
- `TypeError: Cannot read properties of undefined (reading 'value')`
- `TypeError: Cannot convert undefined or null to object`

### Green
On the draft PR branch, the targeted regression suite passes:
- `pnpm exec vitest run src/core/graph/widgets/dynamicWidgets.test.ts
src/stores/resultItemParsing.test.ts
src/renderer/extensions/linearMode/flattenNodeOutput.test.ts
src/extensions/core/widgetValuePropagation.test.ts
src/extensions/core/customWidgets.test.ts`

Result:
- `5` test files passed
- `49` tests passed

## Validation
- `pnpm exec vitest run src/core/graph/widgets/dynamicWidgets.test.ts
src/stores/resultItemParsing.test.ts
src/renderer/extensions/linearMode/flattenNodeOutput.test.ts
src/extensions/core/widgetValuePropagation.test.ts
src/extensions/core/customWidgets.test.ts`
- `pnpm exec eslint --no-ignore src/core/graph/widgets/dynamicWidgets.ts
src/core/graph/widgets/dynamicWidgets.test.ts
src/stores/resultItemParsing.ts src/stores/resultItemParsing.test.ts
src/renderer/extensions/linearMode/flattenNodeOutput.ts
src/renderer/extensions/linearMode/flattenNodeOutput.test.ts
src/extensions/core/customWidgets.ts src/extensions/core/widgetInputs.ts
src/extensions/core/widgetValuePropagation.ts
src/extensions/core/widgetValuePropagation.test.ts`
- `pnpm exec oxfmt --check src/core/graph/widgets/dynamicWidgets.ts
src/core/graph/widgets/dynamicWidgets.test.ts
src/stores/resultItemParsing.ts src/stores/resultItemParsing.test.ts
src/renderer/extensions/linearMode/flattenNodeOutput.ts
src/renderer/extensions/linearMode/flattenNodeOutput.test.ts
src/extensions/core/customWidgets.ts src/extensions/core/widgetInputs.ts
src/extensions/core/widgetValuePropagation.ts
src/extensions/core/widgetValuePropagation.test.ts`

## Notes
- I explicitly skipped the `getCanvas: canvas is null` issue because it
is already covered by open PRs `#11173` / `#11174`.
- `pnpm typecheck` was not included in validation because the temporary
PR worktree used for publication hits local path-resolution issues
through the shared dependency install, which is unrelated to the changes
in this PR.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11180-codex-fix-cloud-frontend-runtime-guard-regressions-3416d73d365081e0af6ec612c9d0d8aa)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-23 12:02:46 +00:00
pythongosssss
cc73baaf57 test: Test subgraph breadcrumbs (#11472)
## Summary

Add test coverage to subgraph breadcrumbs

## Changes

- **What**: 
- Add subgraph breadcrumb helpers
- Add tests for entering/navigating/menu/collapsing

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11472-test-Test-subgraph-breadcrumbs-3486d73d365081e1a75bf57404eaa63b)
by [Unito](https://www.unito.io)
2026-04-23 01:49:29 -07:00
Dante
700fcb6bda test: add unit tests for FormDropdownMenuActions (#11443)
## Summary

Adds 14 unit tests for \`FormDropdownMenuActions\` — isolated into its
own PR because the component is denser (three PrimeVue Popovers,
multiple filter models) than the sibling components in the form-dropdown
PR. Part of a widget-test-coverage sequence.

## Changes

- **What**: \`FormDropdownMenuActions.test.ts\` — search v-model, sort
popover options + selection, ownership popover visibility gated by
\`showOwnershipFilter\` + options present, base-model multi-select
toggle (add/remove/multiple), Clear Filters, list/grid layout-mode
v-model.

## Review Focus

- PrimeVue \`Popover\` stubbed as an always-slotted \`<div>\` with
\`toggle\`/\`hide\` methods on the Options-API \`methods\` (stub
\`expose\` did not satisfy template-ref access).
- Sort/ownership/base-model option discovery uses
\`within(popover-body)\` to disambiguate buttons across the three
popovers.
- Layout-switch locator uses an \`.icon-*\` class probe since the button
has no accessible name; covered by an \`eslint-disable-next-line
testing-library/no-node-access\`.
- No changes to the component source.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11443-test-add-unit-tests-for-FormDropdownMenuActions-3486d73d365081ed80dcfdf5d83655e1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-23 08:00:54 +00:00
Dante
30c07dc9ec fix: cancel-subscription dialog renders Invalid Date for ISO fractional seconds (#11539)
## Summary

`CancelSubscriptionDialogContent` calls `new Date(dateStr)` directly on
both `cancelAt` and `subscription.value?.endDate`. Strict ISO 8601
parsers (Safari and some WebViews) reject fractional seconds whose
length is anything other than 3 digits, so a Go-style backend
timestamp such as `2026-04-18T10:04:55.6513Z` rendered as
`Your access continues until Invalid Date.` in a destructive billing
flow.

PR #11358 already added the tolerant `parseIsoDateSafe` helper and
applied it to the Secrets panel. This PR closes the same gap in the
cancellation dialog and adds regression coverage that exercises the
strict-parser code path (V8 alone is too lenient to fail without it).

## Changes

- `CancelSubscriptionDialogContent.vue` — pipe both date sources through
  `parseIsoDateSafe`; collapse the two-step null check into one. When
  the value is missing OR unparseable, fall back to the existing
  `subscription.cancelDialog.endOfBillingPeriod` translation instead of
  emitting `Invalid Date`.
- `CancelSubscriptionDialogContent.test.ts` (new) — wraps the assertions
  in the same `withStrictMillisecondParser` shim used by
  `dateTimeUtil.test.ts`, so 1-, 4-, and 9-digit fractional inputs
  actually exercise the broken path. Also covers the missing/unparseable
  fallbacks and the `cancelAt`-takes-precedence ordering.

## Red-green proof (local)

Confirmed locally before splitting the commits:

| Commit | `pnpm exec vitest run
…CancelSubscriptionDialogContent.test.ts` |
|---|---|
| `c8aecd07f test: regression cover …` (test-only) | 5 failed / 1 passed
— DOM rendered `"Your access continues until Invalid Date."` |
| `f24cb903f fix: parse cancel-subscription dialog ISO timestamps …` | 6
passed |

CI on this branch only runs on PR HEAD; happy to force-push a transient
red commit if you want a recorded red CI run alongside the green one.

## Test plan

- [x] `pnpm exec vitest run
src/components/dialog/content/subscription/CancelSubscriptionDialogContent.test.ts`
(6 passing on green)
- [x] `pnpm typecheck`
- [x] `pnpm exec eslint
src/components/dialog/content/subscription/CancelSubscriptionDialogContent.{vue,test.ts}`
- [x] `pnpm exec oxfmt --check` on changed files
- [ ] Manual repro on Safari with a Go-emitted cancel timestamp (out of
scope here; the unit test asserts the equivalent strict-parser behavior)

## Origin

Surfaced by `/codex:adversarial-review` as the gap left after PR #11358
(Secrets panel) — the same `new Date(...)` hazard survived in a
destructive billing flow with no regression coverage.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11539-fix-cancel-subscription-dialog-renders-Invalid-Date-for-ISO-fractional-seconds-34a6d73d365081e4bdd6c941afd8cef3)
by [Unito](https://www.unito.io)
2026-04-23 03:30:31 +00:00
Comfy Org PR Bot
d81f3a9278 1.44.8 (#11514)
Patch version increment to 1.44.8

**Base branch:** `main`

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-04-22 20:28:18 -07:00
Dante
bab2870131 test: harden strict parser coverage for long ISO fractions (#11529)
## Summary

- run every `parseIsoDateSafe` case with more than 3 fractional-second
digits through `withStrictMillisecondParser`
- assert the normalized 3-digit timestamp string passed into `Date` for
each long-fraction variant
- keep the follow-up scoped to test coverage only

## Root cause

V8 already accepts and truncates ISO timestamps with more than 3
fractional-second digits, so the existing tests could stay green even if
`parseIsoDateSafe` failed to normalize those values before constructing
`Date`. Wrapping the long-fraction cases in the strict parser shim makes
CI exercise the Safari/WebView-sensitive path the feature is meant to
protect.

## Testing

- `pnpm exec vitest run src/utils/dateTimeUtil.test.ts`
- `pnpm exec eslint src/utils/dateTimeUtil.test.ts`

Fixes #11528

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11529-codex-test-harden-strict-parser-coverage-for-long-ISO-fractions-34a6d73d365081119577eb5fb6d4992c)
by [Unito](https://www.unito.io)
2026-04-23 03:05:03 +00:00
Christian Byrne
e5cb244a2d test: add E2E tests for keyboard shortcut actions (#11210)
## Summary

Add Playwright E2E tests covering core keyboard shortcut actions.

## Changes

- **What**: 7 new E2E tests in
`browser_tests/tests/keyboardShortcutActions.spec.ts` covering:
  - Ctrl+Z undoes the last graph change
  - Ctrl+Shift+Z redoes after undo
  - Ctrl+S opens save dialog
  - Ctrl+, opens settings dialog
  - Escape closes settings dialog
  - Delete key removes selected nodes
  - Ctrl+A selects all nodes

## Review Focus

- Tests use `expect.poll()` for async graph state assertions
- `@keyboard` tag for selective test runs
- Uses `comfyPage.nodeOps` and `comfyPage.menu.topbar` helpers

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11210-test-add-E2E-tests-for-keyboard-shortcut-actions-3416d73d3650812eafd5e789cf8463a6)
by [Unito](https://www.unito.io)
2026-04-23 02:43:57 +00:00
Christian Byrne
c5b6fd9c40 test: clarify inert getClientFeatureFlags mock in progress_text binary parsing tests (#11385)
## Summary

Adds inline comments to three
`vi.mocked(api.getClientFeatureFlags).mockReturnValue()` calls in the
`progress_text binary message parsing` describe block, clarifying they
are intentionally inert — the parser checks `serverFeatureFlags` only.

This prevents future readers from being confused about whether the mock
has any effect.

- Fixes #11186

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11385-test-clarify-inert-getClientFeatureFlags-mock-in-progress_text-binary-parsing-tests-3476d73d365081a98c06c43c4737fdd9)
by [Unito](https://www.unito.io)
2026-04-23 02:43:17 +00:00
Christian Byrne
0f0210e482 test: add unit tests for workspaceApi (#11393)
## Summary
Adds 27 unit tests for `src/platform/workspace/api/workspaceApi.ts`,
increasing line coverage from 2.9% to 83.2%.

## Test Coverage
- Authentication: auth header null checks (workspace auth + firebase
auth)
- Error handling: axios error wrapping, fallback messages, non-axios
rethrow
- Workspace CRUD: list, create, update, delete, leave
- Member management: listMembers, removeMember
- Invite management: listInvites, createInvite, revokeInvite,
acceptInvite
- Billing: getBillingStatus, getBillingBalance, getBillingPlans
- Subscription: previewSubscribe, subscribe, cancelSubscription,
resubscribe
- Payment: getPaymentPortalUrl, createTopup
- Billing events: getBillingEvents, getBillingOpStatus

## Testing
```bash
pnpm vitest run src/platform/workspace/api/workspaceApi.test.ts
# 27 tests pass
```

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11393-test-add-unit-tests-for-workspaceApi-3476d73d36508192b8f6d1af6aa543f4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-23 02:43:10 +00:00
Christian Byrne
7d1d7c8315 fix: remove deleted workflow from search results in sidebar (#11425)
*PR Created by the Glary-Bot Agent*

---

## Summary

Deleting a workflow in the sidebar while search is active left the
deleted workflow visible in the search results. Interacting with the
stale entry (e.g. duplicate) caused undefined behavior.

## Root Cause

`filteredWorkflows` in `BaseWorkflowsSidebarTab.vue` was a `ref`
populated once per search event — a static snapshot that never reacted
to store mutations. When `workflowStore.deleteWorkflow()` removed a
workflow from `workflowLookup`, the search-panel's `filteredWorkflows`
still held a stale reference.

## Fix

Convert `filteredWorkflows` from a `ref` to a `computed` that reactively
derives from `searchQuery` and `workflowStore.workflows`. This follows
the same pattern already used by `filteredPersistedWorkflows` and
`filteredBookmarkedWorkflows` in the same component. `handleSearch` is
simplified to only manage tree expansion (its only remaining side
effect).

## Test Plan

Three e2e regression tests added to `workflowSearch.spec.ts`:
- **Delete during search removes from results** — deletes a workflow
while search is active, asserts it disappears
- **Delete during search preserves siblings** — asserts all other
matched workflows remain visible after one is deleted
- **Clear search after delete shows correct browse view** — deletes
during search, clears search, verifies browse view is consistent

## Screenshots

![Search active with 3 workflows
visible](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/3a9cc4daaaf582a87638fc62ec96258cea63981a28bcb282dd625956286c582c/pr-images/1776596412591-c8cc80fb-e57e-4c76-b574-8ab776076105.png)

![After deleting test-alpha during search — removed from results
correctly](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/3a9cc4daaaf582a87638fc62ec96258cea63981a28bcb282dd625956286c582c/pr-images/1776596412913-23eebf62-aac8-4848-8468-8ecb56c0dc8f.png)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11425-fix-remove-deleted-workflow-from-search-results-in-sidebar-3476d73d365081f19ef6c6b9261a1ee9)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
2026-04-23 02:42:21 +00:00
Christian Byrne
739d4b6136 fix: move template distribution filter from v-show to data pipeline (#11418)
*PR Created by the Glary-Bot Agent*

---

## Summary

- Moves distribution-based template filtering from a CSS-level `v-show`
gate into the `useTemplateFiltering` composable's data pipeline,
guaranteeing that templates not meant for the current distribution never
reach the view layer
- Fixes "Showing 19 of 419" count mismatch when only 2 templates are
visible on Cloud with "Wan 2.2" filter active
- Derives `availableModels` and `availableUseCases` from
distribution-visible templates so filter dropdowns don't show options
that only exist on other distributions
- Always prunes `activeModels`/`activeUseCases` against available
options to prevent stale persisted selections from causing zero-result
filtering

## Root Cause

The template selector dialog used
`v-show="isTemplateVisibleOnDistribution(template)"` to hide templates
that don't match the current distribution (cloud/desktop/local). But
`filteredCount` and `totalCount` were computed upstream in the pipeline
before this visual filter, so the count text showed all matching
templates regardless of distribution visibility.

## Changes

- **`useTemplateFiltering.ts`**: Added `visibleTemplates` computed that
applies distribution filter at the top of the pipeline. All downstream
computeds (`fuse`, `availableModels`, `availableUseCases`,
`filteredBySearch`, counts) now operate on this distribution-filtered
set. `activeModels`/`activeUseCases` always prune against available
options.
- **`WorkflowTemplateSelectorDialog.vue`**: Passes `distributions` ref
to composable, removes `v-show` gate and
`isTemplateVisibleOnDistribution` function.
- **`useTemplateFiltering.test.ts`**: 10 new unit tests covering
distribution filtering, filter composition (search + model + use case +
runsOn), stale persisted selections, multi-distribution templates, and
Mac distribution.
- **`templateFilteringCount.spec.ts`**: 5 new `@cloud` e2e tests
verifying count/card consistency, DOM leak prevention, and filter reset
behavior with mocked template data.

## Verification

- 22 unit tests passing (12 existing + 10 new)
- `pnpm typecheck` clean
- `pnpm typecheck:browser` clean
- `oxlint` + `eslint` clean on all changed files
- E2E tests tagged `@cloud` — designed for CI cloud build execution

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11418-fix-move-template-distribution-filter-from-v-show-to-data-pipeline-3476d73d365081c3ba09fc8a42eb4c9b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-23 02:42:04 +00:00
Christian Byrne
0d5f8161f3 test: improve promotedWidgetView coverage to 100% (+9.2%) (#11403)
## Summary

Adds 11 unit tests covering all previously uncovered branches in
`src/core/graph/subgraph/promotedWidgetView.ts`, bringing line coverage
from 90.8% to 100.0%.

## Coverage Delta

| File | Before | After | Delta | Missed |
|------|--------|-------|-------|--------|
| promotedWidgetView.ts | 90.8% | 100.0% | +9.2% | 0 |

## What is covered

- `tooltip` getter
- `hidden` getter
- `label` setter
- `isWidgetValue` — number, boolean, and object branches
- `onPointerDown` — interior handler, concrete widget, and fallthrough
paths
- `bindConcretePointerHandlers` — onClick and onDrag lambdas
- `getProjectedWidget` — null concrete widget reset
- `resolveDeepest` — frame cache path
- `draw` — unknown widget type branch

## Testing

```bash
npx vitest run src/core/graph/subgraph/promotedWidgetView.test.ts
```

75/75 tests pass. All quality gates verified: `pnpm typecheck`, `pnpm
lint`, `pnpm format:check`.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11403-test-improve-promotedWidgetView-coverage-to-100-9-2-3476d73d3650818a9558e70de3a92432)
by [Unito](https://www.unito.io)
2026-04-23 02:41:40 +00:00
Christian Byrne
7fc0c357da test: cover error branch in useWorkflowThumbnail (+6.1%) (#11404)
## Summary

Cover the remaining uncovered error-handling branch in
`useWorkflowThumbnail`, bringing unit test coverage from 93.9% to 100%.

## Changes

- **What**: Add 2 unit tests for `createMinimapPreview` error path and
`storeThumbnail` null-thumbnail branch

## Review Focus

Tests verify that when `createGraphThumbnail` throws,
`createMinimapPreview` returns `null` and `storeThumbnail` does not
persist anything.

## Coverage Delta

| File | Before | After | Delta | Missed |
|------|--------|-------|-------|--------|
| `useWorkflowThumbnail.ts` | 93.9% | 100.0% | 🟢 +6.1% | 0 |

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11404-test-cover-error-branch-in-useWorkflowThumbnail-6-1-3476d73d36508148a135ee29f7abf22e)
by [Unito](https://www.unito.io)
2026-04-22 19:47:13 -07:00
Christian Byrne
25ff147176 test: add unit tests for CanvasPathRenderer (#11387)
## Summary

Add 51 unit tests for `CanvasPathRenderer`, improving line coverage from
23.2% to 84.19% (100% function coverage).

## Changes

- **What**: New test file
`src/renderer/core/canvas/pathRenderer.test.ts` covering color
determination, border rendering, linear/straight/spline path modes,
`findPointOnBezier`, center point calculation, arrows, flow animation,
center markers, disabled patterns, and `drawDraggingLink`.

## Review Focus

Pure test addition — no production code changes. Tests mock `Path2D` via
`vi.stubGlobal` and `CanvasRenderingContext2D` via a plain object with
`vi.fn()` methods.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11387-test-add-unit-tests-for-CanvasPathRenderer-3476d73d365081bbb526d584cc41b723)
by [Unito](https://www.unito.io)
2026-04-22 19:46:57 -07:00
Christian Byrne
0efc0c4d72 test: exclude legacy UI component library from e2e coverage (#11377)
*PR Created by the Glary-Bot Agent*

---

## Summary

- Excludes `src/scripts/ui/**` (legacy DOM component library) from
Playwright e2e coverage reports — this code is kept solely for extension
backwards-compatibility and shouldn't count toward coverage metrics
- Extracts Monocart coverage config (`outputDir`, `sourceFilter`) into
`browser_tests/coverageConfig.ts` so coverage exclusions are
discoverable and centralized instead of buried in `globalTeardown.ts`

## Details

Monocart does support external config files (`mcr.config.ts`
auto-discovery), but since MCR is instantiated in two places with
different configs (per-worker collection in `ComfyPage.ts` vs final
report in `globalTeardown.ts`), auto-discovery would affect both
instances. A shared TypeScript constant is safer and more explicit.

## Changes
- **New**: `browser_tests/coverageConfig.ts` — shared
`COVERAGE_OUTPUT_DIR` and `coverageSourceFilter`
- **Modified**: `browser_tests/globalTeardown.ts` — imports from shared
config
- **Modified**: `browser_tests/fixtures/ComfyPage.ts` — imports
`COVERAGE_OUTPUT_DIR`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11377-test-exclude-legacy-UI-component-library-from-e2e-coverage-3466d73d365081b78dc9e4e14d913295)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
2026-04-22 19:46:34 -07:00
Christian Byrne
d0e3b7ebf0 fix: handle EPERM/EBUSY in global teardown restorePath (#11013)
Fixes #11009

## Summary

On Windows, Chromium may still hold file handles on the user-data
directory when global teardown runs `restorePath`. The `fs.moveSync(...,
{ overwrite: true })` call fails with EPERM because it can't remove the
target while handles are held.

## Changes

- Split `restorePath` into explicit remove-then-move
- Added `removeWithRetry` that retries up to 3× on EPERM/EBUSY with
500ms delay between attempts
- Downgraded the catch from `console.error` (which looks like a test
failure) to `console.warn` so teardown noise doesn't mask real failures

No E2E regression test added: this is a test-infrastructure fix for a
Windows-specific race condition in teardown that cannot be reliably
reproduced in CI.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11013-fix-handle-EPERM-EBUSY-in-global-teardown-restorePath-33e6d73d3650815ebe0cd42af23e6c0e)
by [Unito](https://www.unito.io)
2026-04-22 19:44:43 -07:00
Christian Byrne
f2677aa598 fix: include actual slot index in InputSlot/OutputSlot keys to prevent stale indices after autogrow (#11423)
*PR Created by the Glary-Bot Agent*

---

## Summary

Fixes auto-grow input slot connections breaking in Nodes 2.0 when a node
has multiple auto-grow groups (e.g., `Wan 2.7 Reference to Video` with
both image and video auto-grow inputs).

## Problem

When auto-grow adds inputs to one group, it splices new entries into
`node.inputs`, shifting the indices of all subsequent groups. The data
layer handles this correctly via `spliceInputs()`, but Nodes 2.0 Vue
components retained stale slot indices because:

1. **`NodeSlots.vue`** keyed `InputSlot`/`OutputSlot` by name only — Vue
reused components without remounting when indices shifted
2. **`useSlotElementTracking`** registered `data-slot-key` once at mount
and stopped its watcher — stale keys persisted in the DOM
3. **`useSlotLinkInteraction`** captured `index` in closures at mount —
stale closures targeted wrong slots

This caused connections to land on wrong inputs, incorrect hover
indicators, and some slot types becoming unreachable.

## Fix

Include the actual slot index in the component key for `InputSlot` (both
in `NodeSlots.vue` and `NodeWidgets.vue`) and `OutputSlot`. When
autogrow shifts a slot's position or an output is removed, the key
changes, forcing Vue to remount — which re-registers `data-slot-key` and
refreshes all interaction closures with the correct index.

## Testing

- **Remount verification**: Tests use setup() invocation counting to
prove components are actually remounted (not just prop-patched) when
indices shift — directly validating that `useSlotElementTracking` and
`useSlotLinkInteraction` are re-initialized
- **Multi-group autogrow**: Verifies data-layer index correctness when
first group growth shifts second group
- **Output removal**: Verifies OutputSlot remount when earlier output
removal shifts later output indices
- All existing tests pass, lint/typecheck/format clean

## Screenshots

![ComfyUI frontend loads correctly with Nodes 2.0 after the
fix](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/ed95fd14fd7d0e7797f7fa7a2737ed737829303e39a30afe453eb05e19218a2d/pr-images/1776594420504-a5c1b967-626d-4463-b1e3-ae800535c57b.png)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11423-fix-include-actual-slot-index-in-InputSlot-OutputSlot-keys-to-prevent-stale-indices-aft-3476d73d365081859da6c450a840a625)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
2026-04-22 19:42:45 -07:00
AustinMroz
fc9a1d6bfb Add audio/video preview tests (#11523)
Adds tests for the vue audio preview widget and vue video previews
(which are not widgets).

Also
- Fixes a bug where muted audio previews would incorrectly display a
'low volume' indicator instead of a muted indicator.
- Add test helper for deleting uploaded files after a test completes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11523-Add-audio-preview-tests-3496d73d365081be8630ede6dae1726a)
by [Unito](https://www.unito.io)
2026-04-23 02:14:48 +00:00
Yourz
bbb043c9cc feat(website): Polish and fix UI (#11363)
## Summary

<!-- One sentence describing what changed and why. -->

Polish and fix UI for new website

## Changes

- **What**: <!-- Core functionality added/modified -->
  - [x] update about video
  - [x] update Moment factory story content
  - [x] update homepage visual
  - [x] update customer story visual
  - [x] put images and videos to bucket

## Review Focus

<!-- Critical design decisions or edge cases that need attention -->

<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->

## Screenshots (if applicable)

<!-- Add screenshots or video recording to help explain your changes -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11363-feat-website-Polish-and-fix-UI-3466d73d365081f895aff84b594450c9)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL0424@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2026-04-22 18:45:27 -07:00
Christian Byrne
ef59f46495 refactor: migrate cn imports from @/utils/tailwindUtil shim to @comfyorg/tailwind-utils directly (#11453)
*PR Created by the Glary-Bot Agent*

---

## Summary

- Replace all `cn` / `ClassValue` imports from the
`@/utils/tailwindUtil` re-export shim with direct imports from
`@comfyorg/tailwind-utils` across 198 source files in `src/` and 3 in
`apps/desktop-ui/`
- Delete both shim files (`src/utils/tailwindUtil.ts` and
`apps/desktop-ui/src/utils/tailwindUtil.ts`)
- Add explicit `@comfyorg/tailwind-utils` dependency to
`apps/desktop-ui/package.json`
- Update documentation references in `AGENTS.md`,
`docs/guidance/design-standards.md`, and
`docs/guidance/vue-components.md`

Fixes #11288

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11453-refactor-migrate-cn-imports-from-utils-tailwindUtil-shim-to-comfyorg-tailwind-utils--3486d73d365081ec92cce91fbf88e6e4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-04-22 18:39:57 -07:00
Christian Byrne
8e7e4d6faa [chore] Update Comfy Registry API types from comfy-api@36463e1 (#11550)
## Automated API Type Update

This PR updates the Comfy Registry API types from the latest comfy-api
OpenAPI specification.

- API commit: 36463e1
- Generated on: 2026-04-22T18:49:09Z

These types are automatically generated using openapi-typescript.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11550-chore-Update-Comfy-Registry-API-types-from-comfy-api-36463e1-34a6d73d36508183bbf6e228429ae7a1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: coderfromthenorth93 <213232275+coderfromthenorth93@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-04-23 01:23:45 +00:00
pythongosssss
975d5e5ec0 fix: add GLSL live update when custom size is changed (#11517)
## Summary

Changing the custom size mode width & height were not reactive and so
did not live update on the FE

## Changes

- **What**: 
- change `customResolution` to be computed, changing width/height then
triggers a live update
- add tests

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11517-fix-add-GLSL-live-update-when-custom-size-is-changed-3496d73d3650816e940bf821f4e18db5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-22 20:05:15 -04:00
pythongosssss
9522f68ae6 test: additional load3d e2e coverage (#11521)
## Summary

Expands the e2e coverage for the load3d node

## Changes

- **What**: 
- test recording, grid and background upload

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11521-test-additional-load3d-e2e-coverage-3496d73d3650814595e1eabe97448993)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-04-22 20:03:37 -04:00
614 changed files with 13374 additions and 3349 deletions

201
.github/workflows/ci-website-e2e.yaml vendored Normal file
View File

@@ -0,0 +1,201 @@
name: 'CI: Website E2E'
on:
push:
branches: [main, website/*]
paths:
- 'apps/website/**'
- 'packages/design-system/**'
- 'packages/tailwind-utils/**'
- 'pnpm-lock.yaml'
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths:
- 'apps/website/**'
- 'packages/design-system/**'
- 'packages/tailwind-utils/**'
- 'pnpm-lock.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
website-e2e:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.1-noble
timeout-minutes: 15
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Post starting comment
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
uses: actions/github-script@v8
with:
script: |
const marker = '<!-- WEBSITE_E2E_STATUS -->'
const body = [
marker,
'## 🌐 Website E2E',
'',
'> [!NOTE]',
`> Tests are running… [View workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
].join('\n')
const { data: comments } = await github.rest.issues.listComments({
...context.repo,
issue_number: context.issue.number
})
const existing = comments.find(c => c.body.includes(marker))
if (existing) {
await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body })
} else {
await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body })
}
- name: Install pnpm
run: corepack enable && corepack prepare
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build website
run: pnpm --filter @comfyorg/website build
- name: Run Playwright tests
id: tests
run: pnpm --filter @comfyorg/website test:e2e
- name: Upload test report
uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: website-playwright-report
path: apps/website/playwright-report/
retention-days: 30
- name: Deploy report to Cloudflare
id: deploy
if: always() && !cancelled()
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
HEAD_REF: ${{ github.head_ref || github.ref_name }}
run: |
BRANCH=$(echo "$HEAD_REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g;s/--*/-/g;s/^-\|-$//g')
DEPLOY_OK=false
for i in 1 2 3; do
echo "Deployment attempt $i of 3..."
OUTPUT=$(npx wrangler@^4.0.0 pages deploy apps/website/playwright-report \
--project-name=comfyui-website-e2e \
--branch="$BRANCH" 2>&1) && { DEPLOY_OK=true; break; } || echo "$OUTPUT"
[ $i -lt 3 ] && sleep 10
done
echo "$OUTPUT"
if [ "$DEPLOY_OK" != "true" ]; then
echo "::error::All 3 deployment attempts failed"
exit 1
fi
URL=$(echo "$OUTPUT" | grep -oE 'https://[a-zA-Z0-9.-]+\.pages\.dev\S*' | head -1)
echo "url=${URL}" >> $GITHUB_OUTPUT
- name: Categorize failures
id: failures
if: always() && !cancelled() && steps.tests.outcome != 'success'
uses: actions/github-script@v8
with:
script: |
const fs = require('fs')
const report = JSON.parse(fs.readFileSync('apps/website/results.json', 'utf8'))
function isFailed(t) { return t.status === 'unexpected' || t.status === 'flaky' }
function isVisual(spec) {
return spec.file?.includes('visual') ||
spec.tests?.some(t => t.results?.some(r => r.error?.message?.includes('toHaveScreenshot')))
}
function specsOf(suite) {
return [
...(suite.specs || []),
...(suite.suites || []).flatMap(specsOf)
]
}
// True: Visual
// False: Other
const failed = specsOf(report)
.flatMap(spec => (spec.tests || [])
.filter(isFailed)
.map(() => isVisual(spec)))
const screenshotFailures = failed.filter(Boolean).length
core.setOutput('screenshot', screenshotFailures)
core.setOutput('other', failed.length - screenshotFailures)
- name: Post result comment and summary
if: always() && !cancelled()
uses: actions/github-script@v8
with:
script: |
const passed = '${{ steps.tests.outcome }}' === 'success'
const reportUrl = '${{ steps.deploy.outputs.url }}'
const screenshotFailures = parseInt('${{ steps.failures.outputs.screenshot }}') || 0
const otherFailures = parseInt('${{ steps.failures.outputs.other }}') || 0
const lines = ['## 🌐 Website E2E', '']
if (passed) {
lines.push('> [!TIP]', '> All tests passed.')
} else {
lines.push('> [!CAUTION]', '> Some tests failed.')
}
// Results table
const rows = [
['Status', passed ? '✅ Passed' : '❌ Failed'],
['Report', reportUrl ? `[View Report](${reportUrl})` : '_unavailable_']
]
if (!passed) {
rows.push(
['Screenshot diffs', String(screenshotFailures)],
['Other failures', String(otherFailures)]
)
}
lines.push(
'',
'| | |',
'|---|---|',
...rows.map(([k, v]) => `| **${k}** | ${v} |`)
)
if (screenshotFailures > 0) {
const s = screenshotFailures === 1 ? '' : 's'
lines.push('', `- [ ] Update website screenshots (${screenshotFailures} screenshot diff${s})`)
}
if (otherFailures > 0) {
lines.push(
'',
'> [!WARNING]',
`> ${otherFailures} non-screenshot failure${otherFailures === 1 ? '' : 's'} — these require manual review.`
)
}
const summary = lines.join('\n')
await core.summary.addRaw(summary).write()
if (context.eventName === 'pull_request' && !context.payload.pull_request.head.repo.fork) {
const marker = '<!-- WEBSITE_E2E_STATUS -->'
const body = `${marker}\n${summary}`
const { data: comments } = await github.rest.issues.listComments({
...context.repo,
issue_number: context.issue.number
})
const existing = comments.find(c => c.body.includes(marker))
if (existing) {
await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body })
} else {
await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body })
}
}

View File

@@ -0,0 +1,125 @@
name: 'PR: Update Website Screenshots'
on:
pull_request:
types: [labeled]
issue_comment:
types: [created, edited]
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
update-screenshots:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.1-noble
timeout-minutes: 15
# Trigger: (1) label, (2) /slash-command, or (3) checkbox in E2E status comment
if: >
( github.event_name == 'pull_request' &&
github.event.label.name == 'Update Website Screenshots' ) ||
( github.event.issue.pull_request &&
github.event_name == 'issue_comment' &&
(
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
) &&
startsWith(github.event.comment.body, '/update-website-screenshots') ) ||
( github.event.issue.pull_request &&
github.event_name == 'issue_comment' &&
github.event.comment.user.login == 'github-actions[bot]' &&
github.actor != 'github-actions[bot]' &&
contains(github.event.comment.body, '<!-- WEBSITE_E2E_STATUS -->') &&
contains(github.event.comment.body, '- [x] Update website screenshots') )
steps:
- name: Verify sender permissions
if: >
github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '<!-- WEBSITE_E2E_STATUS -->')
uses: actions/github-script@v8
with:
script: |
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
...context.repo,
username: context.actor
})
if (!['admin', 'write'].includes(data.permission)) {
core.setFailed(`User ${context.actor} does not have write access`)
}
- name: Get PR info
id: pr-info
uses: actions/github-script@v8
with:
script: |
const prNumber = ${{ github.event.number || github.event.issue.number }}
const { data: pr } = await github.rest.pulls.get({
...context.repo,
pull_number: prNumber
})
core.setOutput('pr-number', prNumber)
core.setOutput('branch', pr.head.ref)
- uses: actions/checkout@v6
with:
ref: ${{ steps.pr-info.outputs.branch }}
token: ${{ secrets.PR_GH_TOKEN }}
- name: Install pnpm
run: corepack enable && corepack prepare
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build website
run: pnpm --filter @comfyorg/website build
- name: Update screenshots
id: update-screenshots
run: pnpm --filter @comfyorg/website test:visual:update
continue-on-error: true
- name: Commit updated screenshots
id: commit
if: steps.update-screenshots.outcome == 'success'
run: |
git config --global --add safe.directory "$(pwd)"
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
if [ -z "$(git status --porcelain=v1 --untracked-files=all -- apps/website/e2e/)" ]; then
echo "No screenshot changes to commit"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "has-changes=true" >> $GITHUB_OUTPUT
git add apps/website/e2e/
git commit -m "[automated] Update website screenshot expectations"
git push origin ${{ steps.pr-info.outputs.branch }}
- name: Upload test report
uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: website-screenshot-update-report
path: apps/website/playwright-report/
retention-days: 14
- name: Remove label
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v8
with:
script: |
try {
await github.rest.issues.removeLabel({
...context.repo,
issue_number: ${{ steps.pr-info.outputs.pr-number }},
name: 'Update Website Screenshots'
})
} catch (e) {
// Label may already be removed
}

View File

@@ -67,6 +67,7 @@
"ignoreFiles": [
"node_modules/**",
"dist/**",
"**/dist/**",
"playwright-report/**",
"public/**",
"src/lib/litegraph/**"

View File

@@ -1,16 +1,12 @@
{
"recommendations": [
"antfu.vite",
"austenc.tailwind-docs",
"bradlc.vscode-tailwindcss",
"davidanson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"donjayamanne.githistory",
"eamodio.gitlens",
"github.vscode-github-actions",
"github.vscode-pull-request-github",
"hbenl.vscode-test-explorer",
"kisstkondoros.vscode-codemetrics",
"lokalise.i18n-ally",
"ms-playwright.playwright",
"oxc.oxc-vscode",

View File

@@ -312,7 +312,7 @@ When referencing Comfy-Org repos:
- Instead use a semantic value from the `style.css` theme
- e.g. `bg-node-component-surface`
- NEVER use `:class="[]"` to merge class names
- Always use `import { cn } from '@/utils/tailwindUtil'`
- Always use `import { cn } from '@comfyorg/tailwind-utils'`
- e.g. `<div :class="cn('text-node-component-header-icon', hasError && 'text-danger')" />`
- Use `cn()` inline in the template when feasible instead of creating a `computed` to hold the value
- NEVER use `!important` or the `!` important prefix for tailwind classes

View File

@@ -12,6 +12,7 @@
"dependencies": {
"@comfyorg/comfyui-electron-types": "catalog:",
"@comfyorg/shared-frontend-utils": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@primevue/core": "catalog:",
"@primevue/themes": "catalog:",
"@vueuse/core": "catalog:",

View File

@@ -32,7 +32,7 @@ import { useI18n } from 'vue-i18n'
import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { cn } from '@/utils/tailwindUtil'
import { cn } from '@comfyorg/tailwind-utils'
const { t } = useI18n()

View File

@@ -4,7 +4,7 @@
<button
:class="
cn(
'hardware-option w-[170px] h-[190px] p-5 flex flex-col items-center rounded-3xl transition-all duration-200 bg-neutral-900/70 border-4',
'hardware-option flex h-[190px] w-[170px] flex-col items-center rounded-3xl border-4 bg-neutral-900/70 p-5 transition-all duration-200',
selected ? 'border-solid border-brand-yellow' : 'border-transparent'
)
"
@@ -12,13 +12,13 @@
>
<!-- Icon/Logo Area - Rounded square container -->
<div
class="icon-container w-[110px] h-[110px] shrink-0 rounded-2xl bg-neutral-800 flex items-center justify-center overflow-hidden"
class="icon-container flex h-[110px] w-[110px] shrink-0 items-center justify-center overflow-hidden rounded-2xl bg-neutral-800"
>
<img
v-if="imagePath"
:src="imagePath"
:alt="placeholderText"
class="w-full h-full object-cover"
class="size-full object-cover"
style="object-position: 57% center"
draggable="false"
/>
@@ -28,7 +28,7 @@
</div>
<!-- Text Content -->
<div v-if="subtitle" class="text-center mt-4">
<div v-if="subtitle" class="mt-4 text-center">
<div class="text-sm text-neutral-500">{{ subtitle }}</div>
</div>
</button>
@@ -36,7 +36,7 @@
</template>
<script setup lang="ts">
import { cn } from '@/utils/tailwindUtil'
import { cn } from '@comfyorg/tailwind-utils'
interface Props {
imagePath?: string

View File

@@ -64,7 +64,7 @@ import { computed } from 'vue'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { cn } from '@/utils/tailwindUtil'
import { cn } from '@comfyorg/tailwind-utils'
import { useMinLoadingDurationRef } from '@/utils/refUtil'
const taskStore = useMaintenanceTaskStore()

View File

@@ -1 +0,0 @@
export { cn } from '@comfyorg/tailwind-utils'

View File

@@ -1,3 +1,7 @@
dist/
.astro/
test-results/
playwright-report/
# Platform-specific Playwright snapshots (CI runs Linux)
*-win32.png

View File

@@ -6,9 +6,15 @@ import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
site: 'https://comfy.org',
output: 'static',
devToolbar: { enabled: !process.env.NO_TOOLBAR },
integrations: [vue(), sitemap()],
vite: {
plugins: [tailwindcss()]
plugins: [tailwindcss()],
server: {
watch: {
ignored: ['**/playwright-report/**']
}
}
},
i18n: {
locales: ['en', 'zh-CN'],

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

View File

@@ -1,4 +1,6 @@
import { expect, test } from '@playwright/test'
import { expect } from '@playwright/test'
import { test } from './fixtures/blockExternalMedia'
test.describe('Cloud page @smoke', () => {
test.beforeEach(async ({ page }) => {
@@ -41,13 +43,11 @@ test.describe('Cloud page @smoke', () => {
test('AIModelsSection heading and 5 model cards are visible', async ({
page
}) => {
await expect(
page.getByRole('heading', { name: /leading AI models/i })
).toBeVisible()
const heading = page.getByRole('heading', { name: /leading AI models/i })
await expect(heading).toBeVisible()
const grid = page.locator('.grid', {
has: page.getByText('Grok Imagine')
})
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)
})
@@ -100,38 +100,44 @@ test.describe('Cloud FAQ accordion @interaction', () => {
await page.goto('/cloud')
})
test('all FAQs are expanded by default', async ({ page }) => {
await expect(
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
).toBeVisible()
})
test('clicking an expanded FAQ collapses it', async ({ page }) => {
const firstQuestion = page.getByRole('button', {
name: /What is Comfy Cloud/i
})
await firstQuestion.scrollIntoViewIfNeeded()
await firstQuestion.click()
test('all FAQs are collapsed by default', async ({ page }) => {
await expect(
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
).toBeHidden()
})
test('clicking a collapsed FAQ expands it again', async ({ page }) => {
test('clicking a collapsed FAQ expands it', async ({ page }) => {
const firstQuestion = page.getByRole('button', {
name: /What is Comfy Cloud/i
})
await firstQuestion.scrollIntoViewIfNeeded()
// Gate: wait for Vue hydration to bind aria-expanded
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
await firstQuestion.click()
await expect(
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
).toBeHidden()
await firstQuestion.click()
await expect(
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
).toBeVisible()
})
test('clicking an expanded FAQ collapses it again', async ({ page }) => {
const firstQuestion = page.getByRole('button', {
name: /What is Comfy Cloud/i
})
await firstQuestion.scrollIntoViewIfNeeded()
// Gate: wait for Vue hydration to bind aria-expanded
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
await firstQuestion.click()
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'true')
await expect(
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
).toBeVisible()
await firstQuestion.click()
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
await expect(
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
).toBeHidden()
})
})

View File

@@ -1,4 +1,9 @@
import { expect, test } from '@playwright/test'
import { devices, expect } from '@playwright/test'
import { test } from './fixtures/blockExternalMedia'
const WINDOWS_UA =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
test.describe('Download page @smoke', () => {
test.beforeEach(async ({ page }) => {
@@ -22,7 +27,11 @@ test.describe('Download page @smoke', () => {
await expect(page.getByText(/The full ComfyUI engine/)).toBeVisible()
})
test('HeroSection has download and GitHub buttons', async ({ page }) => {
test('HeroSection has download and GitHub buttons', async ({ browser }) => {
const context = await browser.newContext({ userAgent: WINDOWS_UA })
const page = await context.newPage()
await page.goto('/download')
const hero = page.locator('section', {
has: page.getByRole('heading', {
name: /Run on your hardware/i,
@@ -39,6 +48,8 @@ test.describe('Download page @smoke', () => {
'href',
'https://github.com/Comfy-Org/ComfyUI'
)
await context.close()
})
test('ReasonSection heading and reasons are visible', async ({ page }) => {
@@ -93,40 +104,46 @@ test.describe('FAQ accordion @interaction', () => {
await page.goto('/download')
})
test('all FAQs are expanded by default', async ({ page }) => {
test('all FAQs are collapsed by default', async ({ page }) => {
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeVisible()
await expect(page.getByText(/ComfyUI is lightweight/i)).toBeVisible()
).toBeHidden()
await expect(page.getByText(/ComfyUI is lightweight/i)).toBeHidden()
})
test('clicking an expanded FAQ collapses it', async ({ page }) => {
test('clicking a collapsed FAQ expands it', async ({ page }) => {
const firstQuestion = page.getByRole('button', {
name: /Do I need a GPU/i
})
await firstQuestion.scrollIntoViewIfNeeded()
// Gate: wait for Vue hydration to bind aria-expanded
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
await firstQuestion.click()
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeHidden()
).toBeVisible()
})
test('clicking a collapsed FAQ expands it again', async ({ page }) => {
test('clicking an expanded FAQ collapses it again', async ({ page }) => {
const firstQuestion = page.getByRole('button', {
name: /Do I need a GPU/i
})
await firstQuestion.scrollIntoViewIfNeeded()
// Gate: wait for Vue hydration to bind aria-expanded
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
await firstQuestion.click()
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeHidden()
await firstQuestion.click()
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'true')
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeVisible()
await firstQuestion.click()
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeHidden()
})
})
@@ -145,7 +162,14 @@ test.describe('Download page mobile @mobile', () => {
).toBeVisible()
})
test('download buttons are stacked vertically', async ({ page }) => {
test('download buttons are stacked vertically', async ({ browser }) => {
const context = await browser.newContext({
...devices['Pixel 5'],
userAgent: WINDOWS_UA
})
const page = await context.newPage()
await page.goto('/download')
const hero = page.locator('section', {
has: page.getByRole('heading', {
name: /Run on your hardware/i,
@@ -155,13 +179,18 @@ test.describe('Download page mobile @mobile', () => {
const downloadBtn = hero.getByRole('link', { name: /DOWNLOAD LOCAL/i })
const githubBtn = hero.getByRole('link', { name: /INSTALL FROM GITHUB/i })
await downloadBtn.scrollIntoViewIfNeeded()
await expect(downloadBtn).toBeVisible()
await expect(githubBtn).toBeVisible()
await expect
.poll(async () => {
const downloadBox = await downloadBtn.boundingBox()
const githubBox = await githubBtn.boundingBox()
if (!downloadBox || !githubBox) return false
return githubBox.y > downloadBox.y
})
.toBe(true)
expect(downloadBox, 'download button bounding box').not.toBeNull()
expect(githubBox, 'github button bounding box').not.toBeNull()
expect(githubBox!.y).toBeGreaterThan(downloadBox!.y)
await context.close()
})
})

View File

@@ -0,0 +1,47 @@
import { fileURLToPath } from 'node:url'
import type { Route } from '@playwright/test'
import { test as base } from '@playwright/test'
function assetPath(relativePath: string) {
return fileURLToPath(new URL(relativePath, import.meta.url))
}
const IMAGE_PLACEHOLDER = assetPath('../assets/placeholder-1x1.webp')
const VIDEO_PLACEHOLDER = assetPath('../assets/placeholder.webm')
const ANALYTICS_PATTERN = '**/va.vercel-scripts.com/**' as const
const MEDIA_PATTERN =
/^https:\/\/media\.comfy\.org\/.*\.(webp|webm|mp4|png|jpg|jpeg|vtt)(\?.*)?$/i
const VIDEO_PATTERN = /\.(webm|mp4)(\?|$)/i
const SUBTITLE_PATTERN = /\.vtt(\?|$)/i
function blockAnalytics(route: Route) {
return route.abort('blockedbyclient')
}
async function fulfillMedia(route: Route) {
const url = route.request().url()
if (VIDEO_PATTERN.test(url))
return route.fulfill({ path: VIDEO_PLACEHOLDER, status: 200 })
if (SUBTITLE_PATTERN.test(url))
return route.fulfill({
status: 200,
contentType: 'text/vtt',
body: 'WEBVTT\n'
})
await route.fulfill({ path: IMAGE_PLACEHOLDER, status: 200 })
}
export const test = base.extend<{ blockExternalMedia: void }>({
blockExternalMedia: [
async ({ page }, use) => {
await page.route(ANALYTICS_PATTERN, blockAnalytics)
await page.route(MEDIA_PATTERN, fulfillMedia)
await use()
},
{ auto: true }
]
})

View File

@@ -1,4 +1,15 @@
import { expect, test } from '@playwright/test'
import { fileURLToPath } from 'node:url'
import { expect } from '@playwright/test'
import { test } from './fixtures/blockExternalMedia'
const caseStudyVideoPath = fileURLToPath(
new URL(
'../../../public/assets/images/cloud-subscription.webm',
import.meta.url
)
)
test.describe('Homepage @smoke', () => {
test.beforeEach(async ({ page }) => {
@@ -83,17 +94,56 @@ test.describe('Product showcase accordion @interaction', () => {
.first()
await secondFeature.scrollIntoViewIfNeeded()
await expect(async () => {
await secondFeature.click()
await expect(
page.getByText(/If you are new to ComfyUI/).first()
).toBeVisible({ timeout: 1000 })
}).toPass({ timeout: 10000 })
await expect(
page.getByText(/Build powerful AI pipelines by connecting nodes/).first()
).toBeHidden()
secondFeature.getByText(/If you are new to ComfyUI/)
).toBeVisible()
const firstFeature = page
.getByRole('button', { name: /Full Control with Nodes/i })
.first()
await expect(firstFeature).not.toHaveClass(/bg-primary-comfy-yellow/)
await expect(secondFeature).toHaveClass(/bg-primary-comfy-yellow/)
})
})
test.describe('Video player @interaction', () => {
test.beforeEach(async ({ page }) => {
await page.route(
'https://media.comfy.org/website/customers/blackmath/video.webm',
(route) =>
route.fulfill({
contentType: 'video/webm',
path: caseStudyVideoPath
})
)
await page.goto('/')
})
test('clicking play advances playback', async ({ page }) => {
const section = page.locator('section', {
has: page.getByText('Customer Stories')
})
const video = section.locator('video')
await expect
.poll(
async () =>
video.evaluate((element: HTMLVideoElement) => element.duration),
{ timeout: 15_000 }
)
.toBeGreaterThan(0)
await section.getByRole('button', { name: 'Play' }).click()
await expect
.poll(async () =>
video.evaluate((element: HTMLVideoElement) => element.currentTime)
)
.toBeGreaterThan(0)
})
})
@@ -125,6 +175,6 @@ test.describe('Get started section links @smoke', () => {
const cloudLink = section.getByRole('link', { name: 'Launch Cloud' })
await expect(cloudLink).toBeVisible()
await expect(cloudLink).toHaveAttribute('href', 'https://app.comfy.org')
await expect(cloudLink).toHaveAttribute('href', 'https://cloud.comfy.org')
})
})

View File

@@ -1,4 +1,6 @@
import { expect, test } from '@playwright/test'
import { expect } from '@playwright/test'
import { test } from './fixtures/blockExternalMedia'
test.describe('Desktop navigation @smoke', () => {
test.beforeEach(async ({ page }) => {

View File

@@ -1,4 +1,6 @@
import { expect, test } from '@playwright/test'
import { expect } from '@playwright/test'
import { test } from './fixtures/blockExternalMedia'
test.describe('Desktop layout @smoke', () => {
test.beforeEach(async ({ page }) => {

View File

@@ -0,0 +1,6 @@
export const VIEWPORTS = [
{ name: '1-sm', width: 393, height: 851 },
{ name: '2-md', width: 768, height: 1024 },
{ name: '3-lg', width: 1280, height: 800 },
{ name: '4-xl', width: 1536, height: 864 }
] as const

View File

@@ -0,0 +1,159 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { test } from './fixtures/blockExternalMedia'
import { VIEWPORTS } from './viewports'
test.describe.configure({ timeout: 60_000 })
const SMALL_VIEWPORTS = VIEWPORTS.filter(
(v) => v.name === '1-sm' || v.name === '2-md'
)
async function assertNoOverflow(page: Page) {
await expect
.poll(
() =>
page.evaluate(
() =>
document.documentElement.scrollWidth >
document.documentElement.clientWidth
),
{ message: 'page has horizontal overflow', timeout: 5_000 }
)
.toBe(false)
}
async function navigateAndSettle(page: Page, url: string) {
await page.goto(url)
await page.waitForLoadState('networkidle')
}
test.describe('Home', { tag: '@visual' }, () => {
for (const vp of VIEWPORTS) {
test.describe(vp.name, () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height })
await navigateAndSettle(page, '/')
})
test('product-cards screenshot', async ({ page }) => {
const section = page.locator('section', {
has: page.getByRole('heading', { name: /The AI creation/i })
})
await expect(section).toBeVisible()
await section.scrollIntoViewIfNeeded()
await expect(page).toHaveScreenshot(`home-product-cards-${vp.name}.png`)
})
test('get-started screenshot', async ({ page }) => {
const section = page.locator('section', {
has: page.getByRole('heading', { name: /Get started/i })
})
await expect(section).toBeVisible()
await section.scrollIntoViewIfNeeded()
await expect(page).toHaveScreenshot(`home-get-started-${vp.name}.png`)
})
})
}
})
test.describe('Pricing', { tag: '@visual' }, () => {
for (const vp of VIEWPORTS) {
test(`pricing-tiers-${vp.name}`, async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height })
await navigateAndSettle(page, '/cloud/pricing')
await assertNoOverflow(page)
const section = page.locator('section', {
has: page.getByRole('heading', { name: /Pricing/i })
})
await expect(section).toBeVisible()
await section.scrollIntoViewIfNeeded()
await expect(page).toHaveScreenshot(`pricing-tiers-${vp.name}.png`)
})
}
})
test.describe('Contact', { tag: '@visual' }, () => {
for (const vp of SMALL_VIEWPORTS) {
test(`form-${vp.name}`, async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height })
await navigateAndSettle(page, '/contact')
const section = page.locator('section', {
has: page.getByRole('heading', { name: /Create powerful workflows/i })
})
await expect(section).toBeVisible()
await section.scrollIntoViewIfNeeded()
await expect(page).toHaveScreenshot(`contact-form-${vp.name}.png`)
})
}
})
test.describe('Gallery', { tag: '@visual' }, () => {
for (const vp of SMALL_VIEWPORTS) {
test(`gallery-grid-${vp.name}`, async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height })
await navigateAndSettle(page, '/gallery')
const section = page.getByTestId('gallery-grid')
await expect(section).toBeVisible()
await section.scrollIntoViewIfNeeded()
await expect(page).toHaveScreenshot(`gallery-grid-${vp.name}.png`)
})
}
})
test.describe('About', { tag: '@visual' }, () => {
for (const vp of SMALL_VIEWPORTS) {
test(`hero-${vp.name}`, async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height })
await navigateAndSettle(page, '/about')
const hero = page.locator('section', {
has: page.getByRole('heading', { name: /Build the tools/i })
})
await expect(hero).toBeVisible()
await hero.scrollIntoViewIfNeeded()
await expect(page).toHaveScreenshot(`about-hero-${vp.name}.png`)
})
}
})
const OVERFLOW_SKIP = new Set([
'/ 3-lg',
'/ 4-xl',
'/cloud 2-md',
'/cloud 3-lg',
'/cloud 4-xl',
'/download 2-md',
'/download 3-lg',
'/download 4-xl'
])
test.describe('Overflow guards', { tag: '@visual' }, () => {
const pages = [
'/',
'/cloud',
'/cloud/pricing',
'/contact',
'/download',
'/gallery',
'/about',
'/careers'
]
for (const url of pages) {
for (const vp of VIEWPORTS) {
const key = `${url} ${vp.name}`
test(`${url} ${vp.name} no overflow`, async ({ page }) => {
test.skip(OVERFLOW_SKIP.has(key), 'Known overflow bug at this viewport')
await page.setViewportSize({ width: vp.width, height: vp.height })
await page.goto(url)
await assertNoOverflow(page)
})
}
}
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -5,17 +5,22 @@
"type": "module",
"scripts": {
"dev": "astro dev",
"dev:no-toolbar": "cross-env NO_TOOLBAR=1 astro dev",
"build": "astro build",
"preview": "astro preview",
"typecheck": "astro check",
"test:e2e": "playwright test",
"test:e2e:local": "PLAYWRIGHT_LOCAL=1 playwright test"
"test:e2e:local": "cross-env PLAYWRIGHT_LOCAL=1 playwright test",
"test:visual": "playwright test --project visual",
"test:visual:update": "playwright test --project visual --update-snapshots"
},
"dependencies": {
"@astrojs/sitemap": "catalog:",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@vercel/analytics": "catalog:",
"@vueuse/core": "catalog:",
"cva": "catalog:",
"gsap": "catalog:",
"lenis": "catalog:",
"vue": "catalog:"

View File

@@ -1,15 +1,36 @@
import type { PlaywrightTestConfig } from '@playwright/test'
import { defineConfig, devices } from '@playwright/test'
const maybeLocalOptions: PlaywrightTestConfig = process.env.PLAYWRIGHT_LOCAL
? {
timeout: 30_000,
retries: 0,
workers: 1,
use: {
baseURL: 'http://localhost:4321',
trace: 'on',
video: 'on'
}
}
: {
retries: process.env.CI ? 2 : 0,
use: {
baseURL: 'http://localhost:4321',
trace: 'on-first-retry'
}
}
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: 'html',
use: {
baseURL: 'http://localhost:4321',
trace: process.env.CI ? 'on-first-retry' : 'on'
reporter: process.env.CI
? [['html'], ['json', { outputFile: 'results.json' }]]
: 'html',
expect: {
toHaveScreenshot: { maxDiffPixels: 50 }
},
...maybeLocalOptions,
webServer: {
command: 'pnpm preview',
port: 4321,
@@ -19,12 +40,18 @@ export default defineConfig({
{
name: 'desktop',
use: { ...devices['Desktop Chrome'] },
grepInvert: /@mobile/
grepInvert: /@mobile|@visual/
},
{
name: 'mobile',
use: { ...devices['Pixel 5'] },
grep: /@mobile/
},
{
name: 'visual',
use: { ...devices['Desktop Chrome'] },
grep: /@visual/,
fullyParallel: false
}
]
})

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 11.5811L10.2582 18.0581L20 6.05811" stroke="#F2FF59" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 234 B

View File

@@ -0,0 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2871_8492)">
<path d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="#F2FF59" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 6.75V12H17.25" stroke="#F2FF59" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_2871_8492">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 567 B

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 595.3 130">
<!-- Generator: Adobe Illustrator 28.7.1, SVG Export Plug-In . SVG Version: 1.2.0 Build 142) -->
<g>
<g id="Layer_1">
<g id="Group_458" fill="#C2BFB9">
<path id="Path_3625" d="M16.4,109.7c4.4,0,8.6-.3,12.8-11L62.4,16.5h9.6l31.8,79.8c4.1,10.2,7.6,13.5,13,13.5v6.9h-36.7v-6.4c9-.5,12.8-3.5,9.2-13l-6.3-16.2h-36.8l-6,15.4c-3.4,8.7-1.8,13.8,7.6,13.8v6.4h-31.5v-6.9ZM79.8,72.6l-15.1-39.6-15.3,39.6h30.4Z"/>
<path id="Path_3626" d="M126,113.8V26.4l-11.8-2.1v-4.9l20.8-8.3h3.7v47.2c7.8-9.3,13.9-13,22.8-13,16.7,0,28,13.1,28,32.7s-13.9,40.2-38.5,40.2c-8.5-.1-16.9-1.6-24.9-4.4M155.1,111.6c12.2,0,20.5-11.8,20.5-28.7s-7.5-27.1-20.6-27.1c-6.4,0-12.4,3-16.2,8.1v31.5c-.4,8.6,6.3,15.8,14.8,16.2.5,0,1,0,1.5,0"/>
<path id="Path_3627" d="M209,110.9l-7.9,6.7h-2.8l-1.7-25.2h6.1c5.8,12.8,13.8,18.3,23.7,18.3s12.8-3.7,12.8-10.2-2.1-8.3-9.6-11.2l-12.4-4.7c-11.9-4.6-17.7-10.4-17.7-19.7s10.1-19.7,22.3-19.7c6-.2,11.8,2.3,15.7,6.9l7.9-6.4h2.6v22.6h-6.1c-5-12.5-11.2-15.6-18.2-15.6s-12.5,4-12.5,9.6,2.6,7.5,9.3,9.9l13.6,4.9c11,4,17,10.4,17,20s-9.2,21.1-23.2,21.1c-7,0-13.8-2.6-18.9-7.3"/>
<path id="Path_3628" d="M271.6,97.6l.5-41.4h-11.9v-8.4h7.9l13.3-16.7h2.9v16.7h21.9v8.4h-21.9v38.5c0,8.7,4,13.3,11.6,13.3,4.4-.1,8.6-1.9,11.8-4.9l2.3,2.7c-3.8,7.1-10.9,11.7-18.9,12.2-11.6,0-19.6-6.3-19.4-20.5"/>
<path id="Path_3629" d="M327.4,100.5v-39l-10.4-2.1v-4.6l18.2-8.3h4.9v13.1c9.6-10.7,14.7-13.6,19.9-13.6,4.2-.4,7.9,2.7,8.2,6.9,0,.2,0,.4,0,.6-.3,4.1-2.7,7.8-6.4,9.6l-11-6.3c-4.1,1.9-7.7,4.7-10.7,8.1v37c0,6.7,2.1,8.4,10.7,8.4h2.3v6.1h-36.8v-6.1c8.9,0,11.2-1.7,11.2-9.9"/>
<path id="Path_3630" d="M409.6,106.5c-6.6,8.6-12.5,11.8-19.3,11.8-8.2.4-15.2-6-15.6-14.2,0-.5,0-.9,0-1.4,0-9,5.7-14.4,16.7-18.8l18-7v-6c0-9.3-4.4-14.8-13.1-14.8s-11.6,3.1-17.6,11.2l-5-3.4c8.6-13.9,18-18.5,27.4-18.5s21.1,8.9,21.1,23.7v33.3c0,3.5,1.2,6,5.2,6,2.6-.1,5-1.4,6.6-3.4l.9,3.8c-5.3,7.2-9,9.5-13.6,9.5-6.4-.2-11.5-5.4-11.6-11.8M396.6,107.9c4.8-.2,9.4-2.2,12.8-5.7v-17.3l-11.5,2.6c-7.3,1.8-10.7,6-10.7,11.6-.2,4.7,3.5,8.6,8.2,8.8.4,0,.7,0,1.1,0"/>
<path id="Path_3631" d="M436.7,84.5c-.3-21.1,16.6-38.5,37.7-38.8,0,0,0,0,0,0,11.6,0,18.9,5,18.9,11.2-.5,4.3-4,7.6-8.3,7.8l-13.1-12.7c-12.4,1.5-22.5,12.1-22.5,28.3s7,26.1,23.2,26.1c8.7.3,16.7-4.7,20.3-12.5l3.8,3.2c-6.7,15.3-17.6,21.3-29.5,21.3-18.3,0-30.7-15.6-30.7-33.8"/>
<path id="Path_3632" d="M513.3,97.6l.5-41.4h-11.9v-8.4h7.9l13.3-16.7h2.9v16.7h21.9v8.4h-21.9v38.5c0,8.7,4,13.3,11.6,13.3,4.4-.1,8.6-1.9,11.8-4.9l2.3,2.7c-3.8,7.1-10.9,11.7-18.9,12.2-11.6,0-19.6-6.3-19.4-20.5"/>
<path id="Path_3633" d="M562.1,110.5c0-4.6,3.8-8.4,8.4-8.4,4.6,0,8.4,3.8,8.4,8.4,0,4.6-3.7,8.3-8.3,8.4-4.6.1-8.4-3.5-8.6-8.1,0-.1,0-.2,0-.3"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,18 +0,0 @@
<svg width="114" height="23" viewBox="0 0 114 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M59.7261 19.7973C59.7261 21.2999 60.3252 22.0063 61.7621 22.0063C63.1991 22.0063 63.7905 21.2999 63.7905 19.7973V16.8824H64.7098V19.5511C64.7098 21.9979 63.6425 22.7617 61.7621 22.7617C59.8817 22.7617 58.8063 21.9979 58.8063 19.5511V16.8824H59.7261V19.7973Z" fill="#C2BFB9"/>
<path d="M88.2206 16.7512C90.0927 16.7513 91.3324 17.5068 91.349 18.6973H90.4045C90.4127 17.9584 89.5177 17.5146 88.2696 17.5146C86.9233 17.5146 86.2004 17.835 86.2004 18.4097C86.2004 19.8713 91.4883 18.3443 91.4883 20.98C91.4881 22.154 90.1824 22.7617 88.2943 22.7617C86.3566 22.7616 85.125 21.9733 85.1494 20.8155H86.0853C86.0771 21.5463 86.9724 21.9978 88.2287 21.9978C89.7391 21.9896 90.5603 21.6778 90.5604 21.0209C90.5604 19.3295 85.2561 20.9633 85.2558 18.5167C85.2558 17.359 86.4717 16.7512 88.2206 16.7512Z" fill="#C2BFB9"/>
<path d="M25.8672 22.3676L28.6832 16.8824H29.8165L26.7865 22.63H24.9393L21.9093 16.8824H23.0426L25.8672 22.3676Z" fill="#C2BFB9"/>
<path d="M37.4926 17.6463H33.174V19.3624H37.4189V20.0602H33.174V21.8666H37.4926V22.63H32.2541V16.8824H37.4926V17.6463Z" fill="#C2BFB9"/>
<path d="M46.0695 21.5134V16.8824H46.9889V22.63H46.0448L41.4633 18.0076V22.63H40.5435V16.8824H41.4794L46.0695 21.5134Z" fill="#C2BFB9"/>
<path d="M56.2693 17.6463H53.3463V22.63H52.427V17.6463H49.5121V16.8824H56.2693V17.6463Z" fill="#C2BFB9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M72.3161 16.8824C73.5067 16.8824 74.1638 17.5231 74.1638 18.4017C74.1637 19.3212 73.4244 19.8468 72.3161 19.8468H71.7081C73.0301 19.937 73.5643 20.758 74.5821 22.63H73.5805C72.5131 20.7169 72.2256 20.1258 71.1338 20.1258H68.7694L68.777 22.63H67.8577V16.8824H72.3161ZM68.7694 19.3376H72.0285C72.8987 19.3376 73.2439 19.0175 73.244 18.5495C73.244 17.9255 72.8827 17.6544 72.0285 17.6544H68.7608L68.7694 19.3376Z" fill="#C2BFB9"/>
<path d="M82.4683 17.6463H78.1492V19.3624H82.3942V20.0602H78.1492V21.8666H82.4683V22.63H77.2299V16.8824H82.4683V17.6463Z" fill="#C2BFB9"/>
<path d="M6.82474 0C10.6948 4.35571e-05 12.5148 1.24639 12.5148 1.24639V3.24338C12.5148 3.24338 10.6948 1.65668 6.9683 1.65663C4.07267 1.65663 2.17477 2.33495 2.17477 3.61749C2.17519 7.145 13.5522 3.35015 13.5525 9.15639C13.5525 11.688 10.9865 13.041 6.95119 13.041C2.69336 13.041 0.0167388 11.2525 0 11.2413V9.33892C0 9.33892 3.01305 11.2367 6.75677 11.2385C9.99295 11.2385 11.5455 10.7091 11.5455 9.2486C11.5453 5.47073 0.14838 9.46271 0.148312 3.86753C0.148312 1.33587 3.03945 0 6.82474 0Z" fill="#C2BFB9"/>
<path d="M113.4 1.95088H103.178V5.3516H111.851V7.061H103.178V11.1101H113.4V12.7677H101.186V0.293297H113.4V1.95088Z" fill="#C2BFB9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.5959 0.293297C44.1736 0.293325 45.7748 1.89222 45.7748 3.82142C45.7721 6.16157 43.9928 7.33195 41.5931 7.33195H35.2204V12.7667H33.2476V0.293297H41.5959ZM35.2175 5.62256H41.1476C42.9965 5.62256 43.7788 5.08577 43.7788 3.80954C43.7796 2.53432 42.9973 1.96799 41.1476 1.96799H35.2175V5.62256Z" fill="#C2BFB9"/>
<path d="M65.2223 5.34162H74.1286V0.284741H76.1208V12.7577H74.1286V6.99778H65.2223V12.7577H63.2295V0.284741H65.2223V5.34162Z" fill="#C2BFB9"/>
<path d="M81.6388 12.7577H79.6466V0.284741H81.6388V12.7577Z" fill="#C2BFB9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M94.0372 0.270955C96.6238 0.270955 98.0511 1.66293 98.0511 3.57138C98.051 5.56913 96.3736 6.71066 94.0372 6.71066L92.7171 6.72872V6.7639C95.589 6.95987 96.7305 8.69061 98.9424 12.7577H96.7125C94.3941 8.60131 93.5165 7.31681 91.1617 7.31674H87.1444L87.1611 12.7577H85.1627V0.270955H94.0372ZM87.1444 5.6045H93.4135C95.304 5.60445 96.0536 4.90875 96.0536 3.89177C96.0536 2.53619 95.2684 1.94711 93.4135 1.94707H87.1264L87.1444 5.6045Z" fill="#C2BFB9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.5869 0.284741C59.1648 0.284762 60.7658 1.88318 60.7658 3.81239C60.7633 6.15432 58.9847 7.32241 56.584 7.32245H50.2113V12.7568H48.2386V0.284741H56.5869ZM50.2113 5.61495H56.1415C57.9902 5.61495 58.7726 5.07773 58.7726 3.80241C58.7726 2.52713 57.9902 1.96087 56.1415 1.96086H50.2113V5.61495Z" fill="#C2BFB9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.3638 12.7506H29.1187L27.2182 9.18919H18.8124L16.9076 12.7506H14.6458L17.5712 7.56773V7.55632H17.5783L21.6874 0.284741H24.3399L31.3638 12.7506ZM19.6847 7.55632H26.3468L23.0203 1.31675L19.6847 7.55632Z" fill="#C2BFB9"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -1,4 +0,0 @@
WEBVTT
00:00:00.000 --> 00:00:06.000
AI-generated video showcasing Grok Imagine image generation capabilities

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

View File

@@ -1,4 +0,0 @@
WEBVTT
00:00:00.000 --> 00:00:05.000
AI-generated video showcasing Seedance 2.0 video generation capabilities

View File

@@ -1,4 +0,0 @@
WEBVTT
00:00:00.000 --> 00:00:05.000
AI-generated video showcasing Wan 2.2 image-to-video generation capabilities

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -1,110 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 900" fill="none">
<!-- Background geometric lines -->
<g stroke="#49378B" stroke-width="1.5" fill="none" opacity="0.4">
<!-- Outer hexagonal frame layers -->
<path d="M400 80 L600 200 L600 440 L400 560 L200 440 L200 200 Z" />
<path d="M400 120 L570 220 L570 420 L400 520 L230 420 L230 220 Z" />
<!-- Connector lines going up -->
<line x1="300" y1="160" x2="300" y2="60" />
<line x1="400" y1="120" x2="400" y2="20" />
<line x1="500" y1="160" x2="500" y2="60" />
<!-- Bottom platform layers -->
<path d="M250 520 L550 520 L600 560 L600 600 L400 700 L200 600 L200 560 Z" opacity="0.3" />
<path d="M280 620 L520 620 L560 650 L560 680 L400 760 L240 680 L240 650 Z" opacity="0.2" />
<path d="M320 700 L480 700 L510 720 L510 740 L400 800 L290 740 L290 720 Z" opacity="0.15" />
</g>
<!-- 3D Isometric cube cluster -->
<g transform="translate(400, 380)">
<!-- Back layer cubes (purple/dark) -->
<!-- Top back -->
<g transform="translate(0, -100)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<!-- Middle row - left back -->
<g transform="translate(-70, -55)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<!-- Middle row - right back -->
<g transform="translate(70, -55)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<!-- Yellow accent cubes - front facing -->
<!-- Top -->
<g transform="translate(0, -65)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
</g>
<!-- Middle left yellow -->
<g transform="translate(-70, -20)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
</g>
<!-- Middle right yellow -->
<g transform="translate(70, -20)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
</g>
<!-- Center purple -->
<g transform="translate(0, -20)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<!-- Bottom row -->
<g transform="translate(-70, 25)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<g transform="translate(70, 25)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
</g>
<!-- Front bottom yellow -->
<g transform="translate(0, 25)">
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
</g>
<!-- Outer corner yellow accents -->
<g transform="translate(-105, 5)">
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
<polygon points="0,-2 20,-12 20,12 0,25" fill="#d4e04e" />
</g>
<g transform="translate(105, 5)">
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
<polygon points="0,-2 -20,-12 -20,12 0,25" fill="#e0ec50" />
</g>
<g transform="translate(0, -135)">
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
</g>
</g>
<!-- Bottom arrow/chevron shape -->
<path d="M340 780 L400 820 L460 780 L460 850 L400 890 L340 850 Z" fill="#211927" stroke="#49378B" stroke-width="1" opacity="0.5" />
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 866 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg">
<g id="title404" fill="currentColor" transform="matrix(0.9346 -0.02737 -0.00901 1.0422 -597.31 -135.88)">
<path d="M1436.44 522.556V439.749L1462.05 454.536C1478.8 464.21 1489.92 479.348 1489.92 505.313V518.746C1489.92 544.611 1478.72 546.966 1462.05 537.342L1436.44 522.556ZM1442.77 519.899L1461.87 530.926C1477.15 539.748 1483.32 535.087 1483.32 515.137V501.303C1483.32 481.253 1477.15 469.573 1461.87 460.751L1442.77 449.724V519.899Z"/>
<path d="M1369.77 484.067V401.261L1376.02 404.87L1416.65 498.303V428.328L1422.73 431.837V514.643L1416.48 511.034L1375.85 417.601V487.576L1369.77 484.067Z"/>
<path d="M1330.66 462.491C1311.56 451.464 1304.62 438.231 1304.62 417.68V363.645L1311.04 367.354V421.69C1311.04 440.737 1318.34 448.857 1330.66 455.975C1342.99 463.093 1350.2 463.343 1350.2 444.296V389.96L1356.62 393.67V447.704C1356.62 468.757 1349.24 473.218 1330.66 462.491Z"/>
<path d="M1265.37 424.794C1245.75 413.466 1236.11 392.263 1236.11 372.614V358.278C1236.11 338.83 1245.75 328.654 1265.37 339.983C1285.08 351.361 1294.72 372.664 1294.72 392.113V406.448C1294.72 426.097 1285.08 436.173 1265.37 424.794ZM1242.71 376.724C1242.71 394.068 1250.53 409.506 1265.37 418.077C1280.3 426.699 1288.12 420.283 1288.12 402.94V388.002C1288.12 370.96 1280.3 355.321 1265.37 346.699C1250.53 338.128 1242.71 344.745 1242.71 361.787V376.724Z"/>
<path d="M1187.24 378.685V295.879L1231.78 321.593V328.009L1193.58 305.954V341.643L1229.79 362.545V368.961L1193.58 348.059V382.344L1187.24 378.685Z"/>
<path d="M1129.17 345.153V268.763L1106.33 255.58V249.164L1158.34 279.189V285.605L1135.5 272.422V348.812L1129.17 345.153Z"/>
<path d="M1076.63 315.825C1057.01 304.497 1047.37 283.294 1047.37 263.645V249.309C1047.37 229.861 1057.01 219.685 1076.63 231.014C1096.34 242.392 1105.98 263.695 1105.98 283.144V297.479C1105.98 317.128 1096.34 327.204 1076.63 315.825ZM1053.97 267.755C1053.97 285.099 1061.79 300.537 1076.63 309.108C1091.56 317.73 1099.38 311.314 1099.38 293.971V279.033C1099.38 261.991 1091.56 246.352 1076.63 237.73C1061.79 229.159 1053.97 235.776 1053.97 252.818V267.755Z"/>
<path d="M983.922 261.297V178.49L990.173 182.099L1030.8 275.532V205.558L1036.88 209.066V291.873L1030.63 288.264L989.999 194.831V264.805L983.922 261.297Z"/>
<path d="M905.279 215.892V133.086L949.557 158.65V165.066L911.617 143.161V174.94L947.994 195.943V202.359L911.617 181.356V213.135L949.557 235.04V241.456L905.279 215.892Z"/>
<path d="M864.552 193.381C846.059 182.704 836.422 161.702 836.422 141.852V127.517C836.422 107.767 845.972 97.8425 865.507 109.121C883.131 119.296 892.855 137.642 893.723 154.083L893.81 158.243L887.038 154.333V149.822C886.343 135.687 878.356 123.156 865.68 115.837C850.313 106.965 842.934 113.832 842.934 130.875V145.912C842.934 163.356 850.4 178.594 865.073 187.065C878.096 194.584 886.864 191.225 887.125 175.436V170.123L860.732 154.885V148.669L893.81 167.767V209.27L889.208 206.614L888.601 191.426C884.52 198.193 876.706 200.398 864.552 193.381Z"/>
<path d="M777.479 142.107L801.615 73.2351L809.515 77.7965L833.738 174.588L826.966 170.678L821.93 150.027L789.286 131.18L784.251 146.017L777.479 142.107ZM791.11 125.816L820.107 142.558L805.869 84.2124L805.522 83.0094L805.261 83.8616L791.11 125.816Z"/>
<path d="M727.161 113.056V30.2494L756.853 47.3921C772.047 56.164 779.513 69.2967 779.513 86.9407V88.1437C779.513 105.687 772.047 110.6 756.853 101.828L733.499 88.3442V116.715L727.161 113.056ZM733.499 82.0285L756.419 95.2615C767.792 101.828 772.915 98.67 772.915 84.7352V82.63C772.915 68.7955 767.792 60.0236 756.419 53.4572L733.499 40.2242V82.0285Z"/>
<path d="M582.85 5.5V67.98L592.376 62.48V72.38L582.85 77.88V96.36L572.942 102.08V83.6L532.075 107.195V96.855L573.133 11.11L582.85 5.5ZM541.601 91.795L572.942 73.7V27.61L541.601 91.795Z"/>
<path d="M495.102 148.121C475.096 159.671 464.903 150.816 464.903 126.286V111.546C464.903 87.2364 475.192 66.5564 495.102 55.0614C514.631 43.7864 524.824 52.6414 524.824 76.9514V91.6914C524.824 116.221 514.631 136.846 495.102 148.121ZM475.001 121.556C475.001 127.936 475.763 132.666 477.287 136.076L509.296 63.2564C506.057 60.8364 501.294 61.3864 495.102 64.9614C481.765 72.6614 475.001 86.0264 475.001 104.726V121.556ZM480.812 140.311C484.146 142.456 488.91 141.796 495.102 138.221C508.057 130.741 514.726 117.431 514.726 98.5114V81.6814C514.726 75.7414 514.059 71.1764 512.725 67.8764L480.812 140.311Z"/>
<path d="M447.443 83.6773V146.157L456.969 140.657V150.557L447.443 156.057V174.537L437.535 180.257V161.777L396.668 185.372V175.032L437.726 89.2873L447.443 83.6773ZM406.194 169.972L437.535 151.877V105.787L406.194 169.972Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

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