Compare commits
6 Commits
feature/sh
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfcb336499 | ||
|
|
caabebe145 | ||
|
|
7d3d8ce63f | ||
|
|
506e33e7dd | ||
|
|
7376402fc6 | ||
|
|
be38f14619 |
@@ -33,15 +33,15 @@ Flag:
|
||||
- **New circular entity dependencies** — New circular imports between `LGraph` ↔ `Subgraph`, `LGraphNode` ↔ `LGraphCanvas`, or similar entity classes.
|
||||
- **Direct `graph._version++`** — Mutating the private version counter directly instead of through a public API. Extensions already depend on this side-channel; it must become a proper API.
|
||||
|
||||
### Centralized Registries and ECS-Style Access
|
||||
### Dedicated Stores and Data/Behavior Separation
|
||||
|
||||
All entity data access should move toward centralized query patterns, not instance property access.
|
||||
Entity data lives in dedicated Pinia stores keyed by string IDs (`widgetValueStore`, `domWidgetStore`, `layoutStore`, `nodeOutputStore`, `subgraphNavigationStore`, `previewExposureStore`), not on entity instances.
|
||||
|
||||
Flag:
|
||||
|
||||
- **New instance method/property patterns** — Adding `node.someProperty` or `node.someMethod()` for data that should be a component in the World, queried via `world.getComponent(entityId, ComponentType)`.
|
||||
- **New instance method/property patterns** — Adding `node.someProperty` or `node.someMethod()` for data that belongs in a dedicated store (e.g. widget values → `widgetValueStore` keyed by `WidgetId`).
|
||||
- **OOP inheritance for entity modeling** — Extending entity classes with new subclasses instead of composing behavior through components and systems.
|
||||
- **Scattered state** — New entity state stored in multiple locations (class properties, stores, local variables) instead of being consolidated in the World or in a single store.
|
||||
- **Duplicated authority** — Storing the same entity state in both a class property and a store, or across two stores, so ownership becomes ambiguous. Each piece of state should have one owning store.
|
||||
|
||||
### Extension Ecosystem Impact
|
||||
|
||||
|
||||
2
.github/workflows/cla.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
# Allowlist bots so they don't need to sign (optional, comma-separated).
|
||||
# *[bot] is a catch-all for any GitHub App bot account.
|
||||
allowlist: actions-user,ampagent,claude,comfy-pr-bot,github-actions,*[bot],Glary Bot
|
||||
allowlist: action@github.com,actions-user,ampagent,claude,comfy-pr-bot,GitHub Action,github-actions,Glary Bot,Glary-Bot,*[bot]
|
||||
|
||||
# Custom PR comment messages
|
||||
custom-notsigned-prcomment: |
|
||||
|
||||
5
.github/workflows/pr-backport.yaml
vendored
@@ -67,6 +67,11 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Persist a token with `workflow` scope so the backport push can
|
||||
# include changes to .github/workflows/**. The default GITHUB_TOKEN
|
||||
# is refused by GitHub when a push creates/updates workflow files,
|
||||
# which silently aborted the whole job (see PR #12804 backport).
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
|
||||
@@ -246,7 +246,7 @@ All architectural decisions are documented in `docs/adr/`. Code changes must be
|
||||
### Entity Architecture Constraints (ADR 0003 + ADR 0008)
|
||||
|
||||
1. **Command pattern for all mutations**: Every entity state change must be a serializable, idempotent, deterministic command — replayable, undoable, and transmittable over CRDT. No imperative fire-and-forget mutation APIs. Systems produce command batches, not direct side effects.
|
||||
2. **Centralized registries and ECS-style access**: Entity data lives in the World (centralized registry), queried via `world.getComponent(entityId, ComponentType)`. Do not add new instance properties/methods to entity classes. Do not use OOP inheritance for entity modeling.
|
||||
2. **Dedicated stores over instance state**: Entity data lives in dedicated Pinia stores keyed by string IDs — widget values in `widgetValueStore` keyed by `WidgetId` (`graphId:nodeId:name`, see `src/types/widgetId.ts`), plus `domWidgetStore`, `layoutStore`, `nodeOutputStore`, `subgraphNavigationStore`, and `previewExposureStore`. Prefer a focused store to a single unified registry. Do not add new instance properties/methods to entity classes for data that belongs in a store. Do not use OOP inheritance for entity modeling.
|
||||
3. **No god-object growth**: Do not add methods to `LGraphNode`, `LGraphCanvas`, `LGraph`, or `Subgraph`. Extract to systems, stores, or composables.
|
||||
4. **Plain data components**: ECS components are plain data objects — no methods, no back-references to parent entities. Behavior belongs in systems (pure functions).
|
||||
5. **Extension ecosystem impact**: Changes to entity callbacks (`onConnectionsChange`, `onRemoved`, `onAdded`, `onConnectInput/Output`, `onConfigure`, `onWidgetChanged`), `node.widgets` access, `node.serialize`, or `graph._version++` affect 40+ custom node repos and require migration guidance.
|
||||
|
||||
231
apps/website/e2e/launches.spec.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { externalLinks } from '../src/config/routes'
|
||||
import { drops } from '../src/data/drops'
|
||||
import type { Locale } from '../src/i18n/translations'
|
||||
import { t } from '../src/i18n/translations'
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
const PATH_EN = '/launches'
|
||||
const PATH_ZH = '/zh-CN/launches'
|
||||
const CLOUD_URL = 'https://cloud.comfy.org'
|
||||
|
||||
const LOCALES: ReadonlyArray<readonly [string, Locale]> = [
|
||||
[PATH_EN, 'en'],
|
||||
[PATH_ZH, 'zh-CN']
|
||||
]
|
||||
|
||||
function heroSection(page: Page, locale: Locale) {
|
||||
return page.locator('section').filter({
|
||||
has: page.getByRole('heading', {
|
||||
level: 1,
|
||||
name: t('launches.hero.title', locale)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function ctaSection(page: Page, locale: Locale) {
|
||||
return page.locator('section').filter({
|
||||
has: page.getByRole('heading', {
|
||||
level: 2,
|
||||
name: t('launches.cta.heading', locale)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function dropsSection(page: Page, locale: Locale) {
|
||||
return page.locator('section').filter({
|
||||
has: page.getByRole('heading', {
|
||||
level: 2,
|
||||
name: t('launches.section.title', locale)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Launches landing — desktop @smoke', () => {
|
||||
test('renders the configured title at /launches', async ({ page }) => {
|
||||
await page.goto(PATH_EN)
|
||||
await expect(page).toHaveTitle(t('launches.page.title', 'en'))
|
||||
})
|
||||
|
||||
test('renders the localized title at /zh-CN/launches', async ({ page }) => {
|
||||
await page.goto(PATH_ZH)
|
||||
await expect(page).toHaveTitle(t('launches.page.title', 'zh-CN'))
|
||||
})
|
||||
|
||||
test('is indexable at both locales', async ({ page }) => {
|
||||
await page.goto(PATH_EN)
|
||||
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
|
||||
|
||||
await page.goto(PATH_ZH)
|
||||
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('hero h1 renders the localized title in both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(PATH_EN)
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
level: 1,
|
||||
name: t('launches.hero.title', 'en')
|
||||
})
|
||||
).toBeVisible()
|
||||
|
||||
await page.goto(PATH_ZH)
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
level: 1,
|
||||
name: t('launches.hero.title', 'zh-CN')
|
||||
})
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('hero primary CTA links to /download per locale', async ({ page }) => {
|
||||
for (const [path, locale, expectedHref] of [
|
||||
[PATH_EN, 'en', '/download'],
|
||||
[PATH_ZH, 'zh-CN', '/zh-CN/download']
|
||||
] as const) {
|
||||
await page.goto(path)
|
||||
const primary = heroSection(page, locale).getByRole('link', {
|
||||
name: t('launches.hero.primary', locale)
|
||||
})
|
||||
await expect(primary).toBeVisible()
|
||||
await expect(primary).toHaveAttribute('href', expectedHref)
|
||||
}
|
||||
})
|
||||
|
||||
test('hero secondary CTA opens external Cloud in a new tab on both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
for (const [path, locale] of LOCALES) {
|
||||
await page.goto(path)
|
||||
const secondary = heroSection(page, locale).getByRole('link', {
|
||||
name: t('launches.hero.secondary', locale)
|
||||
})
|
||||
await expect(secondary).toBeVisible()
|
||||
await expect(secondary).toHaveAttribute('href', CLOUD_URL)
|
||||
await expect(secondary).toHaveAttribute('target', '_blank')
|
||||
await expect(secondary).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
}
|
||||
})
|
||||
|
||||
test('closing CTA shows heading and both action buttons in both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
for (const [path, locale] of LOCALES) {
|
||||
await page.goto(path)
|
||||
const section = ctaSection(page, locale)
|
||||
await expect(
|
||||
section.getByRole('heading', {
|
||||
level: 2,
|
||||
name: t('launches.cta.heading', locale)
|
||||
})
|
||||
).toBeVisible()
|
||||
|
||||
const primary = section.getByRole('link', {
|
||||
name: t('launches.cta.primary', locale)
|
||||
})
|
||||
await expect(primary).toBeVisible()
|
||||
await expect(primary).toHaveAttribute('href', externalLinks.cloud)
|
||||
await expect(primary).toHaveAttribute('target', '_blank')
|
||||
await expect(primary).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
|
||||
const secondary = section.getByRole('link', {
|
||||
name: t('launches.cta.secondary', locale)
|
||||
})
|
||||
await expect(secondary).toBeVisible()
|
||||
await expect(secondary).toHaveAttribute('href', externalLinks.workflows)
|
||||
await expect(secondary).toHaveAttribute('target', '_blank')
|
||||
await expect(secondary).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
}
|
||||
})
|
||||
|
||||
test('drops section renders one card per data entry with the correct localized href in both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
for (const [path, locale] of LOCALES) {
|
||||
await page.goto(path)
|
||||
const section = dropsSection(page, locale)
|
||||
|
||||
await expect(
|
||||
section.getByRole('heading', {
|
||||
level: 2,
|
||||
name: t('launches.section.title', locale)
|
||||
})
|
||||
).toBeVisible()
|
||||
|
||||
const cards = section.locator('[data-slot="card"]')
|
||||
await expect(cards).toHaveCount(drops.length)
|
||||
|
||||
for (const [i, drop] of drops.entries()) {
|
||||
const card = cards.nth(i)
|
||||
await expect(card).toContainText(drop.title[locale])
|
||||
const explore = card.getByRole('link', {
|
||||
name: drop.cta.label[locale]
|
||||
})
|
||||
await expect(explore).toBeVisible()
|
||||
await expect(explore).toHaveAttribute('href', drop.cta.href[locale])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('desktop: first 4 drop cards are wider than cards 5+', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(PATH_EN)
|
||||
const cards = dropsSection(page, 'en').locator('[data-slot="card"]')
|
||||
await expect(cards).toHaveCount(drops.length)
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const firstWidth = (await cards.nth(0).boundingBox())?.width ?? 0
|
||||
const fifthWidth = (await cards.nth(4).boundingBox())?.width ?? 0
|
||||
return firstWidth - fifthWidth
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Launches landing — mobile @mobile', () => {
|
||||
test('drops grid stacks in a single column at mobile width', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(PATH_EN)
|
||||
const cards = dropsSection(page, 'en').locator('[data-slot="card"]')
|
||||
await expect(cards).toHaveCount(drops.length)
|
||||
|
||||
const viewport = page.viewportSize()
|
||||
expect(viewport, 'viewport size').not.toBeNull()
|
||||
|
||||
await expect
|
||||
.poll(async () => (await cards.nth(0).boundingBox())?.width ?? 0)
|
||||
.toBeGreaterThanOrEqual(viewport!.width * 0.7)
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const firstBox = await cards.nth(0).boundingBox()
|
||||
const secondBox = await cards.nth(1).boundingBox()
|
||||
if (!firstBox || !secondBox) return false
|
||||
return secondBox.y >= firstBox.y + firstBox.height
|
||||
})
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
test('closing CTA heading stays within viewport width', async ({ page }) => {
|
||||
await page.goto(PATH_EN)
|
||||
const heading = page.getByRole('heading', {
|
||||
level: 2,
|
||||
name: t('launches.cta.heading', 'en')
|
||||
})
|
||||
await heading.scrollIntoViewIfNeeded()
|
||||
await expect(heading).toBeVisible()
|
||||
|
||||
const box = await heading.boundingBox()
|
||||
expect(box, 'CTA heading bounding box').not.toBeNull()
|
||||
const viewport = page.viewportSize()
|
||||
expect(viewport, 'viewport size').not.toBeNull()
|
||||
expect(box!.x + box!.width).toBeLessThanOrEqual(viewport!.width + 1)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
import type { AnchorHTMLAttributes } from 'vue'
|
||||
|
||||
import Button from '../ui/button/Button.vue'
|
||||
import { resolveRel } from '../../utils/cta'
|
||||
|
||||
type Cta = {
|
||||
label: string
|
||||
href: string
|
||||
target?: '_blank' | '_self' | '_parent' | '_top'
|
||||
target?: AnchorHTMLAttributes['target']
|
||||
rel?: AnchorHTMLAttributes['rel']
|
||||
}
|
||||
|
||||
type TermsLink = {
|
||||
@@ -12,10 +16,11 @@ type TermsLink = {
|
||||
href: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
const { heading, primaryCta, secondaryCta, termsLink } = defineProps<{
|
||||
heading: string
|
||||
primaryCta: Cta
|
||||
termsLink: TermsLink
|
||||
secondaryCta?: Cta
|
||||
termsLink?: TermsLink
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -24,23 +29,37 @@ defineProps<{
|
||||
class="max-w-9xl mx-auto flex flex-col items-center px-6 py-16 text-center lg:py-24"
|
||||
>
|
||||
<h2
|
||||
class="text-4xl font-light tracking-tight text-primary-comfy-canvas lg:text-6xl"
|
||||
class="max-w-3xl text-4xl/snug font-light tracking-tight text-pretty text-primary-comfy-canvas lg:text-6xl/snug"
|
||||
>
|
||||
{{ heading }}
|
||||
</h2>
|
||||
|
||||
<BrandButton
|
||||
:href="primaryCta.href"
|
||||
:target="primaryCta.target"
|
||||
:rel="primaryCta.target === '_blank' ? 'noopener noreferrer' : undefined"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="mt-10 px-20 py-4 text-base uppercase lg:mt-12"
|
||||
>
|
||||
{{ primaryCta.label }}
|
||||
</BrandButton>
|
||||
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
|
||||
<Button
|
||||
as="a"
|
||||
:href="primaryCta.href"
|
||||
:target="primaryCta.target"
|
||||
:rel="resolveRel(primaryCta)"
|
||||
variant="default"
|
||||
size="lg"
|
||||
>
|
||||
{{ primaryCta.label }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="secondaryCta"
|
||||
as="a"
|
||||
:href="secondaryCta.href"
|
||||
:target="secondaryCta.target"
|
||||
:rel="resolveRel(secondaryCta)"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
>
|
||||
{{ secondaryCta.label }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-if="termsLink"
|
||||
:href="termsLink.href"
|
||||
class="mt-8 text-sm text-primary-comfy-canvas/70 underline underline-offset-4 transition-colors hover:text-primary-comfy-canvas"
|
||||
>
|
||||
|
||||
166
apps/website/src/components/blocks/HeroLivestream01.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import type { AnchorHTMLAttributes } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useNow } from '@vueuse/core'
|
||||
|
||||
import Button from '../ui/button/Button.vue'
|
||||
import { resolveRel } from '../../utils/cta'
|
||||
|
||||
type Cta = {
|
||||
label: string
|
||||
href: string
|
||||
target?: AnchorHTMLAttributes['target']
|
||||
rel?: AnchorHTMLAttributes['rel']
|
||||
}
|
||||
|
||||
type Visual =
|
||||
| {
|
||||
type: 'image'
|
||||
src: string
|
||||
alt: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
| {
|
||||
type: 'video'
|
||||
src: string
|
||||
alt: string
|
||||
poster?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
const {
|
||||
visual,
|
||||
eyebrow,
|
||||
title,
|
||||
subtitle,
|
||||
primaryCta,
|
||||
secondaryCta,
|
||||
youtubeVideoId,
|
||||
startDateTime,
|
||||
endDateTime
|
||||
} = defineProps<{
|
||||
visual?: Visual
|
||||
eyebrow?: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
primaryCta: Cta
|
||||
secondaryCta?: Cta
|
||||
youtubeVideoId: string
|
||||
startDateTime: string
|
||||
endDateTime: string
|
||||
}>()
|
||||
|
||||
const embedUrl = computed(
|
||||
() =>
|
||||
`https://www.youtube-nocookie.com/embed/${youtubeVideoId}?autoplay=1&mute=1&rel=0`
|
||||
)
|
||||
|
||||
// Keep SSR/initial paint deterministic on the logo and only flip to the embed
|
||||
// after client hydration — avoids a build-time `now` leaking into the markup.
|
||||
const mounted = ref(false)
|
||||
onMounted(() => {
|
||||
mounted.value = true
|
||||
})
|
||||
|
||||
const now = useNow({ interval: 30_000 })
|
||||
const startMs = computed(() => new Date(startDateTime).getTime())
|
||||
const endMs = computed(() => new Date(endDateTime).getTime())
|
||||
|
||||
const isLive = computed(
|
||||
() =>
|
||||
mounted.value &&
|
||||
now.value.getTime() >= startMs.value &&
|
||||
now.value.getTime() < endMs.value
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="max-w-9xl mx-auto flex flex-col items-center px-6 py-16 text-center lg:py-24"
|
||||
>
|
||||
<div
|
||||
v-if="isLive"
|
||||
class="mb-10 aspect-video w-full overflow-hidden rounded-2xl lg:mb-12"
|
||||
>
|
||||
<iframe
|
||||
:src="embedUrl"
|
||||
:title="title"
|
||||
class="size-full"
|
||||
loading="lazy"
|
||||
allow="autoplay; encrypted-media; picture-in-picture"
|
||||
allowfullscreen
|
||||
/>
|
||||
</div>
|
||||
<slot v-else name="visual">
|
||||
<img
|
||||
v-if="visual?.type === 'image'"
|
||||
:src="visual.src"
|
||||
:alt="visual.alt"
|
||||
:width="visual.width"
|
||||
:height="visual.height"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
class="mb-10 h-auto w-full max-w-md lg:mb-12 lg:max-w-lg"
|
||||
/>
|
||||
<video
|
||||
v-else-if="visual?.type === 'video'"
|
||||
:src="visual.src"
|
||||
:poster="visual.poster"
|
||||
:aria-label="visual.alt"
|
||||
:width="visual.width"
|
||||
:height="visual.height"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
preload="metadata"
|
||||
class="mb-10 h-auto w-full max-w-md lg:mb-12 lg:max-w-2xl"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<p
|
||||
v-if="eyebrow"
|
||||
class="mb-4 text-sm font-medium tracking-wide text-primary-comfy-canvas/70 uppercase"
|
||||
>
|
||||
{{ eyebrow }}
|
||||
</p>
|
||||
|
||||
<h1
|
||||
class="max-w-3xl text-4xl/snug font-light tracking-tight text-pretty text-primary-comfy-canvas lg:text-6xl/snug"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
|
||||
<p
|
||||
v-if="subtitle"
|
||||
class="mt-6 max-w-2xl text-base text-primary-comfy-canvas/70 lg:text-lg"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
|
||||
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
|
||||
<Button
|
||||
as="a"
|
||||
:href="primaryCta.href"
|
||||
:target="primaryCta.target"
|
||||
:rel="resolveRel(primaryCta)"
|
||||
size="lg"
|
||||
>
|
||||
{{ primaryCta.label }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="secondaryCta"
|
||||
as="a"
|
||||
:href="secondaryCta.href"
|
||||
:target="secondaryCta.target"
|
||||
:rel="resolveRel(secondaryCta)"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
>
|
||||
{{ secondaryCta.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -6,6 +6,7 @@ import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import type { BrandButtonVariants } from './brandButton.variants'
|
||||
import { brandButtonVariants } from './brandButton.variants'
|
||||
import { resolveRel } from '../../utils/cta'
|
||||
|
||||
const props = defineProps<{
|
||||
href?: string
|
||||
@@ -16,9 +17,8 @@ const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const resolvedRel = computed(
|
||||
() =>
|
||||
props.rel ?? (props.target === '_blank' ? 'noopener noreferrer' : undefined)
|
||||
const resolvedRel = computed(() =>
|
||||
resolveRel({ rel: props.rel, target: props.target })
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ const topColumns: { title: string; links: FooterLink[] }[] = [
|
||||
title: t('footer.resources', locale),
|
||||
links: [
|
||||
{ label: t('nav.learning', locale), href: routes.learning },
|
||||
{ label: t('nav.launches', locale), href: routes.launches },
|
||||
{
|
||||
label: t('footer.blog', locale),
|
||||
href: externalLinks.blog,
|
||||
|
||||
@@ -25,7 +25,7 @@ const {
|
||||
data-slot="badge"
|
||||
:data-variant="variant"
|
||||
:data-size="size"
|
||||
:class="cn(badgeVariants({ variant, size }), className)"
|
||||
:class="cn(badgeVariants({ size, variant }), className)"
|
||||
>
|
||||
<slot name="prepend">
|
||||
<component :is="prependIcon" v-if="prependIcon" />
|
||||
|
||||
@@ -4,15 +4,16 @@ import { cva } from 'cva'
|
||||
export const badgeVariants = cva({
|
||||
base: 'text-primary-warm-gray font-formula leading-none focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-transparency-ink-t80',
|
||||
subtle: 'bg-transparency-white-t4 text-primary-comfy-canvas',
|
||||
accent:
|
||||
'before:bg-primary-comfy-yellow relative isolate overflow-visible rounded-none bg-transparent px-2 py-0.5 text-[9px] font-bold tracking-wide text-primary-comfy-ink uppercase before:absolute before:inset-0 before:-z-10 before:-skew-x-12 before:rounded-sm'
|
||||
},
|
||||
size: {
|
||||
md: 'px-4 py-1 text-xs',
|
||||
xs: 'px-2 py-0.5 text-[9px]'
|
||||
},
|
||||
variant: {
|
||||
default: 'bg-transparency-ink-t80',
|
||||
subtle: 'bg-transparency-white-t4 text-primary-comfy-canvas',
|
||||
category: 'text-primary-comfy-yellow px-0 font-semibold uppercase',
|
||||
accent:
|
||||
'before:bg-primary-comfy-yellow relative isolate overflow-visible rounded-none bg-transparent px-2 py-0.5 text-[9px] font-bold tracking-wide text-primary-comfy-ink uppercase before:absolute before:inset-0 before:-z-10 before:-skew-x-12 before:rounded-sm'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -8,7 +8,8 @@ export const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
default: 'h-10 px-6 py-2.5',
|
||||
sm: 'h-8 px-4 py-2 text-xs md:text-sm',
|
||||
default: 'h-10 px-6 py-2.5 text-xs md:text-sm',
|
||||
lg: 'h-14 px-8 py-4 text-base'
|
||||
},
|
||||
variant: {
|
||||
@@ -17,6 +18,8 @@ export const buttonVariants = cva(
|
||||
outline:
|
||||
'text-primary-comfy-yellow hover:bg-primary-comfy-yellow border uppercase hover:text-primary-comfy-ink',
|
||||
link: "text-primary-comfy-yellow h-auto justify-start px-0 py-1 text-base uppercase hover:opacity-90 [&_svg:not([class*='size-'])]:size-6",
|
||||
underlineLink:
|
||||
"text-primary-comfy-yellow relative h-auto justify-start px-0 py-1 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:origin-left after:scale-x-0 after:bg-current after:transition-transform after:duration-200 hover:opacity-90 hover:after:scale-x-100 [&_svg:not([class*='size-'])]:size-6",
|
||||
nav: 'text-primary-warm-white hover:text-primary-comfy-yellow h-auto justify-between px-0 py-1 text-start text-2xl font-medium',
|
||||
navMuted:
|
||||
'hover:text-primary-comfy-yellow h-auto w-full justify-between px-0 py-1 text-start text-2xl font-medium text-primary-comfy-canvas uppercase'
|
||||
|
||||
22
apps/website/src/components/ui/card/Card.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card"
|
||||
:class="
|
||||
cn(
|
||||
'bg-transparency-white-t4 text-primary-warm-white rounded-4.5xl flex flex-col gap-6 shadow-sm',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
apps/website/src/components/ui/card/CardContent.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-slot="card-content" :class="cn('px-6', className)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
19
apps/website/src/components/ui/card/CardDescription.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-description"
|
||||
:class="
|
||||
cn('line-clamp-3 text-base text-primary-comfy-canvas/70', className)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
apps/website/src/components/ui/card/CardFooter.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-slot="card-footer" :class="cn('flex items-center', className)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
apps/website/src/components/ui/card/CardHeader.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-slot="card-header" :class="cn('flex flex-col gap-1.5', className)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
22
apps/website/src/components/ui/card/CardTitle.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-title"
|
||||
:class="
|
||||
cn(
|
||||
'text-xl leading-none font-medium text-primary-comfy-canvas md:text-2xl',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,6 +22,12 @@ interface HeroLogoConfig {
|
||||
cursorTiltStrength: number
|
||||
bgScale: number
|
||||
slideDuration: number
|
||||
svgMarkup: string
|
||||
fitAxis: 'width' | 'height'
|
||||
targetSize: number
|
||||
respectReducedMotion: boolean
|
||||
baseUrl: string
|
||||
fadeInDurationMs: number
|
||||
}
|
||||
|
||||
const DEFAULTS: HeroLogoConfig = {
|
||||
@@ -34,19 +40,25 @@ const DEFAULTS: HeroLogoConfig = {
|
||||
extrudeDepth: 200,
|
||||
cursorTiltStrength: 0.5,
|
||||
bgScale: 0.8,
|
||||
slideDuration: 0.4
|
||||
slideDuration: 0.4,
|
||||
svgMarkup: SVG_MARKUP,
|
||||
fitAxis: 'height',
|
||||
targetSize: 3,
|
||||
respectReducedMotion: true,
|
||||
baseUrl: BASE_URL,
|
||||
fadeInDurationMs: 0
|
||||
}
|
||||
|
||||
function buildImageUrls(): string[] {
|
||||
function buildImageUrls(baseUrl: string): string[] {
|
||||
return Array.from({ length: IMAGE_COUNT }, (_, i) => {
|
||||
const index = String(i).padStart(5, '0')
|
||||
return `${BASE_URL}/image_sequence_${index}.webp`
|
||||
return `${baseUrl}/image_sequence_${index}.webp`
|
||||
})
|
||||
}
|
||||
|
||||
function parseShapes(): THREE.Shape[] {
|
||||
function parseShapes(markup: string): THREE.Shape[] {
|
||||
const loader = new SVGLoader()
|
||||
const svgData = loader.parse(SVG_MARKUP)
|
||||
const svgData = loader.parse(markup)
|
||||
const shapes: THREE.Shape[] = []
|
||||
svgData.paths.forEach((path) => {
|
||||
shapes.push(...SVGLoader.createShapes(path))
|
||||
@@ -85,7 +97,8 @@ export function useHeroLogo(
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const container = containerRef.value
|
||||
if (!container || prefersReducedMotion()) return
|
||||
if (!container || (cfg.respectReducedMotion && prefersReducedMotion()))
|
||||
return
|
||||
|
||||
const { width, height } = container.getBoundingClientRect()
|
||||
|
||||
@@ -102,6 +115,9 @@ export function useHeroLogo(
|
||||
renderer.domElement.style.width = '100%'
|
||||
renderer.domElement.style.height = '100%'
|
||||
renderer.domElement.style.opacity = '0'
|
||||
if (cfg.fadeInDurationMs > 0) {
|
||||
renderer.domElement.style.transition = `opacity ${cfg.fadeInDurationMs}ms ease`
|
||||
}
|
||||
renderer.domElement.setAttribute('aria-hidden', 'true')
|
||||
container.appendChild(renderer.domElement)
|
||||
|
||||
@@ -126,24 +142,36 @@ export function useHeroLogo(
|
||||
camera.position.z = cfg.zoom
|
||||
|
||||
// SVG shape
|
||||
const shapes = parseShapes()
|
||||
const shapes = parseShapes(cfg.svgMarkup)
|
||||
const tempGeo = new THREE.ShapeGeometry(shapes)
|
||||
tempGeo.computeBoundingBox()
|
||||
const bb = tempGeo.boundingBox!
|
||||
const bb = tempGeo.boundingBox
|
||||
if (!bb) {
|
||||
tempGeo.dispose()
|
||||
cleanup?.()
|
||||
return
|
||||
}
|
||||
const cx = (bb.max.x + bb.min.x) / 2
|
||||
const cy = (bb.max.y + bb.min.y) / 2
|
||||
const scaleFactor = 3 / (bb.max.y - bb.min.y)
|
||||
const fitExtent =
|
||||
cfg.fitAxis === 'width' ? bb.max.x - bb.min.x : bb.max.y - bb.min.y
|
||||
if (fitExtent <= 0) {
|
||||
tempGeo.dispose()
|
||||
cleanup?.()
|
||||
return
|
||||
}
|
||||
const scaleFactor = cfg.targetSize / fitExtent
|
||||
tempGeo.dispose()
|
||||
|
||||
// Image sequence textures — load first frame eagerly, rest lazily
|
||||
const urls = buildImageUrls()
|
||||
const urls = buildImageUrls(cfg.baseUrl)
|
||||
const textures = await loadTextures(urls.slice(0, 1))
|
||||
if (disposed) return
|
||||
|
||||
renderer.domElement.style.opacity = '1'
|
||||
loaded.value = true
|
||||
|
||||
loadTextures(urls.slice(1)).then((rest) => {
|
||||
void loadTextures(urls.slice(1)).then((rest) => {
|
||||
if (!disposed) textures.push(...rest)
|
||||
})
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const baseRoutes = {
|
||||
cloudEnterprise: '/cloud/enterprise',
|
||||
api: '/api',
|
||||
gallery: '/gallery',
|
||||
launches: '/launches',
|
||||
about: '/about',
|
||||
careers: '/careers',
|
||||
customers: '/customers',
|
||||
@@ -59,6 +60,7 @@ export const externalLinks = {
|
||||
discord: 'https://discord.com/invite/comfyorg',
|
||||
docs: 'https://docs.comfy.org/',
|
||||
docsApi: 'https://docs.comfy.org/api-reference/cloud',
|
||||
docsMcp: 'https://docs.comfy.org/agent-tools/cloud',
|
||||
docsSubscription: 'https://docs.comfy.org/support/subscription/subscribing',
|
||||
github: 'https://github.com/Comfy-Org/ComfyUI',
|
||||
githubInstall: 'https://github.com/Comfy-Org/ComfyUI#installing',
|
||||
|
||||
248
apps/website/src/data/drops.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
// Image URLs are placeholders at media.comfy.org/website/drops/<id>.png —
|
||||
// asset uploads and native zh-CN review are pending follow-ups (see
|
||||
// apps/website/.scratch/drops-page/PRD.md).
|
||||
import { externalLinks } from '../config/routes'
|
||||
import type { LocalizedText } from '../i18n/translations'
|
||||
|
||||
type DropMedia =
|
||||
| { type: 'image'; src: string; alt: LocalizedText }
|
||||
| { type: 'video'; src: string; alt: LocalizedText; poster?: string }
|
||||
|
||||
export type Drop = {
|
||||
id: string
|
||||
badge?: LocalizedText
|
||||
category: LocalizedText
|
||||
media: DropMedia
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
cta: { label: LocalizedText; href: LocalizedText }
|
||||
}
|
||||
|
||||
const EXPLORE: LocalizedText = { en: 'EXPLORE', 'zh-CN': '探索' }
|
||||
const PLATFORM: LocalizedText = { en: 'Platform', 'zh-CN': '平台' }
|
||||
const CLOUD: LocalizedText = { en: 'Cloud', 'zh-CN': '云端' }
|
||||
const COMMUNITY: LocalizedText = { en: 'Community', 'zh-CN': '社区' }
|
||||
const DEVELOPER: LocalizedText = { en: 'Developer', 'zh-CN': '开发者' }
|
||||
const MODELS_AND_NODES: LocalizedText = {
|
||||
en: 'Models & Nodes',
|
||||
'zh-CN': '模型与节点'
|
||||
}
|
||||
const NEW_BADGE: LocalizedText = { en: 'NEW', 'zh-CN': '新' }
|
||||
const FEATURED_BADGE: LocalizedText = { en: 'FEATURED', 'zh-CN': '精选' }
|
||||
|
||||
function imageFor(fileName: string, alt: LocalizedText): DropMedia {
|
||||
return {
|
||||
type: 'image',
|
||||
src: `https://media.comfy.org/website/drops/${fileName}`,
|
||||
alt
|
||||
}
|
||||
}
|
||||
|
||||
function videoFor(
|
||||
fileName: string,
|
||||
alt: LocalizedText,
|
||||
poster?: string
|
||||
): DropMedia {
|
||||
return {
|
||||
type: 'video',
|
||||
src: `https://media.comfy.org/website/drops/${fileName}`,
|
||||
alt,
|
||||
...(poster && {
|
||||
poster: `https://media.comfy.org/website/drops/${poster}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const drops: readonly Drop[] = [
|
||||
{
|
||||
id: 'desktop-client',
|
||||
badge: NEW_BADGE,
|
||||
category: PLATFORM,
|
||||
media: imageFor('Drops_2x2card_Desktop.jpg', {
|
||||
en: 'New Desktop Client',
|
||||
'zh-CN': '新桌面客户端'
|
||||
}),
|
||||
title: { en: 'New Desktop Client', 'zh-CN': '新桌面客户端' },
|
||||
description: {
|
||||
en: 'A faster, redesigned desktop app for ComfyUI — one-click install and managed updates.',
|
||||
'zh-CN': '更快、重新设计的 ComfyUI 桌面应用程序 — 一键安装与受管更新。'
|
||||
},
|
||||
cta: {
|
||||
label: EXPLORE,
|
||||
href: { en: '/download', 'zh-CN': '/zh-CN/download' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'app-mode',
|
||||
badge: NEW_BADGE,
|
||||
category: PLATFORM,
|
||||
media: videoFor('Drops_2x2card_APP.mp4', {
|
||||
en: 'App Mode',
|
||||
'zh-CN': 'App 模式'
|
||||
}),
|
||||
title: { en: 'App Mode', 'zh-CN': 'App 模式' },
|
||||
description: {
|
||||
en: 'A simplified view of your workflows. Flip back to the node graph anytime to go deeper.',
|
||||
'zh-CN': '工作流的简化视图。随时切换回节点图视图以深入了解。'
|
||||
},
|
||||
// TODO: no destination page yet — link out when App Mode lands.
|
||||
cta: {
|
||||
label: EXPLORE,
|
||||
href: {
|
||||
en: 'https://docs.comfy.org/interface/app-mode',
|
||||
'zh-CN': 'https://docs.comfy.org/zh/interface/app-mode'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'comfy-api',
|
||||
badge: NEW_BADGE,
|
||||
category: DEVELOPER,
|
||||
media: imageFor('Drops_2x2card_API.jpg', {
|
||||
en: 'Comfy API',
|
||||
'zh-CN': 'Comfy API'
|
||||
}),
|
||||
title: { en: 'Comfy API', 'zh-CN': 'Comfy API' },
|
||||
description: {
|
||||
en: 'Turn any workflow into a production endpoint. Automate generation and scale to thousands of outputs.',
|
||||
'zh-CN': '将任意工作流变成生产端点。自动化生成并扩展到数千个输出。'
|
||||
},
|
||||
cta: {
|
||||
label: EXPLORE,
|
||||
href: { en: '/api', 'zh-CN': '/zh-CN/api' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'comfy-mcp',
|
||||
badge: NEW_BADGE,
|
||||
category: CLOUD,
|
||||
media: imageFor('Drops_2x2card_MCP.jpg', {
|
||||
en: 'Comfy MCP',
|
||||
'zh-CN': 'Comfy MCP'
|
||||
}),
|
||||
title: { en: 'Comfy MCP', 'zh-CN': 'Comfy MCP' },
|
||||
description: {
|
||||
en: 'The full power of ComfyUI from anywhere — no setup, no GPU required.',
|
||||
'zh-CN': '随时随地体验 ComfyUI 的全部能力 — 无需配置,无需 GPU。'
|
||||
},
|
||||
cta: {
|
||||
label: EXPLORE,
|
||||
href: { en: externalLinks.docsMcp, 'zh-CN': externalLinks.docsMcp }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'community-workflows',
|
||||
category: COMMUNITY,
|
||||
media: imageFor('Drops_3x3card_Comm Workflows.jpg', {
|
||||
en: 'Community Workflows',
|
||||
'zh-CN': '社区工作流'
|
||||
}),
|
||||
title: {
|
||||
en: 'Community Workflows',
|
||||
'zh-CN': '社区工作流'
|
||||
},
|
||||
description: {
|
||||
en: 'Browse and remix thousands of community-shared workflows. Start from a proven template.',
|
||||
'zh-CN': '浏览和混搭数千个社区共享的工作流。从经过验证的模板开始。'
|
||||
},
|
||||
cta: {
|
||||
label: EXPLORE,
|
||||
href: { en: externalLinks.workflows, 'zh-CN': externalLinks.workflows }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'supported-models',
|
||||
category: MODELS_AND_NODES,
|
||||
media: imageFor('Drops_Supported models.jpg', {
|
||||
en: 'Supported Models',
|
||||
'zh-CN': '支持的模型'
|
||||
}),
|
||||
title: { en: 'Supported Models', 'zh-CN': '支持的模型' },
|
||||
description: {
|
||||
en: 'Run the latest open and partner models — every checkpoint, LoRA, and ControlNet, ready to use in your graph.',
|
||||
'zh-CN':
|
||||
'运行最新的开源和合作伙伴模型 — 每个 checkpoint、LoRA 和 ControlNet 都可直接在工作流中使用。'
|
||||
},
|
||||
cta: {
|
||||
label: EXPLORE,
|
||||
href: { en: '/p/supported-models', 'zh-CN': '/zh-CN/p/supported-models' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'supported-nodes',
|
||||
category: MODELS_AND_NODES,
|
||||
media: videoFor('Drops_3x3card_supported nodes.mp4', {
|
||||
en: 'Supported Nodes',
|
||||
'zh-CN': '支持的节点'
|
||||
}),
|
||||
title: { en: 'Supported Nodes', 'zh-CN': '支持的节点' },
|
||||
description: {
|
||||
en: 'Thousands of community and partner nodes, curated and verified to run on Comfy Cloud.',
|
||||
'zh-CN':
|
||||
'数千个社区与合作伙伴节点,经过精选与验证,可在 Comfy Cloud 上运行。'
|
||||
},
|
||||
cta: {
|
||||
label: EXPLORE,
|
||||
href: {
|
||||
en: '/cloud/supported-nodes',
|
||||
'zh-CN': '/zh-CN/cloud/supported-nodes'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'comfy-enterprise',
|
||||
category: CLOUD,
|
||||
media: imageFor('Drops_3x3card_enterprise.png', {
|
||||
en: 'Comfy Enterprise',
|
||||
'zh-CN': 'Comfy 企业版'
|
||||
}),
|
||||
title: { en: 'Comfy Enterprise', 'zh-CN': 'Comfy 企业版' },
|
||||
description: {
|
||||
en: 'Enterprise-grade infrastructure for the creative engine inside your organization.',
|
||||
'zh-CN': '为您组织内创意引擎提供的企业级基础设施。'
|
||||
},
|
||||
cta: {
|
||||
label: EXPLORE,
|
||||
href: { en: '/cloud/enterprise', 'zh-CN': '/zh-CN/cloud/enterprise' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'learning-hub',
|
||||
category: COMMUNITY,
|
||||
media: imageFor('Drops_3x3_Learninghub.jpg', {
|
||||
en: 'Learning Hub',
|
||||
'zh-CN': '学习中心'
|
||||
}),
|
||||
title: { en: 'Learning Hub', 'zh-CN': '学习中心' },
|
||||
description: {
|
||||
en: 'Walkthroughs and ready-to-run workflows to take you from first render to production pipeline.',
|
||||
'zh-CN': '配套教程与开箱即用的工作流,带您从第一次渲染走向生产管线。'
|
||||
},
|
||||
cta: {
|
||||
label: { en: 'START LEARNING', 'zh-CN': '开始学习' },
|
||||
href: { en: '/learning', 'zh-CN': '/zh-CN/learning' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'share-comfy',
|
||||
badge: NEW_BADGE,
|
||||
category: COMMUNITY,
|
||||
media: videoFor('Drops_3x3card_Affilliate.mp4', {
|
||||
en: 'Comfy Affiliate',
|
||||
'zh-CN': 'Comfy Affiliate'
|
||||
}),
|
||||
title: {
|
||||
en: 'Comfy Affiliate',
|
||||
'zh-CN': 'Comfy Affiliate'
|
||||
},
|
||||
description: {
|
||||
en: 'Share Comfy with your audience and earn for every creator you bring on board.',
|
||||
'zh-CN': '与您的受众分享 Comfy,为您带来的每一位创作者获得回报。'
|
||||
},
|
||||
// /affiliates is locale-invariant: same URL in both locales.
|
||||
cta: {
|
||||
label: { en: 'LEARN MORE', 'zh-CN': '了解更多' },
|
||||
href: { en: '/affiliates', 'zh-CN': '/affiliates' }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -180,6 +180,11 @@ export function getMainNavigation(locale: Locale): NavItem[] {
|
||||
},
|
||||
// TODO: no /brand page yet
|
||||
// { label: t('nav.brand', locale), href: '#' },
|
||||
{
|
||||
label: t('nav.launches', locale),
|
||||
href: routes.launches,
|
||||
badge: 'new'
|
||||
},
|
||||
{
|
||||
label: t('nav.blogs', locale),
|
||||
href: externalLinks.blog,
|
||||
|
||||
@@ -1849,6 +1849,7 @@ const translations = {
|
||||
'nav.aboutUs': { en: 'About Us', 'zh-CN': '关于我们' },
|
||||
'nav.careers': { en: 'Careers', 'zh-CN': '招聘' },
|
||||
'nav.customerStories': { en: 'Customer Stories', 'zh-CN': '客户故事' },
|
||||
'nav.launches': { en: 'Launches', 'zh-CN': '发布' },
|
||||
'nav.downloadLocal': { en: 'DOWNLOAD DESKTOP', 'zh-CN': '下载桌面版' },
|
||||
'nav.launchCloud': { en: 'LAUNCH CLOUD', 'zh-CN': '启动云端' },
|
||||
'nav.ctaDesktopPrefix': { en: 'DOWNLOAD', 'zh-CN': '下载' },
|
||||
@@ -4928,6 +4929,70 @@ const translations = {
|
||||
'affiliate.cta.termsLabel': {
|
||||
en: 'Read the affiliate program terms',
|
||||
'zh-CN': '阅读联盟计划条款'
|
||||
},
|
||||
|
||||
// Launches page (/launches) — head metadata
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'launches.page.title': {
|
||||
en: 'ComfyUI Live Demo & Q&A — June 29 Launch Livestream',
|
||||
'zh-CN': 'ComfyUI 直播演示与问答 — 6 月 29 日发布直播'
|
||||
},
|
||||
'launches.page.description': {
|
||||
en: 'Join the ComfyUI livestream on June 29 for a hands-on product demo and live Q&A. See what’s new across desktop, cloud, and community, and get your questions answered.',
|
||||
'zh-CN':
|
||||
'6 月 29 日加入 ComfyUI 直播,观看实操产品演示并参与实时问答。了解桌面、云端和社区的最新内容,并获得解答。'
|
||||
},
|
||||
|
||||
// Launches page (/launches) — hero section
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'launches.hero.title': {
|
||||
en: 'Everything new in ComfyUI',
|
||||
'zh-CN': 'ComfyUI 全新内容'
|
||||
},
|
||||
'launches.hero.primary': {
|
||||
en: 'Download Desktop',
|
||||
'zh-CN': '下载桌面版'
|
||||
},
|
||||
'launches.hero.secondary': {
|
||||
en: 'Launch Cloud',
|
||||
'zh-CN': '启动云端'
|
||||
},
|
||||
'launches.hero.visualAlt': {
|
||||
en: 'Comfy',
|
||||
'zh-CN': 'Comfy'
|
||||
},
|
||||
|
||||
// Launches page (/launches) — subscribe banner
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'launches.banner.text': {
|
||||
en: 'Join the live stream. Get answers in real time.',
|
||||
'zh-CN': '加入直播,实时获得解答。'
|
||||
},
|
||||
'launches.banner.cta': {
|
||||
en: 'Join livestream',
|
||||
'zh-CN': '加入直播'
|
||||
},
|
||||
|
||||
// Launches page (/launches) — closing CTA
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'launches.cta.heading': {
|
||||
en: 'Everything Comfy ships. All in one place.',
|
||||
'zh-CN': 'Comfy 的全部内容,一处尽享。'
|
||||
},
|
||||
'launches.cta.primary': {
|
||||
en: 'Open Comfy Cloud',
|
||||
'zh-CN': '打开 Comfy Cloud'
|
||||
},
|
||||
'launches.cta.secondary': {
|
||||
en: 'Try Workflow',
|
||||
'zh-CN': '试用工作流'
|
||||
},
|
||||
|
||||
// Launches page (/launches) — launches grid
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'launches.section.title': {
|
||||
en: 'Latest Launches',
|
||||
'zh-CN': '最新发布'
|
||||
}
|
||||
} as const satisfies Record<string, Record<Locale, string>>
|
||||
|
||||
|
||||
20
apps/website/src/pages/launches.astro
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import CtaSection from '../templates/drops/CtaSection.vue'
|
||||
import DropsSection from '../templates/drops/DropsSection.vue'
|
||||
import HeroSection from '../templates/drops/HeroSection.vue'
|
||||
import SubscribeBanner from '../templates/drops/SubscribeBanner.vue'
|
||||
import { t } from '../i18n/translations'
|
||||
|
||||
const locale = 'en' as const
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t('launches.page.title', locale)}
|
||||
description={t('launches.page.description', locale)}
|
||||
>
|
||||
<SubscribeBanner locale={locale} client:load />
|
||||
<HeroSection locale={locale} client:load />
|
||||
<DropsSection locale={locale} />
|
||||
<CtaSection locale={locale} />
|
||||
</BaseLayout>
|
||||
20
apps/website/src/pages/zh-CN/launches.astro
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import CtaSection from '../../templates/drops/CtaSection.vue'
|
||||
import DropsSection from '../../templates/drops/DropsSection.vue'
|
||||
import HeroSection from '../../templates/drops/HeroSection.vue'
|
||||
import SubscribeBanner from '../../templates/drops/SubscribeBanner.vue'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const locale = 'zh-CN' as const
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t('launches.page.title', locale)}
|
||||
description={t('launches.page.description', locale)}
|
||||
>
|
||||
<SubscribeBanner locale={locale} client:load />
|
||||
<HeroSection locale={locale} client:load />
|
||||
<DropsSection locale={locale} />
|
||||
<CtaSection locale={locale} />
|
||||
</BaseLayout>
|
||||
25
apps/website/src/templates/drops/CtaSection.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import CtaCenter01 from '../../components/blocks/CtaCenter01.vue'
|
||||
import { externalLinks } from '../../config/routes'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CtaCenter01
|
||||
:heading="t('launches.cta.heading', locale)"
|
||||
:primary-cta="{
|
||||
label: t('launches.cta.primary', locale),
|
||||
href: externalLinks.cloud,
|
||||
target: '_blank'
|
||||
}"
|
||||
:secondary-cta="{
|
||||
label: t('launches.cta.secondary', locale),
|
||||
href: externalLinks.workflows,
|
||||
target: '_blank'
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
76
apps/website/src/templates/drops/DropCard.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import type { Drop } from '../../data/drops'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import Badge from '../../components/ui/badge/Badge.vue'
|
||||
|
||||
import ButtonPill from '../../components/ui/button-pill/ButtonPill.vue'
|
||||
import Card from '../../components/ui/card/Card.vue'
|
||||
import CardContent from '../../components/ui/card/CardContent.vue'
|
||||
import CardDescription from '../../components/ui/card/CardDescription.vue'
|
||||
import CardFooter from '../../components/ui/card/CardFooter.vue'
|
||||
import CardHeader from '../../components/ui/card/CardHeader.vue'
|
||||
import CardTitle from '../../components/ui/card/CardTitle.vue'
|
||||
|
||||
const { drop, locale } = defineProps<{
|
||||
drop: Drop
|
||||
locale: Locale
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="group/pill-trigger relative h-full overflow-hidden">
|
||||
<a
|
||||
:href="drop.cta.href[locale]"
|
||||
:aria-label="`${drop.title[locale]} — ${drop.cta.label[locale]}`"
|
||||
class="rounded-4.5xl focus-visible:ring-primary-comfy-yellow absolute inset-0 z-10 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col-reverse">
|
||||
<CardHeader class="gap-2 px-6">
|
||||
<Badge variant="category">
|
||||
{{ drop.category[locale] }}
|
||||
</Badge>
|
||||
<CardTitle class="pt-4">
|
||||
{{ drop.title[locale] }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{{ drop.description[locale] }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="relative p-2">
|
||||
<div class="aspect-video w-full overflow-hidden rounded-4xl">
|
||||
<img
|
||||
v-if="drop.media.type === 'image'"
|
||||
:src="drop.media.src"
|
||||
:alt="drop.media.alt[locale]"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="size-full object-cover object-center transition-transform duration-500 ease-out group-hover/pill-trigger:scale-105"
|
||||
/>
|
||||
<video
|
||||
v-else
|
||||
:src="drop.media.src"
|
||||
:poster="drop.media.poster"
|
||||
:aria-label="drop.media.alt[locale]"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
preload="metadata"
|
||||
class="size-full object-cover object-center transition-transform duration-500 ease-out group-hover/pill-trigger:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<Badge v-if="drop.badge" variant="accent" class="absolute top-6 left-8">
|
||||
{{ drop.badge[locale] }}
|
||||
</Badge>
|
||||
</CardContent>
|
||||
</div>
|
||||
|
||||
<CardFooter class="mt-auto px-6 pb-6">
|
||||
<ButtonPill as="span" variant="ghost" icon-position="left">
|
||||
{{ drop.cta.label[locale] }}
|
||||
</ButtonPill>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</template>
|
||||
29
apps/website/src/templates/drops/DropsSection.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import DropCard from './DropCard.vue'
|
||||
import { drops } from '../../data/drops'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="max-w-9xl mx-auto px-6 py-16 lg:py-24">
|
||||
<h2
|
||||
class="text-primary-warm-white text-3xl font-light tracking-tight lg:text-5xl"
|
||||
>
|
||||
{{ t('launches.section.title', locale) }}
|
||||
</h2>
|
||||
|
||||
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-6 lg:mt-12">
|
||||
<div
|
||||
v-for="(drop, index) in drops"
|
||||
:key="drop.id"
|
||||
:class="index < 4 ? 'md:col-span-3' : 'md:col-span-2'"
|
||||
>
|
||||
<DropCard :drop :locale />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
35
apps/website/src/templates/drops/HeroSection.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import HeroLivestream01 from '../../components/blocks/HeroLivestream01.vue'
|
||||
import LaunchesHeroLogo from './LaunchesHeroLogo.vue'
|
||||
import { externalLinks, getRoutes } from '../../config/routes'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { livestream } from './livestream'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const routes = getRoutes(locale)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HeroLivestream01
|
||||
:title="t('launches.hero.title', locale)"
|
||||
:primary-cta="{
|
||||
label: t('launches.hero.primary', locale),
|
||||
href: routes.download
|
||||
}"
|
||||
:secondary-cta="{
|
||||
label: t('launches.hero.secondary', locale),
|
||||
href: externalLinks.cloud,
|
||||
target: '_blank'
|
||||
}"
|
||||
:youtube-video-id="livestream.youtubeVideoId"
|
||||
:start-date-time="livestream.startDateTime"
|
||||
:end-date-time="livestream.endDateTime"
|
||||
>
|
||||
<template #visual>
|
||||
<LaunchesHeroLogo :label="t('launches.hero.visualAlt', locale)" />
|
||||
</template>
|
||||
</HeroLivestream01>
|
||||
</template>
|
||||
55
apps/website/src/templates/drops/LaunchesHeroLogo.vue
Normal file
61
apps/website/src/templates/drops/SubscribeBanner.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { resolveRel } from '../../utils/cta'
|
||||
import { livestream } from './livestream'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const signUpHref = `https://www.youtube.com/watch?v=${livestream.youtubeVideoId}`
|
||||
const signUpRel = resolveRel({ target: '_blank' })
|
||||
|
||||
// Hide once the livestream window closes — both for visitors arriving after
|
||||
// the event and for visitors whose tab is open when it ends.
|
||||
const endMs = new Date(livestream.endDateTime).getTime()
|
||||
const visible = ref(true)
|
||||
|
||||
// useTimeoutFn auto-clears on unmount. Arm it client-side only so SSR never
|
||||
// schedules a long-lived server timer.
|
||||
const { start } = useTimeoutFn(
|
||||
() => {
|
||||
visible.value = false
|
||||
},
|
||||
() => Math.max(0, endMs - Date.now()),
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (endMs - Date.now() <= 0) {
|
||||
visible.value = false
|
||||
} else {
|
||||
start()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="px-4">
|
||||
<div
|
||||
class="bg-primary-comfy-plum max-w-8xl rounded-5xl text-primary-warm-white mx-auto flex w-full flex-col items-center justify-center gap-2 px-6 py-5 text-center text-sm sm:flex-row sm:gap-4"
|
||||
>
|
||||
<p class="ppformula-text-center">
|
||||
{{ t('launches.banner.text', locale) }}
|
||||
</p>
|
||||
<Button
|
||||
:href="signUpHref"
|
||||
as="a"
|
||||
variant="underlineLink"
|
||||
size="sm"
|
||||
target="_blank"
|
||||
:rel="signUpRel"
|
||||
>
|
||||
{{ t('launches.banner.cta', locale) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
5
apps/website/src/templates/drops/livestream.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const livestream = {
|
||||
youtubeVideoId: 'yo7b_zHd20g',
|
||||
startDateTime: '2026-06-29T15:00:00Z',
|
||||
endDateTime: '2026-07-02T17:15:00Z'
|
||||
} as const
|
||||
18
apps/website/src/utils/cta.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { resolveRel } from './cta'
|
||||
|
||||
describe('resolveRel', () => {
|
||||
it('prefers an explicit rel over the target-derived default', () => {
|
||||
expect(resolveRel({ rel: 'nofollow', target: '_blank' })).toBe('nofollow')
|
||||
})
|
||||
|
||||
it('adds noopener noreferrer for _blank targets', () => {
|
||||
expect(resolveRel({ target: '_blank' })).toBe('noopener noreferrer')
|
||||
})
|
||||
|
||||
it('returns undefined for non-blank targets', () => {
|
||||
expect(resolveRel({ target: '_self' })).toBeUndefined()
|
||||
expect(resolveRel({})).toBeUndefined()
|
||||
})
|
||||
})
|
||||
10
apps/website/src/utils/cta.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { AnchorHTMLAttributes } from 'vue'
|
||||
|
||||
export function resolveRel(cta: {
|
||||
rel?: AnchorHTMLAttributes['rel']
|
||||
target?: AnchorHTMLAttributes['target']
|
||||
}): AnchorHTMLAttributes['rel'] {
|
||||
return (
|
||||
cta.rel ?? (cta.target === '_blank' ? 'noopener noreferrer' : undefined)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
{
|
||||
"id": "test-missing-model-nested-promoted-widget",
|
||||
"revision": 0,
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "outer-subgraph-with-promoted-missing-model",
|
||||
"pos": [10, 250],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"title": "Resolved Shared Outer Subgraph",
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["resolved_model.safetensors"]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "outer-subgraph-with-promoted-missing-model",
|
||||
"pos": [450, 250],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"title": "Outer Subgraph with Promoted Missing Model",
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["fake_model.safetensors"]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "outer-subgraph-with-promoted-missing-model",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 2,
|
||||
"lastLinkId": 2,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Outer Subgraph with Promoted Missing Model",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [100, 200, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [600, 200, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "outer-ckpt-name-input-id",
|
||||
"name": "ckpt_name",
|
||||
"type": "COMBO",
|
||||
"linkIds": [2],
|
||||
"pos": { "0": 150, "1": 220 }
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "inner-subgraph-with-promoted-missing-model",
|
||||
"pos": [250, 180],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "ckpt_name",
|
||||
"name": "ckpt_name",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "ckpt_name"
|
||||
},
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["fake_model.safetensors"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 2,
|
||||
"target_slot": 0,
|
||||
"type": "COMBO"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "inner-subgraph-with-promoted-missing-model",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 1,
|
||||
"lastLinkId": 1,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Inner Subgraph with Promoted Missing Model",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [100, 200, 120, 60]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [500, 200, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "inner-ckpt-name-input-id",
|
||||
"name": "ckpt_name",
|
||||
"type": "COMBO",
|
||||
"linkIds": [1],
|
||||
"pos": { "0": 150, "1": 220 }
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [250, 180],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "ckpt_name",
|
||||
"name": "ckpt_name",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "ckpt_name"
|
||||
},
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{ "name": "MODEL", "type": "MODEL", "links": null },
|
||||
{ "name": "CLIP", "type": "CLIP", "links": null },
|
||||
{ "name": "VAE", "type": "VAE", "links": null }
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": ["fake_model.safetensors"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 1,
|
||||
"target_slot": 0,
|
||||
"type": "COMBO"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
"name": "fake_model.safetensors",
|
||||
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
|
||||
"directory": "checkpoints"
|
||||
}
|
||||
],
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export class NodeLibrarySidebarTabV2 extends SidebarTab {
|
||||
public readonly searchInput: Locator
|
||||
public readonly sidebarContent: Locator
|
||||
public readonly allTab: Locator
|
||||
public readonly blueprintsTab: Locator
|
||||
public readonly essentialsTab: Locator
|
||||
public readonly sortButton: Locator
|
||||
public readonly nodePreview: Locator
|
||||
|
||||
@@ -101,8 +101,8 @@ export class NodeLibrarySidebarTabV2 extends SidebarTab {
|
||||
super(page, 'node-library')
|
||||
this.searchInput = page.getByPlaceholder('Search...')
|
||||
this.sidebarContent = page.locator('.sidebar-content-container')
|
||||
this.allTab = this.getTab('All')
|
||||
this.blueprintsTab = this.getTab('Blueprints')
|
||||
this.allTab = this.getTab('All nodes')
|
||||
this.essentialsTab = this.getTab('Essentials')
|
||||
this.sortButton = this.sidebarContent.getByRole('button', { name: 'Sort' })
|
||||
this.nodePreview = page.getByTestId(TestIds.sidebar.nodePreviewCard)
|
||||
}
|
||||
|
||||
136
browser_tests/fixtures/utils/objectInfo.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import type { Page, Route } from '@playwright/test'
|
||||
|
||||
import {
|
||||
getComboSpecComboOptions,
|
||||
isComboInputSpec,
|
||||
isComboInputSpecV1
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import type {
|
||||
ComboInputSpec,
|
||||
ComboInputSpecV2,
|
||||
ComfyNodeDef,
|
||||
InputSpec
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
|
||||
export type ObjectInfoResponse = Record<string, ComfyNodeDef>
|
||||
|
||||
type ComboInput = ComboInputSpec | ComboInputSpecV2
|
||||
|
||||
const OBJECT_INFO_ROUTE = '**/object_info'
|
||||
|
||||
function getRequiredInput(
|
||||
objectInfo: ObjectInfoResponse,
|
||||
nodeType: string,
|
||||
inputName: string
|
||||
): InputSpec {
|
||||
const nodeInfo = objectInfo[nodeType]
|
||||
if (!nodeInfo) {
|
||||
throw new Error(`Missing object_info entry for ${nodeType}`)
|
||||
}
|
||||
|
||||
const requiredInputs = nodeInfo.input?.required
|
||||
if (!requiredInputs) {
|
||||
throw new Error(`Missing required inputs for ${nodeType}`)
|
||||
}
|
||||
|
||||
const input = requiredInputs[inputName]
|
||||
if (!input) {
|
||||
throw new Error(`Missing input ${nodeType}.${inputName}`)
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
function getComboInput(
|
||||
objectInfo: ObjectInfoResponse,
|
||||
nodeType: string,
|
||||
inputName: string
|
||||
): ComboInput {
|
||||
const input = getRequiredInput(objectInfo, nodeType, inputName)
|
||||
if (isComboInputSpec(input)) {
|
||||
return input
|
||||
}
|
||||
|
||||
throw new Error(`Expected ${nodeType}.${inputName} to be a combo input`)
|
||||
}
|
||||
|
||||
export function setComboInputOptions(
|
||||
objectInfo: ObjectInfoResponse,
|
||||
nodeType: string,
|
||||
inputName: string,
|
||||
values: ReadonlyArray<string | number>
|
||||
): void {
|
||||
const input = getComboInput(objectInfo, nodeType, inputName)
|
||||
const nextValues = [...values]
|
||||
|
||||
if (isComboInputSpecV1(input)) {
|
||||
input[0] = nextValues
|
||||
return
|
||||
}
|
||||
|
||||
input[1] = { ...input[1], options: nextValues }
|
||||
}
|
||||
|
||||
export function appendComboInputOptions(
|
||||
objectInfo: ObjectInfoResponse,
|
||||
nodeType: string,
|
||||
inputName: string,
|
||||
values: ReadonlyArray<string | number>
|
||||
): void {
|
||||
const input = getComboInput(objectInfo, nodeType, inputName)
|
||||
setComboInputOptions(objectInfo, nodeType, inputName, [
|
||||
...getComboSpecComboOptions(input),
|
||||
...values
|
||||
])
|
||||
}
|
||||
|
||||
export async function routeObjectInfoFromSetupApi(
|
||||
page: Page,
|
||||
customize?: (objectInfo: ObjectInfoResponse) => void | Promise<void>
|
||||
): Promise<() => Promise<void>> {
|
||||
const setupApiUrl =
|
||||
process.env.PLAYWRIGHT_SETUP_API_URL ?? 'http://127.0.0.1:8188'
|
||||
const objectInfoUrl = new URL('/object_info', setupApiUrl).toString()
|
||||
|
||||
const objectInfoRouteHandler = async (route: Route) => {
|
||||
let objectInfo: ObjectInfoResponse
|
||||
try {
|
||||
const response = await fetch(objectInfoUrl, {
|
||||
signal: AbortSignal.timeout(5_000)
|
||||
})
|
||||
if (!response.ok) {
|
||||
await route.fulfill({
|
||||
status: response.status,
|
||||
contentType: response.headers.get('content-type') ?? 'text/plain',
|
||||
body: await response.text()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
objectInfo = (await response.json()) as ObjectInfoResponse
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
await route.fulfill({
|
||||
status: 502,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
error: `Failed to fetch setup object_info from ${objectInfoUrl}: ${message}`
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await customize?.(objectInfo)
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(objectInfo)
|
||||
})
|
||||
}
|
||||
|
||||
await page.route(OBJECT_INFO_ROUTE, objectInfoRouteHandler)
|
||||
return async () => {
|
||||
if (page.isClosed()) return
|
||||
await page.unroute(OBJECT_INFO_ROUTE, objectInfoRouteHandler)
|
||||
}
|
||||
}
|
||||
424
browser_tests/fixtures/utils/promotedMissingModel.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Locator } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
import { loadWorkflowAndOpenErrorsTab } from '@e2e/fixtures/helpers/ErrorsTabHelper'
|
||||
import { assetPath } from '@e2e/fixtures/utils/paths'
|
||||
import { PropertiesPanelHelper } from '@e2e/tests/propertiesPanel/PropertiesPanelHelper'
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
const PROMOTED_MODEL_WIDGET_NAME = 'ckpt_name'
|
||||
|
||||
export interface PromotedMissingModelWorkflow {
|
||||
workflowName: string
|
||||
hostNodeId: number
|
||||
hostNodeTitle: string
|
||||
sharedDefinitionSiblingHostNodeId?: number
|
||||
sharedDefinitionSiblingHostNodeTitle?: string
|
||||
}
|
||||
|
||||
type RootWorkflowNode = {
|
||||
id: number | string
|
||||
widgets_values?: unknown[] | Record<string, unknown>
|
||||
}
|
||||
|
||||
type RootWorkflowData = ComfyWorkflowJSON & {
|
||||
nodes?: RootWorkflowNode[]
|
||||
}
|
||||
|
||||
export const NESTED_PROMOTED_MISSING_MODEL_WORKFLOW: PromotedMissingModelWorkflow =
|
||||
{
|
||||
workflowName: 'missing/missing_model_nested_promoted_widget',
|
||||
hostNodeId: 4,
|
||||
hostNodeTitle: 'Outer Subgraph with Promoted Missing Model',
|
||||
sharedDefinitionSiblingHostNodeId: 3,
|
||||
sharedDefinitionSiblingHostNodeTitle: 'Resolved Shared Outer Subgraph'
|
||||
}
|
||||
|
||||
export function getMissingModelLabel(group: Locator, modelName: string) {
|
||||
return group.getByRole('button', { name: modelName, exact: true })
|
||||
}
|
||||
|
||||
export async function expectSingleMissingModelReference(
|
||||
group: Locator,
|
||||
modelName: string
|
||||
) {
|
||||
await expectMissingModelReferenceCount(group, modelName, 1)
|
||||
}
|
||||
|
||||
export async function expectMissingModelReferenceCount(
|
||||
group: Locator,
|
||||
modelName: string,
|
||||
count: number
|
||||
) {
|
||||
await expect(getMissingModelLabel(group, modelName)).toHaveCount(1)
|
||||
const badge = group.getByTestId(TestIds.dialogs.missingModelReferenceCount)
|
||||
if (count === 1) {
|
||||
await expect(badge).toBeHidden()
|
||||
return
|
||||
}
|
||||
await expect(badge).toBeVisible()
|
||||
await expect(badge).toHaveText(String(count))
|
||||
}
|
||||
|
||||
export async function loadPromotedMissingModelAndOpenErrorsTab(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow,
|
||||
modelName: string
|
||||
): Promise<Locator> {
|
||||
await loadWorkflowAndOpenErrorsTab(comfyPage, workflow.workflowName)
|
||||
|
||||
const missingModelGroup = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingModelsGroup
|
||||
)
|
||||
await expectSingleMissingModelReference(missingModelGroup, modelName)
|
||||
return missingModelGroup
|
||||
}
|
||||
|
||||
export async function loadPromotedMissingModelWithHostValuesAndOpenErrorsTab(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow,
|
||||
hostValues: Record<number, string>,
|
||||
modelName: string,
|
||||
expectedReferenceCount: number
|
||||
): Promise<Locator> {
|
||||
await loadPromotedMissingModelWithHostValues(comfyPage, workflow, hostValues)
|
||||
|
||||
const errorOverlay = comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
|
||||
await expect(errorOverlay).toBeVisible()
|
||||
await errorOverlay.getByTestId(TestIds.dialogs.errorOverlaySeeErrors).click()
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
|
||||
const missingModelGroup = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingModelsGroup
|
||||
)
|
||||
await expectMissingModelReferenceCount(
|
||||
missingModelGroup,
|
||||
modelName,
|
||||
expectedReferenceCount
|
||||
)
|
||||
return missingModelGroup
|
||||
}
|
||||
|
||||
export async function expectNoMissingModelUi(comfyPage: ComfyPage) {
|
||||
const panel = new PropertiesPanelHelper(comfyPage.page)
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
|
||||
).toBeHidden()
|
||||
await panel.open(comfyPage.actionbar.propertiesButton)
|
||||
await expect(
|
||||
panel.root.getByTestId(TestIds.propertiesPanel.errorsTab)
|
||||
).toBeHidden()
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.missingModelsGroup)
|
||||
).toBeHidden()
|
||||
}
|
||||
|
||||
export async function selectVueComboPromotedModelByTitle(
|
||||
comfyPage: ComfyPage,
|
||||
nodeTitle: string,
|
||||
modelName: string
|
||||
) {
|
||||
await comfyPage.vueNodes.selectComboOption(
|
||||
nodeTitle,
|
||||
PROMOTED_MODEL_WIDGET_NAME,
|
||||
modelName
|
||||
)
|
||||
}
|
||||
|
||||
export async function selectVueAssetPromotedModel(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow,
|
||||
currentModelName: string,
|
||||
modelName: string
|
||||
) {
|
||||
await selectModelFromFormDropdown(
|
||||
comfyPage,
|
||||
comfyPage.vueNodes.getNodeByTitle(workflow.hostNodeTitle),
|
||||
currentModelName,
|
||||
modelName
|
||||
)
|
||||
}
|
||||
|
||||
export async function selectSectionComboPromotedModel(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow,
|
||||
modelName: string
|
||||
) {
|
||||
const panel = await openHostNodeParametersPanel(comfyPage, workflow)
|
||||
const combo = panel.contentArea.getByRole('combobox', {
|
||||
name: PROMOTED_MODEL_WIDGET_NAME,
|
||||
exact: true
|
||||
})
|
||||
await combo.click()
|
||||
await comfyPage.page
|
||||
.getByRole('option', { name: modelName, exact: true })
|
||||
.click()
|
||||
}
|
||||
|
||||
export async function selectSectionAssetPromotedModel(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow,
|
||||
currentModelName: string,
|
||||
modelName: string
|
||||
) {
|
||||
const panel = await openHostNodeParametersPanel(comfyPage, workflow)
|
||||
await selectModelFromFormDropdown(
|
||||
comfyPage,
|
||||
panel.contentArea,
|
||||
currentModelName,
|
||||
modelName
|
||||
)
|
||||
}
|
||||
|
||||
export async function setLegacyPromotedComboModel(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow,
|
||||
modelName: string
|
||||
) {
|
||||
await comfyPage.page.evaluate(
|
||||
({ hostNodeId, widgetName, value }) => {
|
||||
type LegacyPromotedWidget = {
|
||||
name?: string
|
||||
value?: unknown
|
||||
callback?: (value: string) => void
|
||||
setValue?: (
|
||||
value: string,
|
||||
options: {
|
||||
e: PointerEvent
|
||||
node: unknown
|
||||
canvas: unknown
|
||||
}
|
||||
) => void
|
||||
}
|
||||
type LegacyPromotedNode = {
|
||||
onWidgetChanged?: (
|
||||
name: string,
|
||||
newValue: string,
|
||||
oldValue: unknown,
|
||||
widget: LegacyPromotedWidget
|
||||
) => void
|
||||
widgets?: LegacyPromotedWidget[]
|
||||
}
|
||||
type LegacyPromotedGraph = {
|
||||
getNodeById: (nodeId: number) => LegacyPromotedNode | undefined
|
||||
}
|
||||
|
||||
const currentGraph = window.app?.graph as LegacyPromotedGraph | undefined
|
||||
const hostNode: LegacyPromotedNode | undefined =
|
||||
currentGraph?.getNodeById(hostNodeId)
|
||||
if (!hostNode) {
|
||||
throw new Error(`Expected subgraph host node ${hostNodeId}`)
|
||||
}
|
||||
|
||||
const widget = hostNode.widgets?.find(
|
||||
(entry) => entry.name === widgetName
|
||||
) as LegacyPromotedWidget | undefined
|
||||
if (!widget) {
|
||||
throw new Error(`Expected host ${widgetName} widget`)
|
||||
}
|
||||
|
||||
const oldValue = widget.value
|
||||
if (widget.setValue) {
|
||||
widget.setValue(value, {
|
||||
e: new PointerEvent('pointerup'),
|
||||
node: hostNode,
|
||||
canvas: window.app!.canvas
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
widget.value = value
|
||||
widget.callback?.(value)
|
||||
hostNode.onWidgetChanged?.(
|
||||
widget.name ?? widgetName,
|
||||
value,
|
||||
oldValue,
|
||||
widget
|
||||
)
|
||||
},
|
||||
{
|
||||
hostNodeId: workflow.hostNodeId,
|
||||
widgetName: PROMOTED_MODEL_WIDGET_NAME,
|
||||
value: modelName
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function selectLegacyPromotedAssetModel(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow,
|
||||
assetId: string
|
||||
) {
|
||||
await clickLegacyHostPromotedWidget(comfyPage, workflow)
|
||||
|
||||
const modal = comfyPage.page.locator(
|
||||
'[data-component-id="AssetBrowserModal"]'
|
||||
)
|
||||
await expect(modal).toBeVisible()
|
||||
const assetCard = modal.locator(`[data-asset-id="${assetId}"]`)
|
||||
await expect(assetCard).toBeVisible()
|
||||
await assetCard.getByRole('button', { name: 'Use', exact: true }).click()
|
||||
await expect(modal).toBeHidden()
|
||||
}
|
||||
|
||||
export async function expectResolvedPromotedModelSuppressesStaleInteriorErrors(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow,
|
||||
expectedStaleInteriorWidgets: Array<{
|
||||
subgraphNodeIdToEnter: string
|
||||
nodeTitle: string
|
||||
}>,
|
||||
resolvedModelName: string,
|
||||
staleModelName: string
|
||||
) {
|
||||
await loadPromotedMissingModelWithHostValues(comfyPage, workflow, {
|
||||
[workflow.hostNodeId]: resolvedModelName
|
||||
})
|
||||
|
||||
const promotedModelCombo = comfyPage.vueNodes
|
||||
.getNodeByTitle(workflow.hostNodeTitle)
|
||||
.getByRole('combobox', { name: PROMOTED_MODEL_WIDGET_NAME, exact: true })
|
||||
await expect(promotedModelCombo).toContainText(resolvedModelName)
|
||||
await expectNoMissingModelUi(comfyPage)
|
||||
|
||||
for (const step of expectedStaleInteriorWidgets) {
|
||||
await enterSubgraphForStaleInteriorCheck(
|
||||
comfyPage,
|
||||
step.subgraphNodeIdToEnter
|
||||
)
|
||||
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeByTitle(step.nodeTitle)
|
||||
await expect(node).toBeVisible()
|
||||
|
||||
const staleCombo = node.getByRole('combobox', {
|
||||
name: PROMOTED_MODEL_WIDGET_NAME,
|
||||
exact: true
|
||||
})
|
||||
await expect(
|
||||
staleCombo,
|
||||
`${step.nodeTitle} should expose the stale linked interior widget`
|
||||
).toBeDisabled()
|
||||
await expect(
|
||||
staleCombo,
|
||||
`${step.nodeTitle} should keep the stale interior value`
|
||||
).toContainText(staleModelName)
|
||||
await expectNoMissingModelUi(comfyPage)
|
||||
}
|
||||
}
|
||||
|
||||
async function openHostNodeParametersPanel(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow
|
||||
): Promise<PropertiesPanelHelper> {
|
||||
await comfyPage.vueNodes.selectNode(String(workflow.hostNodeId))
|
||||
const panel = new PropertiesPanelHelper(comfyPage.page)
|
||||
await panel.open(comfyPage.actionbar.propertiesButton)
|
||||
await expect(panel.getTab('Parameters')).toBeVisible()
|
||||
await panel.switchToTab('Parameters')
|
||||
return panel
|
||||
}
|
||||
|
||||
async function loadPromotedMissingModelWithHostValues(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow,
|
||||
hostValues: Record<number, string>
|
||||
) {
|
||||
const graphData = readPromotedMissingModelWorkflow(workflow.workflowName)
|
||||
for (const [hostNodeId, value] of Object.entries(hostValues)) {
|
||||
setRootHostWidgetValue(graphData, Number(hostNodeId), value)
|
||||
}
|
||||
|
||||
await comfyPage.workflow.loadGraphData(graphData)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
|
||||
function readPromotedMissingModelWorkflow(workflowName: string) {
|
||||
return JSON.parse(
|
||||
readFileSync(assetPath(`${workflowName}.json`), 'utf-8')
|
||||
) as RootWorkflowData
|
||||
}
|
||||
|
||||
function setRootHostWidgetValue(
|
||||
graphData: RootWorkflowData,
|
||||
hostNodeId: number,
|
||||
value: string
|
||||
) {
|
||||
const hostNode = graphData.nodes?.find(
|
||||
(node) => Number(node.id) === hostNodeId
|
||||
)
|
||||
if (!hostNode) throw new Error(`Expected host node ${hostNodeId}`)
|
||||
|
||||
if (Array.isArray(hostNode.widgets_values)) {
|
||||
hostNode.widgets_values[0] = value
|
||||
return
|
||||
}
|
||||
|
||||
hostNode.widgets_values = {
|
||||
...(hostNode.widgets_values ?? {}),
|
||||
[PROMOTED_MODEL_WIDGET_NAME]: value
|
||||
}
|
||||
}
|
||||
|
||||
async function selectModelFromFormDropdown(
|
||||
comfyPage: ComfyPage,
|
||||
root: Locator,
|
||||
currentModelName: string,
|
||||
nextModelName: string
|
||||
) {
|
||||
const trigger = root
|
||||
.getByRole('button', { name: currentModelName, exact: true })
|
||||
.first()
|
||||
await expect(trigger).toBeVisible()
|
||||
await trigger.click()
|
||||
|
||||
const menu = comfyPage.page.getByTestId('form-dropdown-menu')
|
||||
await expect(menu).toBeVisible()
|
||||
await menu.getByText(nextModelName, { exact: true }).click()
|
||||
await expect(menu).toBeHidden()
|
||||
}
|
||||
|
||||
async function clickLegacyHostPromotedWidget(
|
||||
comfyPage: ComfyPage,
|
||||
workflow: PromotedMissingModelWorkflow
|
||||
) {
|
||||
const hostNode = await comfyPage.nodeOps.getNodeRefById(workflow.hostNodeId)
|
||||
await hostNode.centerOnNode()
|
||||
const widget = await hostNode.getWidgetByName(PROMOTED_MODEL_WIDGET_NAME)
|
||||
await widget.click()
|
||||
}
|
||||
|
||||
async function enterSubgraphForStaleInteriorCheck(
|
||||
comfyPage: ComfyPage,
|
||||
nodeId: string
|
||||
) {
|
||||
const numericNodeId = Number(nodeId)
|
||||
if (Number.isNaN(numericNodeId)) {
|
||||
throw new Error(`Expected numeric subgraph node id, got ${nodeId}`)
|
||||
}
|
||||
|
||||
const normalizedNodeId = String(numericNodeId)
|
||||
const enterButton =
|
||||
comfyPage.vueNodes.getSubgraphEnterButton(normalizedNodeId)
|
||||
if ((await enterButton.count()) > 0) {
|
||||
await comfyPage.vueNodes.enterSubgraph(normalizedNodeId)
|
||||
return
|
||||
}
|
||||
|
||||
await comfyPage.page.evaluate((targetNodeId) => {
|
||||
const graph = window.app?.canvas.graph
|
||||
const node = graph?.getNodeById(targetNodeId)
|
||||
if (!node?.isSubgraphNode()) {
|
||||
throw new Error(`Expected visible subgraph node ${targetNodeId}`)
|
||||
}
|
||||
window.app!.canvas.setGraph(node.subgraph)
|
||||
}, numericNodeId)
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
@@ -33,49 +33,11 @@ test.describe('Node Library Essentials Tab', { tag: '@ui' }, () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('Node library opens via sidebar', async ({ comfyPage }) => {
|
||||
const tabButton = comfyPage.page.locator('.node-library-tab-button')
|
||||
await tabButton.click()
|
||||
|
||||
const sidebarContent = comfyPage.page.locator(
|
||||
'.comfy-vue-side-bar-container'
|
||||
)
|
||||
await expect(sidebarContent).toBeVisible()
|
||||
})
|
||||
|
||||
test('Essentials tab is visible in node library', async ({ comfyPage }) => {
|
||||
const tabButton = comfyPage.page.locator('.node-library-tab-button')
|
||||
await tabButton.click()
|
||||
|
||||
const essentialsTab = comfyPage.page.getByRole('tab', {
|
||||
name: /essentials/i
|
||||
})
|
||||
await expect(essentialsTab).toBeVisible()
|
||||
})
|
||||
|
||||
test('Clicking essentials tab shows essential node cards', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tabButton = comfyPage.page.locator('.node-library-tab-button')
|
||||
await tabButton.click()
|
||||
|
||||
const essentialsTab = comfyPage.page.getByRole('tab', {
|
||||
name: /essentials/i
|
||||
})
|
||||
await essentialsTab.click()
|
||||
|
||||
const essentialCards = comfyPage.page.locator('[data-node-name]')
|
||||
await expect(essentialCards.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('Essential node cards have node names', async ({ comfyPage }) => {
|
||||
const tabButton = comfyPage.page.locator('.node-library-tab-button')
|
||||
await tabButton.click()
|
||||
|
||||
const essentialsTab = comfyPage.page.getByRole('tab', {
|
||||
name: /essentials/i
|
||||
})
|
||||
await essentialsTab.click()
|
||||
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', true)
|
||||
const tab = comfyPage.menu.nodeLibraryTabV2
|
||||
await tab.open()
|
||||
await tab.essentialsTab.click()
|
||||
|
||||
const firstCard = comfyPage.page.locator('[data-node-name]').first()
|
||||
await expect(firstCard).toBeVisible()
|
||||
@@ -86,21 +48,18 @@ test.describe('Node Library Essentials Tab', { tag: '@ui' }, () => {
|
||||
test('Node library can switch between all and essentials tabs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tabButton = comfyPage.page.locator('.node-library-tab-button')
|
||||
await tabButton.click()
|
||||
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', true)
|
||||
const tab = comfyPage.menu.nodeLibraryTabV2
|
||||
await tab.open()
|
||||
await tab.allTab.click()
|
||||
|
||||
const essentialsTab = comfyPage.page.getByRole('tab', {
|
||||
name: /essentials/i
|
||||
})
|
||||
const allNodesTab = comfyPage.page.getByRole('tab', { name: /^all$/i })
|
||||
|
||||
await essentialsTab.click()
|
||||
await expect(essentialsTab).toHaveAttribute('aria-selected', 'true')
|
||||
await tab.essentialsTab.click()
|
||||
await expect(tab.essentialsTab).toHaveAttribute('aria-selected', 'true')
|
||||
const essentialCards = comfyPage.page.locator('[data-node-name]')
|
||||
await expect(essentialCards.first()).toBeVisible()
|
||||
|
||||
await allNodesTab.click()
|
||||
await expect(allNodesTab).toHaveAttribute('aria-selected', 'true')
|
||||
await expect(essentialsTab).toHaveAttribute('aria-selected', 'false')
|
||||
await tab.allTab.click()
|
||||
await expect(tab.allTab).toHaveAttribute('aria-selected', 'true')
|
||||
await expect(tab.essentialsTab).toHaveAttribute('aria-selected', 'false')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,18 @@ import {
|
||||
countAssetRequestsByTag,
|
||||
createCloudAssetsFixture
|
||||
} from '@e2e/fixtures/assetApiFixture'
|
||||
import { loadWorkflowAndOpenErrorsTab } from '@e2e/fixtures/helpers/ErrorsTabHelper'
|
||||
import {
|
||||
cleanupFakeModel,
|
||||
loadWorkflowAndOpenErrorsTab
|
||||
} from '@e2e/fixtures/helpers/ErrorsTabHelper'
|
||||
import {
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
expectNoMissingModelUi,
|
||||
loadPromotedMissingModelAndOpenErrorsTab,
|
||||
selectLegacyPromotedAssetModel,
|
||||
selectSectionAssetPromotedModel,
|
||||
selectVueAssetPromotedModel
|
||||
} from '@e2e/fixtures/utils/promotedMissingModel'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
import { PropertiesPanelHelper } from '@e2e/tests/propertiesPanel/PropertiesPanelHelper'
|
||||
|
||||
@@ -20,6 +31,8 @@ const WORKFLOW = 'missing/nested_subgraph_installed_model'
|
||||
const IMPORT_SECTIONS_WORKFLOW = 'missing/cloud_missing_model_import_sections'
|
||||
const OUTER_SUBGRAPH_NODE_ID = '205'
|
||||
const LOTUS_MODEL_NAME = 'lotus-depth-d-v1-1.safetensors'
|
||||
const FAKE_MODEL_NAME = 'fake_model.safetensors'
|
||||
const RESOLVED_PROMOTED_MODEL_NAME = 'resolved_model.safetensors'
|
||||
const CLOUD_IMPORTABLE_MODEL_NAME = 'cloud_importable_model.safetensors'
|
||||
const CLOUD_UNKNOWN_MODEL_NAME = 'cloud_unknown_model.safetensors'
|
||||
const CLOUD_IMPORTED_CANONICAL_MODEL_NAME =
|
||||
@@ -55,7 +68,25 @@ const EXISTING_CLOUD_IMPORTABLE_MODEL: Asset & { hash?: string } = {
|
||||
}
|
||||
}
|
||||
|
||||
const RESOLVED_PROMOTED_MODEL_ASSET: Asset & { hash?: string } = {
|
||||
id: 'test-resolved-promoted-model',
|
||||
name: RESOLVED_PROMOTED_MODEL_NAME,
|
||||
hash: 'blake3:0000000000000000000000000000000000000000000000000000000000000205',
|
||||
size: 1_024,
|
||||
mime_type: 'application/octet-stream',
|
||||
tags: ['models', 'checkpoints'],
|
||||
created_at: '2026-05-05T00:00:00Z',
|
||||
updated_at: '2026-05-05T00:00:00Z',
|
||||
last_access_time: '2026-05-05T00:00:00Z',
|
||||
user_metadata: {
|
||||
filename: RESOLVED_PROMOTED_MODEL_NAME
|
||||
}
|
||||
}
|
||||
|
||||
const test = createCloudAssetsFixture([LOTUS_DIFFUSION_MODEL])
|
||||
const promotedModelTest = createCloudAssetsFixture([
|
||||
RESOLVED_PROMOTED_MODEL_ASSET
|
||||
])
|
||||
|
||||
function getRequestedIncludeTags(requestUrl: string): string[] {
|
||||
return (
|
||||
@@ -363,3 +394,84 @@ test.describe(
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
promotedModelTest.describe(
|
||||
'Errors tab - Cloud promoted subgraph missing models',
|
||||
{ tag: '@cloud' },
|
||||
() => {
|
||||
promotedModelTest.beforeEach(async ({ comfyPage }) => {
|
||||
await cleanupFakeModel(comfyPage)
|
||||
await comfyPage.settings.setSetting('Comfy.Assets.UseAssetAPI', true)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.RightSidePanel.ShowErrorsTab',
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
promotedModelTest.afterEach(async ({ comfyPage }) => {
|
||||
await cleanupFakeModel(comfyPage)
|
||||
})
|
||||
|
||||
promotedModelTest(
|
||||
'Changing a Cloud Vue promoted asset widget clears a nested subgraph error',
|
||||
{ tag: ['@vue-nodes', '@widget', '@subgraph'] },
|
||||
async ({ comfyPage }) => {
|
||||
await loadPromotedMissingModelAndOpenErrorsTab(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
FAKE_MODEL_NAME
|
||||
)
|
||||
|
||||
await selectVueAssetPromotedModel(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
FAKE_MODEL_NAME,
|
||||
RESOLVED_PROMOTED_MODEL_NAME
|
||||
)
|
||||
|
||||
await expectNoMissingModelUi(comfyPage)
|
||||
}
|
||||
)
|
||||
|
||||
promotedModelTest(
|
||||
'Changing a Cloud Vue promoted asset from the Parameters tab clears a nested subgraph error',
|
||||
{ tag: ['@vue-nodes', '@widget', '@subgraph'] },
|
||||
async ({ comfyPage }) => {
|
||||
await loadPromotedMissingModelAndOpenErrorsTab(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
FAKE_MODEL_NAME
|
||||
)
|
||||
|
||||
await selectSectionAssetPromotedModel(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
FAKE_MODEL_NAME,
|
||||
RESOLVED_PROMOTED_MODEL_NAME
|
||||
)
|
||||
|
||||
await expectNoMissingModelUi(comfyPage)
|
||||
}
|
||||
)
|
||||
|
||||
promotedModelTest(
|
||||
'Changing a Cloud legacy promoted asset clears a nested subgraph error',
|
||||
{ tag: ['@canvas', '@widget', '@subgraph'] },
|
||||
async ({ comfyPage }) => {
|
||||
await loadPromotedMissingModelAndOpenErrorsTab(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
FAKE_MODEL_NAME
|
||||
)
|
||||
|
||||
await selectLegacyPromotedAssetModel(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
RESOLVED_PROMOTED_MODEL_ASSET.id
|
||||
)
|
||||
|
||||
await expectNoMissingModelUi(comfyPage)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
} from '@e2e/fixtures/assetApiFixture'
|
||||
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import {
|
||||
routeObjectInfoFromSetupApi,
|
||||
setComboInputOptions
|
||||
} from '@e2e/fixtures/utils/objectInfo'
|
||||
import {
|
||||
createRouteMockJob,
|
||||
jobsRouteFixture
|
||||
@@ -86,50 +90,6 @@ interface CloudUploadAssetState {
|
||||
isUploadedAssetAvailable: boolean
|
||||
}
|
||||
|
||||
type ObjectInfoResponse = Record<
|
||||
string,
|
||||
{ input?: { required?: Record<string, unknown> } }
|
||||
>
|
||||
|
||||
function setComboInputOptions(
|
||||
objectInfo: ObjectInfoResponse,
|
||||
nodeType: string,
|
||||
inputName: string,
|
||||
values: string[]
|
||||
) {
|
||||
const nodeInfo = objectInfo[nodeType]
|
||||
if (!nodeInfo) {
|
||||
throw new Error(`Missing object_info entry for ${nodeType}`)
|
||||
}
|
||||
|
||||
const requiredInputs = nodeInfo.input?.required
|
||||
if (!requiredInputs) {
|
||||
throw new Error(`Missing required inputs for ${nodeType}`)
|
||||
}
|
||||
|
||||
const input = requiredInputs[inputName]
|
||||
if (!Array.isArray(input)) {
|
||||
throw new Error(`Expected ${nodeType}.${inputName} to be a combo input`)
|
||||
}
|
||||
|
||||
const [valuesOrType, options] = input
|
||||
const optionsObject =
|
||||
options && typeof options === 'object' && !Array.isArray(options)
|
||||
if (Array.isArray(valuesOrType)) {
|
||||
input[0] = values
|
||||
} else if (valuesOrType !== 'COMBO') {
|
||||
throw new Error(`Expected ${nodeType}.${inputName} to have combo options`)
|
||||
}
|
||||
|
||||
if (optionsObject) {
|
||||
Object.assign(options, { options: values })
|
||||
} else if (!Array.isArray(valuesOrType)) {
|
||||
throw new Error(
|
||||
`Expected ${nodeType}.${inputName} to have options metadata`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function routeCloudBootstrapApis(page: Page) {
|
||||
await page.route('**/api/settings**', async (route) => {
|
||||
await route.fulfill({
|
||||
@@ -161,57 +121,10 @@ async function routeCloudBootstrapApis(page: Page) {
|
||||
})
|
||||
}
|
||||
|
||||
async function routeSetupObjectInfo(
|
||||
page: Page,
|
||||
customize?: (objectInfo: ObjectInfoResponse) => void
|
||||
) {
|
||||
const setupApiUrl =
|
||||
process.env.PLAYWRIGHT_SETUP_API_URL ?? 'http://127.0.0.1:8188'
|
||||
const objectInfoUrl = new URL('/object_info', setupApiUrl).toString()
|
||||
|
||||
const objectInfoRouteHandler = async (route: Route) => {
|
||||
try {
|
||||
const response = await fetch(objectInfoUrl, {
|
||||
signal: AbortSignal.timeout(5_000)
|
||||
})
|
||||
if (!response.ok) {
|
||||
await route.fulfill({
|
||||
status: response.status,
|
||||
contentType: response.headers.get('content-type') ?? 'text/plain',
|
||||
body: await response.text()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const objectInfo = (await response.json()) as ObjectInfoResponse
|
||||
customize?.(objectInfo)
|
||||
|
||||
await route.fulfill({
|
||||
status: response.status,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(objectInfo)
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
await route.fulfill({
|
||||
status: 502,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
error: `Failed to fetch setup object_info from ${objectInfoUrl}: ${message}`
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await page.route('**/object_info', objectInfoRouteHandler)
|
||||
return async () =>
|
||||
await page.unroute('**/object_info', objectInfoRouteHandler)
|
||||
}
|
||||
|
||||
const cloudOutputTest = createCloudAssetsFixture([cloudOutputAsset]).extend({
|
||||
page: async ({ page }, use) => {
|
||||
await routeCloudBootstrapApis(page)
|
||||
const unrouteObjectInfo = await routeSetupObjectInfo(page)
|
||||
const unrouteObjectInfo = await routeObjectInfoFromSetupApi(page)
|
||||
|
||||
try {
|
||||
await use(page)
|
||||
@@ -225,13 +138,16 @@ const cloudEmptyMediaInputsTest = createCloudAssetsFixture([]).extend({
|
||||
page: async ({ page }, use) => {
|
||||
await routeCloudBootstrapApis(page)
|
||||
|
||||
const unrouteObjectInfo = await routeSetupObjectInfo(page, (objectInfo) => {
|
||||
for (const node of emptyMediaLoaderNodes) {
|
||||
setComboInputOptions(objectInfo, node.nodeType, node.widgetName, [
|
||||
node.serverOnlyOption
|
||||
])
|
||||
const unrouteObjectInfo = await routeObjectInfoFromSetupApi(
|
||||
page,
|
||||
(objectInfo) => {
|
||||
for (const node of emptyMediaLoaderNodes) {
|
||||
setComboInputOptions(objectInfo, node.nodeType, node.widgetName, [
|
||||
node.serverOnlyOption
|
||||
])
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
try {
|
||||
await use(page)
|
||||
@@ -246,7 +162,7 @@ const cloudUploadRaceTest = comfyPageFixture.extend<{
|
||||
}>({
|
||||
page: async ({ page }, use) => {
|
||||
await routeCloudBootstrapApis(page)
|
||||
const unrouteObjectInfo = await routeSetupObjectInfo(page)
|
||||
const unrouteObjectInfo = await routeObjectInfoFromSetupApi(page)
|
||||
|
||||
const state: CloudUploadAssetState = {
|
||||
isUploadedAssetAvailable: false
|
||||
|
||||
@@ -8,12 +8,46 @@ import {
|
||||
openErrorsTab,
|
||||
loadWorkflowAndOpenErrorsTab
|
||||
} from '@e2e/fixtures/helpers/ErrorsTabHelper'
|
||||
import {
|
||||
appendComboInputOptions,
|
||||
routeObjectInfoFromSetupApi
|
||||
} from '@e2e/fixtures/utils/objectInfo'
|
||||
import {
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
expectMissingModelReferenceCount,
|
||||
expectNoMissingModelUi,
|
||||
expectResolvedPromotedModelSuppressesStaleInteriorErrors,
|
||||
expectSingleMissingModelReference,
|
||||
getMissingModelLabel,
|
||||
loadPromotedMissingModelAndOpenErrorsTab,
|
||||
loadPromotedMissingModelWithHostValuesAndOpenErrorsTab,
|
||||
selectSectionComboPromotedModel,
|
||||
selectVueComboPromotedModelByTitle,
|
||||
setLegacyPromotedComboModel
|
||||
} from '@e2e/fixtures/utils/promotedMissingModel'
|
||||
|
||||
const FAKE_MODEL_NAME = 'fake_model.safetensors'
|
||||
const RESOLVED_PROMOTED_MODEL_NAME = 'resolved_model.safetensors'
|
||||
|
||||
function getModelLabel(group: Locator, modelName: string = FAKE_MODEL_NAME) {
|
||||
return group.getByRole('button', { name: modelName, exact: true })
|
||||
}
|
||||
const promotedModelTest = test.extend({
|
||||
page: async ({ page }, use) => {
|
||||
const unrouteObjectInfo = await routeObjectInfoFromSetupApi(
|
||||
page,
|
||||
(objectInfo) =>
|
||||
appendComboInputOptions(
|
||||
objectInfo,
|
||||
'CheckpointLoaderSimple',
|
||||
'ckpt_name',
|
||||
[RESOLVED_PROMOTED_MODEL_NAME]
|
||||
)
|
||||
)
|
||||
try {
|
||||
await use(page)
|
||||
} finally {
|
||||
await unrouteObjectInfo()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function expectReferenceBadge(group: Locator, count: number) {
|
||||
await expect(
|
||||
@@ -169,7 +203,9 @@ test.describe('Errors tab - Mode-aware errors', { tag: '@ui' }, () => {
|
||||
TestIds.dialogs.missingModelsGroup
|
||||
)
|
||||
await expect(missingModelGroup).toBeVisible()
|
||||
await expect(getModelLabel(missingModelGroup)).toBeVisible()
|
||||
await expect(
|
||||
getMissingModelLabel(missingModelGroup, FAKE_MODEL_NAME)
|
||||
).toBeVisible()
|
||||
|
||||
const node = await comfyPage.nodeOps.getNodeRefById('1')
|
||||
await node.click('title')
|
||||
@@ -265,7 +301,9 @@ test.describe('Errors tab - Mode-aware errors', { tag: '@ui' }, () => {
|
||||
|
||||
const node1 = await comfyPage.nodeOps.getNodeRefById('1')
|
||||
await node1.click('title')
|
||||
await expect(getModelLabel(missingModelGroup)).toBeVisible()
|
||||
await expect(
|
||||
getMissingModelLabel(missingModelGroup, FAKE_MODEL_NAME)
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
missingModelGroup.getByTestId(TestIds.dialogs.missingModelLocate)
|
||||
).toHaveCount(1)
|
||||
@@ -381,92 +419,184 @@ test.describe('Errors tab - Mode-aware errors', { tag: '@ui' }, () => {
|
||||
await cleanupFakeModel(comfyPage)
|
||||
})
|
||||
|
||||
test(
|
||||
'Resolving a promoted missing model widget through the legacy canvas path clears its error',
|
||||
{ tag: ['@canvas', '@widget', '@subgraph'] },
|
||||
promotedModelTest(
|
||||
'Changing an OSS Vue promoted model clears a nested subgraph error',
|
||||
{ tag: ['@vue-nodes', '@widget', '@subgraph'] },
|
||||
async ({ comfyPage }) => {
|
||||
const resolvedModelName = 'v1-5-pruned-emaonly-fp16.safetensors'
|
||||
let missingModelGroup: Locator
|
||||
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
|
||||
await loadWorkflowAndOpenErrorsTab(
|
||||
comfyPage,
|
||||
'missing/missing_model_promoted_widget'
|
||||
)
|
||||
|
||||
const missingModelGroup = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingModelsGroup
|
||||
)
|
||||
await expect(getModelLabel(missingModelGroup)).toBeVisible()
|
||||
|
||||
await comfyPage.page.evaluate((value) => {
|
||||
const hostNode = window.app!.graph!.getNodeById(2)
|
||||
if (!hostNode?.isSubgraphNode()) {
|
||||
throw new Error('Expected subgraph host node')
|
||||
}
|
||||
|
||||
const interiorNode = hostNode.subgraph.getNodeById(1)
|
||||
const widget = interiorNode?.widgets?.find(
|
||||
(entry) => entry.name === 'ckpt_name'
|
||||
await test.step('A: shared-definition active host reports the missing model', async () => {
|
||||
missingModelGroup = await loadPromotedMissingModelAndOpenErrorsTab(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
FAKE_MODEL_NAME
|
||||
)
|
||||
type SettableWidget = typeof widget & {
|
||||
setValue?: (
|
||||
value: string,
|
||||
options: {
|
||||
e: PointerEvent
|
||||
node: unknown
|
||||
canvas: unknown
|
||||
}
|
||||
) => void
|
||||
}
|
||||
const settableWidget = widget as SettableWidget | undefined
|
||||
})
|
||||
|
||||
if (!settableWidget?.setValue) {
|
||||
throw new Error('Expected concrete ckpt_name widget')
|
||||
await test.step('B: bypassing the resolved sibling host keeps the active host error visible', async () => {
|
||||
const siblingHostNodeId =
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW.sharedDefinitionSiblingHostNodeId
|
||||
if (siblingHostNodeId === undefined) {
|
||||
throw new Error('Expected a shared-definition sibling host')
|
||||
}
|
||||
|
||||
settableWidget.setValue(value, {
|
||||
e: new PointerEvent('pointerup'),
|
||||
node: hostNode,
|
||||
canvas: window.app!.canvas
|
||||
})
|
||||
}, resolvedModelName)
|
||||
const siblingHost =
|
||||
await comfyPage.nodeOps.getNodeRefById(siblingHostNodeId)
|
||||
await siblingHost.centerOnNode()
|
||||
await siblingHost.click('title')
|
||||
await comfyPage.keyboard.bypass()
|
||||
await expect.poll(() => siblingHost.isBypassed()).toBeTruthy()
|
||||
await comfyPage.canvas.click({ position: { x: 700, y: 650 } })
|
||||
await openErrorsTab(comfyPage)
|
||||
await expectSingleMissingModelReference(
|
||||
missingModelGroup,
|
||||
FAKE_MODEL_NAME
|
||||
)
|
||||
})
|
||||
|
||||
await expect(missingModelGroup).toBeHidden()
|
||||
await test.step('C: changing the active host promoted widget resolves the model', async () => {
|
||||
const activeHost = await comfyPage.nodeOps.getNodeRefById(
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW.hostNodeId
|
||||
)
|
||||
await activeHost.centerOnNode()
|
||||
await selectVueComboPromotedModelByTitle(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW.hostNodeTitle,
|
||||
RESOLVED_PROMOTED_MODEL_NAME
|
||||
)
|
||||
})
|
||||
|
||||
await test.step('D: the missing model UI clears', async () => {
|
||||
await expectNoMissingModelUi(comfyPage)
|
||||
})
|
||||
|
||||
await test.step('E: two missing shared-definition hosts report two references', async () => {
|
||||
const siblingHostNodeId =
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW.sharedDefinitionSiblingHostNodeId
|
||||
if (siblingHostNodeId === undefined) {
|
||||
throw new Error('Expected a shared-definition sibling host')
|
||||
}
|
||||
|
||||
missingModelGroup =
|
||||
await loadPromotedMissingModelWithHostValuesAndOpenErrorsTab(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
{
|
||||
[siblingHostNodeId]: FAKE_MODEL_NAME,
|
||||
[NESTED_PROMOTED_MISSING_MODEL_WORKFLOW.hostNodeId]:
|
||||
FAKE_MODEL_NAME
|
||||
},
|
||||
FAKE_MODEL_NAME,
|
||||
2
|
||||
)
|
||||
})
|
||||
|
||||
await test.step('F: changing one missing host leaves the other missing reference', async () => {
|
||||
await selectVueComboPromotedModelByTitle(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW.hostNodeTitle,
|
||||
RESOLVED_PROMOTED_MODEL_NAME
|
||||
)
|
||||
await expectMissingModelReferenceCount(
|
||||
missingModelGroup,
|
||||
FAKE_MODEL_NAME,
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
await test.step('G: changing the remaining missing host clears the model error', async () => {
|
||||
const siblingHostTitle =
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW.sharedDefinitionSiblingHostNodeTitle
|
||||
if (siblingHostTitle === undefined) {
|
||||
throw new Error('Expected a shared-definition sibling host title')
|
||||
}
|
||||
|
||||
await selectVueComboPromotedModelByTitle(
|
||||
comfyPage,
|
||||
siblingHostTitle,
|
||||
RESOLVED_PROMOTED_MODEL_NAME
|
||||
)
|
||||
await expectNoMissingModelUi(comfyPage)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
promotedModelTest(
|
||||
'Changing an OSS Vue promoted model from the Parameters tab clears a nested subgraph error',
|
||||
{ tag: ['@vue-nodes', '@widget', '@subgraph'] },
|
||||
async ({ comfyPage }) => {
|
||||
await loadPromotedMissingModelAndOpenErrorsTab(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
FAKE_MODEL_NAME
|
||||
)
|
||||
|
||||
await selectSectionComboPromotedModel(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
RESOLVED_PROMOTED_MODEL_NAME
|
||||
)
|
||||
|
||||
await expectNoMissingModelUi(comfyPage)
|
||||
}
|
||||
)
|
||||
|
||||
promotedModelTest(
|
||||
'Changing an OSS legacy promoted model clears a nested subgraph error',
|
||||
{ tag: ['@canvas', '@widget', '@subgraph'] },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
|
||||
await loadPromotedMissingModelAndOpenErrorsTab(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
FAKE_MODEL_NAME
|
||||
)
|
||||
|
||||
await setLegacyPromotedComboModel(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
RESOLVED_PROMOTED_MODEL_NAME
|
||||
)
|
||||
|
||||
await expectNoMissingModelUi(comfyPage)
|
||||
}
|
||||
)
|
||||
|
||||
promotedModelTest(
|
||||
'Refreshing a resolved promoted missing model clears the combo invalid state',
|
||||
{ tag: ['@widget', '@subgraph'] },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await loadWorkflowAndOpenErrorsTab(
|
||||
comfyPage,
|
||||
'missing/missing_model_promoted_widget'
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW.workflowName
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const missingModelGroup = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingModelsGroup
|
||||
)
|
||||
await expect(getModelLabel(missingModelGroup)).toBeVisible()
|
||||
await expect(
|
||||
getMissingModelLabel(missingModelGroup, FAKE_MODEL_NAME)
|
||||
).toBeVisible()
|
||||
|
||||
const promotedModelCombo = comfyPage.vueNodes
|
||||
.getNodeByTitle('Subgraph with Promoted Missing Model')
|
||||
.getNodeByTitle(NESTED_PROMOTED_MISSING_MODEL_WORKFLOW.hostNodeTitle)
|
||||
.getByRole('combobox', { name: 'ckpt_name', exact: true })
|
||||
await expect(promotedModelCombo).toHaveAttribute('aria-invalid', 'true')
|
||||
|
||||
const objectInfoRoute = /\/object_info$/
|
||||
try {
|
||||
await comfyPage.page.route(objectInfoRoute, async (route) => {
|
||||
const response = await route.fetch()
|
||||
const objectInfo = await response.json()
|
||||
const ckptName =
|
||||
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name
|
||||
ckptName[0] = [...ckptName[0], 'fake_model.safetensors']
|
||||
await route.fulfill({ response, json: objectInfo })
|
||||
})
|
||||
const unrouteObjectInfo = await routeObjectInfoFromSetupApi(
|
||||
comfyPage.page,
|
||||
(objectInfo) =>
|
||||
appendComboInputOptions(
|
||||
objectInfo,
|
||||
'CheckpointLoaderSimple',
|
||||
'ckpt_name',
|
||||
[FAKE_MODEL_NAME, RESOLVED_PROMOTED_MODEL_NAME]
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
await comfyPage.page
|
||||
.getByTestId(TestIds.dialogs.missingModelRefresh)
|
||||
.click()
|
||||
@@ -478,11 +608,31 @@ test.describe('Errors tab - Mode-aware errors', { tag: '@ui' }, () => {
|
||||
'true'
|
||||
)
|
||||
} finally {
|
||||
await comfyPage.page.unroute(objectInfoRoute)
|
||||
await unrouteObjectInfo()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
promotedModelTest(
|
||||
'Reloading a resolved nested promoted model ignores stale interior values',
|
||||
{ tag: ['@vue-nodes', '@widget', '@subgraph'] },
|
||||
async ({ comfyPage }) => {
|
||||
await expectResolvedPromotedModelSuppressesStaleInteriorErrors(
|
||||
comfyPage,
|
||||
NESTED_PROMOTED_MISSING_MODEL_WORKFLOW,
|
||||
[
|
||||
{
|
||||
subgraphNodeIdToEnter: '4',
|
||||
nodeTitle: 'Inner Subgraph with Promoted Missing Model'
|
||||
},
|
||||
{ subgraphNodeIdToEnter: '2', nodeTitle: 'Load Checkpoint' }
|
||||
],
|
||||
RESOLVED_PROMOTED_MODEL_NAME,
|
||||
FAKE_MODEL_NAME
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test('Bypassing a subgraph hides interior errors, un-bypassing restores them', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -10,20 +10,6 @@ test.describe('Node library sidebar V2', () => {
|
||||
await tab.open()
|
||||
})
|
||||
|
||||
test('Can switch between tabs', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.nodeLibraryTabV2
|
||||
|
||||
await expect(tab.allTab).toHaveAttribute('aria-selected', 'true')
|
||||
|
||||
await tab.blueprintsTab.click()
|
||||
await expect(tab.blueprintsTab).toHaveAttribute('aria-selected', 'true')
|
||||
await expect(tab.allTab).toHaveAttribute('aria-selected', 'false')
|
||||
|
||||
await tab.allTab.click()
|
||||
await expect(tab.allTab).toHaveAttribute('aria-selected', 'true')
|
||||
await expect(tab.blueprintsTab).toHaveAttribute('aria-selected', 'false')
|
||||
})
|
||||
|
||||
test('All tab displays node tree with folders', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.nodeLibraryTabV2
|
||||
|
||||
@@ -123,8 +109,9 @@ test.describe('Node library sidebar V2', () => {
|
||||
|
||||
test('Blueprint previews include description', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.nodeLibraryTabV2
|
||||
await tab.blueprintsTab.click()
|
||||
await tab.allTab.click()
|
||||
|
||||
await tab.expandFolder('Comfy Blueprints')
|
||||
await tab.getNode('test blueprint').hover()
|
||||
await expect(tab.nodePreview, 'Preview displays on hover').toBeVisible()
|
||||
await expect(tab.nodePreview).toContainText('Inverts the image')
|
||||
|
||||
@@ -471,11 +471,10 @@ test.describe(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
|
||||
let initialWidgetCount = 0
|
||||
await expect
|
||||
.poll(() => getPromotedWidgetCount(comfyPage, '11'))
|
||||
.toBeGreaterThan(0)
|
||||
initialWidgetCount = await getPromotedWidgetCount(comfyPage, '11')
|
||||
const initialWidgetCount = await getPromotedWidgetCount(comfyPage, '11')
|
||||
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
@@ -6,6 +6,21 @@ Date: 2026-03-23
|
||||
|
||||
Proposed
|
||||
|
||||
### Amendment (2026-06-19, PR 12617)
|
||||
|
||||
The single central registry this ADR calls the "World" was superseded during
|
||||
implementation. Runtime entity data is held in dedicated Pinia stores keyed by
|
||||
string IDs — `widgetValueStore`, `domWidgetStore`, `layoutStore`,
|
||||
`nodeOutputStore`, `subgraphNavigationStore`, and `previewExposureStore`.
|
||||
Widget values are keyed by `WidgetId` (`graphId:nodeId:name`, see
|
||||
`src/types/widgetId.ts`); the `world/*` layer (`widgetValueIO`, `entityIds`,
|
||||
`brand`, `WidgetEntityId`) was deleted. The ECS principles below still hold —
|
||||
plain-data components, separation of data from behavior, command-driven
|
||||
mutation, and no god-object growth — realized across those stores. Where the
|
||||
text below says "the World," read "the set of dedicated stores"; where it shows
|
||||
`world.getComponent(id, Component)`, read the matching store getter (for
|
||||
example `widgetValueStore.getWidget(widgetId)`).
|
||||
|
||||
## Context
|
||||
|
||||
The litegraph layer is built on deeply coupled OOP classes (`LGraphNode`, `LLink`, `Subgraph`, `BaseWidget`, `Reroute`, `LGraphGroup`, `SlotBase`). Each entity directly references its container and children — nodes hold widget arrays, widgets back-reference their node, links reference origin/target node IDs, subgraphs extend the graph class, and so on.
|
||||
@@ -40,7 +55,7 @@ Six entity kinds, each with a branded ID type:
|
||||
| ----------- | ------------------------------------------------- | --------------------------- | ----------------- |
|
||||
| Node | `LGraphNode` | `NodeId = number \| string` | `NodeEntityId` |
|
||||
| Link | `LLink` | `LinkId = number` | `LinkEntityId` |
|
||||
| Widget | `BaseWidget` subclasses (25+) | name + parent node | `WidgetEntityId` |
|
||||
| Widget | `BaseWidget` subclasses (25+) | name + parent node | `WidgetId` |
|
||||
| Slot | `SlotBase` / `INodeInputSlot` / `INodeOutputSlot` | index on parent node | `SlotEntityId` |
|
||||
| Reroute | `Reroute` | `RerouteId = number` | `RerouteEntityId` |
|
||||
| Group | `LGraphGroup` | `number` | `GroupEntityId` |
|
||||
@@ -54,7 +69,6 @@ Each entity kind gets a nominal/branded type wrapping its underlying primitive.
|
||||
```ts
|
||||
type NodeEntityId = number & { readonly __brand: 'NodeEntityId' }
|
||||
type LinkEntityId = number & { readonly __brand: 'LinkEntityId' }
|
||||
type WidgetEntityId = number & { readonly __brand: 'WidgetEntityId' }
|
||||
type SlotEntityId = number & { readonly __brand: 'SlotEntityId' }
|
||||
type RerouteEntityId = number & { readonly __brand: 'RerouteEntityId' }
|
||||
type GroupEntityId = number & { readonly __brand: 'GroupEntityId' }
|
||||
@@ -63,7 +77,12 @@ type GroupEntityId = number & { readonly __brand: 'GroupEntityId' }
|
||||
type GraphId = string & { readonly __brand: 'GraphId' }
|
||||
```
|
||||
|
||||
Widgets and Slots currently lack independent IDs. The ECS will assign synthetic IDs at entity creation time via an auto-incrementing counter (matching the pattern used by `lastNodeId`, `lastLinkId`, etc. in `LGraphState`).
|
||||
> **Amended (PR 12617):** Widgets are keyed by a branded composite **string**,
|
||||
> `WidgetId = graphId:nodeId:name` (`src/types/widgetId.ts`), rather than a
|
||||
> synthetic numeric `WidgetEntityId`. The composite stays self-documenting and
|
||||
> survives renames at the store layer. The numeric per-kind brands above for
|
||||
> Node/Link/Reroute/Group remain aspirational and unshipped; treat them as
|
||||
> design intent. Slots have no independent ID yet.
|
||||
|
||||
### Component Decomposition
|
||||
|
||||
@@ -139,18 +158,24 @@ A node carrying a subgraph gains these additional components. Subgraphs are not
|
||||
| `GroupVisual` | `color` |
|
||||
| `GroupChildren` | child entity refs (nodes, reroutes) |
|
||||
|
||||
### World
|
||||
### Dedicated stores
|
||||
|
||||
A central registry (the "World") maps entity IDs to their component sets. One
|
||||
World exists per workflow instance, containing all entities across all nesting
|
||||
levels. Each entity carries a `graphScope` identifier linking it to its
|
||||
containing graph. The World also maintains a scope registry mapping each
|
||||
`graphId` to its parent (or null for the root graph).
|
||||
Component data lives in a set of dedicated Pinia stores, each owning one
|
||||
concern and keyed by a string ID that embeds its graph scope (for example
|
||||
`widgetValueStore` keyed by `WidgetId = graphId:nodeId:name`, `layoutStore`
|
||||
keyed by `nodeId`/`linkId`/`rerouteId`, `nodeOutputStore` keyed by
|
||||
`subgraphId:nodeId`). Each store provides a clear-by-graph lifecycle hook
|
||||
(`clearGraph(graphId)`) and query helpers. A scope registry maps each `graphId`
|
||||
to its parent (or null for the root graph).
|
||||
|
||||
> The original design centralized this in one "World" registry per workflow
|
||||
> instance; PR 12617 replaced that with the dedicated stores above. The
|
||||
> remainder of this section describes scoping, which applies per store.
|
||||
|
||||
The "single source of truth" claim in this ADR is scoped to one workflow
|
||||
instance. In a future linked-subgraph model, shared definitions can be loaded
|
||||
into multiple workflow instances, but mutable runtime components
|
||||
(`WidgetValue`, execution state, selection, transient layout caches) remain
|
||||
instance, per concern. In a future linked-subgraph model, shared definitions
|
||||
can be loaded into multiple workflow instances, but mutable runtime state
|
||||
(widget values, execution state, selection, transient layout caches) remains
|
||||
instance-scoped unless explicitly declared shareable.
|
||||
|
||||
### Subgraph recursion model
|
||||
@@ -166,7 +191,7 @@ queries by `graphScope`.
|
||||
|
||||
### Systems (future work)
|
||||
|
||||
Systems are pure functions that query the World for entities with specific component combinations. Initial candidates:
|
||||
Systems are pure functions that query the relevant store(s) for entities with specific component combinations. Initial candidates:
|
||||
|
||||
- **RenderSystem** — queries `Position` + `Dimensions` (where present) + `*Visual` components
|
||||
- **SerializationSystem** — queries all components to produce/consume workflow JSON
|
||||
@@ -178,25 +203,23 @@ System design is deferred to a future ADR. For detailed before/after walkthrough
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
1. **Define types** — branded IDs, component interfaces, World type in a new `src/ecs/` directory
|
||||
2. **Bridge layer** — adapter functions that read ECS components from existing class instances (zero-copy where possible)
|
||||
3. **New features first** — any new cross-cutting feature (e.g., CRDT sync) builds on ECS components rather than class properties
|
||||
4. **Incremental extraction** — migrate one component at a time from classes to the World, using the bridge layer for backward compatibility
|
||||
5. **Deprecate class properties** — once all consumers read from the World, mark class properties as deprecated
|
||||
1. **Define types** — string-key ID types (for example `WidgetId`) and plain-data component interfaces, owned by the store for each concern
|
||||
2. **Bridge layer** — adapter functions that read component data from existing class instances (zero-copy where possible)
|
||||
3. **New features first** — any new cross-cutting feature (e.g., CRDT sync) builds on store-backed components rather than class properties
|
||||
4. **Incremental extraction** — migrate one component at a time from classes into its dedicated store, using the bridge layer for backward compatibility
|
||||
5. **Deprecate class properties** — once all consumers read from the store, mark class properties as deprecated
|
||||
|
||||
For the phased migration roadmap with shipping milestones, see [ECS Migration Plan](../architecture/ecs-migration-plan.md). For the full target architecture, see [ECS Target Architecture](../architecture/ecs-target-architecture.md). For an inventory of existing stores that already partially implement ECS patterns, see [Proto-ECS Stores](../architecture/proto-ecs-stores.md).
|
||||
|
||||
### Relationship to ADR 0003 (Command Pattern / CRDT)
|
||||
|
||||
[ADR 0003](0003-crdt-based-layout-system.md) establishes that all mutations flow through serializable, idempotent commands. This ADR (0008) defines the entity data model and the World store. They are complementary architectural layers:
|
||||
[ADR 0003](0003-crdt-based-layout-system.md) establishes that all mutations flow through serializable, idempotent commands. This ADR (0008) defines the entity data model and the dedicated stores that hold it. They are complementary architectural layers:
|
||||
|
||||
- **Commands** (ADR 0003) describe mutation intent — serializable objects that can be logged, replayed, sent over a wire, or undone.
|
||||
- **Systems** (ADR 0008) are command handlers — they validate and execute mutations against the World.
|
||||
- **The World** (ADR 0008) is the store — it holds component data. It does not know about commands.
|
||||
- **Systems** (ADR 0008) are command handlers — they validate and execute mutations against the relevant stores.
|
||||
- **The dedicated stores** (ADR 0008) hold component data and expose mutation APIs (for example `useLayoutMutations()`, `widgetValueStore.setValue`); each owns its own transaction boundary.
|
||||
|
||||
The World's imperative API (`setComponent`, `deleteEntity`, etc.) is internal. External callers submit commands; the command executor wraps each in a World transaction. This is analogous to Redux: the store's internal mutation is imperative, but the public API is action-based.
|
||||
|
||||
For the full design showing how each lifecycle scenario maps to a command, see [World API and Command Layer](../architecture/ecs-world-command-api.md).
|
||||
A store's imperative mutators are internal implementation. External callers submit commands; each mutating store wraps its writes in a transaction (the Y.js-backed `layoutStore` already does this). This follows Redux: internal mutation is imperative, while the public API is action-based.
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
@@ -210,26 +233,26 @@ For the full design showing how each lifecycle scenario maps to a command, see [
|
||||
|
||||
- Cross-cutting concerns (undo/redo, CRDT sync, serialization) can be implemented as systems without modifying entity classes
|
||||
- Components are independently testable — no need to construct an entire `LGraphNode` to test position logic
|
||||
- Branded IDs prevent a class of bugs where IDs are accidentally used across entity kinds
|
||||
- The World provides a single source of truth for runtime entity state inside a workflow instance, simplifying debugging and state inspection
|
||||
- Branded IDs (including the composite `WidgetId` string) prevent a class of bugs where IDs are accidentally used across entity kinds
|
||||
- Each dedicated store provides a single source of truth for its concern inside a workflow instance, simplifying debugging and state inspection
|
||||
- Aligns with the CRDT layout system direction from ADR 0003
|
||||
|
||||
### Negative
|
||||
|
||||
- Additional indirection: reading a node's position requires a World lookup instead of `node.pos`
|
||||
- Additional indirection: reading a node's position requires a store lookup instead of `node.pos`
|
||||
- Learning curve for contributors unfamiliar with ECS patterns
|
||||
- Migration period where both OOP and ECS patterns coexist, increasing cognitive load
|
||||
- Widgets and Slots need synthetic IDs, adding ID management complexity
|
||||
|
||||
### Render-Loop Performance Implications and Mitigations
|
||||
|
||||
Replacing direct property reads (`node.pos`) with component lookups (`world.getComponent(nodeId, Position)`) does add per-read overhead in the hot render path. In modern JS engines, hot `Map.get()` paths are heavily optimized and are often within a low constant factor of object property reads, but this ADR treats render-loop cost as a first-class risk rather than assuming it is free.
|
||||
Replacing direct property reads (`node.pos`) with store lookups (for example `layoutStore` position reads) does add per-read overhead in the hot render path. In modern JS engines, hot `Map.get()` paths are heavily optimized and are often within a low constant factor of object property reads, but this ADR treats render-loop cost as a first-class risk rather than assuming it is free.
|
||||
|
||||
Planned mitigations for the ECS render path:
|
||||
|
||||
1. Pre-collect render queries into frame-stable caches (`visibleNodeIds`, `visibleLinkIds`, and resolved component references) and rebuild only on topology/layout dirty signals, not on every draw call.
|
||||
2. Keep archetype-style buckets for common render signatures (for example: `Node = Position+Dimensions+NodeVisual`, `Reroute = Position+RerouteVisual`) so systems iterate arrays instead of probing unrelated entities.
|
||||
3. Allow a hot-path storage upgrade behind the World API (for example, SoA-style typed arrays for `Position` and `Dimensions`) if profiling shows `Map.get()` dominates frame time.
|
||||
3. Allow a hot-path storage upgrade behind a store's API (for example, SoA-style typed arrays for `Position` and `Dimensions`) if profiling shows `Map.get()` dominates frame time.
|
||||
4. Gate migration of each render concern with profiling parity checks against the legacy path (same workflow, same viewport, same frame budget).
|
||||
5. Treat parity as a release gate: ECS render path must stay within agreed frame-time budgets (for example, no statistically significant regression in p95 frame time on representative 200-node and 500-node workflows).
|
||||
|
||||
@@ -247,7 +270,6 @@ Companion architecture documents that expand on the design in this ADR:
|
||||
| [ECS Target Architecture](../architecture/ecs-target-architecture.md) | Full target architecture showing how entities and interactions transform under ECS |
|
||||
| [ECS Migration Plan](../architecture/ecs-migration-plan.md) | Phased migration roadmap with shipping milestones and go/no-go criteria |
|
||||
| [ECS Lifecycle Scenarios](../architecture/ecs-lifecycle-scenarios.md) | Before/after walkthroughs of lifecycle operations (node removal, link creation, etc.) |
|
||||
| [World API and Command Layer](../architecture/ecs-world-command-api.md) | How each lifecycle scenario maps to a command in the World API |
|
||||
| [Subgraph Boundaries and Widget Promotion](../architecture/subgraph-boundaries-and-promotion.md) | Design rationale for modeling subgraphs as node components, not separate entities |
|
||||
| [ADR 0009: Subgraph promoted widgets](0009-subgraph-promoted-widgets-use-linked-inputs.md) | Follow-up decision for promoted widget identity and value ownership at subgraph boundaries |
|
||||
| [Appendix: Critical Analysis](../architecture/appendix-critical-analysis.md) | Independent verification of the accuracy of the architecture documents |
|
||||
@@ -258,5 +280,5 @@ Companion architecture documents that expand on the design in this ADR:
|
||||
|
||||
- The 25+ widget types (`BooleanWidget`, `NumberWidget`, `ComboWidget`, etc.) will share the same ECS component schema. Widget-type-specific behavior lives in systems, not in component data.
|
||||
- Subgraphs are not a separate entity kind. A `GraphId` scope identifier (branded `string`) tracks which graph an entity belongs to. The scope DAG must be acyclic — see [Subgraph Boundaries](../architecture/subgraph-boundaries-and-promotion.md).
|
||||
- The existing `LGraphState.lastNodeId` / `lastLinkId` / `lastRerouteId` counters extend naturally to `lastWidgetId` and `lastSlotId`.
|
||||
- The internal ECS model and the serialization format are deliberately separate concerns. The `SerializationSystem` translates between the flat World and the nested serialization format. Backward-compatible loading of all prior workflow formats is a hard, indefinite constraint.
|
||||
- Widgets are addressed by the composite `WidgetId` string, so they need no synthetic counter. The existing `LGraphState.lastNodeId` / `lastLinkId` / `lastRerouteId` counters cover the kinds that have numeric IDs.
|
||||
- The internal ECS model and the serialization format are deliberately separate concerns. The `SerializationSystem` translates between the store-backed component data and the nested serialization format. Backward-compatible loading of all prior workflow formats is a hard, indefinite constraint.
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
_In which we examine the shadow material of a codebase in individuation, verify its self-reported symptoms, and note where the ego's aspirations outpace the psyche's readiness for transformation._
|
||||
|
||||
> **Post-pivot status (PR 12617).** This analysis was written against the
|
||||
> single-World ECS target. The project has since chosen dedicated Pinia stores
|
||||
> over one unified World, which acts on several of the concerns raised below.
|
||||
> Resolution notes are inlined where the pivot answers a critique; the still-open
|
||||
> gaps (extension-callback continuity, atomicity/undo, Y.js ↔ ECS coexistence)
|
||||
> remain live. Verification snapshots predate PR 12617.
|
||||
|
||||
---
|
||||
|
||||
## I. On the Accuracy of Self-Diagnosis
|
||||
@@ -15,7 +22,7 @@ The god-objects are as large as claimed. `LGraphCanvas` contains 9,094 lines —
|
||||
|
||||
Some thirty specific line references were verified against the living code. The `renderingColor` getter sits precisely at line 328. The `drawNode()` method begins exactly at line 5554, and within it, at lines 5562 and 5564, the render pass mutates state — `_setConcreteSlots()` and `arrange()` — just as the documents confess. The scattered `_version++` increments appear at every claimed location across all three files. The module-scope store invocations in `LLink.ts:24` and `Reroute.ts:23` are exactly where indicated.
|
||||
|
||||
The stores — all six of them — exist at their stated paths with their described APIs. The `WidgetValueStore` does indeed hold plain `WidgetState` objects. The `PromotionStore` does maintain its ref-counted maps. The `LayoutStore` does wrap Y.js CRDTs.
|
||||
The stores — six of them — exist at their stated paths with their described APIs. The `WidgetValueStore` does indeed hold plain `WidgetState` objects, keyed by `WidgetId`. The `LayoutStore` does wrap Y.js CRDTs. (The `PromotionStore` named in the original snapshot was removed by PR 12617; promoted value state now lives in `WidgetValueStore`, and `PreviewExposureStore` holds host-scoped preview exposures.)
|
||||
|
||||
This level of factual accuracy — 28 out of 30 sampled citation checks
|
||||
(93.3%) — is, one might say, the work of a consciousness that has genuinely
|
||||
@@ -47,6 +54,12 @@ This is the individuation dream: the fragmented psyche imagines itself unified,
|
||||
|
||||
It is a beautiful vision. It is also, in several respects, a fantasy that has not yet been tested against reality.
|
||||
|
||||
> **Resolved (PR 12617).** The single World was set aside. The project keeps the
|
||||
> fragments deliberately apart — dedicated stores, each holding one concern and
|
||||
> keyed by its own string identity. The integration the dream sought lives in the
|
||||
> shared discipline (plain-data components, command-driven mutation), with the
|
||||
> stores standing on their own.
|
||||
|
||||
### The Line-Count Comparisons
|
||||
|
||||
The lifecycle scenarios compare current implementations against projected ECS equivalents:
|
||||
@@ -93,6 +106,12 @@ But one must be careful not to mistake diversity for disorder. Some of these com
|
||||
|
||||
The documents present branded IDs as an unqualified improvement. They are an improvement in _type safety_. Whether they are an improvement in _comprehensibility_ depends on whether the system provides good lookup APIs. The analysis would benefit from acknowledging this tradeoff rather than presenting it as a pure gain.
|
||||
|
||||
> **Resolved (PR 12617).** The pivot honored this concern. `WidgetValueStore`
|
||||
> keeps the self-documenting composite as its key, branded as a string
|
||||
> (`WidgetId = graphId:nodeId:name`, `src/types/widgetId.ts`), gaining cross-kind
|
||||
> safety while preserving the structural meaning the synthetic integer would have
|
||||
> shed.
|
||||
|
||||
## V. On the Subgraph: The Child Who Contains the Parent
|
||||
|
||||
The documents describe the `Subgraph extends LGraph` relationship as a circular dependency. This is technically accurate and architecturally concerning. But it is also, symbolically, the most interesting structure in the entire system.
|
||||
@@ -115,13 +134,13 @@ This is sound. The documents would benefit from being equally realistic about th
|
||||
|
||||
### Factual Corrections Required
|
||||
|
||||
| Document | Error | Correction |
|
||||
| --------------------- | ---------------------------------- | ---------------------------------- |
|
||||
| `entity-problems.md` | `toJSON() (line 1033)` | `toString() (line 1033)` |
|
||||
| `entity-problems.md` | `execute() (line 1418)` | `doExecute() (line 1411)` |
|
||||
| `entity-problems.md` | `~539 method/property definitions` | ~848; methodology should be stated |
|
||||
| `entity-problems.md` | `configure()` ~180 lines | ~247 lines |
|
||||
| `proto-ecs-stores.md` | `resolveDeepest()` in diagram | `reconcile()` / `getOrCreate()` |
|
||||
| Document | Error | Correction |
|
||||
| --------------------- | ---------------------------------- | ----------------------------------------------------- |
|
||||
| `entity-problems.md` | `toJSON() (line 1033)` | `toString() (line 1033)` |
|
||||
| `entity-problems.md` | `execute() (line 1418)` | `doExecute() (line 1411)` |
|
||||
| `entity-problems.md` | `~539 method/property definitions` | ~848; methodology should be stated |
|
||||
| `entity-problems.md` | `configure()` ~180 lines | ~247 lines |
|
||||
| `proto-ecs-stores.md` | `resolveDeepest()` in diagram | moot — `PromotedWidgetViewManager` removed (PR 12617) |
|
||||
|
||||
### Analytical Gaps
|
||||
|
||||
@@ -129,7 +148,7 @@ This is sound. The documents would benefit from being equally realistic about th
|
||||
2. **Atomicity guarantees** are claimed but not mechanically specified.
|
||||
3. **Y.js / ECS coexistence** is an open architectural question the documents do not engage.
|
||||
4. **ECS line-count projections** are aspirational and should be marked as estimates.
|
||||
5. **Composite key tradeoffs** deserve more nuance than "branded IDs fix everything."
|
||||
5. **Composite key tradeoffs** deserve more nuance than "branded IDs fix everything." _(Resolved by PR 12617: `WidgetId` keeps the composite as a branded string.)_
|
||||
|
||||
### What the Documents Do Well
|
||||
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
# Appendix: ECS Pattern Survey
|
||||
|
||||
_A survey of mainstream Entity Component System libraries — bitECS, miniplex,
|
||||
koota, ECSY, Thyseus, and Bevy — captured during the world-consolidation
|
||||
analysis that shipped slice 1 of
|
||||
[ADR 0008](../adr/0008-entity-component-system.md). This appendix records
|
||||
which structural patterns our `src/world/` substrate adopts, which it
|
||||
deliberately departs from, and where the trade-offs are load-bearing rather
|
||||
than incidental. Thyseus is called out specifically because it is the most
|
||||
Bevy-shaped of the TypeScript ECSs surveyed — its `Commands` parameter is the
|
||||
closest external analog to the command layer ADR 0003 / ADR 0008 are
|
||||
converging on, so it gets dedicated treatment in §2.5 and §3.5._
|
||||
> **Superseded (PR 12617).** The single `src/world/` substrate this appendix
|
||||
> analyzes was removed; the project adopted dedicated Pinia stores
|
||||
> (`widgetValueStore`, `domWidgetStore`, `layoutStore`, `nodeOutputStore`,
|
||||
> `subgraphNavigationStore`, `previewExposureStore`) keyed by string IDs. §1
|
||||
> (the external library survey) remains valid reference material and supports
|
||||
> the dedicated-store direction — its first unanimous finding, that components
|
||||
> live with the code that owns them, is exactly what per-domain stores do. §2–§4
|
||||
> describe the deleted `src/world/` substrate (`world.ts`, `entityIds.ts`,
|
||||
> `widgetComponents.ts`, `WidgetEntityId`) and are retained for historical
|
||||
> rationale only; read their references to "the World" as "the relevant
|
||||
> dedicated store."
|
||||
|
||||
The in-code anchors for the load-bearing constraints discussed below are the
|
||||
doc-comments in [src/world/world.ts](../../src/world/world.ts) (storage
|
||||
strategy) and [src/world/entityIds.ts](../../src/world/entityIds.ts) (identity
|
||||
contract) — see §3 below.
|
||||
_A survey of mainstream Entity Component System libraries — bitECS, miniplex,
|
||||
koota, ECSY, Thyseus, and Bevy. This appendix records which structural patterns
|
||||
the surveyed libraries share, which the project departs from, and where the
|
||||
trade-offs carry weight. Thyseus is called out specifically because it is the
|
||||
most Bevy-shaped of the TypeScript ECSs surveyed — its `Commands` parameter is
|
||||
the closest external analog to the command layer ADR 0003 / ADR 0008 converge
|
||||
on, so it gets dedicated treatment in §2.5 and §3.5._
|
||||
|
||||
---
|
||||
|
||||
@@ -49,9 +53,9 @@ Two structural patterns are unanimous across the surveyed libraries:
|
||||
because it commits to a full system-execution runtime, not just
|
||||
storage.
|
||||
|
||||
Our slice-1 end state — five source files under
|
||||
[src/world/](../../src/world/), ~14 exported names total — sits squarely in
|
||||
this band.
|
||||
The dedicated-store end state — each store a small, focused module keyed by a
|
||||
string ID — sits squarely in this band: a small surface per store, with
|
||||
component shapes defined next to the store that owns them.
|
||||
|
||||
---
|
||||
|
||||
@@ -141,12 +145,11 @@ export function spawnEntities(commands: Commands) {
|
||||
```
|
||||
|
||||
`commands.spawn()`, `.add(component)`, and `.remove(component)` enqueue
|
||||
deferred mutations against a command buffer; the World applies them at
|
||||
deferred mutations against a command buffer; the substrate applies them at
|
||||
defined sync points in the schedule. This is the same shape Bevy uses
|
||||
and is the closest direct external analog to the mutation layer
|
||||
[ADR 0003](../adr/0003-crdt-based-layout-system.md) and the
|
||||
[World API and Command Layer](./ecs-world-command-api.md) describe for
|
||||
this codebase.
|
||||
and is the closest direct external analog to the per-store mutation layer
|
||||
[ADR 0003](../adr/0003-crdt-based-layout-system.md) describes for this
|
||||
codebase (realized as store mutation APIs such as `useLayoutMutations()`).
|
||||
|
||||
We deliberately match the **shape** of this pattern: external callers
|
||||
submit commands; only the executor calls the World's imperative
|
||||
@@ -172,8 +175,8 @@ yet:
|
||||
|
||||
The point of calling Thyseus out separately is that when ADR 0008 lands
|
||||
its command executor slice, "what does this look like in Thyseus?" is a
|
||||
load-bearing comparison point — not a curiosity. Diverging from the
|
||||
Bevy/Thyseus shape there should require an explicit justification, not
|
||||
comparison point worth taking seriously. Diverging from the
|
||||
Bevy/Thyseus shape there should require an explicit justification rather than
|
||||
silent drift.
|
||||
|
||||
---
|
||||
@@ -181,7 +184,7 @@ silent drift.
|
||||
## 3. Patterns We Explicitly Do NOT Adopt
|
||||
|
||||
Each of the following is a real industry idiom we considered and rejected
|
||||
on load-bearing grounds. None of these are pure performance trade-offs.
|
||||
on structural grounds. None of these are pure performance trade-offs.
|
||||
|
||||
### 3.1 Replace-on-write usage idioms
|
||||
|
||||
@@ -215,8 +218,8 @@ SoA storage spreads each component's fields across parallel typed arrays,
|
||||
so the per-entity "row object" is reconstructed on read. **A future
|
||||
migration to SoA would lose the proxy on the row object** — and with it
|
||||
the shared-reactive-identity contract that `BaseWidget._state` and the
|
||||
`widgetValueStore` facade rely on. This is a load-bearing constraint, not
|
||||
just a perf optimization decision.
|
||||
`widgetValueStore` facade rely on. This constraint carries real weight
|
||||
beyond a perf optimization decision.
|
||||
|
||||
The contract is pinned in the doc-comment at the top of
|
||||
[src/world/world.ts](../../src/world/world.ts) — copied here for
|
||||
@@ -261,7 +264,7 @@ The contract is pinned in the doc-comment at the top of
|
||||
* Entity IDs are deterministic, content-addressed, and string-prefix
|
||||
* encoded — NOT opaque numeric IDs (cf. bitECS, koota, miniplex).
|
||||
*
|
||||
* `widgetEntityId(rootGraphId, nodeId, name)` is load-bearing:
|
||||
* `widgetEntityId(rootGraphId, nodeId, name)` carries real weight:
|
||||
* consumers consistently pass `rootGraph.id` so widgets viewed at
|
||||
* different subgraph depths share identity. Migrating to numeric IDs
|
||||
* would break cross-subgraph value sharing. See ADR 0008 and
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
This document walks through the major entity lifecycle operations — showing the current imperative implementation and how each transforms under the ECS architecture from [ADR 0008](../adr/0008-entity-component-system.md).
|
||||
|
||||
Each scenario follows the same structure: **Current Flow** (what happens today), **ECS Flow** (what it looks like with the World), and a **Key Differences** table.
|
||||
ECS principles are realized across a set of dedicated Pinia stores keyed by string IDs (shipped in PR 12617): `widgetValueStore` (keyed by `WidgetId` = `graphId:nodeId:name`, see `src/types/widgetId.ts`), `layoutStore` (mutated via `useLayoutMutations()`), `nodeOutputStore`, `domWidgetStore`, `subgraphNavigationStore`, and `previewExposureStore`. Components live as plain-data entries in these stores; systems read and mutate them through store getters and command-style mutations.
|
||||
|
||||
Each scenario follows the same structure: **Current Flow** (what happens today), **ECS Flow** (the store-backed target), and a **Key Differences** table.
|
||||
|
||||
## 1. Node Removal
|
||||
|
||||
@@ -63,47 +65,43 @@ Problems: the graph method manually disconnects every slot, cleans up reroutes,
|
||||
sequenceDiagram
|
||||
participant Caller
|
||||
participant CS as ConnectivitySystem
|
||||
participant W as World
|
||||
participant VS as VersionSystem
|
||||
participant LM as useLayoutMutations()
|
||||
participant LS as layoutStore
|
||||
participant WVS as widgetValueStore
|
||||
participant NOS as nodeOutputStore
|
||||
participant DWS as domWidgetStore
|
||||
|
||||
Caller->>CS: removeNode(world, nodeId)
|
||||
Caller->>CS: removeNode(nodeId)
|
||||
|
||||
CS->>W: getComponent(nodeId, Connectivity)
|
||||
W-->>CS: { inputSlotIds, outputSlotIds }
|
||||
CS->>LS: read node links (incoming + outgoing)
|
||||
LS-->>CS: linkIds
|
||||
|
||||
loop each slotId
|
||||
CS->>W: getComponent(slotId, SlotConnection)
|
||||
W-->>CS: { linkIds }
|
||||
loop each linkId
|
||||
CS->>CS: removeLink(world, linkId)
|
||||
Note over CS,W: removes Link entity + updates remote slots
|
||||
end
|
||||
CS->>W: deleteEntity(slotId)
|
||||
loop each linkId
|
||||
CS->>LM: deleteLink(linkId)
|
||||
Note over LM,LS: removes link entry +<br/>updates both slot endpoints
|
||||
end
|
||||
|
||||
CS->>W: getComponent(nodeId, WidgetContainer)
|
||||
W-->>CS: { widgetIds }
|
||||
loop each widgetId
|
||||
CS->>W: deleteEntity(widgetId)
|
||||
loop each widget on node
|
||||
CS->>WVS: deleteWidget(widgetId)
|
||||
CS->>DWS: unregisterWidget(widgetId)
|
||||
end
|
||||
|
||||
CS->>W: deleteEntity(nodeId)
|
||||
Note over W: removes Position, NodeVisual, NodeType,<br/>Connectivity, Execution, Properties,<br/>WidgetContainer — all at once
|
||||
|
||||
CS->>VS: markChanged()
|
||||
CS->>NOS: removeNodeOutputs(nodeId)
|
||||
CS->>LM: deleteNode(nodeId)
|
||||
Note over CS,LS: coordinated cleanup across stores —<br/>each store drops its entry for the node
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
| Aspect | Current | ECS |
|
||||
| ------------------- | ------------------------------------------------ | ------------------------------------------------------ |
|
||||
| Lines of code | ~107 in one method | ~30 in system function |
|
||||
| Entity types known | Graph knows about all 6+ types | ConnectivitySystem knows Connectivity + SlotConnection |
|
||||
| Cleanup | Manual per-slot, per-link, per-reroute | `deleteEntity()` removes all components atomically |
|
||||
| Canvas notification | `setDirtyCanvas()` called explicitly | RenderSystem sees missing entity on next frame |
|
||||
| Store cleanup | WidgetValueStore/LayoutStore NOT cleaned up | World deletion IS the cleanup |
|
||||
| Undo/redo | `beforeChange()`/`afterChange()` manually placed | System snapshots affected components before deletion |
|
||||
| Testability | Needs full LGraph + LGraphCanvas | Needs only World + ConnectivitySystem |
|
||||
| Aspect | Current | ECS |
|
||||
| ------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| Lines of code | ~107 in one method | ~30 in system function |
|
||||
| Entity types known | Graph knows about all 6+ types | ConnectivitySystem coordinates layoutStore + widget/output stores |
|
||||
| Cleanup | Manual per-slot, per-link, per-reroute | `deleteLink()`/`deleteNode()` mutations per layout entry |
|
||||
| Canvas notification | `setDirtyCanvas()` called explicitly | Vue reactivity: components re-render when store entries change |
|
||||
| Store cleanup | WidgetValueStore/LayoutStore NOT cleaned up | Coordinated: `deleteWidget`, `deleteLink`/`deleteNode`, `removeNodeOutputs`, `unregisterWidget` |
|
||||
| Undo/redo | `beforeChange()`/`afterChange()` manually placed | Layout mutations are command records, replayable and undoable |
|
||||
| Testability | Needs full LGraph + LGraphCanvas | Needs only the relevant stores + ConnectivitySystem |
|
||||
|
||||
## 2. Serialization
|
||||
|
||||
@@ -165,41 +163,37 @@ Problems: serialization logic lives in 6 different `serialize()` methods across
|
||||
sequenceDiagram
|
||||
participant Caller
|
||||
participant SS as SerializationSystem
|
||||
participant W as World
|
||||
participant LS as layoutStore
|
||||
participant WVS as widgetValueStore
|
||||
participant CLS as node class state
|
||||
|
||||
Caller->>SS: serialize(world)
|
||||
Caller->>SS: serialize(graphId)
|
||||
|
||||
SS->>W: queryAll(NodeType, Position, Properties, WidgetContainer, Connectivity)
|
||||
W-->>SS: all node entities with their components
|
||||
SS->>LS: read node layouts (position, size, z-index)
|
||||
LS-->>SS: layout entries for graphId
|
||||
|
||||
SS->>W: queryAll(LinkEndpoints)
|
||||
W-->>SS: all link entities
|
||||
SS->>LS: read links + reroutes for graphId
|
||||
LS-->>SS: link / reroute entries
|
||||
|
||||
SS->>W: queryAll(SlotIdentity, SlotConnection)
|
||||
W-->>SS: all slot entities
|
||||
SS->>WVS: getWidget(widgetId) per node widget
|
||||
WVS-->>SS: WidgetState values
|
||||
|
||||
SS->>W: queryAll(RerouteLinks, Position)
|
||||
W-->>SS: all reroute entities
|
||||
SS->>CLS: read type / properties / flags
|
||||
CLS-->>SS: per-node class data
|
||||
|
||||
SS->>W: queryAll(GroupMeta, GroupChildren, Position)
|
||||
W-->>SS: all group entities
|
||||
|
||||
SS->>W: queryAll(SubgraphStructure, SubgraphMeta)
|
||||
W-->>SS: all subgraph entities
|
||||
|
||||
SS->>SS: assemble JSON from component data
|
||||
SS->>SS: assemble JSON from store entries + class state
|
||||
SS-->>Caller: SerializedGraph
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
| Aspect | Current | ECS |
|
||||
| ---------------------- | ----------------------------------------------- | ---------------------------------------------- |
|
||||
| Serialization logic | Spread across 6 classes (`serialize()` on each) | Single SerializationSystem |
|
||||
| Widget values | Collected inline during `node.serialize()` | WidgetValue component queried directly |
|
||||
| Subgraph recursion | `asSerialisable()` recursively calls itself | Flat query — SubgraphStructure has entity refs |
|
||||
| Adding a new component | Modify the entity's `serialize()` method | Add component to query in SerializationSystem |
|
||||
| Testing | Need full object graph to test serialization | Mock World with test components |
|
||||
| Aspect | Current | ECS |
|
||||
| ---------------------- | ----------------------------------------------- | --------------------------------------------------------- |
|
||||
| Serialization logic | Spread across 6 classes (`serialize()` on each) | Single SerializationSystem reading the stores |
|
||||
| Widget values | Collected inline during `node.serialize()` | `widgetValueStore.getWidget(widgetId)` read directly |
|
||||
| Subgraph recursion | `asSerialisable()` recursively calls itself | Flat read — layout entries carry scope tags, no recursion |
|
||||
| Adding a new component | Modify the entity's `serialize()` method | Read one more store in SerializationSystem |
|
||||
| Testing | Need full object graph to test serialization | Seed the stores with test entries |
|
||||
|
||||
## 3. Deserialization
|
||||
|
||||
@@ -274,64 +268,48 @@ Problems: two-phase creation is necessary because nodes need to reference each o
|
||||
sequenceDiagram
|
||||
participant Caller
|
||||
participant SS as SerializationSystem
|
||||
participant W as World
|
||||
participant LS as LayoutSystem
|
||||
participant LM as useLayoutMutations()
|
||||
participant WVS as widgetValueStore
|
||||
participant ES as ExecutionSystem
|
||||
|
||||
Caller->>SS: deserialize(world, data)
|
||||
Caller->>SS: deserialize(graphId, data)
|
||||
|
||||
SS->>W: clear() [remove all entities]
|
||||
SS->>WVS: clearGraph(graphId)
|
||||
Note over SS,WVS: drop stale widget entries for this graph
|
||||
|
||||
Note over SS,W: All entities created in one pass — no two-phase needed
|
||||
Note over SS,LM: All entries created in one pass — no two-phase needed
|
||||
|
||||
loop each node in data
|
||||
SS->>W: createEntity(NodeEntityId)
|
||||
SS->>W: setComponent(id, Position, {...})
|
||||
SS->>W: setComponent(id, NodeType, {...})
|
||||
SS->>W: setComponent(id, NodeVisual, {...})
|
||||
SS->>W: setComponent(id, Properties, {...})
|
||||
SS->>W: setComponent(id, Execution, {...})
|
||||
SS->>LM: createNode(nodeId, { position, size, ... })
|
||||
end
|
||||
|
||||
loop each slot in data
|
||||
SS->>W: createEntity(SlotEntityId)
|
||||
SS->>W: setComponent(id, SlotIdentity, {...})
|
||||
SS->>W: setComponent(id, SlotConnection, {...})
|
||||
end
|
||||
|
||||
Note over SS,W: Slots reference links by ID — no resolution needed yet
|
||||
|
||||
loop each link in data
|
||||
SS->>W: createEntity(LinkEntityId)
|
||||
SS->>W: setComponent(id, LinkEndpoints, {...})
|
||||
SS->>LM: createLink(linkId, source, target)
|
||||
end
|
||||
|
||||
Note over SS,W: Connectivity assembled from slot/link components
|
||||
Note over SS,LM: links reference node + slot IDs directly,<br/>no instance resolution needed
|
||||
|
||||
loop each widget in data
|
||||
SS->>W: createEntity(WidgetEntityId)
|
||||
SS->>W: setComponent(id, WidgetIdentity, {...})
|
||||
SS->>W: setComponent(id, WidgetValue, {...})
|
||||
SS->>WVS: registerWidget(widgetId, { value, ... })
|
||||
end
|
||||
|
||||
SS->>SS: create reroutes, groups, subgraphs similarly
|
||||
SS->>SS: create reroutes, groups via layout mutations;<br/>subgraph scopes tagged on entries
|
||||
|
||||
Note over SS,W: Systems react to populated World
|
||||
Note over SS,ES: Systems read the populated stores
|
||||
|
||||
SS->>LS: runLayout(world)
|
||||
SS->>ES: computeExecutionOrder(world)
|
||||
SS->>ES: computeExecutionOrder(graphId)
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
| Aspect | Current | ECS |
|
||||
| ------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Two-phase creation | Required (nodes must exist before link resolution) | Not needed — components reference IDs, not instances |
|
||||
| Widget restoration | Hidden inside `node.configure()` line ~900 | Explicit: WidgetValue component written directly |
|
||||
| Store population | Side effect of `widget.setNodeId()` | World IS the store — writing component IS population |
|
||||
| Callback cascade | `onConnectionsChange`, `onInputAdded`, `onConfigure` fire during configure | No callbacks — systems query World after deserialization |
|
||||
| Subgraph ordering | Topological sort required | Flat write — SubgraphStructure just holds entity IDs |
|
||||
| Error handling | Failed node → placeholder with `has_errors=true` | Failed entity → skip; components that loaded are still valid |
|
||||
| Two-phase creation | Required (nodes must exist before link resolution) | Not needed — links reference string IDs, not instances |
|
||||
| Widget restoration | Hidden inside `node.configure()` line ~900 | Explicit: `widgetValueStore.registerWidget(widgetId, state)` |
|
||||
| Store population | Side effect of `widget.setNodeId()` | Direct: writing the store entry is the population |
|
||||
| Callback cascade | `onConnectionsChange`, `onInputAdded`, `onConfigure` fire during configure | No callbacks — systems read the stores after deserialization |
|
||||
| Subgraph ordering | Topological sort required | Flat write — scope tags on entries, no instance ordering |
|
||||
| Error handling | Failed node → placeholder with `has_errors=true` | Failed entry → skip; entries that loaded are still valid |
|
||||
|
||||
## 4. Pack Subgraph
|
||||
|
||||
@@ -394,50 +372,50 @@ Problems: 200+ lines in one method. Manual boundary link analysis. Clone-seriali
|
||||
sequenceDiagram
|
||||
participant Caller
|
||||
participant CS as ConnectivitySystem
|
||||
participant W as World
|
||||
participant LS as layoutStore
|
||||
participant LM as useLayoutMutations()
|
||||
participant SNS as subgraphNavigationStore
|
||||
|
||||
Caller->>CS: packSubgraph(world, selectedEntityIds)
|
||||
Caller->>CS: packSubgraph(selectedNodeIds)
|
||||
|
||||
CS->>W: query Connectivity + SlotConnection for selected nodes
|
||||
CS->>LS: read links for selected nodes
|
||||
CS->>CS: classify links as internal vs boundary
|
||||
|
||||
CS->>W: create new GraphId scope in scopes registry
|
||||
CS->>SNS: register new subgraph graphId
|
||||
|
||||
Note over CS,W: Create SubgraphNode entity in parent scope
|
||||
Note over CS,LM: Create SubgraphNode layout entry in parent graph
|
||||
|
||||
CS->>W: createEntity(NodeEntityId) [the SubgraphNode]
|
||||
CS->>W: setComponent(nodeId, Position, { center of selection })
|
||||
CS->>W: setComponent(nodeId, SubgraphStructure, { graphId, interface })
|
||||
CS->>W: setComponent(nodeId, SubgraphMeta, { name: 'New Subgraph' })
|
||||
CS->>LM: createNode(subgraphNodeId, { position: center of selection })
|
||||
CS->>CS: record SubgraphNode interface (boundary slots)
|
||||
|
||||
Note over CS,W: Re-parent selected entities into new graph scope
|
||||
Note over CS,LS: Re-tag selected entries into new graph scope
|
||||
|
||||
loop each selected entity
|
||||
CS->>W: update graphScope to new graphId
|
||||
loop each selected node + link
|
||||
CS->>LS: set graphId scope tag to new subgraph graphId
|
||||
end
|
||||
|
||||
Note over CS,W: Create boundary slots on SubgraphNode
|
||||
Note over CS,LM: Reconnect boundary links to SubgraphNode slots
|
||||
|
||||
loop each boundary input link
|
||||
CS->>W: create SlotEntity on SubgraphNode
|
||||
CS->>W: update LinkEndpoints to target new slot
|
||||
CS->>LM: deleteLink(oldLinkId)
|
||||
CS->>LM: createLink(newLinkId, source, subgraphNode input slot)
|
||||
end
|
||||
|
||||
loop each boundary output link
|
||||
CS->>W: create SlotEntity on SubgraphNode
|
||||
CS->>W: update LinkEndpoints to source from new slot
|
||||
CS->>LM: deleteLink(oldLinkId)
|
||||
CS->>LM: createLink(newLinkId, subgraphNode output slot, target)
|
||||
end
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
| Aspect | Current | ECS |
|
||||
| -------------------------- | ------------------------------------------------- | ------------------------------------------------------- |
|
||||
| Entity movement | Clone → serialize → configure → remove originals | Re-parent entities: update graphScope to new GraphId |
|
||||
| Boundary links | Disconnect → remove → recreate → reconnect | Update LinkEndpoints to point at new SubgraphNode slots |
|
||||
| Intermediate inconsistency | Graph is partially disconnected during operation | Atomic: all component writes happen together |
|
||||
| Code size | 200+ lines | ~50 lines in system |
|
||||
| Undo | `beforeChange()`/`afterChange()` wraps everything | Snapshot affected components before mutation |
|
||||
| Aspect | Current | ECS |
|
||||
| -------------------------- | ------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Entity movement | Clone → serialize → configure → remove originals | Re-tag entries: change graphId scope tag on store entries |
|
||||
| Boundary links | Disconnect → remove → recreate → reconnect | `deleteLink`/`createLink` against the new SubgraphNode slots |
|
||||
| Intermediate inconsistency | Graph is partially disconnected during operation | Mutations batch together as one command sequence |
|
||||
| Code size | 200+ lines | ~50 lines in system |
|
||||
| Undo | `beforeChange()`/`afterChange()` wraps everything | Layout mutation commands replay and undo as a batch |
|
||||
|
||||
## 5. Unpack Subgraph
|
||||
|
||||
@@ -496,48 +474,49 @@ Problems: ID remapping is complex and error-prone. Magic IDs (SUBGRAPH_INPUT_ID
|
||||
sequenceDiagram
|
||||
participant Caller
|
||||
participant CS as ConnectivitySystem
|
||||
participant W as World
|
||||
participant LS as layoutStore
|
||||
participant LM as useLayoutMutations()
|
||||
participant SNS as subgraphNavigationStore
|
||||
|
||||
Caller->>CS: unpackSubgraph(world, subgraphNodeId)
|
||||
Caller->>CS: unpackSubgraph(subgraphNodeId)
|
||||
|
||||
CS->>W: getComponent(subgraphNodeId, SubgraphStructure)
|
||||
W-->>CS: { graphId, interface }
|
||||
CS->>CS: read SubgraphNode interface (boundary slots)
|
||||
|
||||
CS->>W: query entities where graphScope = graphId
|
||||
W-->>CS: all child entities (nodes, links, reroutes, etc.)
|
||||
CS->>LS: query entries where graphId scope = subgraph graphId
|
||||
LS-->>CS: child entries (nodes, links, reroutes)
|
||||
|
||||
Note over CS,W: Re-parent entities to containing graph scope
|
||||
Note over CS,LS: Re-tag entries to containing graph scope
|
||||
|
||||
loop each child entity
|
||||
CS->>W: update graphScope to parent scope
|
||||
loop each child entry
|
||||
CS->>LS: set graphId scope tag to parent scope
|
||||
end
|
||||
|
||||
Note over CS,W: Reconnect boundary links
|
||||
Note over CS,LM: Reconnect boundary links
|
||||
|
||||
loop each boundary slot in interface
|
||||
CS->>W: getComponent(slotId, SlotConnection)
|
||||
CS->>W: update LinkEndpoints: SubgraphNode slot → internal node slot
|
||||
CS->>LM: deleteLink(boundaryLinkId)
|
||||
CS->>LM: createLink(newLinkId, external slot → internal node slot)
|
||||
end
|
||||
|
||||
CS->>W: deleteEntity(subgraphNodeId)
|
||||
CS->>W: remove graphId from scopes registry
|
||||
CS->>LM: deleteNode(subgraphNodeId)
|
||||
CS->>SNS: drop subgraph graphId
|
||||
|
||||
Note over CS,W: Offset positions
|
||||
Note over CS,LM: Offset positions
|
||||
|
||||
loop each moved entity
|
||||
CS->>W: update Position component
|
||||
loop each moved node
|
||||
CS->>LM: moveNode(nodeId, position + offset)
|
||||
end
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
| Aspect | Current | ECS |
|
||||
| ----------------- | --------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| ID remapping | `nodeIdMap[oldId] = newId` for every node and link | No remapping — entities keep their IDs, only graphScope changes |
|
||||
| Magic IDs | SUBGRAPH_INPUT_ID = -10, SUBGRAPH_OUTPUT_ID = -20 | No magic IDs — boundary modeled as slot entities |
|
||||
| Clone vs move | Clone nodes, assign new IDs, configure from scratch | Move entity references between scopes |
|
||||
| Link reconnection | Remap origin_id/target_id, create new LLink objects | Update LinkEndpoints component in place |
|
||||
| Complexity | ~200 lines with deduplication and reroute remapping | ~40 lines, no remapping needed |
|
||||
| Aspect | Current | ECS |
|
||||
| ----------------- | --------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| ID remapping | `nodeIdMap[oldId] = newId` for every node and link | No remapping — entries keep their IDs, only the scope tag changes |
|
||||
| Magic IDs | SUBGRAPH_INPUT_ID = -10, SUBGRAPH_OUTPUT_ID = -20 | No magic IDs — boundary modeled as SubgraphNode interface slots |
|
||||
| Clone vs move | Clone nodes, assign new IDs, configure from scratch | Re-tag store entries between scopes |
|
||||
| Link reconnection | Remap origin_id/target_id, create new LLink objects | `deleteLink`/`createLink` against the resolved endpoints |
|
||||
| Complexity | ~200 lines with deduplication and reroute remapping | ~40 lines, no remapping needed |
|
||||
|
||||
## 6. Connect Slots
|
||||
|
||||
@@ -591,33 +570,28 @@ Problems: the source node orchestrates everything — it reaches into the graph'
|
||||
sequenceDiagram
|
||||
participant Caller
|
||||
participant CS as ConnectivitySystem
|
||||
participant W as World
|
||||
participant VS as VersionSystem
|
||||
participant LS as layoutStore
|
||||
participant LM as useLayoutMutations()
|
||||
|
||||
Caller->>CS: connect(world, outputSlotId, inputSlotId)
|
||||
Caller->>CS: connect(outputSlot, inputSlot)
|
||||
|
||||
CS->>W: getComponent(inputSlotId, SlotConnection)
|
||||
CS->>LS: read input slot link
|
||||
opt already connected
|
||||
CS->>CS: removeLink(world, existingLinkId)
|
||||
CS->>LM: deleteLink(existingLinkId)
|
||||
end
|
||||
|
||||
CS->>W: createEntity(LinkEntityId)
|
||||
CS->>W: setComponent(linkId, LinkEndpoints, {<br/> originNodeId, originSlotIndex,<br/> targetNodeId, targetSlotIndex, type<br/>})
|
||||
|
||||
CS->>W: update SlotConnection on outputSlotId (add linkId)
|
||||
CS->>W: update SlotConnection on inputSlotId (set linkId)
|
||||
|
||||
CS->>VS: markChanged()
|
||||
CS->>LM: createLink(linkId, {<br/> originNodeId, originSlotIndex,<br/> targetNodeId, targetSlotIndex, type<br/>})
|
||||
Note over LM,LS: createLink updates both slot endpoints<br/>and emits a command record
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
| Aspect | Current | ECS |
|
||||
| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------- |
|
||||
| Orchestrator | Source node (reaches into graph, target, reroutes) | ConnectivitySystem (queries World) |
|
||||
| Side effects | `_version++`, `setDirtyCanvas()`, `afterChange()`, callbacks | `markChanged()` — one call |
|
||||
| Reroute handling | Manual: iterate chain, add linkId to each | RerouteLinks component updated by system |
|
||||
| Slot mutation | Direct: `output.links.push()`, `input.link = id` | Component update: `setComponent(slotId, SlotConnection, ...)` |
|
||||
| Orchestrator | Source node (reaches into graph, target, reroutes) | ConnectivitySystem (reads layoutStore) |
|
||||
| Side effects | `_version++`, `setDirtyCanvas()`, `afterChange()`, callbacks | `createLink()` command — endpoints + change tracking included |
|
||||
| Reroute handling | Manual: iterate chain, add linkId to each | Reroute entries updated via layout mutations |
|
||||
| Slot mutation | Direct: `output.links.push()`, `input.link = id` | `createLink(linkId, ...)` updates both endpoints |
|
||||
| Validation | `onConnectInput`/`onConnectOutput` callbacks on nodes | Validation system or guard function |
|
||||
|
||||
## 7. Copy / Paste
|
||||
@@ -688,57 +662,61 @@ parent IDs all remapped independently. ~300 lines across multiple methods.
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant CS as ClipboardSystem
|
||||
participant W as World
|
||||
participant LS as layoutStore
|
||||
participant WVS as widgetValueStore
|
||||
participant LM as useLayoutMutations()
|
||||
participant CB as Clipboard
|
||||
|
||||
rect rgb(40, 40, 60)
|
||||
Note over User,CB: Copy
|
||||
User->>CS: copy(world, selectedEntityIds)
|
||||
CS->>W: snapshot all components for selected entities
|
||||
CS->>W: snapshot components for child entities (slots, widgets)
|
||||
CS->>W: snapshot connected links (LinkEndpoints)
|
||||
CS->>CB: store component snapshot
|
||||
User->>CS: copy(selectedNodeIds)
|
||||
CS->>LS: snapshot layout entries (nodes, links, reroutes)
|
||||
CS->>WVS: snapshot WidgetState for each widgetId
|
||||
CS->>CB: store cross-store snapshot
|
||||
end
|
||||
|
||||
rect rgb(40, 60, 40)
|
||||
Note over User,CB: Paste
|
||||
User->>CS: paste(world, position)
|
||||
User->>CS: paste(position)
|
||||
CS->>CB: retrieve snapshot
|
||||
|
||||
CS->>CS: generate ID remap table (old → new branded IDs)
|
||||
CS->>CS: build ID remap table (old → new nodeId / WidgetId)
|
||||
|
||||
loop each entity in snapshot
|
||||
CS->>W: createEntity(newId)
|
||||
loop each component
|
||||
CS->>W: setComponent(newId, type, remappedData)
|
||||
Note over CS,W: entity ID refs in component data<br/>are remapped via table
|
||||
end
|
||||
loop each node in snapshot
|
||||
CS->>LM: createNode(newNodeId, remapped layout)
|
||||
end
|
||||
loop each link in snapshot
|
||||
CS->>LM: createLink(newLinkId, remapped endpoints)
|
||||
Note over CS,LM: node + slot refs remapped via table
|
||||
end
|
||||
loop each widget in snapshot
|
||||
CS->>WVS: registerWidget(newWidgetId, WidgetState)
|
||||
end
|
||||
|
||||
CS->>CS: offset all Position components to cursor
|
||||
CS->>LM: batchMoveNodes(offset all to cursor)
|
||||
end
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
| Aspect | Current | ECS |
|
||||
| -------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
|
||||
| Copy format | Clone → serialize → JSON (format depends on class) | Component snapshot (uniform format for all entities) |
|
||||
| ID remapping | Separate logic per entity type (nodes, reroutes, subgraphs, links) | Single remap table applied to all entity ID refs in all components |
|
||||
| Paste reconstruction | `createNode()` → `add()` → `configure()` → `connect()` per node | `createEntity()` → `setComponent()` per entity (flat) |
|
||||
| Subgraph handling | Recursive clone + UUID remap + deduplication | Snapshot includes SubgraphStructure component with entity refs |
|
||||
| Code complexity | ~300 lines across 4 methods | ~60 lines in one system |
|
||||
| Aspect | Current | ECS |
|
||||
| -------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------- |
|
||||
| Copy format | Clone → serialize → JSON (format depends on class) | Store-entry snapshot (uniform shape across stores) |
|
||||
| ID remapping | Separate logic per entity type (nodes, reroutes, subgraphs, links) | One remap table applied to string keys (`nodeId`, `WidgetId`) |
|
||||
| Paste reconstruction | `createNode()` → `add()` → `configure()` → `connect()` per node | `createNode`/`createLink`/`registerWidget` per entry (flat) |
|
||||
| Subgraph handling | Recursive clone + UUID remap + deduplication | Snapshot carries scope tags; remap rewrites graphId keys |
|
||||
| Code complexity | ~300 lines across 4 methods | ~60 lines in one system |
|
||||
|
||||
## Summary: Cross-Cutting Benefits
|
||||
|
||||
| Benefit | Scenarios Where It Applies |
|
||||
| ----------------------------- | -------------------------------------------------------------------------- |
|
||||
| **Atomic operations** | Node Removal, Pack/Unpack — no intermediate inconsistent state |
|
||||
| **No scattered `_version++`** | All scenarios — VersionSystem handles change tracking |
|
||||
| **No callback cascades** | Deserialization, Connect — systems query World instead of firing callbacks |
|
||||
| **Uniform ID handling** | Copy/Paste, Unpack — one remap table instead of per-type logic |
|
||||
| **Entity deletion = cleanup** | Node Removal — `deleteEntity()` removes all components |
|
||||
| **No two-phase creation** | Deserialization — components reference IDs, not instances |
|
||||
| **Move instead of clone** | Pack/Unpack — entities keep their IDs, just change scope |
|
||||
| **Testable in isolation** | All scenarios — mock World, test one system |
|
||||
| **Undo/redo for free** | All scenarios — snapshot components before mutation, restore on undo |
|
||||
| Benefit | Scenarios Where It Applies |
|
||||
| ----------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| **Batched operations** | Node Removal, Pack/Unpack — mutations apply together as one command sequence |
|
||||
| **No scattered `_version++`** | All scenarios — layout mutation commands carry change tracking |
|
||||
| **No callback cascades** | Deserialization, Connect — systems read the stores instead of firing callbacks |
|
||||
| **Uniform ID handling** | Copy/Paste, Unpack — one remap table over string keys instead of per-type logic |
|
||||
| **Coordinated cleanup** | Node Removal — `deleteWidget` + `deleteLink`/`deleteNode` + `removeNodeOutputs` + `unregisterWidget` |
|
||||
| **No two-phase creation** | Deserialization — store entries reference string IDs, not instances |
|
||||
| **Move instead of clone** | Pack/Unpack — entries keep their IDs, only the scope tag changes |
|
||||
| **Testable in isolation** | All scenarios — seed the relevant stores, test one system |
|
||||
| **Undo/redo for free** | All scenarios — layout mutation commands replay and undo |
|
||||
|
||||
@@ -10,6 +10,14 @@ target architecture, see [ECS Target Architecture](ecs-target-architecture.md).
|
||||
For verified accuracy of these documents, see
|
||||
[Appendix: Critical Analysis](appendix-critical-analysis.md).
|
||||
|
||||
> **Target end-state (revised):** N dedicated Pinia stores keyed by composite
|
||||
> string IDs, one store per concern (widget values, DOM widgets, layout, node
|
||||
> outputs, subgraph navigation, preview exposure). The earlier "single unified
|
||||
> World with branded numeric entity IDs and `getComponent`/`setComponent`" model
|
||||
> was rejected. PR 12617 shipped the first stores against composite
|
||||
> `graphId:nodeId:name` string keys (`WidgetId`). Phases below are reframed
|
||||
> around dedicated stores; shipped work is marked ✅.
|
||||
|
||||
## Planning assumptions
|
||||
|
||||
- The bridge period is expected to span 2-3 release cycles.
|
||||
@@ -23,36 +31,16 @@ For verified accuracy of these documents, see
|
||||
Zero behavioral risk. Prepares the codebase for extraction without changing
|
||||
runtime semantics. All items are independently shippable.
|
||||
|
||||
### 0a. Centralize version counter
|
||||
### 0a. Centralize version counter ✅ Shipped
|
||||
|
||||
`graph._version++` appears in 19 locations across 7 files. The counter is only
|
||||
read once — for debug display in `LGraphCanvas.renderInfo()` (line 5389). It
|
||||
is not used for dirty-checking, caching, or reactivity.
|
||||
`LGraph.incrementVersion()` exists and is used everywhere. The counter is only
|
||||
read for debug display in `LGraphCanvas.renderInfo()`; it is not used for
|
||||
dirty-checking, caching, or reactivity.
|
||||
|
||||
**Change:** Add `LGraph.incrementVersion()` and replace all 19 direct
|
||||
increments.
|
||||
**Remaining cleanup:** One stray direct `_version++` at `LGraph.ts:831` should
|
||||
be replaced with `incrementVersion()`.
|
||||
|
||||
```
|
||||
incrementVersion(): void {
|
||||
this._version++
|
||||
}
|
||||
```
|
||||
|
||||
| File | Sites |
|
||||
| ---------------------- | ------------------------------------------------------- |
|
||||
| `LGraph.ts` | 5 (lines 956, 989, 1042, 1109, 2643) |
|
||||
| `LGraphNode.ts` | 8 (lines 833, 2989, 3138, 3176, 3304, 3539, 3550, 3567) |
|
||||
| `LGraphCanvas.ts` | 2 (lines 3084, 7880) |
|
||||
| `BaseWidget.ts` | 1 (line 439) |
|
||||
| `SubgraphInput.ts` | 1 (line 137) |
|
||||
| `SubgraphInputNode.ts` | 1 (line 190) |
|
||||
| `SubgraphOutput.ts` | 1 (line 102) |
|
||||
|
||||
**Why first:** Creates the seam where a VersionSystem can later intercept,
|
||||
batch, or replace the mechanism. Mechanical find-and-replace with zero
|
||||
behavioral change.
|
||||
|
||||
**Risk:** None. Existing null guards at call sites are preserved.
|
||||
**Risk:** None. Mechanical one-line change; existing null guards preserved.
|
||||
|
||||
### 0b. Add missing ID type aliases
|
||||
|
||||
@@ -79,246 +67,198 @@ Five factual errors verified during code review (see
|
||||
- `entity-problems.md`: `toJSON()` should be `toString()`, `execute()` should
|
||||
be `doExecute()`, method count ~539 should be ~848, `configure()` is ~240
|
||||
lines not ~180
|
||||
- `proto-ecs-stores.md`: `resolveDeepest()` does not exist on
|
||||
PromotedWidgetViewManager; actual methods are `reconcile()` / `getOrCreate()`
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Types and World Shell
|
||||
## Phase 1: Types and Dedicated Stores
|
||||
|
||||
Introduces the ECS type vocabulary and an empty World. No migration of existing
|
||||
code — new types coexist with old ones.
|
||||
Introduces the ID type vocabulary and the dedicated stores. Phase 1 end-state is
|
||||
N dedicated Pinia stores, each keyed by a composite string ID, coexisting with
|
||||
legacy class instances.
|
||||
|
||||
### 1a. Branded entity ID types
|
||||
### 1a. Branded string ID types ✅ Shipped (PR 12617)
|
||||
|
||||
Define branded types in a new `src/ecs/entityId.ts`:
|
||||
|
||||
```
|
||||
type NodeEntityId = number & { readonly __brand: 'NodeEntityId' }
|
||||
type LinkEntityId = number & { readonly __brand: 'LinkEntityId' }
|
||||
type WidgetEntityId = number & { readonly __brand: 'WidgetEntityId' }
|
||||
type SlotEntityId = number & { readonly __brand: 'SlotEntityId' }
|
||||
type RerouteEntityId = number & { readonly __brand: 'RerouteEntityId' }
|
||||
type GroupEntityId = number & { readonly __brand: 'GroupEntityId' }
|
||||
type GraphId = string & { readonly __brand: 'GraphId' } // scope, not entity
|
||||
```
|
||||
|
||||
Add cast helpers (`asNodeEntityId(id: number): NodeEntityId`) for use at
|
||||
system boundaries (deserialization, legacy bridge).
|
||||
|
||||
**Does NOT change existing code.** The branded types are new exports consumed
|
||||
only by new ECS code.
|
||||
|
||||
**Risk:** Low. New files, no modifications to existing code.
|
||||
|
||||
**Consideration:** `NodeId = number | string` is the current type. The branded
|
||||
`NodeEntityId` narrows to `number`. The `string` branch exists solely for
|
||||
subgraph-related nodes (GroupNode hack). The migration must decide whether to:
|
||||
|
||||
- Keep `NodeEntityId = number` and handle the string case at the bridge layer
|
||||
- Or define `NodeEntityId = number | string` with branding (less safe)
|
||||
|
||||
Recommend the former: the bridge layer coerces string IDs to a numeric
|
||||
mapping, and only branded numeric IDs enter the World.
|
||||
|
||||
### 1b. Component interfaces
|
||||
|
||||
Define component interfaces in `src/ecs/components/`:
|
||||
|
||||
```
|
||||
src/ecs/
|
||||
entityId.ts # Branded ID types
|
||||
components/
|
||||
position.ts # Position (shared by Node, Reroute, Group)
|
||||
nodeType.ts # NodeType
|
||||
nodeVisual.ts # NodeVisual
|
||||
connectivity.ts # Connectivity
|
||||
execution.ts # Execution
|
||||
properties.ts # Properties
|
||||
widgetContainer.ts # WidgetContainer
|
||||
linkEndpoints.ts # LinkEndpoints
|
||||
...
|
||||
world.ts # World type and factory
|
||||
```
|
||||
|
||||
Components are TypeScript interfaces only — no runtime code. They mirror
|
||||
the decomposition in ADR 0008 Section "Component Decomposition."
|
||||
|
||||
**Risk:** None. Interface-only files.
|
||||
|
||||
### 1c. World type
|
||||
|
||||
Define the World as a typed container:
|
||||
`src/types/widgetId.ts` ships the branded string `WidgetId`:
|
||||
|
||||
```ts
|
||||
interface World {
|
||||
nodes: Map<NodeEntityId, NodeComponents>
|
||||
links: Map<LinkEntityId, LinkComponents>
|
||||
widgets: Map<WidgetEntityId, WidgetComponents>
|
||||
slots: Map<SlotEntityId, SlotComponents>
|
||||
reroutes: Map<RerouteEntityId, RerouteComponents>
|
||||
groups: Map<GroupEntityId, GroupComponents>
|
||||
scopes: Map<GraphId, GraphId | null> // graph scope DAG (parent or null for root)
|
||||
|
||||
createEntity<K extends EntityKind>(kind: K): EntityIdFor<K>
|
||||
deleteEntity<K extends EntityKind>(kind: K, id: EntityIdFor<K>): void
|
||||
getComponent<C>(id: EntityId, component: ComponentKey<C>): C | undefined
|
||||
setComponent<C>(id: EntityId, component: ComponentKey<C>, data: C): void
|
||||
}
|
||||
type WidgetId = string & { readonly __brand: 'WidgetId' }
|
||||
```
|
||||
|
||||
Subgraphs are not a separate entity kind. A node with a `SubgraphStructure`
|
||||
component represents a subgraph. The `scopes` map tracks the graph nesting DAG.
|
||||
See [Subgraph Boundaries](subgraph-boundaries-and-promotion.md) for the full
|
||||
model.
|
||||
Format: `graphId:nodeId:name`. A `parseWidgetId()` helper splits a `WidgetId`
|
||||
back into its `{ graphId, nodeId, name }` parts at store boundaries.
|
||||
|
||||
World scope is per workflow instance. Linked subgraph definitions can be reused
|
||||
The composite string key carries the structural relationship (graph -> node ->
|
||||
widget) directly in the key. There is no synthetic opaque number and no reverse
|
||||
lookup index.
|
||||
|
||||
**Consideration:** `NodeId = number | string`. The `string` branch exists for
|
||||
subgraph-related nodes (GroupNode hack). The `WidgetId` format stringifies the
|
||||
`nodeId` segment, so both numeric and string node IDs flow through unchanged.
|
||||
|
||||
### 1b. Plain-data store state shapes
|
||||
|
||||
Each dedicated store holds plain-data records for its concern — no methods on the
|
||||
records, behavior lives in store actions and composables. State shapes mirror the
|
||||
decomposition in ADR 0008 Section "Component Decomposition" (position, node type,
|
||||
node visual, connectivity, execution, properties, widget container, link
|
||||
endpoints).
|
||||
|
||||
**Risk:** None. Type-only definitions.
|
||||
|
||||
### 1c. Dedicated stores
|
||||
|
||||
Phase 1 end-state is a set of dedicated Pinia stores, one per concern, each
|
||||
keyed by its own composite string ID. Each store owns its data and exposes a
|
||||
narrow accessor surface. There is no single container that fronts all entities.
|
||||
|
||||
Shipped stores:
|
||||
|
||||
| Store | File |
|
||||
| ------------------------- | ----------------------------------------------- |
|
||||
| `widgetValueStore` | `src/stores/widgetValueStore.ts` |
|
||||
| `domWidgetStore` | `src/stores/domWidgetStore.ts` |
|
||||
| `layoutStore` | `src/renderer/core/layout/store/layoutStore.ts` |
|
||||
| `nodeOutputStore` | `src/stores/nodeOutputStore.ts` |
|
||||
| `subgraphNavigationStore` | `src/stores/subgraphNavigationStore.ts` |
|
||||
| `previewExposureStore` | `src/stores/previewExposureStore.ts` |
|
||||
|
||||
`widgetValueStore` exposes `registerWidget`, `getWidget`, `setValue`,
|
||||
`deleteWidget`, `getNodeWidgets`, and `clearGraph`, all `WidgetId`-native. There
|
||||
is no shared `lastWidgetId` counter; identity comes from the composite key.
|
||||
|
||||
Store scope is per workflow instance. Linked subgraph definitions can be reused
|
||||
across instances, but mutable runtime state (widget values, execution state,
|
||||
selection/transient view state) remains instance-scoped through `graphId`.
|
||||
selection/transient view state) stays instance-scoped through `graphId` embedded
|
||||
in each composite key.
|
||||
|
||||
Initial implementation: plain `Map`-backed. No reactivity, no CRDT, no
|
||||
persistence. The World exists but nothing populates it yet.
|
||||
Subgraphs are not a separate store. Subgraph nesting is tracked in
|
||||
`subgraphNavigationStore`. See
|
||||
[Subgraph Boundaries](subgraph-boundaries-and-promotion.md) for the full model.
|
||||
|
||||
**Risk:** Low. New code, no integration points.
|
||||
**Risk:** Low. Stores are additive; integration happens in Phase 2.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Bridge Layer
|
||||
## Phase 2: Store Integration
|
||||
|
||||
Connects the legacy class instances to the World. Both old and new code can
|
||||
read entity state; writes still go through legacy classes.
|
||||
Connects the legacy class instances to the dedicated stores. Both old and new
|
||||
code can read entity state; writes for not-yet-migrated concerns still go through
|
||||
legacy classes.
|
||||
|
||||
### 2a. Read-only bridge for Position
|
||||
### 2a. Position reads through layoutStore
|
||||
|
||||
The LayoutStore (`src/renderer/core/layout/store/layoutStore.ts`) already
|
||||
extracts position data for nodes, links, and reroutes into Y.js CRDTs. The
|
||||
bridge reads from LayoutStore and populates the World's `Position` component.
|
||||
`layoutStore` (`src/renderer/core/layout/store/layoutStore.ts`) already extracts
|
||||
position data for nodes, links, and reroutes into Y.js CRDTs and is the source of
|
||||
truth for layout.
|
||||
|
||||
**Approach:** A `PositionBridge` that observes LayoutStore changes and mirrors
|
||||
them into the World. New code reads `world.getComponent(nodeId, Position)`;
|
||||
legacy code continues to read `node.pos` / LayoutStore directly.
|
||||
**Approach:** New code reads position via `layoutStore` queries (and
|
||||
`useLayoutMutations()` for writes); legacy code continues to read `node.pos`
|
||||
directly during the transition. No second copy of position data is introduced —
|
||||
`layoutStore` stays authoritative.
|
||||
|
||||
**Open question:** Should the World wrap the Y.js maps or maintain its own
|
||||
plain-data copy? Options:
|
||||
**Risk:** Medium. The legacy `node.pos` read path must stay consistent with
|
||||
`layoutStore` during the transition. Watch for stale reads during render.
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
| ---------------------- | ------------------------------------- | ----------------------------------------------- |
|
||||
| World wraps Y.js | Single source of truth; no sync lag | World API becomes CRDT-aware; harder to test |
|
||||
| World copies from Y.js | Clean World API; easy to test | Two copies of position data; sync overhead |
|
||||
| World replaces Y.js | Pure ECS; no CRDT dependency in World | Breaks collaboration (ADR 0003); massive change |
|
||||
### 2b. Consolidate widget callers onto widgetValueStore ✅ Largely shipped (PR 12617)
|
||||
|
||||
**Recommendation:** Start with "World copies from Y.js" for simplicity. The
|
||||
copy is cheap (position is small data). Revisit if sync overhead becomes
|
||||
measurable.
|
||||
`widgetValueStore` (`src/stores/widgetValueStore.ts`) holds widget state in
|
||||
plain records keyed by `WidgetId` (`graphId:nodeId:name`) and is the source of
|
||||
truth for widget values. PR 12617 reverted the earlier synthetic-numeric-ID
|
||||
bridge approach.
|
||||
|
||||
**Risk:** Medium. Introduces a sync point between two state systems. Must
|
||||
ensure the bridge doesn't create subtle ordering bugs (e.g., World reads stale
|
||||
position during render).
|
||||
**Remaining work:** Consolidate the remaining widget callers onto
|
||||
`widgetValueStore`. Reads use `getWidget(widgetId)` / `getNodeWidgets(graphId,
|
||||
nodeId)`; writes use `setValue(widgetId, value)`; `parseWidgetId()` recovers the
|
||||
`{ graphId, nodeId, name }` parts at boundaries.
|
||||
|
||||
### 2b. Read-only bridge for WidgetValue
|
||||
|
||||
WidgetValueStore (`src/stores/widgetValueStore.ts`) already extracts widget
|
||||
state into plain `WidgetState` objects keyed by `graphId:nodeId:name`. This is
|
||||
the closest proto-ECS store.
|
||||
|
||||
**Approach:** A `WidgetBridge` that maps `WidgetValueStore` entries into
|
||||
`WidgetValue` components in the World, keyed by `WidgetEntityId`. Requires
|
||||
assigning synthetic widget IDs (via `lastWidgetId` counter on LGraphState).
|
||||
|
||||
**Dependency:** Requires 1a (branded IDs) for `WidgetEntityId`.
|
||||
|
||||
**Risk:** Low-Medium. WidgetValueStore is well-structured. Main complexity is
|
||||
the ID mapping — widgets currently lack independent IDs, so the bridge must
|
||||
maintain a `(nodeId, widgetName) -> WidgetEntityId` lookup.
|
||||
**Risk:** Low. The store is well-structured and `WidgetId`-native; identity comes
|
||||
from the composite key with no separate lookup index.
|
||||
|
||||
**Promoted-widget caveat:** ADR 0009 assigns promoted value widgets a
|
||||
host-boundary identity (`host node locator + SubgraphInput.name`). Interior
|
||||
source node/widget identity is preserved only as migration and diagnostic
|
||||
metadata.
|
||||
|
||||
### 2c. Read-only bridge for Node metadata
|
||||
### 2c. Node metadata stores
|
||||
|
||||
Populate `NodeType`, `NodeVisual`, `Properties`, `Execution` components by
|
||||
reading from `LGraphNode` instances. These are simple property copies.
|
||||
Populate node-metadata records (node type, visual, properties, execution) by
|
||||
reading from `LGraphNode` instances. These are simple property copies into the
|
||||
relevant store.
|
||||
|
||||
**Approach:** When a node is added to the graph (`LGraph.add()`), the bridge
|
||||
creates the corresponding entity in the World and populates its components.
|
||||
When a node is removed, the bridge deletes the entity.
|
||||
|
||||
The `incrementVersion()` method from Phase 0a becomes the hook point — when
|
||||
version increments, the bridge can re-sync changed components. (This is why
|
||||
centralizing version first matters.)
|
||||
**Approach:** When a node is added to the graph (`LGraph.add()`), the store
|
||||
records its metadata. When a node is removed, the store drops it. The
|
||||
`incrementVersion()` seam from Phase 0a is a candidate hook point for re-sync
|
||||
when changed.
|
||||
|
||||
**Risk:** Medium. Must handle the full node lifecycle (add, configure, remove)
|
||||
without breaking existing behavior. The bridge is read-only (World mirrors
|
||||
classes, not the reverse), which limits blast radius.
|
||||
without breaking existing behavior. Stores mirror the classes during the
|
||||
transition, which limits blast radius.
|
||||
|
||||
### Bridge sunset criteria (applies to every Phase 2 bridge)
|
||||
### Store sunset criteria (applies to every Phase 2 concern)
|
||||
|
||||
A bridge can move from "transitional" to "removal candidate" only when:
|
||||
A legacy path can move from "transitional" to "removal candidate" only when:
|
||||
|
||||
- All production reads for that concern flow through World component queries.
|
||||
- All production writes for that concern flow through system APIs.
|
||||
- Serialization parity tests show no diff between legacy and World paths.
|
||||
- Extension compatibility tests pass without bridge-only fallback paths.
|
||||
- All production reads for that concern flow through store accessors.
|
||||
- All production writes for that concern flow through store actions.
|
||||
- Serialization parity tests show no diff between legacy and store-driven paths.
|
||||
- Extension compatibility tests pass without legacy-only fallback paths.
|
||||
|
||||
These criteria prevent the bridge from becoming permanent by default.
|
||||
These criteria prevent the dual path from becoming permanent by default.
|
||||
|
||||
### Bridge duration and maintenance controls
|
||||
### Dual-path duration and maintenance controls
|
||||
|
||||
To contain dual-path maintenance cost during Phases 2-4:
|
||||
|
||||
- Every bridge concern has a named owner and target sunset release.
|
||||
- Every PR touching bridge-covered data paths must include parity tests for both
|
||||
legacy and World-driven execution.
|
||||
- Bridge fallback usage is instrumented in integration/e2e and reviewed every
|
||||
milestone; upward trends block new bridge expansion.
|
||||
- Any bridge that misses its target sunset release requires an explicit risk
|
||||
- Every concern has a named owner and target sunset release.
|
||||
- Every PR touching store-covered data paths must include parity tests for both
|
||||
legacy and store-driven execution.
|
||||
- Legacy fallback usage is instrumented in integration/e2e and reviewed every
|
||||
milestone; upward trends block new dual-path expansion.
|
||||
- Any concern that misses its target sunset release requires an explicit risk
|
||||
review and revised removal plan.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Systems
|
||||
|
||||
Introduce system functions that operate on World data. Systems coexist with
|
||||
Introduce system functions that operate on store data. Systems coexist with
|
||||
legacy methods — they don't replace them yet.
|
||||
|
||||
### 3a. SerializationSystem (read-only)
|
||||
|
||||
A function `serializeFromWorld(world: World): SerializedGraph` that produces
|
||||
workflow JSON by querying World components. Run alongside the existing
|
||||
`LGraph.serialize()` in tests to verify equivalence.
|
||||
A function `serializeFromStores(): SerializedGraph` that produces workflow JSON
|
||||
by querying the dedicated stores. Run alongside the existing `LGraph.serialize()`
|
||||
in tests to verify equivalence.
|
||||
|
||||
**Why first:** Serialization is read-only and has a clear correctness check
|
||||
(output must match existing serialization). It exercises every component type
|
||||
and proves the World contains sufficient data.
|
||||
(output must match existing serialization). It exercises every store and proves
|
||||
the stores contain sufficient data.
|
||||
|
||||
**Risk:** Low. Runs in parallel with existing code; does not replace it.
|
||||
|
||||
### 3b. VersionSystem
|
||||
|
||||
Replace the `incrementVersion()` method with a system that owns all change
|
||||
tracking. The system observes component mutations on the World and
|
||||
auto-increments the version counter.
|
||||
Move change tracking behind a system that observes store mutations and
|
||||
auto-increments the version counter, replacing scattered explicit increment
|
||||
calls.
|
||||
|
||||
**Dependency:** Requires Phase 2 bridges to be in place (otherwise the World
|
||||
doesn't see changes).
|
||||
**Dependency:** Requires Phase 2 store integration (otherwise the system doesn't
|
||||
see changes).
|
||||
|
||||
**Risk:** Medium. Must not miss any change that the scattered `_version++`
|
||||
currently catches. The 19-site inventory from Phase 0a serves as the test
|
||||
matrix.
|
||||
historically caught.
|
||||
|
||||
### 3c. ConnectivitySystem (queries only)
|
||||
|
||||
A system that can answer connectivity queries by reading `Connectivity`,
|
||||
`SlotConnection`, and `LinkEndpoints` components from the World:
|
||||
A system that answers connectivity queries by reading connectivity, slot, and
|
||||
link-endpoint records from the relevant stores:
|
||||
|
||||
- "What nodes are connected to this node's inputs?"
|
||||
- "What links pass through this reroute?"
|
||||
- "What is the execution order?"
|
||||
|
||||
Does not perform mutations yet — just queries. Validates that the World's
|
||||
connectivity data is complete and consistent with the class-based graph.
|
||||
Does not perform mutations yet — just queries. Validates that store connectivity
|
||||
data is complete and consistent with the class-based graph.
|
||||
|
||||
**Risk:** Low. Read-only system with equivalence tests.
|
||||
|
||||
@@ -326,27 +266,27 @@ connectivity data is complete and consistent with the class-based graph.
|
||||
|
||||
## Phase 4: Write Path Migration
|
||||
|
||||
Systems begin owning mutations. Legacy class methods delegate to systems.
|
||||
This is the highest-risk phase.
|
||||
Systems begin owning mutations. Legacy class methods delegate to stores and
|
||||
systems. This is the highest-risk phase.
|
||||
|
||||
### 4a. Position writes through World
|
||||
### 4a. Position writes through layoutStore
|
||||
|
||||
New code writes position via `world.setComponent(nodeId, Position, ...)`.
|
||||
The bridge propagates changes back to LayoutStore and `LGraphNode.pos`.
|
||||
New code writes position via `useLayoutMutations()` against `layoutStore`. A
|
||||
compatibility shim propagates changes back to `LGraphNode.pos` for legacy
|
||||
readers.
|
||||
|
||||
**This inverts the data flow:** Phase 2 had legacy -> World (read bridge).
|
||||
Phase 4 has World -> legacy (write bridge). Both paths must work during the
|
||||
transition.
|
||||
**This inverts the data flow:** Phase 2 had legacy -> store (read path). Phase 4
|
||||
has store -> legacy (write path). Both must work during the transition.
|
||||
|
||||
**Risk:** High. Two-way sync between World and legacy state. Must handle
|
||||
re-entrant updates (World write triggers bridge, which writes to legacy,
|
||||
which must NOT trigger another World write).
|
||||
**Risk:** High. Two-way sync between `layoutStore` and legacy state. Must handle
|
||||
re-entrant updates (store write triggers the shim, which writes to legacy, which
|
||||
must NOT trigger another store write).
|
||||
|
||||
### 4b. ConnectivitySystem mutations
|
||||
|
||||
`connect()`, `disconnect()`, `removeNode()` operations implemented as system
|
||||
functions on the World. Legacy `LGraphNode.connect()` etc. delegate to the
|
||||
system.
|
||||
functions over the connectivity stores. Legacy `LGraphNode.connect()` etc.
|
||||
delegate to the system.
|
||||
|
||||
**Extension API concern:** The current system fires callbacks at each step:
|
||||
|
||||
@@ -363,8 +303,8 @@ the system knowing about the callback API.
|
||||
|
||||
**Phase 4 callback contract (locked):**
|
||||
|
||||
- `onConnectOutput()` and `onConnectInput()` run before any World mutation.
|
||||
- If either callback rejects, abort with no component writes, no version bump,
|
||||
- `onConnectOutput()` and `onConnectInput()` run before any store mutation.
|
||||
- If either callback rejects, abort with no store writes, no version bump,
|
||||
and no lifecycle events.
|
||||
- `onConnectionsChange()` fires synchronously after commit, preserving current
|
||||
source-then-target ordering.
|
||||
@@ -374,14 +314,14 @@ the system knowing about the callback API.
|
||||
**Risk:** High. Extensions depend on callback ordering and timing. Must be
|
||||
validated against real-world extensions.
|
||||
|
||||
### 4c. Widget write path
|
||||
### 4c. Widget write path ✅ Largely shipped (PR 12617)
|
||||
|
||||
Widget value changes go through the World instead of directly through
|
||||
WidgetValueStore. The World's `WidgetValue` component becomes the single
|
||||
source of truth; WidgetValueStore becomes a read-through cache or is removed.
|
||||
`widgetValueStore.setValue()` is already the widget write path and the source of
|
||||
truth for widget values. Remaining work routes the last legacy widget writers
|
||||
through `setValue()` rather than mutating widget instances directly.
|
||||
|
||||
**Risk:** Medium. WidgetValueStore is already well-abstracted. The main
|
||||
change is routing writes through the World instead of the store.
|
||||
**Risk:** Medium. The store is well-abstracted and `WidgetId`-native. The main
|
||||
change is migrating the remaining direct-mutation call sites onto `setValue()`.
|
||||
|
||||
### 4d. Layout write path and render decoupling
|
||||
|
||||
@@ -407,25 +347,25 @@ Before enabling ECS render reads as default for any migrated family:
|
||||
- Compare legacy vs ECS p95 frame time and mean draw cost.
|
||||
- Block rollout on statistically significant regression beyond agreed budget
|
||||
(default budget: 5% p95 frame-time regression ceiling).
|
||||
- Capture profiler traces proving the dominant cost is not repeated
|
||||
`world.getComponent()` lookups.
|
||||
- Capture profiler traces proving the dominant cost is not repeated store
|
||||
accessor lookups.
|
||||
|
||||
### Phase 3 -> 4 gate (required)
|
||||
|
||||
Phase 4 starts only when all of the following are true:
|
||||
|
||||
- A transaction wrapper API exists on the World and is used by connectivity and
|
||||
widget write paths in integration tests.
|
||||
- A store/command-executor transaction wrapper exists and is used by connectivity
|
||||
and widget write paths in integration tests.
|
||||
- Undo batching parity is proven: one logical user action yields one undo
|
||||
checkpoint in both legacy and ECS paths.
|
||||
checkpoint in both legacy and store-driven paths.
|
||||
- Callback timing and rejection semantics from Phase 4b are covered by
|
||||
integration tests.
|
||||
- A representative extension suite passes, including `rgthree-comfy`.
|
||||
- Write bridge re-entrancy tests prove there is no World <-> legacy feedback
|
||||
- Write-path re-entrancy tests prove there is no store <-> legacy feedback
|
||||
loop.
|
||||
- Layout migration for any enabled node family passes read-only render checks
|
||||
(no `arrange()` writes during draw).
|
||||
- Render hot-path benchmark gate passes for every family moving to ECS-first
|
||||
- Render hot-path benchmark gate passes for every family moving to store-first
|
||||
reads.
|
||||
|
||||
---
|
||||
@@ -435,10 +375,11 @@ Phase 4 starts only when all of the following are true:
|
||||
Remove bridge layers and deprecated class properties. This phase happens
|
||||
per-component, not all at once.
|
||||
|
||||
### 5a. Remove Position bridge
|
||||
### 5a. Remove Position compatibility shim
|
||||
|
||||
Once all position reads and writes go through the World, remove the bridge
|
||||
and the `pos`/`size` properties from `LGraphNode`, `Reroute`, `LGraphGroup`.
|
||||
Once all position reads and writes go through `layoutStore`, remove the
|
||||
compatibility shim and the `pos`/`size` properties from `LGraphNode`, `Reroute`,
|
||||
`LGraphGroup`.
|
||||
|
||||
### 5b. Remove widget class hierarchy
|
||||
|
||||
@@ -448,9 +389,9 @@ replaced with component data + system functions. `BaseWidget`, `NumberWidget`,
|
||||
|
||||
### 5c. Dissolve god objects
|
||||
|
||||
`LGraphNode`, `LLink`, `LGraph` become thin shells — their only role is
|
||||
holding the entity ID and delegating to the World. Eventually, they can be
|
||||
removed entirely, replaced by entity ID + component queries.
|
||||
`LGraphNode`, `LLink`, `LGraph` become thin shells — their only role is holding
|
||||
the composite ID and delegating to the stores. Eventually, they can be removed
|
||||
entirely, replaced by composite IDs + store queries.
|
||||
|
||||
**Risk:** Very High. This is the irreversible step. Must be done only after
|
||||
thorough validation that all consumers (including extensions) work with the
|
||||
@@ -460,8 +401,8 @@ ECS path.
|
||||
|
||||
Legacy removal starts only when all of the following are true:
|
||||
|
||||
- The component being removed has no remaining direct reads or writes outside
|
||||
World/system APIs.
|
||||
- The concern being removed has no remaining direct reads or writes outside
|
||||
store/system APIs.
|
||||
- Serialization equivalence tests pass continuously for one release cycle.
|
||||
- A representative extension compatibility matrix is green, including
|
||||
`rgthree-comfy`.
|
||||
@@ -489,20 +430,20 @@ The team prepares a single go/no-go packet containing:
|
||||
|
||||
### CRDT / ECS coexistence
|
||||
|
||||
The LayoutStore uses Y.js CRDTs for collaboration-ready position data
|
||||
(per [ADR 0003](../adr/0003-crdt-based-layout-system.md)). The ECS World
|
||||
uses plain `Map`s. These must coexist.
|
||||
`layoutStore` uses Y.js CRDTs for collaboration-ready position data
|
||||
(per [ADR 0003](../adr/0003-crdt-based-layout-system.md)). The other dedicated
|
||||
stores hold plain reactive data. These must coexist.
|
||||
|
||||
**Options explored in Phase 2a.** The recommended path (World copies from Y.js)
|
||||
defers the hard question. Eventually, the World may need to be CRDT-native —
|
||||
but this requires a separate ADR.
|
||||
`layoutStore` stays authoritative for layout (Phase 2a), so position data has a
|
||||
single CRDT-backed home. Whether other stores need CRDT backing is open and
|
||||
requires a separate ADR.
|
||||
|
||||
**Questions to resolve:**
|
||||
|
||||
- Should non-position components also be CRDT-backed for collaboration?
|
||||
- Does the World need an operation log for undo/redo, or can that remain
|
||||
external (Y.js undo manager)?
|
||||
- How does conflict resolution work when two users modify the same component?
|
||||
- Should non-position stores also be CRDT-backed for collaboration?
|
||||
- Do the stores need an operation log for undo/redo, or can that remain external
|
||||
(Y.js undo manager)?
|
||||
- How does conflict resolution work when two users modify the same record?
|
||||
|
||||
### Extension API preservation
|
||||
|
||||
@@ -529,7 +470,7 @@ event listeners instead of callbacks.
|
||||
|
||||
**Phase 4 decisions:**
|
||||
|
||||
- Rejection callbacks act as pre-commit guards (reject before World mutation).
|
||||
- Rejection callbacks act as pre-commit guards (reject before store mutation).
|
||||
- Callback dispatch remains synchronous during the bridge period.
|
||||
- Callback order remains: output validation -> input validation -> commit ->
|
||||
output change notification -> input change notification.
|
||||
@@ -546,16 +487,12 @@ incrementally to ECS-native patterns.
|
||||
const seedWidget = node.widgets?.find((w) => w.name === 'seed')
|
||||
seedWidget?.setValue(42)
|
||||
|
||||
// ECS pattern (using the bridge/world widget lookup index)
|
||||
const seedWidgetId = world.widgetIndex.getByNodeAndName(nodeId, 'seed')
|
||||
// Store pattern (composite WidgetId, no reverse-lookup index needed)
|
||||
const seedWidgetId = widgetValueStore
|
||||
.getNodeWidgets(graphId, nodeId)
|
||||
.find((id) => parseWidgetId(id).name === 'seed')
|
||||
if (seedWidgetId) {
|
||||
const widgetValue = world.getComponent(seedWidgetId, WidgetValue)
|
||||
if (widgetValue) {
|
||||
world.setComponent(seedWidgetId, WidgetValue, {
|
||||
...widgetValue,
|
||||
value: 42
|
||||
})
|
||||
}
|
||||
widgetValueStore.setValue(seedWidgetId, 42)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -606,17 +543,15 @@ lifecycleEvents.on('entity.removed', (event) => {
|
||||
// Legacy pattern (do not add new usages)
|
||||
graph._version++
|
||||
|
||||
// Bridge-safe transitional pattern (Phase 0a)
|
||||
// Transitional pattern (Phase 0a)
|
||||
graph.incrementVersion()
|
||||
|
||||
// ECS-native pattern: mutate through command/system API.
|
||||
// Store-native pattern: mutate through the command/system API.
|
||||
// VersionSystem bumps once at transaction commit.
|
||||
executor.run({
|
||||
type: 'SetWidgetValue',
|
||||
execute(world) {
|
||||
const value = world.getComponent(widgetId, WidgetValue)
|
||||
if (!value) return
|
||||
world.setComponent(widgetId, WidgetValue, { ...value, value: 42 })
|
||||
execute() {
|
||||
widgetValueStore.setValue(widgetId, 42)
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -628,9 +563,11 @@ executor.run({
|
||||
|
||||
### Atomicity and transactions
|
||||
|
||||
The ECS lifecycle scenarios claim operations are "atomic." This requires
|
||||
the World to support transactions — the ability to batch multiple component
|
||||
writes and commit or rollback as a unit.
|
||||
The lifecycle scenarios claim operations are "atomic." This requires a
|
||||
store/command-executor transaction — the ability to batch multiple store writes
|
||||
and commit or rollback as a unit. `layoutStore` already wraps its mutations in
|
||||
Y.js transactions; the command executor extends the same discipline across
|
||||
stores.
|
||||
|
||||
**Current state:** `beforeChange()` / `afterChange()` provide undo/redo
|
||||
checkpoints but not true transactions. The graph can be in an inconsistent
|
||||
@@ -638,10 +575,10 @@ state between these calls.
|
||||
|
||||
**Phase 4 baseline semantics:**
|
||||
|
||||
- Mutating systems run inside `world.transaction(label, fn)`.
|
||||
- The bridge maps one World transaction to one `beforeChange()` /
|
||||
- Mutating systems run inside a single command-executor transaction.
|
||||
- The bridge maps one executor transaction to one `beforeChange()` /
|
||||
`afterChange()` bracket.
|
||||
- Operations with multiple component writes (for example `connect()` touching
|
||||
- Operations with multiple store writes (for example `connect()` touching
|
||||
slots, links, and node metadata) still commit as one transaction and therefore
|
||||
one undo entry.
|
||||
- Failed transactions do not publish partial writes, lifecycle events, or
|
||||
@@ -649,65 +586,64 @@ state between these calls.
|
||||
|
||||
**Questions to resolve:**
|
||||
|
||||
- How should `world.transaction()` interact with Y.js transactions when a
|
||||
component is CRDT-backed?
|
||||
- How should the command-executor transaction interact with the Y.js
|
||||
transactions that `layoutStore` already runs?
|
||||
- Is eventual consistency acceptable for derived data updates between
|
||||
transactions, or must post-transaction state always be immediately
|
||||
consistent?
|
||||
|
||||
### Keying strategy unification
|
||||
|
||||
The 6 proto-ECS stores use 6 different keying strategies:
|
||||
The dedicated stores use per-concern keying strategies:
|
||||
|
||||
| Store | Key Format |
|
||||
| ----------------------- | --------------------------------- |
|
||||
| WidgetValueStore | `"${nodeId}:${widgetName}"` |
|
||||
| PromotionStore | `"${sourceNodeId}:${widgetName}"` |
|
||||
| DomWidgetStore | Widget UUID |
|
||||
| LayoutStore | Raw nodeId/linkId/rerouteId |
|
||||
| NodeOutputStore | `"${subgraphId}:${nodeId}"` |
|
||||
| SubgraphNavigationStore | subgraphId or `'root'` |
|
||||
| Store | Key Format |
|
||||
| ------------------------- | ---------------------------------- |
|
||||
| `widgetValueStore` | `WidgetId` (`graphId:nodeId:name`) |
|
||||
| `domWidgetStore` | Widget UUID |
|
||||
| `layoutStore` | Raw nodeId/linkId/rerouteId |
|
||||
| `nodeOutputStore` | `"${subgraphId}:${nodeId}"` |
|
||||
| `subgraphNavigationStore` | subgraphId or `'root'` |
|
||||
|
||||
ADR 0009 refines the promoted-widget target: promoted value widgets should use
|
||||
host boundary identity (`host node locator + SubgraphInput.name`), not interior
|
||||
source node/widget identity.
|
||||
|
||||
The World unifies these under branded entity IDs. But stores that use
|
||||
composite keys (e.g., `nodeId:widgetName`) reflect a genuine structural
|
||||
reality — a widget is identified by its relationship to a node. Synthetic
|
||||
`WidgetEntityId`s replace this with an opaque number, requiring a reverse
|
||||
lookup index.
|
||||
Composite string keys won over synthetic numeric IDs. A widget is identified by
|
||||
its relationship to a graph and node, and the `graphId:nodeId:name` key carries
|
||||
that relationship directly. PR 12617 kept the composite string instead of an
|
||||
opaque number, so no reverse lookup index is required — `parseWidgetId()`
|
||||
recovers the parts on demand.
|
||||
|
||||
**Trade-off:** Type safety and uniformity vs. self-documenting keys. The
|
||||
World should maintain a lookup index (`(nodeId, widgetName) -> WidgetEntityId`)
|
||||
for the transition period.
|
||||
**Resolution:** Self-documenting composite keys, parsed at boundaries. Each store
|
||||
keeps the key format that matches its concern; there is no forced unification
|
||||
under a single ID space.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Phase 0a (incrementVersion) ──┐
|
||||
Phase 0b (ID type aliases) ───┤
|
||||
Phase 0a (incrementVersion) ──── ✅ shipped (one stray cleanup remaining)
|
||||
Phase 0b (ID type aliases) ───┐
|
||||
Phase 0c (doc fixes) ─────────┤── no dependencies between these
|
||||
│
|
||||
Phase 1a (branded IDs) ────────┤
|
||||
Phase 1b (component interfaces) ┤── 1b depends on 1a
|
||||
Phase 1c (World type) ─────────┘── 1c depends on 1a, 1b
|
||||
|
||||
Phase 2a (Position bridge) ────┐── depends on 1c
|
||||
Phase 2b (Widget bridge) ──────┤── depends on 1a, 1c
|
||||
Phase 2c (Node metadata bridge) ┘── depends on 0a, 1c
|
||||
Phase 1a (branded WidgetId) ── ✅ shipped (PR 12617)
|
||||
Phase 1b (store state shapes) ─┐── depends on 1a
|
||||
Phase 1c (dedicated stores) ──┘── widgetValueStore + 5 others shipped (PR 12617)
|
||||
|
||||
Phase 2a (Position via layoutStore) ─┐── depends on 1c
|
||||
Phase 2b (Widget consolidation) ────┤── ✅ largely shipped; depends on 1a, 1c
|
||||
Phase 2c (Node metadata stores) ────┘── depends on 1c
|
||||
|
||||
Phase 3a (SerializationSystem) ─── depends on 2a, 2b, 2c
|
||||
Phase 3b (VersionSystem) ──────── depends on 0a, 2c
|
||||
Phase 3b (VersionSystem) ──────── depends on 2c (store-level change tracking)
|
||||
Phase 3c (ConnectivitySystem) ──── depends on 2c
|
||||
|
||||
Phase 3->4 gate checklist ──────── depends on 3a, 3b, 3c
|
||||
|
||||
Phase 4a (Position writes) ────── depends on 2a, 3b
|
||||
Phase 4b (Connectivity mutations) ─ depends on 3c, 3->4 gate
|
||||
Phase 4c (Widget writes) ─────── depends on 2b
|
||||
Phase 4c (Widget writes) ─────── ✅ largely shipped; depends on 2b
|
||||
Phase 4d (Layout decoupling) ─── depends on 2a, 3->4 gate
|
||||
|
||||
Phase 4->5 exit criteria ──────── depends on all of Phase 4
|
||||
@@ -715,16 +651,19 @@ Phase 4->5 exit criteria ──────── depends on all of Phase 4
|
||||
Phase 5 (legacy removal) ─────── depends on 4->5 exit criteria
|
||||
```
|
||||
|
||||
The dedicated stores (1c) are the hub: Phase 2 routes legacy data into them,
|
||||
Phase 3 systems read from them, Phase 4 routes writes through them.
|
||||
|
||||
## Risk Summary
|
||||
|
||||
| Phase | Risk | Reversibility | Extension Impact |
|
||||
| ------------------ | ---------- | ----------------------- | --------------------------- |
|
||||
| 0 (Foundation) | None | Fully reversible | None |
|
||||
| 1 (Types/World) | Low | New files, deletable | None |
|
||||
| 2 (Bridge) | Low-Medium | Bridge is additive | None |
|
||||
| 3 (Systems) | Low-Medium | Systems run in parallel | None |
|
||||
| 4 (Write path) | High | Two-way sync is fragile | Callbacks must be preserved |
|
||||
| 5 (Legacy removal) | Very High | Irreversible | Extensions must migrate |
|
||||
| Phase | Risk | Reversibility | Extension Impact |
|
||||
| --------------------- | ---------- | ----------------------- | --------------------------- |
|
||||
| 0 (Foundation) | None | Fully reversible | None |
|
||||
| 1 (Types/Stores) | Low | New files, deletable | None |
|
||||
| 2 (Store integration) | Low-Medium | Additive store reads | None |
|
||||
| 3 (Systems) | Low-Medium | Systems run in parallel | None |
|
||||
| 4 (Write path) | High | Two-way sync is fragile | Callbacks must be preserved |
|
||||
| 5 (Legacy removal) | Very High | Irreversible | Extensions must migrate |
|
||||
|
||||
The plan is designed so that Phases 0-3 can ship without any risk to
|
||||
extensions or existing behavior. Phase 4 is where the real migration begins,
|
||||
|
||||
@@ -2,30 +2,29 @@
|
||||
|
||||
This document describes the target ECS architecture for the litegraph entity system. It shows how the entities and interactions from the [current system](entity-interactions.md) transform under ECS, and how the [structural problems](entity-problems.md) are resolved. For the full design rationale, see [ADR 0008](../adr/0008-entity-component-system.md).
|
||||
|
||||
## 1. World Overview
|
||||
## 1. Store Overview
|
||||
|
||||
The World is the single source of truth for runtime entity state in one
|
||||
workflow instance. Entities are just branded IDs. Components are plain data
|
||||
objects. Systems are functions that query the World.
|
||||
The source of truth for runtime entity state in one workflow instance is the set
|
||||
of dedicated Pinia stores. Each store is keyed by per-store string IDs.
|
||||
Components are plain data objects. Systems are functions that query the relevant
|
||||
store(s).
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph World["World (Central Registry)"]
|
||||
subgraph Stores["Dedicated Stores (source of truth)"]
|
||||
direction TB
|
||||
NodeStore["Nodes
|
||||
Map<NodeEntityId, NodeComponents>"]
|
||||
LinkStore["Links
|
||||
Map<LinkEntityId, LinkComponents>"]
|
||||
ScopeRegistry["Graph Scopes
|
||||
Map<GraphId, ParentGraphId | null>"]
|
||||
WidgetStore["Widgets
|
||||
Map<WidgetEntityId, WidgetComponents>"]
|
||||
SlotStore["Slots
|
||||
Map<SlotEntityId, SlotComponents>"]
|
||||
RerouteStore["Reroutes
|
||||
Map<RerouteEntityId, RerouteComponents>"]
|
||||
GroupStore["Groups
|
||||
Map<GroupEntityId, GroupComponents>"]
|
||||
WidgetValueStore["widgetValueStore
|
||||
Map<WidgetId, WidgetValue>"]
|
||||
DomWidgetStore["domWidgetStore
|
||||
Map<WidgetId, DomWidgetState>"]
|
||||
LayoutStore["layoutStore (Y.js CRDT)
|
||||
nodeId / linkId / rerouteId → layout"]
|
||||
NodeOutputStore["nodeOutputStore
|
||||
Map<nodeLocatorId, outputs>"]
|
||||
SubgraphNavStore["subgraphNavigationStore
|
||||
active subgraph path"]
|
||||
PreviewExposureStore["previewExposureStore
|
||||
preview exposure state"]
|
||||
end
|
||||
|
||||
subgraph Systems["Systems (Behavior)"]
|
||||
@@ -38,53 +37,50 @@ Map<GroupEntityId, GroupComponents>"]
|
||||
VS["VersionSystem"]
|
||||
end
|
||||
|
||||
RS -->|reads| World
|
||||
SS -->|reads/writes| World
|
||||
CS -->|reads/writes| World
|
||||
LS -->|reads/writes| World
|
||||
ES -->|reads| World
|
||||
VS -->|reads/writes| World
|
||||
RS -->|reads| Stores
|
||||
SS -->|reads/writes| Stores
|
||||
CS -->|reads/writes| LayoutStore
|
||||
LS -->|reads/writes| LayoutStore
|
||||
ES -->|reads| NodeOutputStore
|
||||
VS -->|reads/writes| LayoutStore
|
||||
|
||||
style World fill:#1a1a2e,stroke:#16213e,color:#e0e0e0
|
||||
style Stores fill:#1a1a2e,stroke:#16213e,color:#e0e0e0
|
||||
style Systems fill:#0f3460,stroke:#16213e,color:#e0e0e0
|
||||
```
|
||||
|
||||
### Entity IDs
|
||||
### Entity Keys
|
||||
|
||||
Each store addresses entities by its own string-key convention.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "Branded IDs (compile-time distinct)"
|
||||
NID["NodeEntityId
|
||||
number & { __brand: 'NodeEntityId' }"]
|
||||
LID["LinkEntityId
|
||||
number & { __brand: 'LinkEntityId' }"]
|
||||
WID["WidgetEntityId
|
||||
number & { __brand: 'WidgetEntityId' }"]
|
||||
SLID["SlotEntityId
|
||||
number & { __brand: 'SlotEntityId' }"]
|
||||
RID["RerouteEntityId
|
||||
number & { __brand: 'RerouteEntityId' }"]
|
||||
GID["GroupEntityId
|
||||
number & { __brand: 'GroupEntityId' }"]
|
||||
subgraph "Per-store string keys"
|
||||
WID["WidgetId
|
||||
graphId:nodeId:name
|
||||
(branded string, src/types/widgetId.ts)"]
|
||||
NLID["nodeLocatorId
|
||||
subgraphId:nodeId"]
|
||||
NID["nodeId (raw)"]
|
||||
LID["linkId (raw)"]
|
||||
RID["rerouteId (raw)"]
|
||||
end
|
||||
|
||||
GRID["GraphId
|
||||
string & { __brand: 'GraphId' }"]:::scopeId
|
||||
|
||||
NID -.-x LID
|
||||
LID -.-x WID
|
||||
WID -.-x SLID
|
||||
|
||||
classDef scopeId fill:#2a2a4a,stroke:#4a4a6a,color:#e0e0e0,stroke-dasharray:5
|
||||
|
||||
linkStyle 0 stroke:red,stroke-dasharray:5
|
||||
linkStyle 1 stroke:red,stroke-dasharray:5
|
||||
linkStyle 2 stroke:red,stroke-dasharray:5
|
||||
WID -->|widgetValueStore, domWidgetStore| W["keyed lookups"]
|
||||
NLID -->|nodeOutputStore| W
|
||||
NID -->|layoutStore| W
|
||||
LID -->|layoutStore| W
|
||||
RID -->|layoutStore| W
|
||||
```
|
||||
|
||||
Red dashed lines = compile-time errors if mixed. No more accidentally passing a `LinkId` where a `NodeId` is expected.
|
||||
`WidgetId = graphId:nodeId:name` is itself a branded string (see
|
||||
`src/types/widgetId.ts`). `nodeLocatorId = subgraphId:nodeId` addresses node
|
||||
outputs. `layoutStore` keys layout records by raw `nodeId` / `linkId` /
|
||||
`rerouteId`. Each store enforces its own key shape; there is no single shared
|
||||
entity-ID type across stores.
|
||||
|
||||
Note: `GraphId` is a scope identifier, not an entity ID. It identifies which graph an entity belongs to. Subgraphs are nodes with a `SubgraphStructure` component — see [Subgraph Boundaries](subgraph-boundaries-and-promotion.md).
|
||||
Note: `graphId` is a scope identifier. It identifies which graph an entity
|
||||
belongs to and forms the prefix of `WidgetId`. Subgraphs are nodes with a
|
||||
`SubgraphStructure` component — see [Subgraph Boundaries](subgraph-boundaries-and-promotion.md).
|
||||
|
||||
### Linked subgraphs and instance-varying state
|
||||
|
||||
@@ -94,9 +90,10 @@ instance-scoped.
|
||||
- Shared definition-level data (interface shape, default metadata) can be reused
|
||||
across instances.
|
||||
- Runtime state (`WidgetValue`, execution/transient state, selection) is scoped
|
||||
to the containing `graphId` chain inside one World instance.
|
||||
- "Single source of truth" therefore means one source per workflow instance,
|
||||
not one global source across all linked instances.
|
||||
to the containing `graphId` chain inside one workflow instance.
|
||||
- "Single source of truth" therefore means one source per workflow instance
|
||||
across the dedicated stores, with no global source shared across all linked
|
||||
instances.
|
||||
|
||||
### Recursive subgraphs without inheritance
|
||||
|
||||
@@ -130,7 +127,7 @@ graph LR
|
||||
B12["connect(), disconnect()"]
|
||||
end
|
||||
|
||||
subgraph After["NodeEntityId + Components"]
|
||||
subgraph After["nodeId-keyed components (across stores)"]
|
||||
direction TB
|
||||
A1["Position
|
||||
{ pos, size, bounding }"]
|
||||
@@ -180,7 +177,7 @@ target_id, target_slot, type"]
|
||||
B5["resolve()"]
|
||||
end
|
||||
|
||||
subgraph After["LinkEntityId + Components"]
|
||||
subgraph After["linkId-keyed components (layoutStore)"]
|
||||
direction TB
|
||||
A1["LinkEndpoints
|
||||
{ originId, originSlot,
|
||||
@@ -214,7 +211,7 @@ graph LR
|
||||
B5["useWidgetValueStore()"]
|
||||
end
|
||||
|
||||
subgraph After["WidgetEntityId + Components"]
|
||||
subgraph After["WidgetId + components"]
|
||||
direction TB
|
||||
A1["WidgetIdentity
|
||||
{ name, widgetType, parentNodeId }"]
|
||||
@@ -228,8 +225,7 @@ graph LR
|
||||
B2 -.-> A2
|
||||
B3 -.-> A3
|
||||
B4 -.->|"moves to"| SYS1["RenderSystem"]
|
||||
B5 -.->|"absorbed by"| SYS2["World (is the store)"]
|
||||
B6 -.->|"moves to"| SYS3["PromotionSystem"]
|
||||
B5 -.->|"absorbed by"| SYS2["widgetValueStore"]
|
||||
|
||||
style Before fill:#4a1a1a,stroke:#6a2a2a,color:#e0e0e0
|
||||
style After fill:#1a4a1a,stroke:#2a6a2a,color:#e0e0e0
|
||||
@@ -237,7 +233,7 @@ graph LR
|
||||
|
||||
## 3. System Architecture
|
||||
|
||||
Systems are pure functions that query the World for entities with specific component combinations. Each system owns exactly one concern.
|
||||
Systems are pure functions that query the relevant store(s) for entities with specific component combinations. Each system owns exactly one concern.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
@@ -389,30 +385,25 @@ graph TD
|
||||
VS["VersionSystem"]
|
||||
end
|
||||
|
||||
World["World
|
||||
(instance-scoped source of truth)"]
|
||||
|
||||
subgraph Components["Component Stores"]
|
||||
Pos["Position"]
|
||||
Vis["*Visual"]
|
||||
Con["Connectivity"]
|
||||
Val["*Value"]
|
||||
subgraph Stores["Dedicated Stores (instance-scoped source of truth)"]
|
||||
LayoutStore["layoutStore"]
|
||||
WidgetValueStore["widgetValueStore"]
|
||||
DomWidgetStore["domWidgetStore"]
|
||||
NodeOutputStore["nodeOutputStore"]
|
||||
end
|
||||
|
||||
Systems -->|"query/mutate"| World
|
||||
World -->|"contains"| Components
|
||||
Systems -->|"query/mutate"| Stores
|
||||
|
||||
style Systems fill:#1a4a1a,stroke:#2a6a2a,color:#e0e0e0
|
||||
style World fill:#1a1a4a,stroke:#2a2a6a,color:#e0e0e0
|
||||
style Components fill:#1a3a3a,stroke:#2a4a4a,color:#e0e0e0
|
||||
style Stores fill:#1a3a3a,stroke:#2a4a4a,color:#e0e0e0
|
||||
```
|
||||
|
||||
Key differences:
|
||||
|
||||
- **No circular dependencies**: entities are IDs, not class instances
|
||||
- **No Demeter violations**: systems query the World directly, never reach through entities
|
||||
- **No scattered store access**: the World _is_ the store; systems are the only writers
|
||||
- **Unidirectional**: Input → Systems → World → Render (no back-edges)
|
||||
- **No Demeter violations**: systems query stores directly, never reach through entities
|
||||
- **Data lives in dedicated stores**: systems are the only writers
|
||||
- **Unidirectional**: Input → Systems → Stores → Render (no back-edges)
|
||||
- **Instance safety**: linked definitions can be reused without forcing shared
|
||||
mutable widget/execution state across instances
|
||||
|
||||
@@ -447,9 +438,10 @@ No inheritance hierarchy.
|
||||
Subgraph = node + component."]
|
||||
S3["One system per concern.
|
||||
Systems don't overlap."]
|
||||
S4["Branded per-kind IDs.
|
||||
Compile-time type errors."]
|
||||
S5["Systems query World.
|
||||
S4["Consistent per-store
|
||||
string-key conventions
|
||||
(WidgetId, nodeLocatorId, raw ids)."]
|
||||
S5["Systems query stores.
|
||||
No entity→entity refs."]
|
||||
S6["VersionSystem owns
|
||||
all change tracking."]
|
||||
@@ -479,30 +471,30 @@ sequenceDiagram
|
||||
participant Legacy as Legacy Code
|
||||
participant Class as LGraphNode (class)
|
||||
participant Bridge as Bridge Adapter
|
||||
participant World as World (ECS)
|
||||
participant Store as layoutStore (ECS)
|
||||
participant New as New Code / Systems
|
||||
|
||||
Note over Legacy,New: Phase 1: Bridge reads from class, writes to World
|
||||
Note over Legacy,New: Phase 1: Bridge reads from class, writes to store
|
||||
|
||||
Legacy->>Class: node.pos = [100, 200]
|
||||
Class->>Bridge: pos setter intercepted
|
||||
Bridge->>World: world.setComponent(nodeId, Position, { pos: [100, 200] })
|
||||
Bridge->>Store: useLayoutMutations().moveNode(nodeId, { pos: [100, 200] })
|
||||
|
||||
New->>World: world.getComponent(nodeId, Position)
|
||||
World-->>New: { pos: [100, 200], size: [...] }
|
||||
New->>Store: layoutStore read for nodeId
|
||||
Store-->>New: { pos: [100, 200], size: [...] }
|
||||
|
||||
Note over Legacy,New: Phase 2: New features build on ECS directly
|
||||
|
||||
New->>World: world.setComponent(nodeId, Position, { pos: [150, 250] })
|
||||
World->>Bridge: change detected
|
||||
New->>Store: useLayoutMutations().moveNode(nodeId, { pos: [150, 250] })
|
||||
Store->>Bridge: change detected
|
||||
Bridge->>Class: node._pos = [150, 250]
|
||||
Legacy->>Class: node.pos
|
||||
Class-->>Legacy: [150, 250]
|
||||
|
||||
Note over Legacy,New: Phase 3: Legacy code migrated, bridge removed
|
||||
|
||||
New->>World: world.getComponent(nodeId, Position)
|
||||
World-->>New: { pos: [150, 250] }
|
||||
New->>Store: layoutStore read for nodeId
|
||||
Store-->>New: { pos: [150, 250] }
|
||||
```
|
||||
|
||||
### Incremental layout/render separation
|
||||
@@ -524,16 +516,17 @@ incremental rollout safety.
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Phase1["Phase 1: Types Only"]
|
||||
T1["Define branded IDs"]
|
||||
T1["Define string-key types
|
||||
(WidgetId, nodeLocatorId)"]
|
||||
T2["Define component interfaces"]
|
||||
T3["Define World type"]
|
||||
T3["Define store shapes"]
|
||||
end
|
||||
|
||||
subgraph Phase2["Phase 2: Bridge"]
|
||||
B1["Bridge adapters
|
||||
class ↔ World sync"]
|
||||
class ↔ store sync"]
|
||||
B2["New features use
|
||||
World as source"]
|
||||
stores as source"]
|
||||
B3["Old code unchanged"]
|
||||
end
|
||||
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
# World API and Command Layer
|
||||
|
||||
How the ECS World's imperative API relates to ADR 0003's command pattern
|
||||
requirement, and why the two are complementary rather than conflicting.
|
||||
|
||||
This document responds to the concern that `world.setComponent()` and
|
||||
`ConnectivitySystem.connect()` are "imperative mutators" incompatible with
|
||||
serializable, idempotent commands. The short answer: they are the
|
||||
**implementation** of commands, not a replacement for them.
|
||||
|
||||
## Architectural Layering
|
||||
|
||||
```
|
||||
Caller → Command → System (handler) → World (store) → Y.js (sync)
|
||||
↓
|
||||
Command Log (undo, replay, sync)
|
||||
```
|
||||
|
||||
- **Commands** describe intent. They are serializable, deterministic, and
|
||||
idempotent.
|
||||
- **Systems** are command handlers. They validate, execute, and emit lifecycle
|
||||
events.
|
||||
- **The World** is the store. It holds component data. It does not know about
|
||||
commands.
|
||||
|
||||
This is the same relationship Redux has between actions, reducers, and the
|
||||
store. The store's `dispatch()` is imperative. That does not make Redux
|
||||
incompatible with serializable actions.
|
||||
|
||||
## Proposed World Mutation API
|
||||
|
||||
The World exposes a thin imperative surface. Every mutation goes through a
|
||||
system, and every system call is invoked by a command.
|
||||
|
||||
### World Core API
|
||||
|
||||
```ts
|
||||
interface World {
|
||||
// Reads (no command needed)
|
||||
getComponent<C>(id: EntityId, key: ComponentKey<C>): C | undefined
|
||||
hasComponent(id: EntityId, key: ComponentKey<C>): boolean
|
||||
queryAll<C extends ComponentKey[]>(...keys: C): QueryResult<C>[]
|
||||
|
||||
// Mutations (called only by systems, inside transactions)
|
||||
createEntity<K extends EntityKind>(kind: K): EntityIdFor<K>
|
||||
deleteEntity<K extends EntityKind>(kind: K, id: EntityIdFor<K>): void
|
||||
setComponent<C>(id: EntityId, key: ComponentKey<C>, data: C): void
|
||||
removeComponent(id: EntityId, key: ComponentKey<C>): void
|
||||
|
||||
// Transaction boundary
|
||||
transaction<T>(label: string, fn: () => T): T
|
||||
}
|
||||
```
|
||||
|
||||
These methods are **internal**. External callers never call
|
||||
`world.setComponent()` directly — they submit commands.
|
||||
|
||||
### Command Interface
|
||||
|
||||
```ts
|
||||
interface Command<T = void> {
|
||||
readonly type: string
|
||||
execute(world: World): T
|
||||
}
|
||||
```
|
||||
|
||||
A command is a plain object with a `type` discriminator and an `execute`
|
||||
method that receives the World. The command executor wraps every
|
||||
`execute()` call in a World transaction.
|
||||
|
||||
### Command Executor
|
||||
|
||||
```ts
|
||||
interface CommandExecutor {
|
||||
run<T>(command: Command<T>): T
|
||||
batch(label: string, commands: Command[]): void
|
||||
}
|
||||
|
||||
function createCommandExecutor(world: World): CommandExecutor {
|
||||
return {
|
||||
run(command) {
|
||||
return world.transaction(command.type, () => command.execute(world))
|
||||
},
|
||||
batch(label, commands) {
|
||||
world.transaction(label, () => {
|
||||
for (const cmd of commands) cmd.execute(world)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every command execution:
|
||||
|
||||
1. Opens a World transaction (maps to one `beforeChange`/`afterChange`
|
||||
bracket for undo).
|
||||
2. Calls the command's `execute()`, which invokes system functions.
|
||||
3. Commits the transaction. On failure, rolls back — no partial writes, no
|
||||
lifecycle events, no version bump.
|
||||
|
||||
## From Imperative Calls to Commands
|
||||
|
||||
The lifecycle scenarios in
|
||||
[ecs-lifecycle-scenarios.md](ecs-lifecycle-scenarios.md) show system calls
|
||||
like `ConnectivitySystem.connect(world, outputSlotId, inputSlotId)`. These
|
||||
are the **internals** of a command. Here is how each scenario maps:
|
||||
|
||||
### Connect Slots
|
||||
|
||||
The lifecycle scenario shows:
|
||||
|
||||
```ts
|
||||
// Inside ConnectivitySystem — this is the handler, not the public API
|
||||
ConnectivitySystem.connect(world, outputSlotId, inputSlotId)
|
||||
```
|
||||
|
||||
The public API is a command:
|
||||
|
||||
```ts
|
||||
const connectSlots: Command = {
|
||||
type: 'ConnectSlots',
|
||||
outputSlotId,
|
||||
inputSlotId,
|
||||
|
||||
execute(world) {
|
||||
ConnectivitySystem.connect(world, this.outputSlotId, this.inputSlotId)
|
||||
}
|
||||
}
|
||||
|
||||
executor.run(connectSlots)
|
||||
```
|
||||
|
||||
The command object is serializable (`{ type, outputSlotId, inputSlotId }`).
|
||||
It can be sent over a wire, stored in a log, or replayed.
|
||||
|
||||
### Move Node
|
||||
|
||||
```ts
|
||||
const moveNode: Command = {
|
||||
type: 'MoveNode',
|
||||
nodeId,
|
||||
pos: [150, 250],
|
||||
|
||||
execute(world) {
|
||||
LayoutSystem.moveNode(world, this.nodeId, this.pos)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Remove Node
|
||||
|
||||
```ts
|
||||
const removeNode: Command = {
|
||||
type: 'RemoveNode',
|
||||
nodeId,
|
||||
|
||||
execute(world) {
|
||||
ConnectivitySystem.removeNode(world, this.nodeId)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Set Widget Value
|
||||
|
||||
```ts
|
||||
const setWidgetValue: Command = {
|
||||
type: 'SetWidgetValue',
|
||||
widgetId,
|
||||
value,
|
||||
|
||||
execute(world) {
|
||||
world.setComponent(this.widgetId, WidgetValue, {
|
||||
...world.getComponent(this.widgetId, WidgetValue)!,
|
||||
value: this.value
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Batch: Paste
|
||||
|
||||
Paste is a compound operation — many entities created in one undo step:
|
||||
|
||||
```ts
|
||||
const paste: Command = {
|
||||
type: 'Paste',
|
||||
snapshot,
|
||||
offset,
|
||||
|
||||
execute(world) {
|
||||
const remap = new Map<EntityId, EntityId>()
|
||||
|
||||
for (const entity of this.snapshot.entities) {
|
||||
const newId = world.createEntity(entity.kind)
|
||||
remap.set(entity.id, newId)
|
||||
|
||||
for (const [key, data] of entity.components) {
|
||||
world.setComponent(newId, key, remapEntityRefs(data, remap))
|
||||
}
|
||||
}
|
||||
|
||||
// Offset positions
|
||||
for (const [, newId] of remap) {
|
||||
const pos = world.getComponent(newId, Position)
|
||||
if (pos) {
|
||||
world.setComponent(newId, Position, {
|
||||
...pos,
|
||||
pos: [pos.pos[0] + this.offset[0], pos.pos[1] + this.offset[1]]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executor.run(paste) // one transaction, one undo step
|
||||
```
|
||||
|
||||
## Addressing the Six Concerns
|
||||
|
||||
The PR review raised six "critical conflicts." Here is how the command layer
|
||||
resolves each:
|
||||
|
||||
### 1. "The World API is imperative, not command-based"
|
||||
|
||||
Correct — by design. The World is the store. Commands are the public
|
||||
mutation API above it. `world.setComponent()` is to commands what
|
||||
`state[key] = value` is to Redux reducers.
|
||||
|
||||
### 2. "Systems are orchestrators, not command producers"
|
||||
|
||||
Systems are command **handlers**. A command's `execute()` calls system
|
||||
functions. Systems do not spontaneously mutate the World — they are invoked
|
||||
by commands.
|
||||
|
||||
### 3. "Auto-incrementing IDs are non-stable in concurrent environments"
|
||||
|
||||
For local-only operations, auto-increment is fine. For CRDT sync, entity
|
||||
creation goes through a CRDT-aware ID generator (Y.js provides this via
|
||||
`doc.clientID` + logical clock). The command layer can select the ID
|
||||
strategy:
|
||||
|
||||
```ts
|
||||
// Local-only command
|
||||
world.createEntity(kind) // auto-increment
|
||||
|
||||
// CRDT-aware command (future)
|
||||
world.createEntityWithId(kind, crdtGeneratedId)
|
||||
```
|
||||
|
||||
This is an ID generation concern, not an ECS architecture concern.
|
||||
|
||||
### 4. "No transaction primitive exists"
|
||||
|
||||
`world.transaction(label, fn)` is the primitive. It maps to one
|
||||
`beforeChange`/`afterChange` bracket. The command executor wraps every
|
||||
`execute()` call in a transaction. See the [migration plan's Phase 3→4
|
||||
gate](ecs-migration-plan.md#phase-3---4-gate-required) for the acceptance
|
||||
criteria.
|
||||
|
||||
### 5. "No idempotency guarantees"
|
||||
|
||||
Idempotency is a property of the command, not the store. Two strategies:
|
||||
|
||||
- **Content-addressed IDs**: The command specifies the entity ID rather than
|
||||
auto-generating. Replaying the command with the same ID is a no-op if the
|
||||
entity already exists.
|
||||
- **Command deduplication**: The command log tracks applied command IDs.
|
||||
Replaying an already-applied command is skipped.
|
||||
|
||||
Both are standard CRDT patterns and belong in the command executor, not the
|
||||
World.
|
||||
|
||||
### 6. "No error semantics"
|
||||
|
||||
Commands return results. The executor can wrap execution:
|
||||
|
||||
```ts
|
||||
type CommandResult<T> =
|
||||
| { status: 'applied'; value: T }
|
||||
| { status: 'rejected'; reason: string }
|
||||
| { status: 'no-op' }
|
||||
|
||||
function run<T>(command: Command<T>): CommandResult<T> {
|
||||
try {
|
||||
const value = world.transaction(command.type, () => command.execute(world))
|
||||
return { status: 'applied', value }
|
||||
} catch (e) {
|
||||
if (e instanceof RejectionError) {
|
||||
return { status: 'rejected', reason: e.message }
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rejection semantics (e.g., `onConnectInput` returning false) throw a
|
||||
`RejectionError` inside the system, which the transaction rolls back.
|
||||
|
||||
## Why Two ADRs
|
||||
|
||||
ADR 0003 defines the command pattern and CRDT sync layer.
|
||||
ADR 0008 defines the entity data model.
|
||||
|
||||
They are **complementary architectural layers**, not competing proposals:
|
||||
|
||||
| Concern | Owns It |
|
||||
| ------------------------- | -------- |
|
||||
| Entity taxonomy and IDs | ADR 0008 |
|
||||
| Component decomposition | ADR 0008 |
|
||||
| World (store) | ADR 0008 |
|
||||
| Command interface | ADR 0003 |
|
||||
| Undo/redo via command log | ADR 0003 |
|
||||
| CRDT sync | ADR 0003 |
|
||||
| Serialization format | ADR 0008 |
|
||||
| Replay and idempotency | ADR 0003 |
|
||||
|
||||
Merging them into a single mega-ADR would conflate the data model with the
|
||||
mutation strategy. Keeping them separate allows each to evolve independently
|
||||
— the World can change its internal representation without affecting the
|
||||
command API, and the command layer can adopt new sync strategies without
|
||||
restructuring the entity model.
|
||||
|
||||
## Relationship to Lifecycle Scenarios
|
||||
|
||||
The [lifecycle scenarios](ecs-lifecycle-scenarios.md) show system-level
|
||||
calls (`ConnectivitySystem.connect()`, `ClipboardSystem.paste()`, etc.).
|
||||
These are the **inside** of a command — what the command handler does when
|
||||
the command is executed.
|
||||
|
||||
The scenarios deliberately omit the command layer to focus on how systems
|
||||
interact with the World. Adding command wrappers is mechanical: every
|
||||
system call shown in the scenarios becomes the body of a command's
|
||||
`execute()` method.
|
||||
|
||||
## When This Gets Built
|
||||
|
||||
The command layer is not part of the initial ECS migration phases (0–3).
|
||||
During Phases 0–3, the bridge layer provides mutation entry points that
|
||||
will later become command handlers. The command layer is introduced in
|
||||
Phase 4 when write paths migrate from legacy to ECS:
|
||||
|
||||
- **Phase 4a**: Position write commands replace direct `node.pos =` assignment
|
||||
- **Phase 4b**: Connectivity commands replace `node.connect()` /
|
||||
`node.disconnect()`
|
||||
- **Phase 4c**: Widget value commands replace direct store writes
|
||||
|
||||
Each Phase 4 step introduces commands for one concern, with the system
|
||||
function as the handler and the World transaction as the atomicity
|
||||
boundary.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Entity Interactions (Current System)
|
||||
|
||||
This document maps the relationships and interaction patterns between all entity types in the litegraph layer as it exists today. It serves as a baseline for the ECS migration planned in [ADR 0008](../adr/0008-entity-component-system.md).
|
||||
This document maps the relationships and interaction patterns between all entity types in the litegraph layer as it exists today. It serves as a baseline for the ECS migration planned in [ADR 0008](../adr/0008-entity-component-system.md), whose target is realized as a set of dedicated Pinia stores (see [Proto-ECS Stores](proto-ecs-stores.md)).
|
||||
|
||||
## Entities
|
||||
|
||||
@@ -361,7 +361,7 @@ graph TD
|
||||
subgraph Stores
|
||||
WVS["WidgetValueStore
|
||||
(Pinia)"]
|
||||
PS["PromotionStore
|
||||
PES["PreviewExposureStore
|
||||
(Pinia)"]
|
||||
LM["LayoutMutations
|
||||
(composable)"]
|
||||
@@ -379,9 +379,9 @@ lastRerouteId, lastGroupId)"]
|
||||
Widget <-->|"value, label, disabled"| WVS
|
||||
WVS -.->|"keyed by graphId:nodeId:name"| Widget
|
||||
|
||||
%% PromotionStore
|
||||
SGNode -->|"tracks promoted widgets"| PS
|
||||
Widget -.->|"isPromotedByAny() query"| PS
|
||||
%% PreviewExposureStore
|
||||
SGNode -->|"host-scoped preview exposures"| PES
|
||||
PES -.->|"keyed by host node locator"| SGNode
|
||||
|
||||
%% LayoutMutations
|
||||
Node -->|"pos/size setter"| LM
|
||||
|
||||
@@ -115,7 +115,7 @@ If slots are reordered (e.g., by an extension adding a slot), all links referenc
|
||||
|
||||
### No Cross-Kind ID Safety
|
||||
|
||||
Nothing prevents passing a `LinkId` where a `NodeId` is expected — they're both `number`. This is the core motivation for the branded ID types proposed in ADR 0008.
|
||||
Nothing prevents passing a `LinkId` where a `NodeId` is expected — they're both `number`. The dedicated-store direction addresses this with branded string keys where cross-kind safety pays off (for example `WidgetId` in `widgetValueStore`, `src/types/widgetId.ts`).
|
||||
|
||||
## 5. Law of Demeter Violations
|
||||
|
||||
@@ -201,12 +201,12 @@ This means:
|
||||
|
||||
## How ECS Addresses These Problems
|
||||
|
||||
| Problem | ECS Solution |
|
||||
| ---------------------- | ----------------------------------------------------------------------------- |
|
||||
| God objects | Data split into small, focused components; behavior lives in systems |
|
||||
| Circular dependencies | Entities are just IDs; components have no inheritance hierarchy |
|
||||
| Mixed concerns | Each system handles exactly one concern (render, serialize, execute) |
|
||||
| Inconsistent IDs | Branded per-kind IDs with compile-time safety |
|
||||
| Demeter violations | Systems query the World directly; no entity-to-entity references |
|
||||
| Scattered side effects | Version tracking becomes a system responsibility; stores become systems |
|
||||
| Render-time mutations | Render system reads components without writing; layout system runs separately |
|
||||
| Problem | ECS Solution |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| God objects | Data split into small, focused components in dedicated stores; behavior lives in systems |
|
||||
| Circular dependencies | Entities are addressed by string keys; components have no inheritance hierarchy |
|
||||
| Mixed concerns | Each system handles exactly one concern (render, serialize, execute) |
|
||||
| Inconsistent IDs | Branded string keys per store (for example `WidgetId`) for cross-kind safety |
|
||||
| Demeter violations | Systems query the relevant store directly; no entity-to-entity references |
|
||||
| Scattered side effects | Version tracking becomes a system responsibility; mutations flow through store command APIs |
|
||||
| Render-time mutations | Render system reads components without writing; layout system runs separately |
|
||||
|
||||
@@ -6,17 +6,20 @@ For the full problem analysis, see [Entity Problems](entity-problems.md). For th
|
||||
|
||||
## 1. What's Already Extracted
|
||||
|
||||
Five stores extract entity state out of class instances into centralized,
|
||||
queryable registries. Promoted value-widget topology is no longer a store; ADR
|
||||
0009 represents it as ordinary linked `SubgraphInput` state.
|
||||
Six dedicated stores extract entity state out of class instances into focused,
|
||||
queryable registries, each owning one concern. Promoted value-widget topology is
|
||||
no longer a store; ADR 0009 represents it as ordinary linked `SubgraphInput`
|
||||
state, and promoted value data lives in `WidgetValueStore` keyed by the input's
|
||||
`WidgetId`.
|
||||
|
||||
| Store | Extracts From | Scoping | Key Format | Data Shape |
|
||||
| ----------------------- | ------------------- | ----------------------- | ------------------------------- | ----------------------------- |
|
||||
| WidgetValueStore | `BaseWidget` | `graphId → nodeId:name` | `"${nodeId}:${widgetName}"` | Plain `WidgetState` object |
|
||||
| DomWidgetStore | `BaseDOMWidget` | Global | `widgetId` (UUID) | Position, visibility, z-index |
|
||||
| LayoutStore | Node, Link, Reroute | Workflow-level | `nodeId`, `linkId`, `rerouteId` | Y.js CRDT maps (pos, size) |
|
||||
| NodeOutputStore | Execution results | `nodeLocatorId` | `"${subgraphId}:${nodeId}"` | Output data, preview URLs |
|
||||
| SubgraphNavigationStore | Canvas viewport | `subgraphId` | `subgraphId` or `'root'` | LRU viewport cache |
|
||||
| Store | Extracts From | Scoping | Key Format | Data Shape |
|
||||
| ----------------------- | ------------------- | ----------------- | ---------------------------------- | ----------------------------- |
|
||||
| WidgetValueStore | `BaseWidget` | `graphId` | `WidgetId` (`graphId:nodeId:name`) | Plain `WidgetState` object |
|
||||
| DomWidgetStore | `BaseDOMWidget` | Global | `widgetId` (UUID) | Position, visibility, z-index |
|
||||
| LayoutStore | Node, Link, Reroute | Workflow-level | `nodeId`, `linkId`, `rerouteId` | Y.js CRDT maps (pos, size) |
|
||||
| NodeOutputStore | Execution results | `nodeLocatorId` | `"${subgraphId}:${nodeId}"` | Output data, preview URLs |
|
||||
| SubgraphNavigationStore | Canvas viewport | `subgraphId` | `subgraphId` or `'root'` | LRU viewport cache |
|
||||
| PreviewExposureStore | Subgraph host node | host node locator | host locator + exposure name | Display-only preview state |
|
||||
|
||||
ADR 0009 refines promoted-widget identity: promoted value widgets are keyed by
|
||||
the host boundary (`host node locator + SubgraphInput.name`), while interior
|
||||
@@ -31,9 +34,9 @@ The closest thing to a true ECS component store in the codebase today.
|
||||
### State Shape
|
||||
|
||||
```
|
||||
Map<UUID, Map<WidgetKey, WidgetState>>
|
||||
Map<UUID, Map<WidgetId, WidgetState>>
|
||||
│ │ │
|
||||
graphId "nodeId:name" pure data object
|
||||
graphId "graphId:nodeId:name" pure data object
|
||||
```
|
||||
|
||||
`WidgetState` is a plain data object with no methods:
|
||||
@@ -56,7 +59,7 @@ Map<UUID, Map<WidgetKey, WidgetState>>
|
||||
**Phase 2 — `setNodeId()`:** Widget replaces its `_state` with a reference to the store's object:
|
||||
|
||||
```
|
||||
widget._state = useWidgetValueStore().registerWidget(graphId, { ...this._state, nodeId })
|
||||
widget._state = useWidgetValueStore().registerWidget(widgetId, { ...this._state, nodeId })
|
||||
```
|
||||
|
||||
After registration, the widget's getters/setters (`value`, `label`, `disabled`) are pass-throughs to the store. Mutations to the widget automatically sync to the store via shared object reference.
|
||||
@@ -119,20 +122,22 @@ Legacy `properties.proxyWidgets` is load-time migration input only.
|
||||
╰────────────────╯ ╰──────────────────────╯
|
||||
```
|
||||
|
||||
`PromotedWidgetViewManager`
|
||||
(`src/lib/litegraph/src/subgraph/PromotedWidgetViewManager.ts`) now reconciles
|
||||
synthetic widget views derived from linked subgraph inputs. It does not sit on
|
||||
top of a promotion registry.
|
||||
A promoted host widget is ordinary `WidgetState` in `WidgetValueStore`, keyed by
|
||||
the `WidgetId` carried on the `SubgraphInput` (`input.widgetId`). `SubgraphNode.widgets`
|
||||
is a read-only projection over the node's inputs that resolves each value via
|
||||
`useWidgetValueStore().getWidget(input.widgetId)`. There is no synthetic widget
|
||||
view object and no view cache to reconcile (PR 12617 deleted `PromotedWidgetView`
|
||||
and `PromotedWidgetViewManager`).
|
||||
|
||||
### ECS Alignment
|
||||
|
||||
| Aspect | ECS-like | Why |
|
||||
| ----------------------------- | --------- | ------------------------------------------------------------- |
|
||||
| Canonical topology | Yes | Value exposure is ordinary subgraph input/link state |
|
||||
| Host-scoped preview state | Yes | Preview exposure data is keyed by host locator |
|
||||
| Legacy migration boundary | Yes | `proxyWidgets` is consumed into canonical state or quarantine |
|
||||
| View reconciliation | Partially | ViewManager preserves synthetic widget object identity |
|
||||
| Entity class drives view sync | **No** | SubgraphNode still owns synthetic view cache invalidation |
|
||||
| Aspect | ECS-like | Why |
|
||||
| ---------------------------- | -------- | -------------------------------------------------------------- |
|
||||
| Canonical topology | Yes | Value exposure is ordinary subgraph input/link state |
|
||||
| Host-scoped preview state | Yes | Preview exposure data is keyed by host locator |
|
||||
| Legacy migration boundary | Yes | `proxyWidgets` is consumed into canonical state or quarantine |
|
||||
| Promoted value is plain data | Yes | Host widget is `WidgetState` in the store, keyed by `WidgetId` |
|
||||
| Projection over data | Yes | `SubgraphNode.widgets` derives from inputs; no view cache |
|
||||
|
||||
## 4. LayoutStore (CRDT)
|
||||
|
||||
@@ -171,14 +176,14 @@ These module-scope calls create implicit dependencies on the Vue runtime and mak
|
||||
|
||||
### ECS Alignment
|
||||
|
||||
| Aspect | ECS-like | Why |
|
||||
| ---------------------------- | --------- | --------------------------------------------------- |
|
||||
| Position data extracted | Yes | Closest to the ECS `Position` component |
|
||||
| CRDT-ready | Yes | Enables collaboration (ADR 0003) |
|
||||
| Covers multiple entity kinds | Yes | Nodes, links, reroutes in one store |
|
||||
| Mutation API (composable) | Partially | System-like, but called from entities, not a system |
|
||||
| Module-scope access | **No** | Domain objects import store at module level |
|
||||
| No entity ID branding | **No** | Plain numbers, no type safety across kinds |
|
||||
| Aspect | ECS-like | Why |
|
||||
| ---------------------------- | --------- | ------------------------------------------------------- |
|
||||
| Position data extracted | Yes | Closest to the ECS `Position` component |
|
||||
| CRDT-ready | Yes | Enables collaboration (ADR 0003) |
|
||||
| Covers multiple entity kinds | Yes | Nodes, links, reroutes in one store |
|
||||
| Mutation API (composable) | Partially | System-like, but called from entities, not a system |
|
||||
| Module-scope access | **No** | Domain objects import store at module level |
|
||||
| Per-store keying | Yes | Owns `nodeId`/`linkId`/`rerouteId` keys for its concern |
|
||||
|
||||
## 5. Pattern Analysis
|
||||
|
||||
@@ -190,30 +195,32 @@ These module-scope calls create implicit dependencies on the Vue runtime and mak
|
||||
4. **Query APIs**: `getWidget()`, preview exposure queries, `getNodeWidgets()` — system-like queries
|
||||
5. **Separation of data from behavior**: The stores hold data; classes retain behavior
|
||||
|
||||
### What's Missing vs Full ECS
|
||||
### Target Design and Remaining Gaps
|
||||
|
||||
Dedicated per-domain stores with their own string keys are the target, not a way
|
||||
station toward one unified registry. The remaining gaps are about behavior and
|
||||
data flow, not about collapsing the stores together.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Have["What We Have"]
|
||||
subgraph Have["What We Have (and Want)"]
|
||||
style Have fill:#1a4a1a,stroke:#2a6a2a,color:#e0e0e0
|
||||
H1["Centralized data stores"]
|
||||
H1["Dedicated per-domain stores"]
|
||||
H2["Plain data components
|
||||
(WidgetState, LayoutMap)"]
|
||||
H3["Query APIs
|
||||
(getWidget, preview exposures)"]
|
||||
H4["Graph-scoped lifecycle"]
|
||||
H5["Partial position extraction
|
||||
H5["Per-store string keys
|
||||
(WidgetId, nodeLocatorId)"]
|
||||
H6["Position extraction
|
||||
(LayoutStore)"]
|
||||
end
|
||||
|
||||
subgraph Missing["What's Missing"]
|
||||
style Missing fill:#4a1a1a,stroke:#6a2a2a,color:#e0e0e0
|
||||
M1["Unified World
|
||||
(6 stores, 6 keying strategies)"]
|
||||
M2["Branded entity IDs
|
||||
(keys are string concatenations)"]
|
||||
M3["System layer
|
||||
(mutations from anywhere)"]
|
||||
M3["System / command layer
|
||||
(sanctioned mutation path)"]
|
||||
M4["Complete extraction
|
||||
(behavior still on classes)"]
|
||||
M5["No entity-to-entity refs
|
||||
@@ -225,19 +232,21 @@ graph TD
|
||||
|
||||
### Keying Strategy Comparison
|
||||
|
||||
Each store invents its own identity scheme:
|
||||
Each store owns the identity scheme that fits its concern:
|
||||
|
||||
| Store | Key Format | Entity ID Used | Type-Safe? |
|
||||
| ---------------- | --------------------------- | ----------------------- | ---------- |
|
||||
| WidgetValueStore | `"${nodeId}:${widgetName}"` | NodeId (number\|string) | No |
|
||||
| DomWidgetStore | Widget UUID | UUID (string) | No |
|
||||
| LayoutStore | Raw nodeId/linkId/rerouteId | Mixed number types | No |
|
||||
| NodeOutputStore | `"${subgraphId}:${nodeId}"` | Composite string | No |
|
||||
| Store | Key Format | Key Type | Type-Safe? |
|
||||
| ---------------- | ---------------------------------- | ------------------ | ---------------- |
|
||||
| WidgetValueStore | `WidgetId` (`graphId:nodeId:name`) | branded string | Yes (`WidgetId`) |
|
||||
| DomWidgetStore | Widget UUID | UUID (string) | No |
|
||||
| LayoutStore | Raw nodeId/linkId/rerouteId | Mixed number types | No |
|
||||
| NodeOutputStore | `"${subgraphId}:${nodeId}"` | Composite string | No |
|
||||
|
||||
In the ECS target, all of these would use branded entity IDs (`WidgetEntityId`, `NodeEntityId`, etc.) with compile-time cross-kind protection.
|
||||
For promoted value widgets, ADR 0009 narrows the target key to host boundary
|
||||
identity (`host node locator + SubgraphInput.name`) instead of interior source
|
||||
identity.
|
||||
`WidgetValueStore` already keys on a branded `WidgetId` string (`src/types/widgetId.ts`),
|
||||
which carries its scope and survives renames at the store layer. The remaining
|
||||
stores can adopt their own branded string keys where cross-kind safety pays off,
|
||||
without a shared entity-ID space. For promoted value widgets, ADR 0009 keys on
|
||||
the host boundary: the input's `WidgetId` (host node locator + `SubgraphInput.name`),
|
||||
not interior source identity.
|
||||
|
||||
## 6. Extraction Map
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ graph TD
|
||||
|
||||
subgraph Unified["Unified: Composition"]
|
||||
direction TB
|
||||
W["World (flat)"]
|
||||
W["Dedicated stores (flat)"]
|
||||
N1["Node A
|
||||
graphScope: root"]
|
||||
N2["Node B (subgraph carrier)
|
||||
@@ -95,30 +95,29 @@ graph TD
|
||||
style Unified fill:#1a2a1a,stroke:#2a4a2a,color:#e0e0e0
|
||||
```
|
||||
|
||||
In the ECS World:
|
||||
Across the dedicated stores:
|
||||
|
||||
- **Every graph is a graph.** The "root" graph is simply the one whose
|
||||
`graphScope` has no parent.
|
||||
- **Nesting is a component, not a type.** A node can carry a
|
||||
`SubgraphStructure` component, which references another graph scope. That
|
||||
scope contains its own entities — nodes, links, widgets, reroutes, groups —
|
||||
all living in the same flat World.
|
||||
- **One World per workflow.** All entities across all nesting levels coexist in
|
||||
a single World, each tagged with a `graphScope` identifier. There are no
|
||||
sub-worlds, no recursive containers. The fractal structure is encoded in the
|
||||
data, not in the container hierarchy.
|
||||
- **Entity taxonomy: six kinds, not seven.** ADR 0008 defines seven entity kinds
|
||||
including `SubgraphEntityId`. Under unification, "subgraph" is not an entity
|
||||
kind — it is a node with a component. The taxonomy becomes: Node, Link,
|
||||
Widget, Slot, Reroute, Group.
|
||||
- **ID counters remain global.** All entity IDs are allocated from a single
|
||||
counter space, shared across all nesting levels. This preserves the current
|
||||
`rootGraph.state` behavior and guarantees ID uniqueness across the entire
|
||||
World.
|
||||
- **Graph scope parentage is tracked.** The World maintains a scope registry:
|
||||
each `graphId` maps to its parent `graphId` (or null for the root). This
|
||||
enables the ancestor walk required by the acyclicity invariant and supports
|
||||
queries like "all entities transitively contained by this graph."
|
||||
- **Nesting is a component.** A node can carry a `SubgraphStructure` component,
|
||||
which references another graph scope. That scope contains its own entities —
|
||||
nodes, links, widgets, reroutes, groups — and each store entry tags the scope
|
||||
it belongs to.
|
||||
- **Scope tagging over container nesting.** Entries across all nesting levels
|
||||
live in the same stores, each tagged with a `graphScope` identifier. The
|
||||
stores stay flat; the fractal structure is encoded in the keys and scope tags,
|
||||
with no recursive containers.
|
||||
- **Entity taxonomy: six kinds.** A subgraph is a node carrying a
|
||||
`SubgraphStructure` component. The taxonomy is: Node, Link, Widget, Slot,
|
||||
Reroute, Group. `SubgraphEntityId` is replaced by a `GraphId` scope identifier.
|
||||
- **Numeric ID counters stay shared.** Node/link/reroute IDs are allocated from
|
||||
a single counter space across nesting levels, preserving the current
|
||||
`rootGraph.state` behavior and guaranteeing uniqueness. Widget keys embed
|
||||
their scope directly (`WidgetId = graphId:nodeId:name`).
|
||||
- **Graph scope parentage is tracked.** A scope registry maps each `graphId` to
|
||||
its parent `graphId` (or null for the root). This enables the ancestor walk
|
||||
required by the acyclicity invariant and supports queries like "all entities
|
||||
transitively contained by this graph."
|
||||
|
||||
### The acyclicity invariant
|
||||
|
||||
@@ -367,15 +366,17 @@ sequenceDiagram
|
||||
Exec->>IW: reads input value (42)
|
||||
```
|
||||
|
||||
### Candidate B: Simplified component promotion
|
||||
### Candidate B: Simplified component promotion (rejected)
|
||||
|
||||
ADR 0009 chose Candidate A. Candidate B is retained here as the rejected
|
||||
alternative; it relied on a source-widget lookup model that no longer exists.
|
||||
|
||||
Promotion remains a first-class concept, simplified from three layers to one:
|
||||
|
||||
- A `WidgetPromotion` component on a widget entity:
|
||||
`{ promotedTo: NodeEntityId, sourceWidget: WidgetEntityId }`
|
||||
- The SubgraphNode's widget list includes promoted widget entity IDs directly
|
||||
- Value reads/writes delegate to the source widget's `WidgetValue` component via
|
||||
World lookup
|
||||
- A `WidgetPromotion` component on a widget entity referencing the host node and
|
||||
source widget
|
||||
- The SubgraphNode's widget list includes promoted widget references directly
|
||||
- Value reads/writes delegate to the source widget's value via a store lookup
|
||||
- Serialized as `properties.proxyWidgets` (unchanged)
|
||||
|
||||
This removes the ViewManager and proxy widget reconciliation but preserves the
|
||||
@@ -399,8 +400,9 @@ concept of promotion as distinct from connection.
|
||||
|
||||
Whichever candidate is chosen:
|
||||
|
||||
- **`WidgetEntityId` is internal.** Serialization uses widget name + parent node
|
||||
reference. This is settled (see Section 4).
|
||||
- **Internal identity is the `WidgetId` string.** Serialization uses widget name
|
||||
- parent node reference, while runtime state keys on `WidgetId`
|
||||
(`graphId:nodeId:name`). This is settled (see Section 4).
|
||||
- **The type → widget mapping is authoritative.** The widget registry
|
||||
(`widgetStore.widgets`) is the single source of truth for which types produce
|
||||
widgets. No parallel mechanism should duplicate this.
|
||||
@@ -473,10 +475,10 @@ and produces the recursive `ExportedSubgraph` structure, matching the current
|
||||
format exactly. Existing workflows, the ComfyUI backend, and third-party tools
|
||||
see no change.
|
||||
|
||||
| Direction | Format | Notes |
|
||||
| --------------- | ------------------------------- | ------------------------------------------ |
|
||||
| **Save/export** | Nested (current shape) | SerializationSystem walks scope tree |
|
||||
| **Load/import** | Nested (current) or future flat | Migration: normalize to flat World on load |
|
||||
| Direction | Format | Notes |
|
||||
| --------------- | ------------------------------- | -------------------------------------------------- |
|
||||
| **Save/export** | Nested (current shape) | SerializationSystem walks scope tree |
|
||||
| **Load/import** | Nested (current) or future flat | Migration: normalize to store-backed state on load |
|
||||
|
||||
The migration pattern: load any supported format and normalize to the internal
|
||||
model. The system accepts old formats indefinitely but produces the current
|
||||
@@ -486,7 +488,7 @@ format on save.
|
||||
|
||||
| Context | Identity | Example |
|
||||
| -------------------- | ---------------------------------------------------------- | ---------------------------------- |
|
||||
| **Internal (World)** | `WidgetEntityId` (opaque branded number) | `42 as WidgetEntityId` |
|
||||
| **Internal (store)** | `WidgetId` composite string | `'graphId:42:seed' as WidgetId` |
|
||||
| **Serialized** | Position in `widgets_values[]` + name from node definition | `widgets_values[2]` → third widget |
|
||||
|
||||
On save: the `SerializationSystem` queries `WidgetIdentity.name` and
|
||||
@@ -494,7 +496,7 @@ On save: the `SerializationSystem` queries `WidgetIdentity.name` and
|
||||
order.
|
||||
|
||||
On load: widget values are matched by name against the node definition's input
|
||||
specs, then assigned `WidgetEntityId`s from the global counter.
|
||||
specs, then registered in `WidgetValueStore` under their `WidgetId`.
|
||||
|
||||
This is the existing contract, preserved exactly.
|
||||
|
||||
@@ -553,7 +555,7 @@ This document proposes or surfaces the following changes to
|
||||
| Entity taxonomy | 7 kinds including `SubgraphEntityId` | 6 kinds — subgraph is a node with `SubgraphStructure` component |
|
||||
| `SubgraphEntityId` | `string & { __brand: 'SubgraphEntityId' }` | Eliminated; replaced by `GraphId` scope identifier |
|
||||
| Subgraph components | `SubgraphStructure`, `SubgraphMeta` listed as separate-entity components | Become node components on SubgraphNode entities |
|
||||
| World structure | Implied per-graph containment | Flat World with `graphScope` tags; one World per workflow |
|
||||
| Storage structure | Implied per-graph containment | Dedicated stores with `graphScope`-tagged entries; no single registry |
|
||||
| Acyclicity | Not addressed | DAG invariant on `SubgraphStructure.graphId` references, enforced on mutation |
|
||||
| Boundary model | Deferred | Typed interface contracts on `SubgraphStructure`; no virtual nodes or magic IDs |
|
||||
| Widget promotion | Treated as a given feature to migrate | ADR 0009 chooses Candidate A: promoted value widgets are linked inputs |
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
@plugin "./lucideStrokePlugin.js";
|
||||
|
||||
/* Safelist dynamic comfy icons for node library folders */
|
||||
@source inline("icon-[comfy--{ai-model,anthropic,bfl,bria,bytedance,bytedance-mono,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}]");
|
||||
|
||||
/* Safelist dynamic comfy icons for essential nodes (kebab-case of node names) */
|
||||
@source inline("icon-[comfy--{save-image,load-video,save-video,load-3-d,save-glb,image-batch,batch-images-node,image-crop,image-scale,image-rotate,image-blur,image-invert,canny,recraft-remove-background-node,kling-lip-sync-audio-to-video-node,load-audio,save-audio,stability-text-to-audio,lora-loader,lora-loader-model-only,primitive-string-multiline,get-video-components,video-slice,tencent-text-to-model-node,tencent-image-to-model-node,open-ai-chat-node,preview-image,image-and-mask-preview,layer-mask-mask-preview,mask-preview,image-preview-from-latent,i-tools-preview-image,i-tools-compare-image,canny-to-image,image-edit,text-to-image,pose-to-image,depth-to-video,image-to-image,canny-to-video,depth-to-image,image-to-video,pose-to-video,text-to-video,image-inpainting,image-outpainting}]");
|
||||
@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));
|
||||
|
||||
|
||||
3
packages/design-system/src/icons/3d-decomp.svg
Normal 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="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>
|
||||
|
After Width: | Height: | Size: 1014 B |
3
packages/design-system/src/icons/brightness-contrast.svg
Normal 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="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>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -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 22L7 20M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M7 20L5 18M9.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="#8A8A8A" 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 |
@@ -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 22L7 20M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M7 20L5 18M9.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="#8A8A8A" 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 |
5
packages/design-system/src/icons/channels.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 9C18 12.3137 15.3137 15 12 15C8.68629 15 6 12.3137 6 9C6 5.68629 8.68629 3 12 3C15.3137 3 18 5.68629 18 9Z" stroke="white" stroke-width="1.3"/>
|
||||
<path d="M14.5 15C14.5 18.3137 11.8137 21 8.5 21C5.18629 21 2.5 18.3137 2.5 15C2.5 11.6863 5.18629 9 8.5 9C11.8137 9 14.5 11.6863 14.5 15Z" stroke="white" stroke-width="1.3"/>
|
||||
<path d="M21.5 15C21.5 18.3137 18.8137 21 15.5 21C12.1863 21 9.5 18.3137 9.5 15C9.5 11.6863 12.1863 9 15.5 9C18.8137 9 21.5 11.6863 21.5 15Z" stroke="white" stroke-width="1.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 614 B |
@@ -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="M10.6154 18.8127C10.0968 18.9352 9.55598 19 9 19C5.13401 19 2 15.866 2 12C2 8.13401 5.13401 5 9 5C9.55598 5 10.0968 5.06482 10.6154 5.18731M13.3846 18.8127C13.9032 18.9352 14.444 19 15 19C18.866 19 22 15.866 22 12C22 8.13401 18.866 5 15 5C14.444 5 13.9032 5.06482 13.3846 5.18731M19 12C19 15.866 15.866 19 12 19C8.13401 19 5 15.866 5 12C5 8.13401 8.13401 5 12 5C15.866 5 19 8.13401 19 12Z" stroke="white" stroke-width="1.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 538 B |
1
packages/design-system/src/icons/claude.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fill-rule="nonzero"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -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 10H17M2.29912 4.63977C2.11384 4.8332 2 5.09561 2 5.38461V12.9231C2 13.5178 2.48215 14 3.07692 14H10.6154C10.8974 14 11.1541 13.8916 11.3461 13.7141M2.29912 4.63977C2.49515 4.43512 2.77116 4.30769 3.07692 4.30769H10.6154C10.9061 4.30769 11.1524 4.46662 11.3461 4.65384M2.29912 4.63977L4.59359 2.34615C4.79033 2.13329 5.07191 2 5.38463 2H12.9231C13.2201 2 13.4891 2.12025 13.6839 2.31473M11.3461 13.7141C11.559 13.5174 11.6923 13.2358 11.6923 12.9231V5.38461C11.6923 5.08055 11.5488 4.84967 11.3461 4.65384M11.3461 13.7141L13.6538 11.4064C13.8667 11.2097 14 10.9281 14 10.6154V3.07692C14 2.77918 13.8792 2.50967 13.6839 2.31473M11.3461 4.65384L13.6839 2.31473M5 22L7 20M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M7 20L5 18" stroke="#8A8A8A" 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 10H17M11.3461 4.65384C11.1524 4.46662 10.9061 4.30769 10.6154 4.30769H3.07692C2.77116 4.30769 2.49515 4.43512 2.29912 4.63977C2.11384 4.8332 2 5.09561 2 5.38461V12.9231C2 13.5178 2.48215 14 3.07692 14H10.6154C10.8974 14 11.1541 13.8916 11.3461 13.7141C11.559 13.5174 11.6923 13.2358 11.6923 12.9231V5.38461C11.6923 5.08055 11.5488 4.84967 11.3461 4.65384ZM11.3461 13.7141L13.6538 11.4064C13.8667 11.2097 14 10.9281 14 10.6154V3.07692C14 2.77918 13.8792 2.50967 13.6839 2.31473C13.4891 2.12025 13.2201 2 12.9231 2H5.38463C5.07191 2 4.79033 2.13329 4.59359 2.34615L2.29912 4.63977M11.3461 4.65384L13.6839 2.31473M5 18L7 20L5 22M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -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="M17.5 10H20.6667C21.403 10 22 10.597 22 11.3333V20.6667C22 21.403 21.403 22 20.6667 22H11.3333C10.597 22 10 21.403 10 20.6667V17M14 15.5V18.6667L18 16L15 14M2.29912 4.63977C2.11384 4.8332 2 5.09561 2 5.38461V12.9231C2 13.5178 2.48215 14 3.07692 14H10.6154C10.8974 14 11.1541 13.8916 11.3461 13.7141M2.29912 4.63977C2.49515 4.43512 2.77116 4.30769 3.07692 4.30769H10.6154C10.9061 4.30769 11.1524 4.46662 11.3461 4.65384M2.29912 4.63977L4.59359 2.34615C4.79033 2.13329 5.07191 2 5.38463 2H12.9231C13.2201 2 13.4891 2.12025 13.6839 2.31473M11.3461 13.7141C11.559 13.5174 11.6923 13.2358 11.6923 12.9231V5.38461C11.6923 5.08055 11.5488 4.84967 11.3461 4.65384M11.3461 13.7141L13.6538 11.4064C13.8667 11.2097 14 10.9281 14 10.6154V3.07692C14 2.77918 13.8792 2.50967 13.6839 2.31473M11.3461 4.65384L13.6839 2.31473M5 22L7 20M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M7 20L5 18" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M17.5 10H20.6667C21.403 10 22 10.597 22 11.3333V20.6667C22 21.403 21.403 22 20.6667 22H11.3333C10.597 22 10 21.403 10 20.6667V17M14 15.5V18.6667L18 16L15 14M11.3461 4.65384C11.1524 4.46662 10.9061 4.30769 10.6154 4.30769H3.07692C2.77116 4.30769 2.49515 4.43512 2.29912 4.63977C2.11384 4.8332 2 5.09561 2 5.38461V12.9231C2 13.5178 2.48215 14 3.07692 14H10.6154C10.8974 14 11.1541 13.8916 11.3461 13.7141C11.559 13.5174 11.6923 13.2358 11.6923 12.9231V5.38461C11.6923 5.08055 11.5488 4.84967 11.3461 4.65384ZM11.3461 13.7141L13.6538 11.4064C13.8667 11.2097 14 10.9281 14 10.6154V3.07692C14 2.77918 13.8792 2.50967 13.6839 2.31473C13.4891 2.12025 13.2201 2 12.9231 2H5.38463C5.07191 2 4.79033 2.13329 4.59359 2.34615L2.29912 4.63977M11.3461 4.65384L13.6839 2.31473M5 18L7 20L5 22M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17" 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.0 KiB |
3
packages/design-system/src/icons/dial.svg
Normal 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="M18.8 12H20M4 12H5.2M16.8083 16.8083L17.6569 17.6569M6.34315 6.34315L7.19167 7.19167M7.1907 16.8083L6.34217 17.6569M17.6559 6.34315L16.8074 7.19167M12 18.8V20M12 4V5.2M11.4448 10.6596L9.7218 6.49994M16.2426 7.75736C18.5858 10.1005 18.5858 13.8995 16.2426 16.2426C13.8995 18.5858 10.1005 18.5858 7.75736 16.2426C5.41421 13.8995 5.41421 10.1005 7.75736 7.75736C10.1005 5.41421 13.8995 5.41421 16.2426 7.75736ZM18.0104 5.98959C21.3299 9.30905 21.3299 14.691 18.0104 18.0104C14.691 21.3299 9.30905 21.3299 5.98959 18.0104C2.67014 14.691 2.67014 9.30905 5.98959 5.98959C9.30905 2.67014 14.691 2.67014 18.0104 5.98959Z" stroke="white" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 785 B |
3
packages/design-system/src/icons/frames-to-video.svg
Normal 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="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.1053M5 18L7 20L5 22M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M2 11.3333H8M5 14V11.3333M8 14L8 11.3333M8 2H13C13.5523 2 14 2.44772 14 3L14 4.66667V11.3333L14 13C14 13.5523 13.5523 14 13 14H8L3 14C2.44771 14 2 13.5523 2 13V3C2 2.44771 2.44772 2 3 2L8 2ZM8 14V2M8 2L8 4.66667M2 4.66667H8M5 4.66667V2M8 4.66667V11.3333M8 4.66667H14M8 11.3333H14M11 14V11.3333M11 4.66667V2" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 764 B |
4
packages/design-system/src/icons/glow.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.0104 18.0104C21.3299 14.691 21.3299 9.30905 18.0104 5.98959C14.691 2.67014 9.30905 2.67014 5.98959 5.98959C2.67014 9.30905 2.67014 14.691 5.98959 18.0104C9.30905 21.3299 14.691 21.3299 18.0104 18.0104Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-dasharray="1 2"/>
|
||||
<path d="M20.5 12H22M2 12H3.5M18.0103 18.0103L19.778 19.778M4.22168 4.22168L5.98945 5.98945M5.98974 18.0103L4.22197 19.778M19.7783 4.22168L18.0106 5.98945M12 20.5V22M12 2V3.5M16.2426 7.75736C18.5858 10.1005 18.5858 13.8995 16.2426 16.2426C13.8995 18.5858 10.1005 18.5858 7.75736 16.2426C5.41421 13.8995 5.41421 10.1005 7.75736 7.75736C10.1005 5.41421 13.8995 5.41421 16.2426 7.75736Z" stroke="white" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
4
packages/design-system/src/icons/grain.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.6504 18.3252C13.8297 18.3254 13.9746 18.471 13.9746 18.6504C13.9744 18.8296 13.8296 18.9744 13.6504 18.9746C13.471 18.9746 13.3254 18.8297 13.3252 18.6504C13.3252 18.4709 13.4709 18.3252 13.6504 18.3252ZM17.6504 17.8252C17.8297 17.8254 17.9746 17.971 17.9746 18.1504C17.9744 18.3296 17.8296 18.4744 17.6504 18.4746C17.471 18.4746 17.3254 18.3297 17.3252 18.1504C17.3252 17.9709 17.4709 17.8252 17.6504 17.8252ZM7.65039 17.3252C7.8297 17.3254 7.97461 17.471 7.97461 17.6504C7.9744 17.8296 7.82957 17.9744 7.65039 17.9746C7.47103 17.9746 7.32541 17.8297 7.3252 17.6504C7.3252 17.4709 7.4709 17.3252 7.65039 17.3252ZM11.6504 16.3252C11.8297 16.3254 11.9746 16.471 11.9746 16.6504C11.9744 16.8296 11.8296 16.9744 11.6504 16.9746C11.471 16.9746 11.3254 16.8297 11.3252 16.6504C11.3252 16.4709 11.4709 16.3252 11.6504 16.3252ZM15.6504 16.3252C15.8297 16.3254 15.9746 16.471 15.9746 16.6504C15.9744 16.8296 15.8296 16.9744 15.6504 16.9746C15.471 16.9746 15.3254 16.8297 15.3252 16.6504C15.3252 16.4709 15.4709 16.3252 15.6504 16.3252ZM18.6504 15.8252C18.8297 15.8254 18.9746 15.971 18.9746 16.1504C18.9744 16.3296 18.8296 16.4744 18.6504 16.4746C18.471 16.4746 18.3254 16.3297 18.3252 16.1504C18.3252 15.9709 18.4709 15.8252 18.6504 15.8252ZM9.65039 14.3252C9.8297 14.3254 9.97461 14.471 9.97461 14.6504C9.9744 14.8296 9.82957 14.9744 9.65039 14.9746C9.47103 14.9746 9.32541 14.8297 9.3252 14.6504C9.3252 14.4709 9.4709 14.3252 9.65039 14.3252ZM13.6504 14.3252C13.8297 14.3254 13.9746 14.471 13.9746 14.6504C13.9744 14.8296 13.8296 14.9744 13.6504 14.9746C13.471 14.9746 13.3254 14.8297 13.3252 14.6504C13.3252 14.4709 13.4709 14.3252 13.6504 14.3252ZM17.6504 13.8252C17.8297 13.8254 17.9746 13.971 17.9746 14.1504C17.9744 14.3296 17.8296 14.4744 17.6504 14.4746C17.471 14.4746 17.3254 14.3297 17.3252 14.1504C17.3252 13.9709 17.4709 13.8252 17.6504 13.8252ZM15.6504 12.3252C15.8297 12.3254 15.9746 12.471 15.9746 12.6504C15.9744 12.8296 15.8296 12.9744 15.6504 12.9746C15.471 12.9746 15.3254 12.8297 15.3252 12.6504C15.3252 12.4709 15.4709 12.3252 15.6504 12.3252ZM18.6504 11.8252C18.8297 11.8254 18.9746 11.971 18.9746 12.1504C18.9744 12.3296 18.8296 12.4744 18.6504 12.4746C18.471 12.4746 18.3254 12.3297 18.3252 12.1504C18.3252 11.9709 18.4709 11.8252 18.6504 11.8252ZM11.6504 11.3252C11.8297 11.3254 11.9746 11.471 11.9746 11.6504C11.9744 11.8296 11.8296 11.9744 11.6504 11.9746C11.471 11.9746 11.3254 11.8297 11.3252 11.6504C11.3252 11.4709 11.4709 11.3252 11.6504 11.3252ZM17.6504 9.8252C17.8297 9.82541 17.9746 9.97103 17.9746 10.1504C17.9744 10.3296 17.8296 10.4744 17.6504 10.4746C17.471 10.4746 17.3254 10.3297 17.3252 10.1504C17.3252 9.9709 17.4709 9.8252 17.6504 9.8252ZM7.65039 9.3252C7.8297 9.32541 7.97461 9.47103 7.97461 9.65039C7.9744 9.82957 7.82957 9.9744 7.65039 9.97461C7.47103 9.97461 7.32541 9.8297 7.3252 9.65039C7.3252 9.4709 7.4709 9.3252 7.65039 9.3252ZM13.6504 9.3252C13.8297 9.32541 13.9746 9.47103 13.9746 9.65039C13.9744 9.82957 13.8296 9.9744 13.6504 9.97461C13.471 9.97461 13.3254 9.8297 13.3252 9.65039C13.3252 9.4709 13.4709 9.3252 13.6504 9.3252ZM18.6504 7.8252C18.8297 7.82541 18.9746 7.97103 18.9746 8.15039C18.9744 8.32957 18.8296 8.4744 18.6504 8.47461C18.471 8.47461 18.3254 8.3297 18.3252 8.15039C18.3252 7.9709 18.4709 7.8252 18.6504 7.8252ZM11.6504 7.3252C11.8297 7.32541 11.9746 7.47103 11.9746 7.65039C11.9744 7.82957 11.8296 7.9744 11.6504 7.97461C11.471 7.97461 11.3254 7.8297 11.3252 7.65039C11.3252 7.4709 11.4709 7.3252 11.6504 7.3252ZM15.6504 7.3252C15.8297 7.32541 15.9746 7.47103 15.9746 7.65039C15.9744 7.82957 15.8296 7.9744 15.6504 7.97461C15.471 7.97461 15.3254 7.8297 15.3252 7.65039C15.3252 7.4709 15.4709 7.3252 15.6504 7.3252ZM17.6504 5.8252C17.8297 5.82541 17.9746 5.97103 17.9746 6.15039C17.9744 6.32957 17.8296 6.4744 17.6504 6.47461C17.471 6.47461 17.3254 6.3297 17.3252 6.15039C17.3252 5.9709 17.4709 5.8252 17.6504 5.8252ZM9.65039 5.3252C9.8297 5.32541 9.97461 5.47103 9.97461 5.65039C9.9744 5.82957 9.82957 5.9744 9.65039 5.97461C9.47103 5.97461 9.32541 5.8297 9.3252 5.65039C9.3252 5.4709 9.4709 5.3252 9.65039 5.3252ZM13.6504 5.3252C13.8297 5.32541 13.9746 5.47103 13.9746 5.65039C13.9744 5.82957 13.8296 5.9744 13.6504 5.97461C13.471 5.97461 13.3254 5.8297 13.3252 5.65039C13.3252 5.4709 13.4709 5.3252 13.6504 5.3252Z" stroke="white" stroke-width="0.65"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 14.9999L17.914 11.9139C17.5389 11.539 17.0303 11.3284 16.5 11.3284C15.9697 11.3284 15.4611 11.539 15.086 11.9139L12.6935 14.3064M14.5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3H5C3.89543 3 3 3.89543 3 5V13M11 9C11 10.1046 10.1046 11 9 11C7.89543 11 7 10.1046 7 9C7 7.89543 7.89543 7 9 7C10.1046 7 11 7.89543 11 9ZM2.03125 18.6735C1.98958 18.5613 1.98958 18.4378 2.03125 18.3255C2.43708 17.3415 3.12595 16.5001 4.01054 15.9081C4.89512 15.3161 5.93558 15 7 15C8.06442 15 9.10488 15.3161 9.98946 15.9081C10.874 16.5001 11.5629 17.3415 11.9687 18.3255C12.0104 18.4378 12.0104 18.5613 11.9687 18.6735C11.5629 19.6575 10.874 20.4989 9.98946 21.0909C9.10488 21.683 8.06442 21.999 7 21.999C5.93558 21.999 4.89512 21.683 4.01054 21.0909C3.12595 20.4989 2.43708 19.6575 2.03125 18.6735ZM8.49992 18.4995C8.49992 19.3278 7.82838 19.9994 6.99999 19.9994C6.17161 19.9994 5.50007 19.3278 5.50007 18.4995C5.50007 17.6711 6.17161 16.9995 6.99999 16.9995C7.82838 16.9995 8.49992 17.6711 8.49992 18.4995Z" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.03125 18.6735L1.42188 18.8997C1.42457 18.907 1.4274 18.9142 1.43035 18.9213L2.03125 18.6735ZM2.03125 18.3255L1.43035 18.0777C1.4274 18.0849 1.42457 18.0921 1.42188 18.0993L2.03125 18.3255ZM11.9687 18.3255L12.5781 18.0993C12.5754 18.0921 12.5726 18.0849 12.5697 18.0777L11.9687 18.3255ZM11.9687 18.6735L12.5697 18.9213C12.5726 18.9142 12.5754 18.907 12.5781 18.8997L11.9687 18.6735ZM9.54038 7.54038C9.28654 7.79422 9.28654 8.20578 9.54038 8.45962C9.79422 8.71346 10.2058 8.71346 10.4596 8.45962L10 8L9.54038 7.54038ZM15.4596 3.45962C15.7135 3.20578 15.7135 2.79422 15.4596 2.54038C15.2058 2.28654 14.7942 2.28654 14.5404 2.54038L15 3L15.4596 3.45962ZM12.5404 10.5404L12.0808 11L13 11.9192L13.4596 11.4596L13 11L12.5404 10.5404ZM20.4596 4.45962C20.7135 4.20578 20.7135 3.79422 20.4596 3.54038C20.2058 3.28654 19.7942 3.28654 19.5404 3.54038L20 4L20.4596 4.45962ZM15.5404 13.5404C15.2865 13.7942 15.2865 14.2058 15.5404 14.4596C15.7942 14.7135 16.2058 14.7135 16.4596 14.4596L16 14L15.5404 13.5404ZM21.4596 9.45962C21.7135 9.20578 21.7135 8.79422 21.4596 8.54038C21.2058 8.28654 20.7942 8.28654 20.5404 8.54038L21 9L21.4596 9.45962ZM14.5 20.35C14.141 20.35 13.85 20.641 13.85 21C13.85 21.359 14.141 21.65 14.5 21.65V21V20.35ZM2.35 13C2.35 13.359 2.64101 13.65 3 13.65C3.35899 13.65 3.65 13.359 3.65 13H3H2.35ZM2.03125 18.6735L2.64062 18.4473C2.65313 18.481 2.65313 18.518 2.64062 18.5517L2.03125 18.3255L1.42188 18.0993C1.32604 18.3575 1.32604 18.6415 1.42188 18.8997L2.03125 18.6735ZM2.03125 18.3255L2.63215 18.5733C2.9889 17.7083 3.59447 16.9687 4.37207 16.4483L4.01054 15.9081L3.649 15.3679C2.65744 16.0316 1.88526 16.9747 1.43035 18.0777L2.03125 18.3255ZM4.01054 15.9081L4.37207 16.4483C5.14968 15.9278 6.0643 15.65 7 15.65V15V14.35C5.80685 14.35 4.64056 14.7043 3.649 15.3679L4.01054 15.9081ZM7 15V15.65C7.9357 15.65 8.85032 15.9278 9.62793 16.4483L9.98946 15.9081L10.351 15.3679C9.35944 14.7043 8.19315 14.35 7 14.35V15ZM9.98946 15.9081L9.62793 16.4483C10.4055 16.9687 11.0111 17.7083 11.3678 18.5733L11.9687 18.3255L12.5697 18.0777C12.1147 16.9747 11.3426 16.0316 10.351 15.3679L9.98946 15.9081ZM11.9687 18.3255L11.3594 18.5517C11.3469 18.518 11.3469 18.481 11.3594 18.4473L11.9687 18.6735L12.5781 18.8997C12.674 18.6415 12.674 18.3575 12.5781 18.0993L11.9687 18.3255ZM11.9687 18.6735L11.3678 18.4257C11.0111 19.2907 10.4055 20.0303 9.62793 20.5508L9.98946 21.0909L10.351 21.6311C11.3426 20.9675 12.1147 20.0244 12.5697 18.9213L11.9687 18.6735ZM9.98946 21.0909L9.62793 20.5508C8.85032 21.0712 7.9357 21.349 7 21.349V21.999V22.649C8.19315 22.649 9.35944 22.2948 10.351 21.6311L9.98946 21.0909ZM7 21.999V21.349C6.0643 21.349 5.14968 21.0712 4.37207 20.5508L4.01054 21.0909L3.649 21.6311C4.64056 22.2948 5.80685 22.649 7 22.649V21.999ZM4.01054 21.0909L4.37207 20.5508C3.59447 20.0303 2.9889 19.2907 2.63215 18.4257L2.03125 18.6735L1.43035 18.9213C1.88526 20.0244 2.65744 20.9675 3.649 21.6311L4.01054 21.0909ZM8.49992 18.4995H7.84992C7.84992 18.9689 7.46939 19.3494 6.99999 19.3494V19.9994V20.6494C8.18736 20.6494 9.14992 19.6868 9.14992 18.4995H8.49992ZM6.99999 19.9994V19.3494C6.53059 19.3494 6.15007 18.9689 6.15007 18.4995H5.50007H4.85007C4.85007 19.6868 5.81262 20.6494 6.99999 20.6494V19.9994ZM5.50007 18.4995H6.15007C6.15007 18.0301 6.53059 17.6495 6.99999 17.6495V16.9995V16.3495C5.81262 16.3495 4.85007 17.3121 4.85007 18.4995H5.50007ZM6.99999 16.9995V17.6495C7.46939 17.6495 7.84992 18.0301 7.84992 18.4995H8.49992H9.14992C9.14992 17.3121 8.18736 16.3495 6.99999 16.3495V16.9995ZM10 8L10.4596 8.45962L15.4596 3.45962L15 3L14.5404 2.54038L9.54038 7.54038L10 8ZM13 11L13.4596 11.4596L20.4596 4.45962L20 4L19.5404 3.54038L12.5404 10.5404L13 11ZM16 14L16.4596 14.4596L21.4596 9.45962L21 9L20.5404 8.54038L15.5404 13.5404L16 14ZM5 3V3.65H19V3V2.35H5V3ZM19 3V3.65C19.7456 3.65 20.35 4.25442 20.35 5H21H21.65C21.65 3.53645 20.4636 2.35 19 2.35V3ZM21 5H20.35V19H21H21.65V5H21ZM21 19H20.35C20.35 19.7456 19.7456 20.35 19 20.35V21V21.65C20.4636 21.65 21.65 20.4636 21.65 19H21ZM3 5H3.65C3.65 4.25442 4.25442 3.65 5 3.65V3V2.35C3.53645 2.35 2.35 3.53645 2.35 5H3ZM19 21V20.35H14.5V21V21.65H19V21ZM3 13H3.65V5H3H2.35V13H3ZM5 3L4.54038 3.45962L20.5404 19.4596L21 19L21.4596 18.5404L5.45962 2.54038L5 3Z" fill="#8A8A8A"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -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="#8A8A8A" 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="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 935 B |
@@ -1,11 +1,3 @@
|
||||
<svg width="48" height="24" viewBox="0 0 48 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1683_13217)">
|
||||
<path d="M33.2579 14.2434C33.2579 8.04845 24.8306 0.0257698 24.4718 -0.312477C24.2037 -0.565892 23.7862 -0.561497 23.5226 -0.305153C23.1637 0.0434776 14.7439 8.30814 14.7439 14.2436C14.7439 19.3484 18.8966 23.5 24.0003 23.5C29.1067 23.5015 33.2579 19.3486 33.2579 14.2434ZM24.0015 22.1299C19.6538 22.1299 16.1165 18.5924 16.1165 14.2449C16.1165 9.63656 22.2409 2.98187 24.009 1.15676C25.7829 2.94092 31.8885 9.42993 31.8885 14.2449C31.887 18.5926 28.349 22.1299 24.0015 22.1299Z" fill="#8A8A8A"/>
|
||||
<path d="M28.4502 12.5882C28.0766 12.6482 27.8218 12.9998 27.8804 13.3733C28.1265 14.9143 27.5918 16.2605 26.0933 17.8748C25.8355 18.1516 25.8516 18.5852 26.1284 18.843C26.2603 18.9661 26.4273 19.0261 26.5943 19.0261C26.7788 19.0261 26.9619 18.9529 27.0967 18.8064C28.3037 17.5071 29.6381 15.6907 29.2339 13.1552C29.1753 12.7831 28.8222 12.5295 28.4502 12.5882Z" fill="#8A8A8A"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1683_13217">
|
||||
<rect width="48" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.4292 17.4286C12.4292 17.4286 10.7681 17.4818 9.85773 16.5714C8.94733 15.661 9.00058 14.8571 9.00058 14M12 20.6429C13.5913 20.6429 15.1174 20.0107 16.2426 18.8855C17.3679 17.7603 18 16.2342 18 14.6429C18 9.5 12 4.35715 12 4.35715C12 4.35715 6 9.5 6 14.6429C6 16.2342 6.63214 17.7603 7.75736 18.8855C8.88258 20.0107 10.4087 20.6429 12 20.6429Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 542 B |
3
packages/design-system/src/icons/image-canny.svg
Normal 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="M13.2508 12.7276C12.4587 13.0908 11.5409 13.0908 10.7488 12.7276M10.7488 7.2724C11.5409 6.9092 12.4587 6.9092 13.2508 7.2724M9.27202 11.251C8.90883 10.4589 8.90883 9.54111 9.27202 8.74897M14.7272 8.74897C15.0904 9.54111 15.0904 10.4589 14.7272 11.251M6.18003 19.5412C6.06141 20.0143 6 20.504 6 21M18 21C18 20.504 17.9386 20.0143 17.82 19.5412M7 17.6833C7.21936 17.3526 7.47257 17.0421 7.75736 16.7574C8.04215 16.4726 8.35262 16.2194 8.68333 16M17 17.6833C16.7806 17.3526 16.5274 17.0421 16.2426 16.7574C15.9579 16.4726 15.6474 16.2194 15.3167 16M10.6747 15.1482C11.1062 15.0504 11.5505 15 12 15C12.4495 15 12.8938 15.0504 13.3253 15.1482M5 3H19C20.1046 3 21 3.89543 21 5V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V5C3 3.89543 3.89543 3 5 3Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 953 B |
3
packages/design-system/src/icons/image-captioning.svg
Normal 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="M20 12L17.9427 9.94263C17.6926 9.69267 17.3536 9.55225 17 9.55225C16.6464 9.55225 16.3074 9.69267 16.0573 9.94263L10 16M5 16H3M15 18.5H3M11.0667 20.9333H3M9.33333 4H18.6667C19.403 4 20 4.59695 20 5.33333V14.6667C20 15.403 19.403 16 18.6667 16H9.33333C8.59695 16 8 15.403 8 14.6667V5.33333C8 4.59695 8.59695 4 9.33333 4ZM13.3333 8C13.3333 8.73638 12.7364 9.33333 12 9.33333C11.2636 9.33333 10.6667 8.73638 10.6667 8C10.6667 7.26362 11.2636 6.66667 12 6.66667C12.7364 6.66667 13.3333 7.26362 13.3333 8Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 697 B |
3
packages/design-system/src/icons/image-collage.svg
Normal 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="M11 8.33331L9.62844 6.96175C9.46175 6.79511 9.2357 6.7015 9 6.7015C8.7643 6.7015 8.53825 6.79511 8.37156 6.96175L4.33333 11M11 18.3333L9.62844 16.9618C9.46175 16.7951 9.2357 16.7015 9 16.7015C8.7643 16.7015 8.53825 16.7951 8.37156 16.9618L4.33333 21M21 8.33331L19.6284 6.96175C19.4618 6.79511 19.2357 6.7015 19 6.7015C18.7643 6.7015 18.5382 6.79511 18.3716 6.96175L14.3333 11M21 18.3333L19.6284 16.9618C19.4618 16.7951 19.2357 16.7015 19 16.7015C18.7643 16.7015 18.5382 16.7951 18.3716 16.9618L14.3333 21M3.88889 3H10.1111C10.602 3 11 3.39797 11 3.88889V10.1111C11 10.602 10.602 11 10.1111 11H3.88889C3.39797 11 3 10.602 3 10.1111V3.88889C3 3.39797 3.39797 3 3.88889 3ZM3.88889 13H10.1111C10.602 13 11 13.398 11 13.8889V20.1111C11 20.602 10.602 21 10.1111 21H3.88889C3.39797 21 3 20.602 3 20.1111V13.8889C3 13.398 3.39797 13 3.88889 13ZM13.8889 3H20.1111C20.602 3 21 3.39797 21 3.88889V10.1111C21 10.602 20.602 11 20.1111 11H13.8889C13.398 11 13 10.602 13 10.1111V3.88889C13 3.39797 13.398 3 13.8889 3ZM13.8889 13H20.1111C20.602 13 21 13.398 21 13.8889V20.1111C21 20.602 20.602 21 20.1111 21H13.8889C13.398 21 13 20.602 13 20.1111V13.8889C13 13.398 13.398 13 13.8889 13Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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 19C3 20.1046 3.79594 21 4.77778 21H11V3H4.77778C3.79594 3 3 3.89543 3 5M3 19V5M3 19C3 19.5304 3.21071 20.0391 3.58579 20.4142C3.96086 20.7893 4.46957 21 5 21M3 5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3M11 1L11 23M21 15L17.9 11.9C17.5237 11.5312 17.017 11.3258 16.4901 11.3284C15.9632 11.331 15.4586 11.5415 15.086 11.914L14 13M11 16L6 21M14 3H19.1538C20.1734 3 21 3.89543 21 5V19C21 20.1046 20.1734 21 19.1538 21H14V3ZM11 9C11 10.1046 10.1046 11 9 11C7.89543 11 7 10.1046 7 9C7 7.89543 7.89543 7 9 7C10.1046 7 11 7.89543 11 9Z" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 19C3 20.1046 3.79594 21 4.77778 21H11V3H4.77778C3.79594 3 3 3.89543 3 5M3 19V5M3 19C3 19.5304 3.21071 20.0391 3.58579 20.4142C3.96086 20.7893 4.46957 21 5 21M3 5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3M11 1L11 23M21 15L17.9 11.9C17.5237 11.5312 17.017 11.3258 16.4901 11.3284C15.9632 11.331 15.4586 11.5415 15.086 11.914L14 13M11 16L6 21M14 3H19.1538C20.1734 3 21 3.89543 21 5V19C21 20.1046 20.1734 21 19.1538 21H14V3ZM11 9C11 10.1046 10.1046 11 9 11C7.89543 11 7 10.1046 7 9C7 7.89543 7.89543 7 9 7C10.1046 7 11 7.89543 11 9Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 760 B After Width: | Height: | Size: 758 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="48" height="24" viewBox="0 0 48 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 1.2002C18.4418 1.2002 18.7998 1.55817 18.7998 2V5.2002H29C29.9941 5.2002 30.7998 6.00589 30.7998 7V17.7002H34C34.4418 17.7002 34.7998 18.0582 34.7998 18.5C34.7998 18.9418 34.4418 19.2998 34 19.2998H30.7998V22.5C30.7998 22.9418 30.4418 23.2998 30 23.2998C29.5582 23.2998 29.2002 22.9418 29.2002 22.5V19.2998H19C18.0059 19.2998 17.2002 18.4941 17.2002 17.5V6.7998H14C13.5582 6.7998 13.2002 6.44183 13.2002 6C13.2002 5.55817 13.5582 5.2002 14 5.2002H17.2002V2C17.2002 1.55817 17.5582 1.2002 18 1.2002ZM18.7998 17.5C18.7998 17.6105 18.8895 17.7002 19 17.7002H29.2002V7C29.2002 6.88954 29.1105 6.79981 29 6.7998H18.7998V17.5Z" fill="#8A8A8A"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 755 B |
3
packages/design-system/src/icons/image-depth.svg
Normal 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="M16.125 21C16.125 19.6076 15.5719 18.2723 14.5873 17.2877C13.6027 16.3031 12.2674 15.75 10.875 15.75M10.875 15.75C9.48261 15.75 8.14726 16.3031 7.16269 17.2877C6.17812 18.2723 5.625 19.6076 5.625 21M10.875 15.75C12.808 15.75 14.375 14.183 14.375 12.25C14.375 10.317 12.808 8.75 10.875 8.75C8.942 8.75 7.375 10.317 7.375 12.25C7.375 14.183 8.942 15.75 10.875 15.75ZM18.1875 5.8125C17.8727 5.50826 17.4724 5.25 17 5.25H4.75C4.25313 5.25 3.80462 5.45707 3.48607 5.78963C3.18499 6.10395 3 6.53037 3 7V19.25C3 20.2165 3.7835 21 4.75 21H17C17.4583 21 17.8755 20.8238 18.1875 20.5354C18.5334 20.2157 18.75 19.7582 18.75 19.25V7C18.75 6.5059 18.5168 6.13071 18.1875 5.8125ZM18.1875 20.5354L20.4375 18.2854C20.7834 17.9657 21 17.5082 21 17V4.75C21 4.26618 20.8037 3.82821 20.4863 3.51144C20.1697 3.19541 19.7327 3 19.25 3H7.00003C6.49187 3 6.03429 3.21659 5.71458 3.5625L3.48607 5.78963M18.1875 5.8125L20.4863 3.51144" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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="M8.5 21H5M5 21C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V9M5 21L14.086 11.914C14.4586 11.5415 14.9632 11.331 15.4901 11.3284M18.5 13.7503L20.5 15.7503M11 9C11 10.1046 10.1046 11 9 11C7.89543 11 7 10.1046 7 9C7 7.89543 7.89543 7 9 7C10.1046 7 11 7.89543 11 9ZM21.5871 14.6562C21.8514 14.3919 22 14.0334 22 13.6596C22 13.2858 21.8516 12.9273 21.5873 12.6629C21.323 12.3986 20.9645 12.25 20.5907 12.25C20.2169 12.25 19.8584 12.3984 19.594 12.6627L12.921 19.3373C12.8049 19.453 12.719 19.5955 12.671 19.7523L12.0105 21.9283C11.9975 21.9715 11.9966 22.0175 12.0076 22.0612C12.0187 22.105 12.0414 22.1449 12.0734 22.1768C12.1053 22.2087 12.1453 22.2313 12.189 22.2423C12.2328 22.2533 12.2787 22.2523 12.322 22.2393L14.4985 21.5793C14.6551 21.5317 14.7976 21.4463 14.9135 21.3308L21.5871 14.6562Z" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.5 21H5M5 21C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V9M5 21L14.086 11.914C14.4586 11.5415 14.9632 11.331 15.4901 11.3284M18.5 13.7503L20.5 15.7503M11 9C11 10.1046 10.1046 11 9 11C7.89543 11 7 10.1046 7 9C7 7.89543 7.89543 7 9 7C10.1046 7 11 7.89543 11 9ZM21.5871 14.6562C21.8514 14.3919 22 14.0334 22 13.6596C22 13.2858 21.8516 12.9273 21.5873 12.6629C21.323 12.3986 20.9645 12.25 20.5907 12.25C20.2169 12.25 19.8584 12.3984 19.594 12.6627L12.921 19.3373C12.8049 19.453 12.719 19.5955 12.671 19.7523L12.0105 21.9283C11.9975 21.9715 11.9966 22.0175 12.0076 22.0612C12.0187 22.105 12.0414 22.1449 12.0734 22.1768C12.1053 22.2087 12.1453 22.2313 12.189 22.2423C12.2328 22.2533 12.2787 22.2523 12.322 22.2393L14.4985 21.5793C14.6551 21.5317 14.7976 21.4463 14.9135 21.3308L21.5871 14.6562Z" 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 |
@@ -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="M8.5 21H5M5 21C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V9.5M5 21L14.086 11.914C14.4586 11.5415 14.9632 11.331 15.4901 11.3284M18.3109 20.2074L12.9705 18.7508M15.4997 15.2586C14.5976 16.6137 13.5146 16.9887 12.208 17.2328C12.1646 17.2407 12.1241 17.2597 12.0904 17.2881C12.0567 17.3164 12.0309 17.3531 12.0157 17.3944C12.0004 17.4358 11.9962 17.4804 12.0035 17.5238C12.0107 17.5673 12.0291 17.6081 12.057 17.6423L15.7172 22.0841C15.7916 22.163 15.8896 22.2157 15.9964 22.2341C16.1033 22.2525 16.2133 22.2356 16.3098 22.1861C17.3673 21.4615 18.9999 19.6549 18.9999 18.7589M11 9C11 10.1046 10.1046 11 9 11C7.89543 11 7 10.1046 7 9C7 7.89543 7.89543 7 9 7C10.1046 7 11 7.89543 11 9ZM20.188 12.5694C20.2866 12.4709 20.4036 12.3927 20.5324 12.3393C20.6611 12.286 20.7992 12.2585 20.9386 12.2585C21.078 12.2585 21.216 12.286 21.3448 12.3393C21.4735 12.3927 21.5905 12.4709 21.6891 12.5694C21.7877 12.668 21.8659 12.785 21.9192 12.9138C21.9725 13.0426 22 13.1806 22 13.32C22 13.4594 21.9725 13.5974 21.9192 13.7262C21.8659 13.855 21.7877 13.972 21.6891 14.0705L19.68 16.0802C19.6331 16.1271 19.6068 16.1906 19.6068 16.2569C19.6068 16.3232 19.6331 16.3868 19.68 16.4337L20.152 16.9057C20.378 17.1317 20.5049 17.4382 20.5049 17.7578C20.5049 18.0774 20.378 18.3838 20.152 18.6098L19.68 19.0819C19.6331 19.1287 19.5695 19.1551 19.5032 19.1551C19.4369 19.1551 19.3733 19.1287 19.3265 19.0819L15.1767 14.9326C15.1298 14.8857 15.1035 14.8221 15.1035 14.7558C15.1035 14.6895 15.1298 14.626 15.1767 14.5791L15.6487 14.107C15.8747 13.8811 16.1812 13.7541 16.5008 13.7541C16.8203 13.7541 17.1268 13.8811 17.3528 14.107L17.8249 14.5791C17.8717 14.6259 17.9353 14.6523 18.0016 14.6523C18.0679 14.6523 18.1315 14.6259 18.1784 14.5791L20.188 12.5694Z" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.5 21H5M5 21C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V9.5M5 21L14.086 11.914C14.4586 11.5415 14.9632 11.331 15.4901 11.3284M18.3109 20.2074L12.9705 18.7508M15.4997 15.2586C14.5976 16.6137 13.5146 16.9887 12.208 17.2328C12.1646 17.2407 12.1241 17.2597 12.0904 17.2881C12.0567 17.3164 12.0309 17.3531 12.0157 17.3944C12.0004 17.4358 11.9962 17.4804 12.0035 17.5238C12.0107 17.5673 12.0291 17.6081 12.057 17.6423L15.7172 22.0841C15.7916 22.163 15.8896 22.2157 15.9964 22.2341C16.1033 22.2525 16.2133 22.2356 16.3098 22.1861C17.3673 21.4615 18.9999 19.6549 18.9999 18.7589M11 9C11 10.1046 10.1046 11 9 11C7.89543 11 7 10.1046 7 9C7 7.89543 7.89543 7 9 7C10.1046 7 11 7.89543 11 9ZM20.188 12.5694C20.2866 12.4709 20.4036 12.3927 20.5324 12.3393C20.6611 12.286 20.7992 12.2585 20.9386 12.2585C21.078 12.2585 21.216 12.286 21.3448 12.3393C21.4735 12.3927 21.5905 12.4709 21.6891 12.5694C21.7877 12.668 21.8659 12.785 21.9192 12.9138C21.9725 13.0426 22 13.1806 22 13.32C22 13.4594 21.9725 13.5974 21.9192 13.7262C21.8659 13.855 21.7877 13.972 21.6891 14.0705L19.68 16.0802C19.6331 16.1271 19.6068 16.1906 19.6068 16.2569C19.6068 16.3232 19.6331 16.3868 19.68 16.4337L20.152 16.9057C20.378 17.1317 20.5049 17.4382 20.5049 17.7578C20.5049 18.0774 20.378 18.3838 20.152 18.6098L19.68 19.0819C19.6331 19.1287 19.5695 19.1551 19.5032 19.1551C19.4369 19.1551 19.3733 19.1287 19.3265 19.0819L15.1767 14.9326C15.1298 14.8857 15.1035 14.8221 15.1035 14.7558C15.1035 14.6895 15.1298 14.626 15.1767 14.5791L15.6487 14.107C15.8747 13.8811 16.1812 13.7541 16.5008 13.7541C16.8203 13.7541 17.1268 13.8811 17.3528 14.107L17.8249 14.5791C17.8717 14.6259 17.9353 14.6523 18.0016 14.6523C18.0679 14.6523 18.1315 14.6259 18.1784 14.5791L20.188 12.5694Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -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="M11.9999 4L11.9999 3.35L11.3499 3.35V4H11.9999ZM11.9999 20H11.3499C11.3499 20.1724 11.4184 20.3377 11.5403 20.4596C11.6622 20.5815 11.8275 20.65 11.9999 20.65L11.9999 20ZM11.9999 11.35H11.3499V12.65H11.9999V12V11.35ZM19.9999 12.65C20.3589 12.65 20.6499 12.359 20.6499 12C20.6499 11.641 20.3589 11.35 19.9999 11.35V12V12.65ZM11.9999 13.35H11.3499V14.65H11.9999V14V13.35ZM19.9999 14.65C20.3589 14.65 20.6499 14.359 20.6499 14C20.6499 13.641 20.3589 13.35 19.9999 13.35V14V14.65ZM11.9999 9.34999H11.3499V10.65H11.9999V9.99999V9.34999ZM19.9999 10.65C20.3589 10.65 20.6499 10.359 20.6499 9.99999C20.6499 9.64101 20.3589 9.34999 19.9999 9.34999V9.99999V10.65ZM11.9999 7.34999H11.3499V8.64999H11.9999V7.99999V7.34999ZM18.9999 8.64999C19.3589 8.64999 19.6499 8.35898 19.6499 7.99999C19.6499 7.64101 19.3589 7.34999 18.9999 7.34999V7.99999V8.64999ZM11.9999 15.35H11.3499V16.65H11.9999V16V15.35ZM18.9999 16.65C19.3589 16.65 19.6499 16.359 19.6499 16C19.6499 15.641 19.3589 15.35 18.9999 15.35V16V16.65ZM11.9999 17.35H11.3499V18.65H11.9999V18V17.35ZM17.4999 18.65C17.8589 18.65 18.1499 18.359 18.1499 18C18.1499 17.641 17.8589 17.35 17.4999 17.35V18V18.65ZM11.9999 5.34999H11.3499V6.64999H11.9999V5.99999V5.34999ZM17.4999 6.64999C17.8589 6.64999 18.1499 6.35898 18.1499 5.99999C18.1499 5.64101 17.8589 5.34999 17.4999 5.34999V5.99999V6.64999ZM14.4999 4.64999C14.8589 4.64999 15.1499 4.35897 15.1499 3.99999C15.1499 3.641 14.8589 3.34999 14.4999 3.34999L14.4999 3.99999L14.4999 4.64999ZM14.4999 20.65C14.8589 20.65 15.1499 20.359 15.1499 20C15.1499 19.641 14.8589 19.35 14.4999 19.35L14.4999 20L14.4999 20.65ZM11.9999 4H11.3499V20H11.9999H12.6499V4H11.9999ZM11.9999 12V12.65H19.9999V12V11.35H11.9999V12ZM11.9999 14V14.65H19.9999V14V13.35H11.9999V14ZM11.9999 9.99999V10.65H19.9999V9.99999V9.34999H11.9999V9.99999ZM11.9999 7.99999V8.64999H18.9999V7.99999V7.34999H11.9999V7.99999ZM11.9999 16V16.65H18.9999V16V15.35H11.9999V16ZM11.9999 18V18.65H17.4999V18V17.35H11.9999V18ZM11.9999 5.99999V6.64999H17.4999V5.99999V5.34999H11.9999V5.99999ZM11.9999 4L11.9999 4.65L14.4999 4.64999L14.4999 3.99999L14.4999 3.34999L11.9999 3.35L11.9999 4ZM11.9999 20L11.9999 20.65L14.4999 20.65L14.4999 20L14.4999 19.35L11.9999 19.35L11.9999 20ZM20.4852 11.9705H19.8352C19.8352 16.2978 16.3272 19.8058 11.9999 19.8058V20.4558V21.1058C17.0452 21.1058 21.1352 17.0158 21.1352 11.9705H20.4852ZM11.9999 20.4558V19.8058C7.67262 19.8058 4.16465 16.2978 4.16465 11.9705H3.51465H2.86465C2.86465 17.0158 6.95465 21.1058 11.9999 21.1058V20.4558ZM3.51465 11.9705H4.16465C4.16465 7.64323 7.67262 4.13526 11.9999 4.13526V3.48526V2.83526C6.95465 2.83526 2.86465 6.92526 2.86465 11.9705H3.51465ZM11.9999 3.48526V4.13526C16.3272 4.13526 19.8352 7.64323 19.8352 11.9705H20.4852H21.1352C21.1352 6.92526 17.0452 2.83526 11.9999 2.83526V3.48526Z" fill="#8A8A8A"/>
|
||||
<path d="M12.0001 4L12.0001 3.35L11.3501 3.35V4H12.0001ZM12.0001 20H11.3501C11.3501 20.1724 11.4185 20.3377 11.5404 20.4596C11.6623 20.5815 11.8277 20.65 12.0001 20.65L12.0001 20ZM12.0001 11.35H11.3501V12.65H12.0001V12V11.35ZM20.0001 12.65C20.359 12.65 20.6501 12.359 20.6501 12C20.6501 11.641 20.359 11.35 20.0001 11.35V12V12.65ZM12.0001 13.35H11.3501V14.65H12.0001V14V13.35ZM20.0001 14.65C20.359 14.65 20.6501 14.359 20.6501 14C20.6501 13.641 20.359 13.35 20.0001 13.35V14V14.65ZM12.0001 9.34999H11.3501V10.65H12.0001V9.99999V9.34999ZM20.0001 10.65C20.359 10.65 20.6501 10.359 20.6501 9.99999C20.6501 9.64101 20.359 9.34999 20.0001 9.34999V9.99999V10.65ZM12.0001 7.34999H11.3501V8.64999H12.0001V7.99999V7.34999ZM19.0001 8.64999C19.359 8.64999 19.6501 8.35898 19.6501 7.99999C19.6501 7.64101 19.359 7.34999 19.0001 7.34999V7.99999V8.64999ZM12.0001 15.35H11.3501V16.65H12.0001V16V15.35ZM19.0001 16.65C19.359 16.65 19.6501 16.359 19.6501 16C19.6501 15.641 19.359 15.35 19.0001 15.35V16V16.65ZM12.0001 17.35H11.3501V18.65H12.0001V18V17.35ZM17.5001 18.65C17.859 18.65 18.1501 18.359 18.1501 18C18.1501 17.641 17.859 17.35 17.5001 17.35V18V18.65ZM12.0001 5.34999H11.3501V6.64999H12.0001V5.99999V5.34999ZM17.5001 6.64999C17.859 6.64999 18.1501 6.35898 18.1501 5.99999C18.1501 5.64101 17.859 5.34999 17.5001 5.34999V5.99999V6.64999ZM14.5001 4.64999C14.859 4.64999 15.1501 4.35897 15.1501 3.99999C15.1501 3.641 14.859 3.34999 14.5001 3.34999L14.5001 3.99999L14.5001 4.64999ZM14.5001 20.65C14.859 20.65 15.1501 20.359 15.1501 20C15.1501 19.641 14.859 19.35 14.5001 19.35L14.5001 20L14.5001 20.65ZM12.0001 4H11.3501V20H12.0001H12.6501V4H12.0001ZM12.0001 12V12.65H20.0001V12V11.35H12.0001V12ZM12.0001 14V14.65H20.0001V14V13.35H12.0001V14ZM12.0001 9.99999V10.65H20.0001V9.99999V9.34999H12.0001V9.99999ZM12.0001 7.99999V8.64999H19.0001V7.99999V7.34999H12.0001V7.99999ZM12.0001 16V16.65H19.0001V16V15.35H12.0001V16ZM12.0001 18V18.65H17.5001V18V17.35H12.0001V18ZM12.0001 5.99999V6.64999H17.5001V5.99999V5.34999H12.0001V5.99999ZM12.0001 4L12.0001 4.65L14.5001 4.64999L14.5001 3.99999L14.5001 3.34999L12.0001 3.35L12.0001 4ZM12.0001 20L12.0001 20.65L14.5001 20.65L14.5001 20L14.5001 19.35L12.0001 19.35L12.0001 20ZM20.4853 11.9705H19.8353C19.8353 16.2978 16.3274 19.8058 12.0001 19.8058V20.4558V21.1058C17.0453 21.1058 21.1353 17.0158 21.1353 11.9705H20.4853ZM12.0001 20.4558V19.8058C7.67275 19.8058 4.16477 16.2978 4.16477 11.9705H3.51477H2.86477C2.86477 17.0158 6.95478 21.1058 12.0001 21.1058V20.4558ZM3.51477 11.9705H4.16477C4.16477 7.64323 7.67275 4.13526 12.0001 4.13526V3.48526V2.83526C6.95478 2.83526 2.86477 6.92526 2.86477 11.9705H3.51477ZM12.0001 3.48526V4.13526C16.3274 4.13526 19.8353 7.64323 19.8353 11.9705H20.4853H21.1353C21.1353 6.92526 17.0453 2.83526 12.0001 2.83526V3.48526Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
3
packages/design-system/src/icons/image-outpaint.svg
Normal 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="M10.5 15H4.33333C3.59695 15 3 14.403 3 13.6667V4.33333C3 3.59695 3.59695 3 4.33333 3H13.6667C14.403 3 15 3.59695 15 4.33333V11L12.9427 8.94263C12.6926 8.69267 12.3536 8.55225 12 8.55225C11.6464 8.55225 11.3074 8.69267 11.0573 8.94263L5 15M18.3109 20.2074L12.9705 18.7508M15.4997 15.2586C14.5976 16.6137 13.5146 16.9887 12.208 17.2328C12.1646 17.2407 12.1241 17.2597 12.0904 17.2881C12.0567 17.3164 12.0309 17.3531 12.0157 17.3944C12.0004 17.4358 11.9962 17.4804 12.0035 17.5238C12.0107 17.5673 12.0291 17.6081 12.057 17.6423L15.7172 22.0841C15.7916 22.163 15.8896 22.2157 15.9964 22.2341C16.1033 22.2525 16.2133 22.2356 16.3098 22.1861C17.3673 21.4615 18.9999 19.6549 18.9999 18.7589M18 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V9M10 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V18M8.33333 7C8.33333 7.73638 7.73638 8.33333 7 8.33333C6.26362 8.33333 5.66667 7.73638 5.66667 7C5.66667 6.26362 6.26362 5.66667 7 5.66667C7.73638 5.66667 8.33333 6.26362 8.33333 7ZM20.188 12.5694C20.2866 12.4709 20.4036 12.3927 20.5324 12.3393C20.6611 12.286 20.7992 12.2585 20.9386 12.2585C21.078 12.2585 21.216 12.286 21.3448 12.3393C21.4735 12.3927 21.5905 12.4709 21.6891 12.5694C21.7877 12.668 21.8659 12.785 21.9192 12.9138C21.9725 13.0426 22 13.1806 22 13.32C22 13.4594 21.9725 13.5974 21.9192 13.7262C21.8659 13.855 21.7877 13.972 21.6891 14.0705L19.68 16.0802C19.6331 16.1271 19.6068 16.1906 19.6068 16.2569C19.6068 16.3232 19.6331 16.3868 19.68 16.4337L20.152 16.9057C20.378 17.1317 20.5049 17.4382 20.5049 17.7578C20.5049 18.0774 20.378 18.3838 20.152 18.6098L19.68 19.0819C19.6331 19.1287 19.5695 19.1551 19.5032 19.1551C19.4369 19.1551 19.3733 19.1287 19.3265 19.0819L15.1767 14.9326C15.1298 14.8857 15.1035 14.8221 15.1035 14.7558C15.1035 14.6895 15.1298 14.626 15.1767 14.5791L15.6487 14.107C15.8747 13.8811 16.1812 13.7541 16.5008 13.7541C16.8203 13.7541 17.1268 13.8811 17.3528 14.107L17.8249 14.5791C17.8717 14.6259 17.9353 14.6523 18.0016 14.6523C18.0679 14.6523 18.1315 14.6259 18.1784 14.5791L20.188 12.5694Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 14.9999L17.914 11.9139C17.5389 11.539 17.0303 11.3284 16.5 11.3284C15.9697 11.3284 15.4611 11.539 15.086 11.9139L12.6935 14.3064M14.5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3H5C3.89543 3 3 3.89543 3 5V13M11 9C11 10.1046 10.1046 11 9 11C7.89543 11 7 10.1046 7 9C7 7.89543 7.89543 7 9 7C10.1046 7 11 7.89543 11 9ZM2.03125 18.6735C1.98958 18.5613 1.98958 18.4378 2.03125 18.3255C2.43708 17.3415 3.12595 16.5001 4.01054 15.9081C4.89512 15.3161 5.93558 15 7 15C8.06442 15 9.10488 15.3161 9.98946 15.9081C10.874 16.5001 11.5629 17.3415 11.9687 18.3255C12.0104 18.4378 12.0104 18.5613 11.9687 18.6735C11.5629 19.6575 10.874 20.4989 9.98946 21.0909C9.10488 21.683 8.06442 21.999 7 21.999C5.93558 21.999 4.89512 21.683 4.01054 21.0909C3.12595 20.4989 2.43708 19.6575 2.03125 18.6735ZM8.49992 18.4995C8.49992 19.3278 7.82838 19.9994 6.99999 19.9994C6.17161 19.9994 5.50007 19.3278 5.50007 18.4995C5.50007 17.6711 6.17161 16.9995 6.99999 16.9995C7.82838 16.9995 8.49992 17.6711 8.49992 18.4995Z" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -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="M19 3C20.1046 3 21 3.89543 21 5M21 19C21 20.1046 20.1046 21 19 21H18C18 19.4087 17.3679 17.8826 16.2426 16.7574C15.1174 15.6321 13.5913 15 12 15C10.4087 15 8.88258 15.6321 7.75736 16.7574C6.63214 17.8826 6 19.4087 6 21H5C3.89543 21 3 20.1046 3 19M3 5C3 3.89543 3.89543 3 5 3M21 13.5V16M21 8V11M3 11V8M3 16V13.5M8 3H10.5M13.5 3H16M10.5 21H8.5M15.5 21H13.5M14.1213 12.1213C15.2929 10.9497 15.2929 9.05025 14.1213 7.87868C12.9497 6.70711 11.0503 6.70711 9.87868 7.87868C8.70711 9.05025 8.70711 10.9497 9.87868 12.1213C11.0503 13.2929 12.9497 13.2929 14.1213 12.1213Z" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19 3C20.1046 3 21 3.89543 21 5M21 19C21 20.1046 20.1046 21 19 21H18C18 19.4087 17.3679 17.8826 16.2426 16.7574C15.1174 15.6321 13.5913 15 12 15C10.4087 15 8.88258 15.6321 7.75736 16.7574C6.63214 17.8826 6 19.4087 6 21H5C3.89543 21 3 20.1046 3 19M3 5C3 3.89543 3.89543 3 5 3M21 13.5V16M21 8V11M3 11V8M3 16V13.5M8 3H10.5M13.5 3H16M10.5 21H8.5M15.5 21H13.5M14.1213 12.1213C15.2929 10.9497 15.2929 9.05025 14.1213 7.87868C12.9497 6.70711 11.0503 6.70711 9.87868 7.87868C8.70711 9.05025 8.70711 10.9497 9.87868 12.1213C11.0503 13.2929 12.9497 13.2929 14.1213 12.1213Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 760 B |
@@ -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 21M23 10L21 12M21 12L19 10M21 12V8C21 5.23858 18.7614 3 16 3H13M4.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"/>
|
||||
<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 21M19 10L21 12L23 10M21 12V8C21 5.23858 18.7614 3 16 3H13M4.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="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 717 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 3H21M21 3V8M21 3L17 7M18 21H19C19.5304 21 20.0391 20.7893 20.4142 20.4142C20.7893 20.0391 21 19.5304 21 19M21 12V15M3 6V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3M9 3H12M15 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 21M4.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: 868 B |
@@ -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="M10.3 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V11.914M17.9 11.9C17.5237 11.5312 17.017 11.3258 16.4901 11.3284C15.9632 11.331 15.4586 11.5415 15.086 11.914M6 21L10.543 16.457M11 9C11 10.1046 10.1046 11 9 11C7.89543 11 7 10.1046 7 9C7 7.89543 7.89543 7 9 7C10.1046 7 11 7.89543 11 9ZM13.0283 14.5459C12.9995 14.4735 12.9925 14.3943 13.0082 14.318C13.0239 14.2418 13.0616 14.1718 13.1167 14.1167C13.1718 14.0616 13.2418 14.0239 13.318 14.0082C13.3943 13.9925 13.4735 13.9995 13.5459 14.0283L20.7456 16.8282C20.8228 16.8584 20.8887 16.9118 20.9342 16.981C20.9798 17.0502 21.0027 17.1319 20.9998 17.2147C20.9969 17.2976 20.9683 17.3774 20.918 17.4433C20.8678 17.5092 20.7983 17.5579 20.7192 17.5826L17.9641 18.4369C17.8398 18.4754 17.7267 18.5435 17.6347 18.6355C17.5427 18.7275 17.4746 18.8406 17.4361 18.9649L16.5826 21.7192C16.5579 21.7983 16.5092 21.8678 16.4433 21.918C16.3774 21.9683 16.2976 21.9969 16.2147 21.9998C16.1319 22.0027 16.0502 21.9798 15.981 21.9342C15.9118 21.8887 15.8584 21.8228 15.8282 21.7456L13.0283 14.5459Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
packages/design-system/src/icons/image-shader.svg
Normal 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="M7.65908 19.0121L19.0121 7.65914M4.98779 16.3408L16.3408 4.98785M11.666 20.3478L20.3477 11.6661M3.65215 12.3339L12.3338 3.65221M18.0104 5.98959C21.3299 9.30905 21.3299 14.691 18.0104 18.0104C14.691 21.3299 9.30905 21.3299 5.98959 18.0104C2.67014 14.691 2.67014 9.30905 5.98959 5.98959C9.30905 2.67014 14.691 2.67014 18.0104 5.98959Z" stroke="white" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 505 B |
3
packages/design-system/src/icons/image-sharpen.svg
Normal 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="M7 21H11V3H7C4.79086 3 3 4.79086 3 7V17C3 19.2091 4.79086 21 7 21ZM7 21C7 21 7 19 8.5 17.5C10 16 11 16 11 16M11 1L11 23M21 15L16.5 10.5L14 13L15.086 11.914M14 3V21H21V3H14ZM11 9C11 10.1046 10.1046 11 9 11C7.89543 11 7 10.1046 7 9C7 7.89543 7.89543 7 9 7C10.1046 7 11 7.89543 11 9Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
3
packages/design-system/src/icons/image-to-3d.svg
Normal 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="M14 9.96658L11.9427 7.90924C11.6926 7.65928 11.3536 7.51886 11 7.51886C10.6464 7.51886 10.3074 7.65928 10.0573 7.90924L4 13.9666M5 18L7 20L5 22M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M16.6001 10.1608L20.8005 12.561C20.9827 12.6662 21.1341 12.8176 21.2394 12.9998C21.3448 13.182 21.4003 13.3887 21.4005 13.5991V18.3996C21.4003 18.61 21.3448 18.8167 21.2394 18.999C21.1341 19.1812 20.9827 19.3325 20.8005 19.4377L16.6001 21.8379C16.4176 21.9433 16.2107 21.9987 16 21.9987C15.7894 21.9987 15.5824 21.9433 15.4 21.8379L11.1995 19.4377C11.0173 19.3325 10.8659 19.1812 10.7606 18.999C10.6553 18.8167 10.5997 18.61 10.5995 18.3996V16.5M16 15.9994L21.2206 12.9991M16 15.9994L15 15.4247M16 15.9994L16 22M3.33333 1.96661H12.6667C13.403 1.96661 14 2.56357 14 3.29995V12.6333C14 13.3697 13.403 13.9666 12.6667 13.9666H3.33333C2.59695 13.9666 2 13.3697 2 12.6333V3.29995C2 2.56357 2.59695 1.96661 3.33333 1.96661ZM7.33333 5.96661C7.33333 6.70299 6.73638 7.29995 6 7.29995C5.26362 7.29995 4.66667 6.70299 4.66667 5.96661C4.66667 5.23023 5.26362 4.63328 6 4.63328C6.73638 4.63328 7.33333 5.23023 7.33333 5.96661Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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 10H17M14 9.96651L11.9427 7.90918C11.6926 7.65922 11.3536 7.5188 11 7.5188C10.6464 7.5188 10.3074 7.65922 10.0573 7.90918L4 13.9665M5 22L7 20M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M7 20L5 18M3.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.96655ZM7.33333 5.96655C7.33333 6.70293 6.73638 7.29989 6 7.29989C5.26362 7.29989 4.66667 6.70293 4.66667 5.96655C4.66667 5.23017 5.26362 4.63322 6 4.63322C6.73638 4.63322 7.33333 5.23017 7.33333 5.96655Z" stroke="#8A8A8A" 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 10H17M14 9.96651L11.9427 7.90918C11.6926 7.65922 11.3536 7.5188 11 7.5188C10.6464 7.5188 10.3074 7.65922 10.0573 7.90918L4 13.9665M5 18L7 20L5 22M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M3.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.96655ZM7.33333 5.96655C7.33333 6.70293 6.73638 7.29989 6 7.29989C5.26362 7.29989 4.66667 6.70293 4.66667 5.96655C4.66667 5.23017 5.26362 4.63322 6 4.63322C6.73638 4.63322 7.33333 5.23017 7.33333 5.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.0 KiB |
3
packages/design-system/src/icons/image-to-layers.svg
Normal 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="M15.6151 11.9999L20.5432 14.8147C20.6816 14.8931 20.7967 15.0068 20.8768 15.1443C20.9569 15.2817 20.9991 15.438 20.9991 15.597C20.9991 15.7561 20.9569 15.9124 20.8768 16.0498C20.7967 16.1872 20.6816 16.301 20.5432 16.3794L15.6151 19.2029M8.38492 11.9999L3.45687 14.8147C3.31847 14.8931 3.20335 15.0068 3.12326 15.1443C3.04317 15.2817 3.00098 15.438 3.00098 15.597C3.00098 15.7561 3.04317 15.9124 3.12326 16.0498C3.20335 16.1872 3.31847 16.301 3.45687 16.3794L4.6883 17.085M4.6883 17.085L11.1007 20.7589C11.3742 20.9168 11.6843 20.9999 12 20.9999C12.3157 20.9999 12.6259 20.9168 12.8993 20.7589L15.6151 19.2029M4.6883 17.085C4.6883 17.085 13.8671 15.4346 15.3747 15.9371C17.062 16.4996 15.6151 19.2029 15.6151 19.2029M12.8993 13.5646C12.6259 13.7224 12.3157 13.8055 12 13.8055C11.6843 13.8055 11.3742 13.7224 11.1007 13.5646L3.45687 9.18508C3.31847 9.10665 3.20335 8.99291 3.12326 8.85546C3.04317 8.71802 3.00098 8.56179 3.00098 8.40271C3.00098 8.24363 3.04317 8.0874 3.12326 7.94995C3.20335 7.81251 3.31847 7.69877 3.45687 7.62033L11.1007 3.24084C11.3742 3.08298 11.6843 2.99988 12 2.99988C12.3157 2.99988 12.6259 3.08298 12.8993 3.24084L20.5432 7.62033C20.6816 7.69877 20.7967 7.81251 20.8768 7.94995C20.9569 8.0874 20.9991 8.24363 20.9991 8.40271C20.9991 8.56179 20.9569 8.71802 20.8768 8.85546C20.7967 8.99291 20.6816 9.10665 20.5432 9.18508L12.8993 13.5646ZM12.6231 6.99949C12.1336 7.38197 11.292 7.45559 10.7435 7.16393C10.195 6.87227 10.1472 6.32577 10.6367 5.94329C11.1263 5.5608 11.9678 5.48718 12.5163 5.77884C13.0649 6.0705 13.1127 6.617 12.6231 6.99949Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -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 17.9999V20.6666C10 21.403 10.597 21.9999 11.3333 21.9999H20.6667C21.403 21.9999 22 21.403 22 20.6666V11.3333C22 10.5969 21.403 9.99994 20.6667 9.99994H18M14 16.9999V18.6666L18 15.9999L16.6579 15.1052M14 9.96651L11.9427 7.90918C11.6926 7.65922 11.3536 7.5188 11 7.5188C10.6464 7.5188 10.3074 7.65922 10.0573 7.90918L4 13.9665M5 21.9999L7 19.9999M7 19.9999H4C3.46957 19.9999 2.96086 19.7892 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 17.9999V16.9999M7 19.9999L5 17.9999M3.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.96655ZM7.33333 5.96655C7.33333 6.70293 6.73638 7.29989 6 7.29989C5.26362 7.29989 4.66667 6.70293 4.66667 5.96655C4.66667 5.23017 5.26362 4.63322 6 4.63322C6.73638 4.63322 7.33333 5.23017 7.33333 5.96655Z" stroke="#8A8A8A" 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.1053M14 9.96658L11.9427 7.90924C11.6926 7.65928 11.3536 7.51886 11 7.51886C10.6464 7.51886 10.3074 7.65928 10.0573 7.90924L4 13.9666M5 18L7 20L5 22M7 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V17M3.33333 1.96661H12.6667C13.403 1.96661 14 2.56357 14 3.29995V12.6333C14 13.3697 13.403 13.9666 12.6667 13.9666H3.33333C2.59695 13.9666 2 13.3697 2 12.6333V3.29995C2 2.56357 2.59695 1.96661 3.33333 1.96661ZM7.33333 5.96661C7.33333 6.70299 6.73638 7.29995 6 7.29995C5.26362 7.29995 4.66667 6.70299 4.66667 5.96661C4.66667 5.23023 5.26362 4.63328 6 4.63328C6.73638 4.63328 7.33333 5.23023 7.33333 5.96661Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 989 B |
3
packages/design-system/src/icons/image-upscale.svg
Normal 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="M21 8V3H16M21 3L17 7M18 21H19C19.5304 21 20.0391 20.7893 20.4142 20.4142C20.7893 20.0391 21 19.5304 21 19M21 12V15M3 6V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3M9 3H12M15 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 21M4.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="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 861 B |
3
packages/design-system/src/icons/image-vectorize.svg
Normal 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 17C5 13.8174 6.26428 10.7652 8.51472 8.51472C10.7652 6.26428 13.8174 5 17 5M5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17ZM17 5C17 6.10457 17.8954 7 19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5ZM12.034 12.681C11.998 12.5906 11.9892 12.4915 12.0088 12.3962C12.0285 12.3008 12.0756 12.2133 12.1445 12.1445C12.2133 12.0756 12.3008 12.0285 12.3962 12.0088C12.4915 11.9892 12.5906 11.998 12.681 12.034L21.681 15.534C21.7775 15.5717 21.8599 15.6384 21.9168 15.725C21.9737 15.8116 22.0023 15.9137 21.9987 16.0172C21.9951 16.1207 21.9594 16.2206 21.8966 16.3029C21.8338 16.3853 21.7469 16.4461 21.648 16.477L18.204 17.545C18.0486 17.593 17.9073 17.6783 17.7923 17.7933C17.6773 17.9083 17.592 18.0496 17.544 18.205L16.477 21.648C16.4461 21.7469 16.3853 21.8338 16.3029 21.8966C16.2206 21.9594 16.1207 21.9951 16.0172 21.9987C15.9137 22.0023 15.8116 21.9737 15.725 21.9168C15.6384 21.8599 15.5717 21.7775 15.534 21.681L12.034 12.681Z" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="48" height="24" viewBox="0 0 48 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.0936 7.58276L16.0996 15.7728C15.9586 15.9654 15.8911 16.2022 15.9095 16.4403C15.9278 16.6783 16.0307 16.902 16.1996 17.0708L17.0166 17.8888C17.1879 18.0599 17.4156 18.1631 17.6573 18.1791C17.8989 18.1951 18.1382 18.1228 18.3306 17.9758L26.1836 11.9818M27.5935 21.1557C26.5935 20.4817 25.4655 19.9817 24.0935 19.9817C22.0355 19.9817 20.1655 22.3377 18.0935 21.9817C16.0215 21.6257 15.3185 18.6127 16.5935 17.4817M32.0935 6.98169C32.0935 9.74311 29.8549 11.9817 27.0935 11.9817C24.3321 11.9817 22.0935 9.74311 22.0935 6.98169C22.0935 4.22027 24.3321 1.98169 27.0935 1.98169C29.8549 1.98169 32.0935 4.22027 32.0935 6.98169Z" stroke="#8A8A8A" stroke-width="1.95" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 824 B |
@@ -1,7 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.07572 3C8.28083 10.9427 13.8455 15.7083 21 14.914" stroke="#8A8A8A" stroke-width="1.3"/>
|
||||
<path d="M9.75 8.25L15 3" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M12 12L18.75 5.25" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="square"/>
|
||||
<path d="M15.75 14.25L21 9" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 679 B |
@@ -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="M11 21.7299L4 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.2699L11 2.2699C11.304 2.09437 11.6489 2.00195 12 2.00195C12.3511 2.00195 12.696 2.09437 13 2.2699L20 6.2699C20.3037 6.44526 20.556 6.69742 20.7315 7.00106C20.9071 7.30471 20.9996 7.64918 21 7.9999V15M3.30005 7L12 12M12 12L20.7001 7M12 12V16.5M14 19L17 22M17 22V16.5M17 22L20 19" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 21.7299L4 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.2699L11 2.2699C11.304 2.09437 11.6489 2.00195 12 2.00195C12.3511 2.00195 12.696 2.09437 13 2.2699L20 6.2699C20.3037 6.44526 20.556 6.69742 20.7315 7.00106C20.9071 7.30471 20.9996 7.64918 21 7.9999V15M3.30005 7L12 12M12 12L20.7001 7M12 12V16.5M20 19.5L17 16.5L14 19.5M17 16.5V22" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 677 B |
@@ -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="M2 10V13M6 6V17M10 3V21M14 8V14.5M18 5V12.5M22 10V13M14 19.5L17 16.5M17 16.5V22M17 16.5L20 19.5" stroke="#8A8A8A" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 10V13M6 6V17M10 3V21M14 8V14.5M18 5V12.5M22 10V13M20 19.5L17 16.5L14 19.5M17 16.5V22" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 284 B |