Compare commits

..

10 Commits

Author SHA1 Message Date
pythongosssss
a483a207a9 feat(app-mode): order get-started templates from a remote flag
- Add remoteUserData module: PostHog feature-flag JSON payloads with
  snapshot/reactive reads, schema validation, and default fallback
- Wire the app-mode-template-order flag into LinearGetStarted via
  resolvePrioritizedIds; gate cards on readiness to avoid reorder flash
- Register the payload source from PostHogTelemetryProvider; add a 3s
  readiness backstop and reload flags once auth settles anonymous
- Cover with unit + e2e tests and document usage in docs/REMOTE_USER_DATA.md
2026-07-08 08:24:00 -07:00
pythongosssss
e0300c62a1 chore: revert hand-edited non-English locale files
- Restore the 12 non-en main.json files; only en/main.json should be
  edited by hand, CI regenerates the rest from it
2026-07-07 07:37:41 -07:00
pythongosssss
5344f0f1f5 remove gradient background, add dark canvas color
restructure empty state panels + add border for contrast
2026-07-06 14:10:24 -07:00
pythongosssss
ac713c2699 refactor: address app mode landing review feedback
- Extract pure template helpers from useTemplateWorkflows into templateUtil
- Disable get-started actions while a template loads, toast on failure
- Open workflow import via Comfy.OpenWorkflow command
- Collapse duplicated welcome card markup into a single shell
- Replace off-scale arbitrary Tailwind values with scale utilities
- Test ready-to-run state, no-app-template fallback, load guard, and helpers
- Add e2e covering featured template load
2026-07-02 13:05:58 -07:00
pythongosssss
9dde0c193d feat: polish App mode landing states
- Style built-app message as a card ("Your app is ready to run", play icon)
- Solid bg on build-prompt card; radial-gradient bg on #linearCenterPanel
- Splitter bg -> base-background (theme-aware)
- Refresh welcome i18n copy; prune stale welcome keys across locales
2026-07-02 09:45:27 -07:00
pythongosssss
5da6a0389e update bg 2026-07-02 07:55:40 -07:00
pythongosssss
6dc61c6ddb remove 2026-07-02 07:50:15 -07:00
pythongosssss
44d066d134 remove conflicting bgs 2026-07-02 07:35:16 -07:00
pythongosssss
2c638d67c3 remove bg color change 2026-07-02 06:40:07 -07:00
pythongosssss
1c1c257f92 feat: redesign App mode empty-graph landing
- Empty graph: new "Get started with Apps" page (templates, import, discover)
- Populated: "Make this workflow an App" build-prompt card
- Share template source/app/thumbnail helpers via useTemplateWorkflows
- Drop unused welcome i18n keys and back-to-workflow button
- Update e2e selectors/specs; add unit tests
2026-07-01 11:25:19 -07:00
300 changed files with 2530 additions and 4500 deletions

86
.claude/settings.json Normal file
View File

@@ -0,0 +1,86 @@
{
"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

@@ -134,27 +134,6 @@ 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

@@ -121,7 +121,7 @@ jobs:
--title "ComfyUI E2E Coverage" \
--no-function-coverage \
--precision 1 \
--ignore-errors source,unmapped \
--ignore-errors source,unmapped,range \
--synthesize-missing
- name: Upload HTML report artifact

View File

@@ -95,7 +95,6 @@ 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

@@ -55,3 +55,6 @@ jobs:
flags: unit
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
- name: Enforce critical coverage gate
run: pnpm test:coverage:critical

View File

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

View File

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

View File

@@ -32,13 +32,12 @@ jobs:
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
(github.event_name != 'pull_request' ||
(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'))))))
(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,7 +21,6 @@ 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') ||

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: 58 KiB

View File

@@ -1,3 +1,3 @@
<svg width="20" height="32" viewBox="0 0 20 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 32V0C20 5.39616 15.5172 9.78053 10 9.78053C4.48276 9.78053 0 5.416 0 0V32C0 26.6038 4.48276 22.2195 10 22.2195C15.5172 22.2195 20 26.6038 20 32Z" fill="#F2FF59"/>
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 20 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M20 32V0C20 5.39616 15.5172 9.78053 10 9.78053C4.48276 9.78053 0 5.416 0 0V32C0 26.6038 4.48276 22.2195 10 22.2195C15.5172 22.2195 20 26.6038 20 32Z" fill="var(--fill-0, #F2FF59)"/>
</svg>

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 380 B

View File

@@ -56,7 +56,7 @@ const columnClass: Record<ColumnCount, string> = {
<template>
<section class="max-w-9xl mx-auto px-6 py-16 lg:py-24">
<SectionHeader max-width="xl" :label="eyebrow" align="start">
<SectionHeader :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">

View File

@@ -38,8 +38,7 @@ const topColumns: { title: string; links: FooterLink[] }[] = [
{ label: t('nav.comfyCloud', locale), href: routes.cloud },
{ label: t('nav.comfyApi', locale), href: routes.api },
{ label: t('nav.comfyEnterprise', locale), href: routes.cloudEnterprise },
{ label: t('nav.mcpServer', locale), href: routes.mcp },
{ label: t('nav.supportedModels', locale), href: routes.models }
{ label: t('nav.mcpServer', locale), href: routes.mcp }
]
},
{

View File

@@ -33,41 +33,36 @@ useHeroAnimation({
</script>
<template>
<section
ref="sectionRef"
class="px-4 py-20 lg:flex lg:gap-16 lg:px-20 lg:py-24"
>
<section ref="sectionRef" class="px-4 py-20 lg:flex lg:px-20 lg:py-24">
<!-- Left column: intro + image -->
<div class="lg:w-1/2">
<div class="lg:max-w-xl">
<SectionLabel ref="badgeRef">
{{ t(tk('badge'), locale) }}
</SectionLabel>
<SectionLabel ref="badgeRef">
{{ t(tk('badge'), locale) }}
</SectionLabel>
<h1
ref="headingRef"
class="mt-4 text-3xl font-light whitespace-pre-line text-primary-comfy-canvas lg:text-5xl"
>
{{ t(tk('heading'), locale) }}
</h1>
<h1
ref="headingRef"
class="text-primary-comfy-canvas mt-4 text-3xl font-light whitespace-pre-line lg:text-5xl"
>
{{ t(tk('heading'), locale) }}
</h1>
<div ref="descRef">
<p class="mt-4 text-sm text-primary-comfy-canvas">
{{ t(tk('description'), locale) }}
</p>
<div ref="descRef">
<p class="text-primary-comfy-canvas mt-4 text-sm">
{{ t(tk('description'), locale) }}
</p>
<p class="mt-4 text-sm text-primary-comfy-canvas">
{{ t(tk('supportLink'), locale) }}
<a
href="https://docs.comfy.org/"
target="_blank"
rel="noopener noreferrer"
class="text-primary-comfy-yellow underline"
>
{{ t(tk('supportLinkCta'), locale) }}
</a>
</p>
</div>
<p class="text-primary-comfy-canvas mt-4 text-sm">
{{ t(tk('supportLink'), locale) }}
<a
href="https://docs.comfy.org/"
target="_blank"
rel="noopener noreferrer"
class="text-primary-comfy-yellow underline"
>
{{ t(tk('supportLinkCta'), locale) }}
</a>
</p>
</div>
<div ref="imageRef" class="mt-8 overflow-hidden rounded-2xl lg:-ml-20">

View File

@@ -15,7 +15,7 @@ const { categories } = defineProps<{
const activeSection = ref(categories[0]?.value ?? '')
const HEADER_OFFSET_PX = -144
const HEADER_OFFSET = -144
const BOTTOM_THRESHOLD_PX = 4
const SCROLL_SAFETY_MS = 1500
@@ -52,7 +52,7 @@ function scrollToSection(id: string) {
const el = document.getElementById(id)
if (el) {
scrollTo(el, {
offset: HEADER_OFFSET_PX,
offset: HEADER_OFFSET,
duration: 0.8,
immediate: prefersReducedMotion(),
onComplete: clearScrollLock

View File

@@ -1,5 +1,5 @@
<li
class="flex items-start gap-2 text-primary-comfy-canvas before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-primary-comfy-yellow"
class="flex items-start gap-2 text-primary-comfy-canvas before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-primary-comfy-yellow before:content-['']"
>
<slot />
</li>

View File

@@ -40,13 +40,13 @@ export function getMainNavigation(locale: Locale): NavItem[] {
{
label: t('nav.products', locale),
featured: {
imageSrc: 'https://media.comfy.org/website/nav/mcp-card.webp',
imageSrc: 'https://media.comfy.org/website/nav/featured-model-card.jpg',
imageAlt: t('nav.featuredProductsAlt', locale),
title: t('nav.featuredProductsTitle', locale),
cta: {
label: t('cta.getStarted', locale),
label: t('cta.tryWorkflow', locale),
ariaLabel: t('nav.featuredProductsCtaAria', locale),
href: routes.mcp
href: 'https://comfy.org/workflows/api_seedance2_0_r2v-64f4db9e3e33/'
}
},
columns: [
@@ -82,7 +82,6 @@ export function getMainNavigation(locale: Locale): NavItem[] {
href: routes.launches,
badge: 'new'
},
{ label: t('nav.supportedModels', locale), href: routes.models },
{
label: t('nav.docs', locale),
href: externalLinks.docs,

View File

@@ -26,10 +26,6 @@ const translations = {
en: 'Try Workflow',
'zh-CN': '试用工作流'
},
'cta.getStarted': {
en: 'GET STARTED',
'zh-CN': '快速开始'
},
'cta.watchNow': {
en: 'Watch Now',
'zh-CN': '立即观看'
@@ -2187,7 +2183,6 @@ const translations = {
'nav.badgeNew': { en: 'NEW', 'zh-CN': '新' },
// Column headers used in HeaderMainDesktop dropdowns
'nav.mcpServer': { en: 'Comfy MCP', 'zh-CN': 'Comfy MCP' },
'nav.supportedModels': { en: 'Supported Models', 'zh-CN': '支持的模型' },
'nav.colFeatures': { en: 'Features', 'zh-CN': '功能' },
'nav.colPrograms': { en: 'Programs', 'zh-CN': '项目' },
'nav.colConnect': { en: 'Connect', 'zh-CN': '联系' },
@@ -2201,16 +2196,16 @@ const translations = {
// Featured dropdown cards — keys are keyed by parent nav item, not card content,
// so the copy can be swapped without renaming the key.
'nav.featuredProductsTitle': {
en: 'NEW: COMFY MCP',
'zh-CN': '全新发布:Comfy MCP'
en: 'New Release: Seedance 2.0',
'zh-CN': '全新发布:Seedance 2.0'
},
'nav.featuredProductsAlt': {
en: 'Comfy MCP feature image',
'zh-CN': 'Comfy MCP 精选图片'
en: 'Seedance 2.0 release feature image',
'zh-CN': 'Seedance 2.0 发布精选图片'
},
'nav.featuredProductsCtaAria': {
en: 'Get started with Comfy MCP',
'zh-CN': '开始使用 Comfy MCP'
en: 'Try the Seedance 2.0 workflow',
'zh-CN': '试用 Seedance 2.0 工作流'
},
'nav.featuredCommunityTitle': {
en: 'Sky Replacement',

View File

@@ -537,6 +537,7 @@ export const comfyPageFixture = base.extend<{
'Comfy.TutorialCompleted': true,
'Comfy.Queue.MaxHistoryItems': 64,
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
'Comfy.VueNodes.AutoScaleLayout': false,
// Disable toast warning about version compatibility, as they may or
// may not appear - depending on upstream ComfyUI dependencies
'Comfy.VersionCompatibility.DisableWarnings': true,

View File

@@ -42,16 +42,16 @@ export class AppModeHelper {
public readonly imagePickerPopover: Locator
/** The Run button in the app mode footer. */
public readonly runButton: Locator
/** The welcome screen shown when app mode has no outputs or no nodes. */
/** The welcome card shown when the graph has nodes or outputs (build prompt / ready to run). */
public readonly welcome: Locator
/** The empty workflow message shown when no nodes exist. */
public readonly emptyWorkflowText: Locator
/** The "Build app" button shown when nodes exist but no outputs. */
public readonly buildAppButton: Locator
/** The "Back to workflow" button on the welcome screen. */
public readonly backToWorkflowButton: Locator
/** The "Load template" button shown when no nodes exist. */
public readonly loadTemplateButton: Locator
/** The get started page shown when the graph is empty. */
public readonly getStarted: Locator
/** The "Discover all templates" button on the get started page. */
public readonly getStartedDiscoverButton: Locator
/** Featured template cards on the get started page. */
public readonly getStartedTemplateCards: Locator
/** The cancel button for an in-progress run in the output history. */
public readonly cancelRunButton: Locator
/** Arrange-step placeholder shown when outputs are configured but no run has happened. */
@@ -111,15 +111,13 @@ export class AppModeHelper {
.getByTestId(TestIds.linear.runButton)
.getByRole('button', { name: /run/i })
this.welcome = this.page.getByTestId(TestIds.appMode.welcome)
this.emptyWorkflowText = this.page.getByTestId(
TestIds.appMode.emptyWorkflow
)
this.buildAppButton = this.page.getByTestId(TestIds.appMode.buildApp)
this.backToWorkflowButton = this.page.getByTestId(
TestIds.appMode.backToWorkflow
this.getStarted = this.page.getByTestId(TestIds.appMode.getStarted)
this.getStartedDiscoverButton = this.page.getByTestId(
TestIds.appMode.getStartedDiscover
)
this.loadTemplateButton = this.page.getByTestId(
TestIds.appMode.loadTemplate
this.getStartedTemplateCards = this.page.getByTestId(
TestIds.appMode.getStartedTemplate
)
this.cancelRunButton = this.page.getByTestId(
TestIds.outputHistory.cancelRun
@@ -236,6 +234,13 @@ export class AppModeHelper {
await this.toggleAppMode()
}
/** Featured template `name`s in the order the cards are rendered. */
async getStartedTemplateNames(): Promise<string[]> {
return this.getStartedTemplateCards.evaluateAll((cards) =>
cards.map((card) => card.getAttribute('data-template-name') ?? '')
)
}
/**
* Get the actions menu trigger for a widget in the app mode widget list.
* @param widgetName Text shown in the widget label (e.g. "seed").

View File

@@ -6,10 +6,6 @@ import type {
} from '@/platform/workflow/templates/types/template'
import { mockTemplateIndex } from '@e2e/fixtures/data/templateFixtures'
const ROUTE_PATTERN_WORKFLOW_TEMPLATES = /\/api\/workflow_templates(?:\?.*)?$/
const ROUTE_PATTERN_TEMPLATE_INDEX = /\/templates\/index\.json(?:\?.*)?$/
const ROUTE_PATTERN_TEMPLATE_THUMBNAILS = /\/templates\/.*\.webp(?:\?.*)?$/
interface TemplateConfig {
readonly templates: readonly TemplateInfo[]
readonly index: readonly WorkflowTemplates[] | null
@@ -45,6 +41,10 @@ export function withTemplates(templates: TemplateInfo[]): TemplateOperator {
export class TemplateHelper {
private templates: TemplateInfo[]
private index: WorkflowTemplates[] | null
private routeHandlers: Array<{
pattern: string
handler: (route: Route) => Promise<void>
}> = []
constructor(
private readonly page: Page,
@@ -64,30 +64,29 @@ export class TemplateHelper {
}
async mock(): Promise<void> {
await this.mockCustomTemplates()
await this.mockIndex()
await this.mockThumbnails()
}
async mockCustomTemplates(): Promise<void> {
async mockIndex(): Promise<void> {
const customTemplatesHandler = async (route: Route) => {
const customTemplates: Record<string, string[]> = {}
await route.fulfill({
status: 200,
body: '{}',
body: JSON.stringify(customTemplates),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
}
})
}
const customTemplatesPattern = '**/api/workflow_templates'
this.routeHandlers.push({
pattern: customTemplatesPattern,
handler: customTemplatesHandler
})
await this.page.route(customTemplatesPattern, customTemplatesHandler)
await this.page.route(
ROUTE_PATTERN_WORKFLOW_TEMPLATES,
customTemplatesHandler
)
}
async mockIndex(): Promise<void> {
const indexHandler = async (route: Route) => {
const payload = this.index ?? mockTemplateIndex(this.templates)
await route.fulfill({
@@ -99,8 +98,9 @@ export class TemplateHelper {
}
})
}
await this.page.route(ROUTE_PATTERN_TEMPLATE_INDEX, indexHandler)
const indexPattern = '**/templates/index.json'
this.routeHandlers.push({ pattern: indexPattern, handler: indexHandler })
await this.page.route(indexPattern, indexHandler)
}
async mockThumbnails(): Promise<void> {
@@ -114,8 +114,12 @@ export class TemplateHelper {
}
})
}
await this.page.route(ROUTE_PATTERN_TEMPLATE_THUMBNAILS, thumbnailHandler)
const thumbnailPattern = '**/templates/**.webp'
this.routeHandlers.push({
pattern: thumbnailPattern,
handler: thumbnailHandler
})
await this.page.route(thumbnailPattern, thumbnailHandler)
}
getTemplates(): TemplateInfo[] {
@@ -125,6 +129,15 @@ export class TemplateHelper {
get templateCount(): number {
return this.templates.length
}
async clearMocks(): Promise<void> {
for (const { pattern, handler } of this.routeHandlers) {
await this.page.unroute(pattern, handler)
}
this.routeHandlers = []
this.templates = []
this.index = null
}
}
export function createTemplateHelper(

View File

@@ -218,10 +218,10 @@ export const TestIds = {
appMode: {
widgetItem: 'app-mode-widget-item',
welcome: 'linear-welcome',
emptyWorkflow: 'linear-welcome-empty-workflow',
buildApp: 'linear-welcome-build-app',
backToWorkflow: 'linear-welcome-back-to-workflow',
loadTemplate: 'linear-welcome-load-template',
getStarted: 'linear-get-started',
getStartedDiscover: 'linear-get-started-discover',
getStartedTemplate: 'linear-get-started-template',
arrangePreview: 'linear-arrange-preview',
arrangeNoOutputs: 'linear-arrange-no-outputs',
arrangeSwitchToOutputs: 'linear-arrange-switch-to-outputs',

View File

@@ -7,6 +7,10 @@ export const templateApiFixture = base.extend<{
templateApi: TemplateHelper
}>({
templateApi: async ({ page }, use) => {
await use(createTemplateHelper(page))
const templateApi = createTemplateHelper(page)
await use(templateApi)
await templateApi.clearMocks()
}
})

View File

@@ -9,14 +9,12 @@ test.describe('App mode welcome states', { tag: '@ui' }, () => {
await comfyPage.appMode.suppressVueNodeSwitchPopup()
})
test('Empty workflow text is visible when no nodes', async ({
comfyPage
}) => {
test('Get started page is visible when no nodes', async ({ comfyPage }) => {
await comfyPage.nodeOps.clearGraph()
await comfyPage.appMode.toggleAppMode()
await expect(comfyPage.appMode.welcome).toBeVisible()
await expect(comfyPage.appMode.emptyWorkflowText).toBeVisible()
await expect(comfyPage.appMode.getStarted).toBeVisible()
await expect(comfyPage.appMode.welcome).toBeHidden()
await expect(comfyPage.appMode.buildAppButton).toBeHidden()
})
@@ -27,39 +25,94 @@ test.describe('App mode welcome states', { tag: '@ui' }, () => {
await expect(comfyPage.appMode.welcome).toBeVisible()
await expect(comfyPage.appMode.buildAppButton).toBeVisible()
await expect(comfyPage.appMode.emptyWorkflowText).toBeHidden()
await expect(comfyPage.appMode.getStarted).toBeHidden()
})
test('Empty workflow and build app are hidden when app has outputs', async ({
test('Get started and build app are hidden when app has outputs', async ({
comfyPage
}) => {
await comfyPage.appMode.enterAppModeWithInputs([['3', 'seed']])
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
await expect(comfyPage.appMode.emptyWorkflowText).toBeHidden()
await expect(comfyPage.appMode.getStarted).toBeHidden()
await expect(comfyPage.appMode.buildAppButton).toBeHidden()
})
test('Back to workflow returns to graph mode', async ({ comfyPage }) => {
await comfyPage.appMode.toggleAppMode()
await expect(comfyPage.appMode.welcome).toBeVisible()
await comfyPage.appMode.backToWorkflowButton.click()
await expect(comfyPage.canvas).toBeVisible()
await expect(comfyPage.appMode.welcome).toBeHidden()
})
test('Load template opens template selector', async ({ comfyPage }) => {
test('Clicking a featured template loads it into the graph', async ({
comfyPage
}) => {
await comfyPage.nodeOps.clearGraph()
await comfyPage.appMode.toggleAppMode()
await expect(comfyPage.appMode.welcome).toBeVisible()
await comfyPage.appMode.loadTemplateButton.click()
await comfyPage.appMode.getStartedTemplateCards.first().click()
await expect(comfyPage.appMode.getStarted).toBeHidden()
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBeGreaterThan(0)
})
test('Discover all templates opens template selector', async ({
comfyPage
}) => {
await comfyPage.nodeOps.clearGraph()
await comfyPage.appMode.toggleAppMode()
await expect(comfyPage.appMode.getStarted).toBeVisible()
await comfyPage.appMode.getStartedDiscoverButton.click()
await expect(comfyPage.templates.content).toBeVisible()
})
test('Remote order flag reorders the featured templates', async ({
comfyPage
}) => {
await comfyPage.nodeOps.clearGraph()
await comfyPage.appMode.toggleAppMode()
await expect(comfyPage.appMode.getStarted).toBeVisible()
const naturalOrder = await comfyPage.appMode.getStartedTemplateNames()
expect(naturalOrder.length).toBeGreaterThan(1)
const reversed = [...naturalOrder].reverse()
await comfyPage.featureFlags.setFlags({
'app-mode-template-order': { templateIds: reversed }
})
// Snapshot mode reads the payload on mount, so remount the page by leaving
// and re-entering app mode.
await comfyPage.appMode.toggleAppMode()
await comfyPage.appMode.toggleAppMode()
await expect(comfyPage.appMode.getStarted).toBeVisible()
await expect
.poll(() => comfyPage.appMode.getStartedTemplateNames())
.toEqual(reversed)
})
test('Invalid remote order payload falls back to the default order', async ({
comfyPage
}) => {
await comfyPage.nodeOps.clearGraph()
await comfyPage.appMode.toggleAppMode()
await expect(comfyPage.appMode.getStarted).toBeVisible()
const naturalOrder = await comfyPage.appMode.getStartedTemplateNames()
expect(naturalOrder.length).toBeGreaterThan(0)
await comfyPage.featureFlags.setFlags({
'app-mode-template-order': { templateIds: 'not-an-array' }
})
await comfyPage.appMode.toggleAppMode()
await comfyPage.appMode.toggleAppMode()
await expect(comfyPage.appMode.getStarted).toBeVisible()
await expect
.poll(() => comfyPage.appMode.getStartedTemplateNames())
.toEqual(naturalOrder)
})
test('Empty workflow dialog blocks entering builder on an empty graph', async ({
comfyPage
}) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -28,12 +28,7 @@ const APP_URL = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
// matches it against the members self-row.
const SELF_EMAIL = 'e2e@test.comfy.org'
// consolidated_billing_enabled routes personal workspaces to the unified
// pricing table asserted here; without it they fall back to the legacy table.
const BOOT_FEATURES = {
team_workspaces_enabled: true,
consolidated_billing_enabled: true
} satisfies RemoteConfig
const BOOT_FEATURES = { team_workspaces_enabled: true } satisfies RemoteConfig
// Disable the experimental Asset API: with it on (cloud default) the unmocked
// asset endpoints 403 and workflow restore throws uncaught, aborting the
// GraphCanvas onMounted chain before the deep-link loader.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 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: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -46,7 +46,6 @@ test.describe('Mask Editor', { tag: '@vue-nodes' }, () => {
{ tag: ['@smoke', '@screenshot'] },
async ({ comfyPage, maskEditor }) => {
const { nodeId } = await maskEditor.loadImageOnNode()
await comfyPage.canvasOps.pan({ x: 0, y: 40 }, { x: 300, y: 300 })
const nodeHeader = comfyPage.vueNodes
.getNodeLocator(nodeId)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -691,8 +691,7 @@ test(
const emptySlotPos = await seedIOSlot.getOpenSlotPosition()
await comfyPage.canvas.hover({ position: emptySlotPos })
await comfyPage.page.mouse.down()
const { width, height } = (await stepsSlot.boundingBox())!
await stepsSlot.hover({ position: { x: (width * 3) / 4, y: height / 2 } })
await stepsSlot.hover()
await expect.poll(hasSnap).toBe(true)
await comfyPage.page.mouse.up()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -17,7 +17,7 @@ test.describe(
'Template distribution filtering count',
{ tag: '@cloud' },
() => {
test.beforeEach(async ({ comfyPage }) => {
test.beforeEach(async ({ comfyPage, templateApi }) => {
await comfyPage.settings.setSetting('Comfy.Templates.SelectedModels', [])
await comfyPage.settings.setSetting(
'Comfy.Templates.SelectedUseCases',
@@ -25,6 +25,8 @@ test.describe(
)
await comfyPage.settings.setSetting('Comfy.Templates.SelectedRunsOn', [])
await comfyPage.settings.setSetting('Comfy.Templates.SortBy', 'default')
await templateApi.mockThumbnails()
})
test('displayed count matches visible cards when distribution filter excludes templates', async ({
@@ -54,7 +56,7 @@ test.describe(
})
])
)
await templateApi.mock()
await templateApi.mockIndex()
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
@@ -99,7 +101,7 @@ test.describe(
})
])
)
await templateApi.mock()
await templateApi.mockIndex()
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
@@ -141,7 +143,7 @@ test.describe(
})
])
)
await templateApi.mock()
await templateApi.mockIndex()
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
@@ -182,7 +184,7 @@ test.describe(
})
])
)
await templateApi.mock()
await templateApi.mockIndex()
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
@@ -220,7 +222,7 @@ test.describe(
})
])
)
await templateApi.mock()
await templateApi.mockIndex()
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1238,7 +1238,7 @@ test(
{ tag: '@vue-nodes' },
async ({ comfyMouse, comfyPage }) => {
async function performDisconnect(slot: Locator, isFast: boolean) {
await comfyMouse.dragElementBy(slot, { x: isFast ? -30 : -80 })
await comfyMouse.dragElementBy(slot, { x: isFast ? -25 : -80 })
if (!isFast) {
await expect(comfyPage.contextMenu.litegraphContextMenu).toBeVisible()
@@ -1251,7 +1251,7 @@ test(
const ksamplerLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
const ksampler = new VueNodeFixture(ksamplerLocator)
await comfyMouse.dragElementBy(ksampler.title, { x: 100 })
await comfyMouse.dragElementBy(ksamplerLocator, { x: 100 })
await test.step('Disconnection with normal links', async () => {
await performDisconnect(ksampler.getSlot('model'), true)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 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: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -234,8 +234,7 @@ test.describe('Vue Node Context Menu', { tag: '@vue-nodes' }, () => {
await comfyPage.page
.context()
.grantPermissions(['clipboard-read', 'clipboard-write'])
await comfyPage.nodeOps.clearGraph()
await comfyPage.searchBoxV2.addNode('Load Image')
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
await comfyPage.vueNodes.waitForNodes(1)
await comfyPage.page
.locator('[data-node-id] img')

View File

@@ -14,8 +14,7 @@ const wstest = mergeTests(test, webSocketFixture)
test.describe('Vue Nodes Image Preview', { tag: '@vue-nodes' }, () => {
async function loadImageOnNode(comfyPage: ComfyPage) {
await comfyPage.nodeOps.clearGraph()
await comfyPage.searchBoxV2.addNode('Load Image')
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
const loadImageNode = (
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -12,14 +12,14 @@ test.describe('Vue Node Moving', { tag: '@vue-nodes' }, () => {
const getHeaderPos = async (
comfyPage: ComfyPage,
title: string
): Promise<{ x: number; y: number }> => {
): Promise<{ x: number; y: number; width: number; height: number }> => {
const box = await comfyPage.vueNodes
.getNodeByTitle(title)
.getByTestId('node-title')
.first()
.boundingBox()
if (!box) throw new Error(`${title} header not found`)
return { x: box.x + box.width / 2, y: box.y + box.height / 2 }
return box
}
const getLoadCheckpointHeaderPos = async (comfyPage: ComfyPage) =>
@@ -84,27 +84,29 @@ test.describe('Vue Node Moving', { tag: '@vue-nodes' }, () => {
await comfyPage.idleFrames(2)
}
test('should allow moving nodes by dragging', async ({
comfyPage,
comfyMouse
}) => {
const initialHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
const node = await comfyPage.vueNodes.getFixtureByTitle('Load Checkpoint')
await comfyMouse.dragElementBy(node.header, { x: 100, y: 100 })
test('should allow moving nodes by dragging', async ({ comfyPage }) => {
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.canvasOps.dragAndDrop(loadCheckpointHeaderPos, {
x: 256,
y: 256
})
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(initialHeaderPos, newHeaderPos)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
})
test('should not move node when pointer moves less than drag threshold', async ({
comfyPage,
comfyMouse
comfyPage
}) => {
const headerPos = await getLoadCheckpointHeaderPos(comfyPage)
// Move only 2px — below the 3px drag threshold in useNodePointerInteractions
const node = await comfyPage.vueNodes.getFixtureByTitle('Load Checkpoint')
await comfyMouse.dragElementBy(node.header, { x: 2, y: 1 })
await comfyPage.page.mouse.move(headerPos.x, headerPos.y)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(headerPos.x + 2, headerPos.y + 1, {
steps: 5
})
await comfyPage.page.mouse.up()
await comfyPage.nextFrame()
const afterPos = await getLoadCheckpointHeaderPos(comfyPage)
@@ -293,12 +295,14 @@ test.describe('Vue Node Moving', { tag: '@vue-nodes' }, () => {
await expect(comfyPage.vueNodes.selectedNodes).toHaveCount(3)
// Re-fetch drag source after clicks in case the header reflowed.
const headerPos = await getHeaderPos(comfyPage, 'Load Checkpoint')
const dragSrc = await getHeaderPos(comfyPage, 'Load Checkpoint')
const centerX = dragSrc.x + dragSrc.width / 2
const centerY = dragSrc.y + dragSrc.height / 2
await comfyPage.page.mouse.move(headerPos.x, headerPos.y)
await comfyPage.page.mouse.move(centerX, centerY)
await comfyPage.page.mouse.down()
await comfyPage.nextFrame()
await comfyPage.page.mouse.move(headerPos.x + dx, headerPos.y + dy, {
await comfyPage.page.mouse.move(centerX + dx, centerY + dy, {
steps: 20
})
await comfyPage.page.mouse.up()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -42,10 +42,7 @@ test.describe('Vue Node Pin', { tag: '@vue-nodes' }, () => {
await expect(pinIndicator2).toBeHidden()
})
test('should not allow dragging pinned nodes', async ({
comfyMouse,
comfyPage
}) => {
test('should not allow dragging pinned nodes', async ({ comfyPage }) => {
const checkpointNodeHeader = comfyPage.page.getByText('Load Checkpoint')
await checkpointNodeHeader.click()
await comfyPage.page.keyboard.press(PIN_HOTKEY)
@@ -53,7 +50,10 @@ test.describe('Vue Node Pin', { tag: '@vue-nodes' }, () => {
// Try to drag the node
const headerPos = await checkpointNodeHeader.boundingBox()
if (!headerPos) throw new Error('Failed to get header position')
await comfyMouse.dragElementBy(checkpointNodeHeader, { x: 256, y: 256 })
await comfyPage.canvasOps.dragAndDrop(
{ x: headerPos.x, y: headerPos.y },
{ x: headerPos.x + 256, y: headerPos.y + 256 }
)
// Verify the node is not dragged (same position before and after click-and-drag)
await expect
@@ -64,7 +64,11 @@ test.describe('Vue Node Pin', { tag: '@vue-nodes' }, () => {
await checkpointNodeHeader.click()
await comfyPage.page.keyboard.press(PIN_HOTKEY)
await comfyMouse.dragElementBy(checkpointNodeHeader, { x: 256, y: 256 })
// Try to drag the node again
await comfyPage.canvasOps.dragAndDrop(
{ x: headerPos.x, y: headerPos.y },
{ x: headerPos.x + 256, y: headerPos.y + 256 }
)
// Verify the node is dragged
await expect

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -5,7 +5,12 @@ import {
test.describe('Widget copy button', { tag: ['@ui', '@vue-nodes'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.searchBoxV2.addNode('Preview as Text')
// Add a PreviewAny node which has a read-only textarea with a copy button
await comfyPage.page.evaluate(() => {
const node = window.LiteGraph!.createNode('PreviewAny')
window.app!.graph.add(node)
})
await comfyPage.vueNodes.waitForNodes()
})

107
docs/REMOTE_USER_DATA.md Normal file
View File

@@ -0,0 +1,107 @@
# Remote User Data (PostHog payloads)
Fetch per-user / per-cohort JSON from PostHog feature-flag payloads to tune UI
behavior (element ordering, which content to surface, survey shape, …) from the
PostHog dashboard **without a frontend release**.
This is for personalization hints only. Every key has a hardcoded default that
must always be shippable — never gate anything critical on a remote value.
## For engineers
### Consuming a value
```ts
import { z } from 'zod'
import { useRemoteUserData } from '@/platform/remoteUserData/useRemoteUserData'
const { data, isLoaded } = useRemoteUserData({
key: 'app-mode-template-order',
schema: z.object({ templateIds: z.array(z.string()) }),
defaultValue: { templateIds: DEFAULT_APP_MODE_TEMPLATE_IDS }
// mode: 'snapshot' (default) | 'reactive'
})
```
- `data` — validated payload, or `defaultValue` if absent/invalid. Never throws.
- `isLoaded` — shared readiness signal. Instantly `true` in OSS/desktop or when
PostHog is disabled; in cloud it is `false` until the first authoritative flag
response (after auth resolves), then `true` forever. Gate UI that must not
render-then-reorder on this (show a skeleton while `false`). A blocked or slow
`/flags` request holds `false` for up to the ~3s backstop, so only gate
reorderable secondary content — render primary, above-the-fold UI on defaults
immediately rather than behind a skeleton.
### Registering a key
Add the flag key to `REMOTE_USER_DATA_KEYS` in
`src/platform/remoteUserData/keys.ts`. Registration is what makes the cloud
provider fetch that key's payload, and keeps the flag inventory greppable.
### snapshot vs reactive
- **`snapshot`** (default) — resolves once (when `isLoaded` flips true, or
immediately if already loaded) then freezes for the instance's lifetime. Later
reloads, including values arriving after the timeout backstop, do not touch it.
Use for anything the user interacts with mid-flow: surveys, welcome tiles,
modal content. A fresh instance (e.g. reopening a flow) takes a fresh snapshot.
- **`reactive`** — tracks every flag reload. Opt-in, for passive UI where a rare
late update is harmless (e.g. sidebar ordering).
Snapshot is the default because it cannot mutate mid-interaction.
### Ordering payloads
For "ordered list of ids" payloads whose ids reference content that ships
separately, resolve through `resolvePrioritizedIds(payloadIds, defaultIds,
validIds, limit)`: it drops ids missing from the registry and backfills from the
defaults, so a stale or typo'd payload can never produce an empty/broken list.
### Segmenting telemetry
Under `snapshot`, a user whose `/flags` response beat the ~3s timeout sees the
targeted config; one whose didn't sees defaults. Give flow-driving payloads a
`version`/`variant` field and attach it to the flow's telemetry events so
analysis segments by what the user actually saw.
### Dev override
In dev builds only:
```js
localStorage.setItem(
'ff:app-mode-template-order',
JSON.stringify({ templateIds: ['flux-schnell'] })
)
localStorage.removeItem('ff:app-mode-template-order')
```
### Constraints
- Values exist only in cloud builds; OSS/desktop always get defaults at zero cost.
- Targeting by cohort/person properties only matches **identified** (logged-in)
users. Anonymous users get percentage rollout at best.
## For marketing / product
You change behavior entirely from the PostHog dashboard — no deploy.
1. **Feature Flags → New feature flag.** Use the exact key an engineer registered
(e.g. `app-mode-template-order`). Kebab-case, describes what it tunes.
2. **Release conditions** — target by cohort, person properties (e.g.
`subscription_tier`, or survey answers already set on the person), or a
percentage rollout. Person-property targeting only reaches logged-in users.
3. **Payload** — add the JSON payload. Edit it anytime; changes take effect on
users' next session without a release.
4. **A/B experiments** — a multivariate flag carries one payload per variant, so
variants can be measured against existing telemetry events.
### Payload shapes per key
| Key | JSON shape | Notes |
| ------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `app-mode-template-order` | `{ "templateIds": ["flux-schnell", "sdxl-turbo", …] }` | Ordered template ids for the app-mode welcome screen. Unknown ids are dropped and the default list backfills, so a typo can't blank the screen. Valid template ids come from the template registry — ask an engineer for the current list. |
Keep every payload valid JSON — a malformed or unexpected payload is ignored and
the user silently gets the shipped default.

25
global.d.ts vendored
View File

@@ -41,29 +41,6 @@ interface GtagFunction {
(...args: unknown[]): void
}
type SyftDataTraits = Record<string, string | number | null | undefined>
interface SyftDataPendingFetch {
args: unknown[]
resolve: (value: unknown) => void
reject: (reason?: unknown) => void
}
interface SyftDataClient {
identify(email: string, traits?: SyftDataTraits): void
signup(email: string, traits?: SyftDataTraits): void
track(event: string, traits?: SyftDataTraits): void
page(...args: unknown[]): void
q?: unknown[][]
fi?: SyftDataPendingFetch[]
fetchID?: (...args: unknown[]) => Promise<unknown>
}
/** Installed by the Syft UMD instead of SyftDataClient when telemetry is opted out */
interface SyftDisabledClient {
enable: () => void
}
interface Window {
__CONFIG__: {
gtm_container_id?: string
@@ -101,8 +78,6 @@ interface Window {
}
dataLayer?: Array<Record<string, unknown>>
gtag?: GtagFunction
syft?: SyftDataClient | SyftDisabledClient
syftc?: { sourceId?: string; enabled?: boolean }
ire_o?: string
ire?: ImpactQueueFunction
rewardful?: RewardfulQueueFunction

View File

@@ -53,6 +53,7 @@
"test:browser:coverage": "cross-env COLLECT_COVERAGE=true pnpm test:browser",
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm test:browser",
"test:coverage": "vitest run --coverage",
"test:coverage:critical": "cross-env COVERAGE_CRITICAL=true vitest run --coverage",
"test:unit": "vitest run",
"typecheck": "vue-tsc --noEmit",
"typecheck:browser": "vue-tsc --project browser_tests/tsconfig.json",

View File

@@ -11,6 +11,7 @@
--color-charcoal-600: #262729;
--color-charcoal-700: #202121;
--color-charcoal-800: #171718;
--color-charcoal-900: #141414;
--color-neutral-550: #636363;

View File

@@ -1,4 +1,10 @@
import { cleanupSVG, importDirectorySync, runSVGO } from '@iconify/tools'
import {
cleanupSVG,
importDirectorySync,
isEmptyColor,
parseColors,
runSVGO
} from '@iconify/tools'
import { resolve } from 'node:path'
export const COMFY_ICON_PREFIX = 'comfy'
@@ -7,6 +13,13 @@ const COMFY_ICONS_DIR = resolve(import.meta.dirname, '../icons')
let cached
/**
* Load the comfy icon folder as a normalized Iconify icon set.
*
* Mirrors the pipeline that `@plugin "@iconify/tailwind4" { from-folder(...) }`
* runs internally so monotone hardcoded colors become `currentColor` and
* outer-svg attributes like `fill="none"` survive the body extraction.
*/
export function loadComfyIconSet() {
if (cached) return cached
const iconSet = importDirectorySync(COMFY_ICONS_DIR)
@@ -19,6 +32,18 @@ export function loadComfyIconSet() {
}
try {
cleanupSVG(svg)
const palette = parseColors(svg)
const colors = palette.colors.filter(
(color) => typeof color === 'string' || !isEmptyColor(color)
)
const totalColors = colors.length + (palette.hasUnsetColor ? 1 : 0)
if (totalColors < 2) {
parseColors(svg, {
defaultColor: 'currentColor',
callback: (_attr, colorStr, color) =>
!color || isEmptyColor(color) ? colorStr : 'currentColor'
})
}
runSVGO(svg)
iconSet.fromSVG(name, svg)
} catch {

View File

@@ -0,0 +1,23 @@
import { getDynamicCSSRules } from '@iconify/tailwind4/lib/plugins/dynamic.js'
import plugin from 'tailwindcss/plugin'
import { COMFY_ICON_PREFIX, loadComfyIconSet } from './comfyIconSet.js'
const SCALE = 1.2
const options = {
iconSets: { [COMFY_ICON_PREFIX]: loadComfyIconSet() },
scale: SCALE
}
export default plugin(({ matchComponents }) => {
matchComponents({
icon: (icon) => {
try {
return getDynamicCSSRules(icon, options)
} catch {
return {}
}
}
})
})

View File

@@ -1,49 +0,0 @@
import { loadIconSet } from '@iconify/tailwind4/lib/helpers/loader.js'
import { getDynamicCSSRules } from '@iconify/tailwind4/lib/plugins/dynamic.js'
import { getIconsCSSData } from '@iconify/utils/lib/css/icons'
import { matchIconName } from '@iconify/utils/lib/icon/name'
import plugin from 'tailwindcss/plugin'
import { COMFY_ICON_PREFIX, loadComfyIconSet } from './comfyIconSet.js'
const SCALE = 1.2
const options = {
iconSets: { [COMFY_ICON_PREFIX]: loadComfyIconSet() },
scale: SCALE
}
function getModeCSSRules(icon: string, mode: 'mask' | 'background') {
const nameParts = icon.split(/--|:/)
if (nameParts.length !== 2) return {}
const [prefix, name] = nameParts
if (!(prefix.match(matchIconName) && name.match(matchIconName))) return {}
const iconSet =
prefix === COMFY_ICON_PREFIX ? loadComfyIconSet() : loadIconSet(prefix)
if (!iconSet) return {}
const generated = getIconsCSSData(iconSet, [name], {
iconSelector: '.icon',
mode
})
if (generated.css.length !== 1) return {}
const size = { width: `${SCALE}em`, height: `${SCALE}em` }
return { ...generated.common?.rules, ...size, ...generated.css[0].rules }
}
export default plugin(({ matchComponents }) => {
matchComponents({
icon: (icon) => {
try {
return getDynamicCSSRules(icon, options)
} catch {
return {}
}
},
'icon-img': (icon) => getModeCSSRules(icon, 'background'),
'icon-mask': (icon) => getModeCSSRules(icon, 'mask')
})
})

View File

@@ -8,12 +8,12 @@
@plugin 'tailwindcss-primeui';
@plugin "./iconifyDynamicPlugin.ts";
@plugin "./iconifyDynamicPlugin.js";
@plugin "./lucideStrokePlugin.js";
/* Safelist dynamic comfy icons for node library folders */
@source inline("icon-[comfy--{ai-model,anthropic,bfl,bria,bytedance,claude,comfy-logo,credits,elevenlabs,extensions-blocks,file-output,gemini,grok,hitpaw,ideogram,image-ai-edit,kling,ltxv,luma,magnific,mask,meshy,minimax,moonvalley-marey,node,openai,pin,pixverse,play,recraft,reve,rodin,runway,sora,stability-ai,template,tencent,topaz,tripo,veo,vidu,wan,wavespeed,workflow,quiver}]");
@source inline("icon-[comfy--{ai-model,anthropic,bfl,bria,bytedance,bytedance-mono,claude,comfy-logo,credits,elevenlabs,extensions-blocks,file-output,gemini,gemini-mono,grok,hitpaw,ideogram,image-ai-edit,kling,ltxv,luma,magnific,mask,meshy,minimax,moonvalley-marey,node,openai,pin,pixverse,play,recraft,reve,rodin,runway,sora,stability-ai,template,tencent,topaz,tripo,veo,vidu,wan,wavespeed,workflow,quiver}]");
@custom-variant touch (@media (hover: none));
@@ -197,7 +197,7 @@
--node-component-executing: var(--color-blue-500);
--node-component-header: var(--fg-color);
--node-component-header-icon: var(--color-ash-800);
--node-component-header-surface: var(--color-smoke-200);
--node-component-header-surface: var(--color-smoke-400);
--node-component-outline: var(--color-black);
--node-component-ring: rgb(from var(--color-smoke-500) r g b / 50%);
--node-component-slot-dot-outline-opacity-mult: 1;
@@ -343,7 +343,7 @@
--node-component-border-executing: var(--color-blue-500);
--node-component-border-selected: var(--color-charcoal-200);
--node-component-header-icon: var(--color-smoke-800);
--node-component-header-surface: var(--color-charcoal-700);
--node-component-header-surface: var(--color-charcoal-800);
--node-component-outline: var(--color-white);
--node-component-ring: rgb(var(--color-smoke-500) / 20%);
--node-component-slot-dot-outline-opacity: 10%;
@@ -457,6 +457,7 @@
);
--color-interface-menu-surface: var(--interface-menu-surface);
--color-interface-menu-stroke: var(--interface-menu-stroke);
--color-interface-canvas-background: var(--color-charcoal-900);
--color-interface-panel-surface: var(--interface-panel-surface);
--color-interface-panel-hover-surface: var(--interface-panel-hover-surface);
--color-interface-panel-selected-surface: var(
@@ -727,14 +728,14 @@ body {
/* Shared markdown content styling for consistent rendering across components */
.comfy-markdown-content {
/* Typography */
font-size: var(--comfy-textarea-font-size);
font-size: 0.875rem; /* text-sm */
line-height: 1.6;
word-wrap: break-word;
}
/* Headings */
.comfy-markdown-content h1 {
font-size: calc(22 / 14 * var(--comfy-textarea-font-size));
font-size: 22px; /* text-[22px] */
font-weight: 700; /* font-bold */
margin-top: 2rem; /* mt-8 */
margin-bottom: 1rem; /* mb-4 */
@@ -745,7 +746,7 @@ body {
}
.comfy-markdown-content h2 {
font-size: calc(18 / 14 * var(--comfy-textarea-font-size));
font-size: 18px; /* text-[18px] */
font-weight: 700; /* font-bold */
margin-top: 2rem; /* mt-8 */
margin-bottom: 1rem; /* mb-4 */
@@ -756,7 +757,7 @@ body {
}
.comfy-markdown-content h3 {
font-size: calc(16 / 14 * var(--comfy-textarea-font-size));
font-size: 16px; /* text-[16px] */
font-weight: 700; /* font-bold */
margin-top: 2rem; /* mt-8 */
margin-bottom: 1rem; /* mb-4 */

View File

@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.30005 7L7.65005 9.5M12 12L12 22M12 12L16.35 9.5M12 12L7.65005 9.5M20.7001 7L16.35 9.5M16.35 9.5V19.8156M16.35 9.5L7.5 4.2699M7.5 4.2699L11 2.2699C11.304 2.09437 11.6489 2.00195 12 2.00195C12.3511 2.00195 12.696 2.09437 13 2.2699L16.5 4.2699L20 6.2699C20.3037 6.44526 20.556 6.69742 20.7315 7.00106C20.9071 7.30471 20.9996 7.64918 21 7.9999V15.9999C20.9996 16.3506 20.9071 16.6951 20.7315 16.9987C20.556 17.3024 20.3037 17.5545 20 17.7299L16.35 19.8156L13 21.7299C12.696 21.9054 12.3511 21.9979 12 21.9979C11.6489 21.9979 11.304 21.9054 11 21.7299L7.65005 19.8156L4 17.7299C3.69626 17.5545 3.44398 17.3024 3.26846 16.9987C3.09294 16.6951 3.00036 16.3506 3 15.9999V7.9999C3.00036 7.64918 3.09294 7.30471 3.26846 7.00106C3.44398 6.69742 3.69626 6.44526 4 6.2699L7.5 4.2699ZM7.65005 9.5V19.8156M7.65005 9.5L16.5 4.2699" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.30005 7L7.65005 9.5M12 12L12 22M12 12L16.35 9.5M12 12L7.65005 9.5M20.7001 7L16.35 9.5M16.35 9.5V19.8156M16.35 9.5L7.5 4.2699M7.5 4.2699L11 2.2699C11.304 2.09437 11.6489 2.00195 12 2.00195C12.3511 2.00195 12.696 2.09437 13 2.2699L16.5 4.2699L20 6.2699C20.3037 6.44526 20.556 6.69742 20.7315 7.00106C20.9071 7.30471 20.9996 7.64918 21 7.9999V15.9999C20.9996 16.3506 20.9071 16.6951 20.7315 16.9987C20.556 17.3024 20.3037 17.5545 20 17.7299L16.35 19.8156L13 21.7299C12.696 21.9054 12.3511 21.9979 12 21.9979C11.6489 21.9979 11.304 21.9054 11 21.7299L7.65005 19.8156L4 17.7299C3.69626 17.5545 3.44398 17.3024 3.26846 16.9987C3.09294 16.6951 3.00036 16.3506 3 15.9999V7.9999C3.00036 7.64918 3.09294 7.30471 3.26846 7.00106C3.44398 6.69742 3.69626 6.44526 4 6.2699L7.5 4.2699ZM7.65005 9.5V19.8156M7.65005 9.5L16.5 4.2699" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1021 B

After

Width:  |  Height:  |  Size: 1014 B

View File

@@ -164,34 +164,6 @@ packages/design-system/src/icons/
No imports needed - icons are auto-discovered!
## Render Mode: Mask vs Image
The default render mode is decided by the SVG's own colors:
- **Mask** — SVG with `currentColor`. The shape is displayed as a background
mask, so it adapts to the theme via `text-*` classes.
- **Image** — the SVG uses concrete colors (e.g. `#d97757`). It is embedded
as an image, preserving its own colors; `text-*` has no effect. Use this for
brand logos that must keep their palette.
```
workflow.svg fill="currentColor" -> mask (themeable)
claude.svg fill="#d97757" -> image (brand color preserved)
```
### Forcing a mode at usage time
```vue
<template>
<i class="icon-[comfy--openai]" />
<!-- default from SVG colors -->
<i class="icon-img-[comfy--openai]" />
<!-- force image, own colors -->
<i class="icon-mask-[comfy--luma]" />
<!-- force themeable mask -->
</template>
```
## Icon Guidelines
### Naming Conventions

View File

@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 17L12.9427 14.9426C12.6926 14.6927 12.3536 14.5522 12 14.5522C11.6464 14.5522 11.3074 14.6927 11.0573 14.9426L5 21M20 15C20.5523 15 21 14.5523 21 14V5C21 4.46957 20.7893 3.96086 20.4142 3.58579C20.0391 3.21071 19.5304 3 19 3H10C9.44772 3 9 3.44772 9 4M17 18C17.5523 18 18 17.5523 18 17V8C18 7.46957 17.7893 6.96086 17.4142 6.58579C17.0391 6.21071 16.5304 6 16 6H7C6.44772 6 6 6.44772 6 7M4.33333 9H13.6667C14.403 9 15 9.59695 15 10.3333V19.6667C15 20.403 14.403 21 13.6667 21H4.33333C3.59695 21 3 20.403 3 19.6667V10.3333C3 9.59695 3.59695 9 4.33333 9ZM8.33333 13C8.33333 13.7364 7.73638 14.3333 7 14.3333C6.26362 14.3333 5.66667 13.7364 5.66667 13C5.66667 12.2636 6.26362 11.6667 7 11.6667C7.73638 11.6667 8.33333 12.2636 8.33333 13Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 17L12.9427 14.9426C12.6926 14.6927 12.3536 14.5522 12 14.5522C11.6464 14.5522 11.3074 14.6927 11.0573 14.9426L5 21M20 15C20.5523 15 21 14.5523 21 14V5C21 4.46957 20.7893 3.96086 20.4142 3.58579C20.0391 3.21071 19.5304 3 19 3H10C9.44772 3 9 3.44772 9 4M17 18C17.5523 18 18 17.5523 18 17V8C18 7.46957 17.7893 6.96086 17.4142 6.58579C17.0391 6.21071 16.5304 6 16 6H7C6.44772 6 6 6.44772 6 7M4.33333 9H13.6667C14.403 9 15 9.59695 15 10.3333V19.6667C15 20.403 14.403 21 13.6667 21H4.33333C3.59695 21 3 20.403 3 19.6667V10.3333C3 9.59695 3.59695 9 4.33333 9ZM8.33333 13C8.33333 13.7364 7.73638 14.3333 7 14.3333C6.26362 14.3333 5.66667 13.7364 5.66667 13C5.66667 12.2636 6.26362 11.6667 7 11.6667C7.73638 11.6667 8.33333 12.2636 8.33333 13Z" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 942 B

After

Width:  |  Height:  |  Size: 937 B

View File

@@ -1,6 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1471_12672)">
<path d="M365.047 229.802H310.584L256.118 153.072L86.2157 392.168H140.796L256.115 229.807H310.581L195.261 392.168H249.994L365.047 229.802L512 436.698H470.893V436.7H426.019V392.343L365.047 306.532L304.415 392.178V436.698H163.632L163.629 436.703H109.164L109.167 436.698H-0.105347L256.118 76L365.047 229.802Z" fill="currentColor"/>
<path d="M365.047 229.802H310.584L256.118 153.072L86.2157 392.168H140.796L256.115 229.807H310.581L195.261 392.168H249.994L365.047 229.802L512 436.698H470.893V436.7H426.019V392.343L365.047 306.532L304.415 392.178V436.698H163.632L163.629 436.703H109.164L109.167 436.698H-0.105347L256.118 76L365.047 229.802Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1471_12672">

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 579 B

View File

@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.65 6.36397V5.71397H11.35V6.36397H12H12.65ZM11.35 17.6777C11.35 18.0367 11.641 18.3277 12 18.3277C12.359 18.3277 12.65 18.0367 12.65 17.6777H12H11.35ZM17.6569 14.0049L18.2695 14.222L17.6569 14.0049ZM17.6586 10L18.2714 9.78339L17.6586 10ZM16.6114 15.8388L17.1107 16.255L16.6114 15.8388ZM12.65 20.5C12.65 20.141 12.359 19.85 12 19.85C11.641 19.85 11.35 20.141 11.35 20.5H12H12.65ZM11.35 22C11.35 22.359 11.641 22.65 12 22.65C12.359 22.65 12.65 22.359 12.65 22H12H11.35ZM12.65 2C12.65 1.64101 12.359 1.35 12 1.35C11.641 1.35 11.35 1.64101 11.35 2H12H12.65ZM11.35 3.5C11.35 3.85899 11.641 4.15 12 4.15C12.359 4.15 12.65 3.85899 12.65 3.5H12H11.35ZM20.5 11.35C20.141 11.35 19.85 11.641 19.85 12C19.85 12.359 20.141 12.65 20.5 12.65V12V11.35ZM22 12.65C22.359 12.65 22.65 12.359 22.65 12C22.65 11.641 22.359 11.35 22 11.35V12V12.65ZM2 11.35C1.64101 11.35 1.35 11.641 1.35 12C1.35 12.359 1.64101 12.65 2 12.65V12V11.35ZM3.5 12.65C3.85899 12.65 4.15 12.359 4.15 12C4.15 11.641 3.85899 11.35 3.5 11.35V12V12.65ZM18.47 17.5508C18.2162 17.2969 17.8046 17.2969 17.5508 17.5508C17.2969 17.8046 17.2969 18.2162 17.5508 18.47L18.0104 18.0104L18.47 17.5508ZM18.6114 19.5307C18.8653 19.7845 19.2768 19.7845 19.5307 19.5307C19.7845 19.2768 19.7845 18.8653 19.5307 18.6114L19.0711 19.0711L18.6114 19.5307ZM5.38855 4.46931C5.13471 4.21547 4.72315 4.21547 4.46931 4.46931C4.21547 4.72315 4.21547 5.13471 4.46931 5.38855L4.92893 4.92893L5.38855 4.46931ZM5.52997 6.44921C5.78381 6.70305 6.19537 6.70305 6.44921 6.44921C6.70305 6.19537 6.70305 5.78381 6.44921 5.52997L5.98959 5.98959L5.52997 6.44921ZM6.44921 18.47C6.70305 18.2162 6.70305 17.8046 6.44921 17.5508C6.19537 17.2969 5.78382 17.2969 5.52997 17.5508L5.98959 18.0104L6.44921 18.47ZM4.46931 18.6114C4.21547 18.8653 4.21547 19.2768 4.46931 19.5307C4.72315 19.7845 5.13471 19.7845 5.38855 19.5307L4.92893 19.0711L4.46931 18.6114ZM19.5307 5.38855C19.7845 5.13471 19.7845 4.72315 19.5307 4.46931C19.2768 4.21547 18.8653 4.21547 18.6114 4.46931L19.0711 4.92893L19.5307 5.38855ZM17.5508 5.52997C17.2969 5.78381 17.2969 6.19537 17.5508 6.44921C17.8046 6.70305 18.2162 6.70305 18.47 6.44921L18.0104 5.98959L17.5508 5.52997ZM12 18V17.35C9.04528 17.35 6.65 14.9547 6.65 12H6H5.35C5.35 15.6727 8.32731 18.65 12 18.65V18ZM6 12H6.65C6.65 9.04528 9.04528 6.65 12 6.65V6V5.35C8.32731 5.35 5.35 8.32731 5.35 12H6ZM18 12H17.35C17.35 12.6281 17.242 13.2296 17.0442 13.7878L17.6569 14.0049L18.2695 14.222C18.5161 13.5264 18.65 12.7781 18.65 12H18ZM12 14L11.9994 14.65L17.6563 14.6549L17.6569 14.0049L17.6574 13.3549L12.0006 13.35L12 14ZM12 12H11.35V14H12H12.65V12H12ZM12 12V12.65H18V12V11.35H12V12ZM12 10H11.35V12H12H12.65V10H12ZM17.6586 10L17.0457 10.2166C17.2426 10.7736 17.35 11.3735 17.35 12H18H18.65C18.65 11.2239 18.5168 10.4776 18.2714 9.78339L17.6586 10ZM12 10V10.65H17.6586V10V9.35H12V10ZM12 14H11.35V15.8388H12H12.65V14H12ZM12 15.8388H11.35V17.6777H12H12.65V15.8388H12ZM17.6569 14.0049L17.0442 13.7878C16.8311 14.3891 16.5132 14.9414 16.1121 15.4227L16.6114 15.8388L17.1107 16.255C17.6087 15.6575 18.0041 14.9708 18.2695 14.222L17.6569 14.0049ZM16.6114 15.8388L16.1121 15.4227C15.1297 16.6015 13.6525 17.35 12 17.35V18V18.65C14.0546 18.65 15.8919 17.7175 17.1107 16.255L16.6114 15.8388ZM12 15.8388V16.4888H16.6114V15.8388V15.1888H12V15.8388ZM12 6.36397H11.35V8.18199H12H12.65V6.36397H12ZM12 8.18199H11.35V10H12H12.65V8.18199H12ZM12 6V6.65C13.6612 6.65 15.1452 7.40634 16.1275 8.59587L16.6287 8.18199L17.1299 7.7681C15.9112 6.29234 14.0654 5.35 12 5.35V6ZM16.6287 8.18199L16.1275 8.59587C16.5223 9.07398 16.8353 9.62135 17.0457 10.2166L17.6586 10L18.2714 9.78339C18.0094 9.0421 17.62 8.36161 17.1299 7.7681L16.6287 8.18199ZM12 8.18199V8.83199H16.6287V8.18199V7.53199H12V8.18199ZM12 20.5H11.35V22H12H12.65V20.5H12ZM12 2H11.35V3.5H12H12.65V2H12ZM20.5 12V12.65H22V12V11.35H20.5V12ZM2 12V12.65H3.5V12V11.35H2V12ZM18.0104 18.0104L17.5508 18.47L18.6114 19.5307L19.0711 19.0711L19.5307 18.6114L18.47 17.5508L18.0104 18.0104ZM4.92893 4.92893L4.46931 5.38855L5.52997 6.44921L5.98959 5.98959L6.44921 5.52997L5.38855 4.46931L4.92893 4.92893ZM5.98959 18.0104L5.52997 17.5508L4.46931 18.6114L4.92893 19.0711L5.38855 19.5307L6.44921 18.47L5.98959 18.0104ZM19.0711 4.92893L18.6114 4.46931L17.5508 5.52997L18.0104 5.98959L18.47 6.44921L19.5307 5.38855L19.0711 4.92893Z" fill="currentColor"/>
<path d="M12.65 6.36397V5.71397H11.35V6.36397H12H12.65ZM11.35 17.6777C11.35 18.0367 11.641 18.3277 12 18.3277C12.359 18.3277 12.65 18.0367 12.65 17.6777H12H11.35ZM17.6569 14.0049L18.2695 14.222L17.6569 14.0049ZM17.6586 10L18.2714 9.78339L17.6586 10ZM16.6114 15.8388L17.1107 16.255L16.6114 15.8388ZM12.65 20.5C12.65 20.141 12.359 19.85 12 19.85C11.641 19.85 11.35 20.141 11.35 20.5H12H12.65ZM11.35 22C11.35 22.359 11.641 22.65 12 22.65C12.359 22.65 12.65 22.359 12.65 22H12H11.35ZM12.65 2C12.65 1.64101 12.359 1.35 12 1.35C11.641 1.35 11.35 1.64101 11.35 2H12H12.65ZM11.35 3.5C11.35 3.85899 11.641 4.15 12 4.15C12.359 4.15 12.65 3.85899 12.65 3.5H12H11.35ZM20.5 11.35C20.141 11.35 19.85 11.641 19.85 12C19.85 12.359 20.141 12.65 20.5 12.65V12V11.35ZM22 12.65C22.359 12.65 22.65 12.359 22.65 12C22.65 11.641 22.359 11.35 22 11.35V12V12.65ZM2 11.35C1.64101 11.35 1.35 11.641 1.35 12C1.35 12.359 1.64101 12.65 2 12.65V12V11.35ZM3.5 12.65C3.85899 12.65 4.15 12.359 4.15 12C4.15 11.641 3.85899 11.35 3.5 11.35V12V12.65ZM18.47 17.5508C18.2162 17.2969 17.8046 17.2969 17.5508 17.5508C17.2969 17.8046 17.2969 18.2162 17.5508 18.47L18.0104 18.0104L18.47 17.5508ZM18.6114 19.5307C18.8653 19.7845 19.2768 19.7845 19.5307 19.5307C19.7845 19.2768 19.7845 18.8653 19.5307 18.6114L19.0711 19.0711L18.6114 19.5307ZM5.38855 4.46931C5.13471 4.21547 4.72315 4.21547 4.46931 4.46931C4.21547 4.72315 4.21547 5.13471 4.46931 5.38855L4.92893 4.92893L5.38855 4.46931ZM5.52997 6.44921C5.78381 6.70305 6.19537 6.70305 6.44921 6.44921C6.70305 6.19537 6.70305 5.78381 6.44921 5.52997L5.98959 5.98959L5.52997 6.44921ZM6.44921 18.47C6.70305 18.2162 6.70305 17.8046 6.44921 17.5508C6.19537 17.2969 5.78382 17.2969 5.52997 17.5508L5.98959 18.0104L6.44921 18.47ZM4.46931 18.6114C4.21547 18.8653 4.21547 19.2768 4.46931 19.5307C4.72315 19.7845 5.13471 19.7845 5.38855 19.5307L4.92893 19.0711L4.46931 18.6114ZM19.5307 5.38855C19.7845 5.13471 19.7845 4.72315 19.5307 4.46931C19.2768 4.21547 18.8653 4.21547 18.6114 4.46931L19.0711 4.92893L19.5307 5.38855ZM17.5508 5.52997C17.2969 5.78381 17.2969 6.19537 17.5508 6.44921C17.8046 6.70305 18.2162 6.70305 18.47 6.44921L18.0104 5.98959L17.5508 5.52997ZM12 18V17.35C9.04528 17.35 6.65 14.9547 6.65 12H6H5.35C5.35 15.6727 8.32731 18.65 12 18.65V18ZM6 12H6.65C6.65 9.04528 9.04528 6.65 12 6.65V6V5.35C8.32731 5.35 5.35 8.32731 5.35 12H6ZM18 12H17.35C17.35 12.6281 17.242 13.2296 17.0442 13.7878L17.6569 14.0049L18.2695 14.222C18.5161 13.5264 18.65 12.7781 18.65 12H18ZM12 14L11.9994 14.65L17.6563 14.6549L17.6569 14.0049L17.6574 13.3549L12.0006 13.35L12 14ZM12 12H11.35V14H12H12.65V12H12ZM12 12V12.65H18V12V11.35H12V12ZM12 10H11.35V12H12H12.65V10H12ZM17.6586 10L17.0457 10.2166C17.2426 10.7736 17.35 11.3735 17.35 12H18H18.65C18.65 11.2239 18.5168 10.4776 18.2714 9.78339L17.6586 10ZM12 10V10.65H17.6586V10V9.35H12V10ZM12 14H11.35V15.8388H12H12.65V14H12ZM12 15.8388H11.35V17.6777H12H12.65V15.8388H12ZM17.6569 14.0049L17.0442 13.7878C16.8311 14.3891 16.5132 14.9414 16.1121 15.4227L16.6114 15.8388L17.1107 16.255C17.6087 15.6575 18.0041 14.9708 18.2695 14.222L17.6569 14.0049ZM16.6114 15.8388L16.1121 15.4227C15.1297 16.6015 13.6525 17.35 12 17.35V18V18.65C14.0546 18.65 15.8919 17.7175 17.1107 16.255L16.6114 15.8388ZM12 15.8388V16.4888H16.6114V15.8388V15.1888H12V15.8388ZM12 6.36397H11.35V8.18199H12H12.65V6.36397H12ZM12 8.18199H11.35V10H12H12.65V8.18199H12ZM12 6V6.65C13.6612 6.65 15.1452 7.40634 16.1275 8.59587L16.6287 8.18199L17.1299 7.7681C15.9112 6.29234 14.0654 5.35 12 5.35V6ZM16.6287 8.18199L16.1275 8.59587C16.5223 9.07398 16.8353 9.62135 17.0457 10.2166L17.6586 10L18.2714 9.78339C18.0094 9.0421 17.62 8.36161 17.1299 7.7681L16.6287 8.18199ZM12 8.18199V8.83199H16.6287V8.18199V7.53199H12V8.18199ZM12 20.5H11.35V22H12H12.65V20.5H12ZM12 2H11.35V3.5H12H12.65V2H12ZM20.5 12V12.65H22V12V11.35H20.5V12ZM2 12V12.65H3.5V12V11.35H2V12ZM18.0104 18.0104L17.5508 18.47L18.6114 19.5307L19.0711 19.0711L19.5307 18.6114L18.47 17.5508L18.0104 18.0104ZM4.92893 4.92893L4.46931 5.38855L5.52997 6.44921L5.98959 5.98959L6.44921 5.52997L5.38855 4.46931L4.92893 4.92893ZM5.98959 18.0104L5.52997 17.5508L4.46931 18.6114L4.92893 19.0711L5.38855 19.5307L6.44921 18.47L5.98959 18.0104ZM19.0711 4.92893L18.6114 4.46931L17.5508 5.52997L18.0104 5.98959L18.47 6.44921L19.5307 5.38855L19.0711 4.92893Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M324.094 389.858L284.667 379.567V191.5L326.871 180.816C350.01 174.941 369.446 170.154 370.371 170.339C371.112 170.339 371.667 222.027 371.667 285.326V400.334L367.594 400.15C365.189 400.15 345.566 395.361 324.094 389.835V389.857V389.858Z"/>
<path d="M138.667 343.325C138.667 279.602 139.229 227.339 140.166 227.339C140.914 227.154 160.573 231.998 184.164 237.913L226.667 248.65L226.292 342.975L225.73 437.278L187.535 447.107C166.565 452.463 146.906 457.47 144.097 458.029L138.667 459.334V343.325Z"/>
<path d="M423.667 248.299C423.667 38.7081 423.853 27.4506 427.037 28.3797C428.722 28.9368 445.386 33.1843 463.921 37.8029C482.458 42.6075 500.807 47.2031 504.739 48.1312L511.667 49.9884L511.293 248.67L510.731 447.539L472.722 457.148C451.939 462.486 432.279 467.291 429.284 468.057L423.667 469.334V248.299Z"/>
<path d="M-0.333038 248.845C-0.333038 140.208 0.222275 51.334 1.14852 51.334C1.88822 51.334 21.3242 56.1412 44.4631 61.8769L86.667 72.583V248.66C86.667 345.267 86.296 424.55 85.9262 424.55C85.3709 424.55 65.7494 429.544 42.4262 435.466L-0.333038 446.334V248.823V248.844V248.845Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 18L19.9427 15.9426C19.6926 15.6927 19.3536 15.5522 19 15.5522C18.6464 15.5522 18.3074 15.6927 18.0573 15.9426L12 22M10 17V20.6667C10 21.403 10.597 22 11.3333 22H20.6667C21.403 22 22 21.403 22 20.6667V11.3333C22 10.597 21.403 10 20.6667 10H17M12 13.9666C12 12.9057 11.5786 11.8883 10.8284 11.1381C10.0783 10.388 9.06087 9.96655 8 9.96655C6.93913 9.96655 5.92172 10.388 5.17157 11.1381C4.42143 11.8883 4 12.9057 4 13.9666M5 18L7 20L5 22M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M9.41415 8.04751C10.1952 7.26647 10.1952 6.00014 9.41415 5.21909C8.6331 4.43804 7.36677 4.43804 6.58572 5.21909C5.80467 6.00014 5.80467 7.26647 6.58572 8.04751C7.36677 8.82856 8.6331 8.82856 9.41415 8.04751ZM3.33333 1.96655H12.6667C13.403 1.96655 14 2.56351 14 3.29989V12.6332C14 13.3696 13.403 13.9666 12.6667 13.9666H3.33333C2.59695 13.9666 2 13.3696 2 12.6332V3.29989C2 2.56351 2.59695 1.96655 3.33333 1.96655Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 18L19.9427 15.9426C19.6926 15.6927 19.3536 15.5522 19 15.5522C18.6464 15.5522 18.3074 15.6927 18.0573 15.9426L12 22M10 17V20.6667C10 21.403 10.597 22 11.3333 22H20.6667C21.403 22 22 21.403 22 20.6667V11.3333C22 10.597 21.403 10 20.6667 10H17M12 13.9666C12 12.9057 11.5786 11.8883 10.8284 11.1381C10.0783 10.388 9.06087 9.96655 8 9.96655C6.93913 9.96655 5.92172 10.388 5.17157 11.1381C4.42143 11.8883 4 12.9057 4 13.9666M5 18L7 20L5 22M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M9.41415 8.04751C10.1952 7.26647 10.1952 6.00014 9.41415 5.21909C8.6331 4.43804 7.36677 4.43804 6.58572 5.21909C5.80467 6.00014 5.80467 7.26647 6.58572 8.04751C7.36677 8.82856 8.6331 8.82856 9.41415 8.04751ZM3.33333 1.96655H12.6667C13.403 1.96655 14 2.56351 14 3.29989V12.6332C14 13.3696 13.403 13.9666 12.6667 13.9666H3.33333C2.59695 13.9666 2 13.3696 2 12.6332V3.29989C2 2.56351 2.59695 1.96655 3.33333 1.96655Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 18V20.6667C10 21.403 10.597 22 11.3333 22H20.6667C21.403 22 22 21.403 22 20.6667V11.3333C22 10.597 21.403 10 20.6667 10H18M14 17V18.6667L18 16L16.6579 15.1053M12 13.9666C12 12.9057 11.5786 11.8883 10.8284 11.1381C10.0783 10.388 9.06087 9.96655 8 9.96655C6.93913 9.96655 5.92172 10.388 5.17157 11.1381C4.42143 11.8883 4 12.9057 4 13.9666M5 18L7 20L5 22M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M9.41415 8.04751C10.1952 7.26647 10.1952 6.00014 9.41415 5.21909C8.6331 4.43804 7.36677 4.43804 6.58572 5.21909C5.80467 6.00014 5.80467 7.26647 6.58572 8.04751C7.36677 8.82856 8.6331 8.82856 9.41415 8.04751ZM3.33333 1.96655H12.6667C13.403 1.96655 14 2.56351 14 3.29989V12.6332C14 13.3696 13.403 13.9666 12.6667 13.9666H3.33333C2.59695 13.9666 2 13.3696 2 12.6332V3.29989C2 2.56351 2.59695 1.96655 3.33333 1.96655Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 18V20.6667C10 21.403 10.597 22 11.3333 22H20.6667C21.403 22 22 21.403 22 20.6667V11.3333C22 10.597 21.403 10 20.6667 10H18M14 17V18.6667L18 16L16.6579 15.1053M12 13.9666C12 12.9057 11.5786 11.8883 10.8284 11.1381C10.0783 10.388 9.06087 9.96655 8 9.96655C6.93913 9.96655 5.92172 10.388 5.17157 11.1381C4.42143 11.8883 4 12.9057 4 13.9666M5 18L7 20L5 22M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M9.41415 8.04751C10.1952 7.26647 10.1952 6.00014 9.41415 5.21909C8.6331 4.43804 7.36677 4.43804 6.58572 5.21909C5.80467 6.00014 5.80467 7.26647 6.58572 8.04751C7.36677 8.82856 8.6331 8.82856 9.41415 8.04751ZM3.33333 1.96655H12.6667C13.403 1.96655 14 2.56351 14 3.29989V12.6332C14 13.3696 13.403 13.9666 12.6667 13.9666H3.33333C2.59695 13.9666 2 13.3696 2 12.6332V3.29989C2 2.56351 2.59695 1.96655 3.33333 1.96655Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

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