Compare commits
24 Commits
test/cover
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6c3ff1a54 | ||
|
|
9599a4e00a | ||
|
|
cc73baaf57 | ||
|
|
700fcb6bda | ||
|
|
30c07dc9ec | ||
|
|
d81f3a9278 | ||
|
|
bab2870131 | ||
|
|
e5cb244a2d | ||
|
|
c5b6fd9c40 | ||
|
|
0f0210e482 | ||
|
|
7d1d7c8315 | ||
|
|
739d4b6136 | ||
|
|
0d5f8161f3 | ||
|
|
7fc0c357da | ||
|
|
25ff147176 | ||
|
|
0efc0c4d72 | ||
|
|
d0e3b7ebf0 | ||
|
|
f2677aa598 | ||
|
|
fc9a1d6bfb | ||
|
|
bbb043c9cc | ||
|
|
ef59f46495 | ||
|
|
8e7e4d6faa | ||
|
|
975d5e5ec0 | ||
|
|
9522f68ae6 |
201
.github/workflows/ci-website-e2e.yaml
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
name: 'CI: Website E2E'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, website/*]
|
||||
paths:
|
||||
- 'apps/website/**'
|
||||
- 'packages/design-system/**'
|
||||
- 'packages/tailwind-utils/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
pull_request:
|
||||
branches-ignore: [wip/*, draft/*, temp/*]
|
||||
paths:
|
||||
- 'apps/website/**'
|
||||
- 'packages/design-system/**'
|
||||
- 'packages/tailwind-utils/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
website-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.58.1-noble
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Post starting comment
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- WEBSITE_E2E_STATUS -->'
|
||||
const body = [
|
||||
marker,
|
||||
'## 🌐 Website E2E',
|
||||
'',
|
||||
'> [!NOTE]',
|
||||
`> Tests are running… [View workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
|
||||
].join('\n')
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
...context.repo,
|
||||
issue_number: context.issue.number
|
||||
})
|
||||
const existing = comments.find(c => c.body.includes(marker))
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body })
|
||||
} else {
|
||||
await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body })
|
||||
}
|
||||
|
||||
- name: Install pnpm
|
||||
run: corepack enable && corepack prepare
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build website
|
||||
run: pnpm --filter @comfyorg/website build
|
||||
|
||||
- name: Run Playwright tests
|
||||
id: tests
|
||||
run: pnpm --filter @comfyorg/website test:e2e
|
||||
|
||||
- name: Upload test report
|
||||
uses: actions/upload-artifact@v6
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: website-playwright-report
|
||||
path: apps/website/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
- name: Deploy report to Cloudflare
|
||||
id: deploy
|
||||
if: always() && !cancelled()
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
HEAD_REF: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
BRANCH=$(echo "$HEAD_REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g;s/--*/-/g;s/^-\|-$//g')
|
||||
DEPLOY_OK=false
|
||||
for i in 1 2 3; do
|
||||
echo "Deployment attempt $i of 3..."
|
||||
OUTPUT=$(npx wrangler@^4.0.0 pages deploy apps/website/playwright-report \
|
||||
--project-name=comfyui-website-e2e \
|
||||
--branch="$BRANCH" 2>&1) && { DEPLOY_OK=true; break; } || echo "$OUTPUT"
|
||||
[ $i -lt 3 ] && sleep 10
|
||||
done
|
||||
echo "$OUTPUT"
|
||||
if [ "$DEPLOY_OK" != "true" ]; then
|
||||
echo "::error::All 3 deployment attempts failed"
|
||||
exit 1
|
||||
fi
|
||||
URL=$(echo "$OUTPUT" | grep -oE 'https://[a-zA-Z0-9.-]+\.pages\.dev\S*' | head -1)
|
||||
echo "url=${URL}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Categorize failures
|
||||
id: failures
|
||||
if: always() && !cancelled() && steps.tests.outcome != 'success'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs')
|
||||
const report = JSON.parse(fs.readFileSync('apps/website/results.json', 'utf8'))
|
||||
|
||||
function isFailed(t) { return t.status === 'unexpected' || t.status === 'flaky' }
|
||||
function isVisual(spec) {
|
||||
return spec.file?.includes('visual') ||
|
||||
spec.tests?.some(t => t.results?.some(r => r.error?.message?.includes('toHaveScreenshot')))
|
||||
}
|
||||
function specsOf(suite) {
|
||||
return [
|
||||
...(suite.specs || []),
|
||||
...(suite.suites || []).flatMap(specsOf)
|
||||
]
|
||||
}
|
||||
|
||||
// True: Visual
|
||||
// False: Other
|
||||
const failed = specsOf(report)
|
||||
.flatMap(spec => (spec.tests || [])
|
||||
.filter(isFailed)
|
||||
.map(() => isVisual(spec)))
|
||||
|
||||
const screenshotFailures = failed.filter(Boolean).length
|
||||
core.setOutput('screenshot', screenshotFailures)
|
||||
core.setOutput('other', failed.length - screenshotFailures)
|
||||
|
||||
- name: Post result comment and summary
|
||||
if: always() && !cancelled()
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const passed = '${{ steps.tests.outcome }}' === 'success'
|
||||
const reportUrl = '${{ steps.deploy.outputs.url }}'
|
||||
const screenshotFailures = parseInt('${{ steps.failures.outputs.screenshot }}') || 0
|
||||
const otherFailures = parseInt('${{ steps.failures.outputs.other }}') || 0
|
||||
|
||||
const lines = ['## 🌐 Website E2E', '']
|
||||
|
||||
if (passed) {
|
||||
lines.push('> [!TIP]', '> All tests passed.')
|
||||
} else {
|
||||
lines.push('> [!CAUTION]', '> Some tests failed.')
|
||||
}
|
||||
|
||||
// Results table
|
||||
const rows = [
|
||||
['Status', passed ? '✅ Passed' : '❌ Failed'],
|
||||
['Report', reportUrl ? `[View Report](${reportUrl})` : '_unavailable_']
|
||||
]
|
||||
if (!passed) {
|
||||
rows.push(
|
||||
['Screenshot diffs', String(screenshotFailures)],
|
||||
['Other failures', String(otherFailures)]
|
||||
)
|
||||
}
|
||||
lines.push(
|
||||
'',
|
||||
'| | |',
|
||||
'|---|---|',
|
||||
...rows.map(([k, v]) => `| **${k}** | ${v} |`)
|
||||
)
|
||||
|
||||
if (screenshotFailures > 0) {
|
||||
const s = screenshotFailures === 1 ? '' : 's'
|
||||
lines.push('', `- [ ] Update website screenshots (${screenshotFailures} screenshot diff${s})`)
|
||||
}
|
||||
if (otherFailures > 0) {
|
||||
lines.push(
|
||||
'',
|
||||
'> [!WARNING]',
|
||||
`> ${otherFailures} non-screenshot failure${otherFailures === 1 ? '' : 's'} — these require manual review.`
|
||||
)
|
||||
}
|
||||
|
||||
const summary = lines.join('\n')
|
||||
await core.summary.addRaw(summary).write()
|
||||
|
||||
if (context.eventName === 'pull_request' && !context.payload.pull_request.head.repo.fork) {
|
||||
const marker = '<!-- WEBSITE_E2E_STATUS -->'
|
||||
const body = `${marker}\n${summary}`
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
...context.repo,
|
||||
issue_number: context.issue.number
|
||||
})
|
||||
const existing = comments.find(c => c.body.includes(marker))
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body })
|
||||
} else {
|
||||
await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body })
|
||||
}
|
||||
}
|
||||
125
.github/workflows/pr-update-website-screenshots.yaml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
name: 'PR: Update Website Screenshots'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-screenshots:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.58.1-noble
|
||||
timeout-minutes: 15
|
||||
# Trigger: (1) label, (2) /slash-command, or (3) checkbox in E2E status comment
|
||||
if: >
|
||||
( github.event_name == 'pull_request' &&
|
||||
github.event.label.name == 'Update Website Screenshots' ) ||
|
||||
( github.event.issue.pull_request &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'
|
||||
) &&
|
||||
startsWith(github.event.comment.body, '/update-website-screenshots') ) ||
|
||||
( github.event.issue.pull_request &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.comment.user.login == 'github-actions[bot]' &&
|
||||
github.actor != 'github-actions[bot]' &&
|
||||
contains(github.event.comment.body, '<!-- WEBSITE_E2E_STATUS -->') &&
|
||||
contains(github.event.comment.body, '- [x] Update website screenshots') )
|
||||
steps:
|
||||
- name: Verify sender permissions
|
||||
if: >
|
||||
github.event_name == 'issue_comment' &&
|
||||
contains(github.event.comment.body, '<!-- WEBSITE_E2E_STATUS -->')
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
...context.repo,
|
||||
username: context.actor
|
||||
})
|
||||
if (!['admin', 'write'].includes(data.permission)) {
|
||||
core.setFailed(`User ${context.actor} does not have write access`)
|
||||
}
|
||||
|
||||
- name: Get PR info
|
||||
id: pr-info
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const prNumber = ${{ github.event.number || github.event.issue.number }}
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number: prNumber
|
||||
})
|
||||
core.setOutput('pr-number', prNumber)
|
||||
core.setOutput('branch', pr.head.ref)
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ steps.pr-info.outputs.branch }}
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
|
||||
- name: Install pnpm
|
||||
run: corepack enable && corepack prepare
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build website
|
||||
run: pnpm --filter @comfyorg/website build
|
||||
|
||||
- name: Update screenshots
|
||||
id: update-screenshots
|
||||
run: pnpm --filter @comfyorg/website test:visual:update
|
||||
continue-on-error: true
|
||||
|
||||
- name: Commit updated screenshots
|
||||
id: commit
|
||||
if: steps.update-screenshots.outcome == 'success'
|
||||
run: |
|
||||
git config --global --add safe.directory "$(pwd)"
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
|
||||
if [ -z "$(git status --porcelain=v1 --untracked-files=all -- apps/website/e2e/)" ]; then
|
||||
echo "No screenshot changes to commit"
|
||||
echo "has-changes=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has-changes=true" >> $GITHUB_OUTPUT
|
||||
git add apps/website/e2e/
|
||||
git commit -m "[automated] Update website screenshot expectations"
|
||||
git push origin ${{ steps.pr-info.outputs.branch }}
|
||||
|
||||
- name: Upload test report
|
||||
uses: actions/upload-artifact@v6
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: website-screenshot-update-report
|
||||
path: apps/website/playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
- name: Remove label
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
...context.repo,
|
||||
issue_number: ${{ steps.pr-info.outputs.pr-number }},
|
||||
name: 'Update Website Screenshots'
|
||||
})
|
||||
} catch (e) {
|
||||
// Label may already be removed
|
||||
}
|
||||
@@ -67,6 +67,7 @@
|
||||
"ignoreFiles": [
|
||||
"node_modules/**",
|
||||
"dist/**",
|
||||
"**/dist/**",
|
||||
"playwright-report/**",
|
||||
"public/**",
|
||||
"src/lib/litegraph/**"
|
||||
|
||||
4
.vscode/extensions.json
vendored
@@ -1,16 +1,12 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"antfu.vite",
|
||||
"austenc.tailwind-docs",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"donjayamanne.githistory",
|
||||
"eamodio.gitlens",
|
||||
"github.vscode-github-actions",
|
||||
"github.vscode-pull-request-github",
|
||||
"hbenl.vscode-test-explorer",
|
||||
"kisstkondoros.vscode-codemetrics",
|
||||
"lokalise.i18n-ally",
|
||||
"ms-playwright.playwright",
|
||||
"oxc.oxc-vscode",
|
||||
|
||||
@@ -312,7 +312,7 @@ When referencing Comfy-Org repos:
|
||||
- Instead use a semantic value from the `style.css` theme
|
||||
- e.g. `bg-node-component-surface`
|
||||
- NEVER use `:class="[]"` to merge class names
|
||||
- Always use `import { cn } from '@/utils/tailwindUtil'`
|
||||
- Always use `import { cn } from '@comfyorg/tailwind-utils'`
|
||||
- e.g. `<div :class="cn('text-node-component-header-icon', hasError && 'text-danger')" />`
|
||||
- Use `cn()` inline in the template when feasible instead of creating a `computed` to hold the value
|
||||
- NEVER use `!important` or the `!` important prefix for tailwind classes
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@comfyorg/comfyui-electron-types": "catalog:",
|
||||
"@comfyorg/shared-frontend-utils": "workspace:*",
|
||||
"@comfyorg/tailwind-utils": "workspace:*",
|
||||
"@primevue/core": "catalog:",
|
||||
"@primevue/themes": "catalog:",
|
||||
"@vueuse/core": "catalog:",
|
||||
|
||||
@@ -32,7 +32,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<button
|
||||
:class="
|
||||
cn(
|
||||
'hardware-option w-[170px] h-[190px] p-5 flex flex-col items-center rounded-3xl transition-all duration-200 bg-neutral-900/70 border-4',
|
||||
'hardware-option flex h-[190px] w-[170px] flex-col items-center rounded-3xl border-4 bg-neutral-900/70 p-5 transition-all duration-200',
|
||||
selected ? 'border-solid border-brand-yellow' : 'border-transparent'
|
||||
)
|
||||
"
|
||||
@@ -12,13 +12,13 @@
|
||||
>
|
||||
<!-- Icon/Logo Area - Rounded square container -->
|
||||
<div
|
||||
class="icon-container w-[110px] h-[110px] shrink-0 rounded-2xl bg-neutral-800 flex items-center justify-center overflow-hidden"
|
||||
class="icon-container flex h-[110px] w-[110px] shrink-0 items-center justify-center overflow-hidden rounded-2xl bg-neutral-800"
|
||||
>
|
||||
<img
|
||||
v-if="imagePath"
|
||||
:src="imagePath"
|
||||
:alt="placeholderText"
|
||||
class="w-full h-full object-cover"
|
||||
class="size-full object-cover"
|
||||
style="object-position: 57% center"
|
||||
draggable="false"
|
||||
/>
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Text Content -->
|
||||
<div v-if="subtitle" class="text-center mt-4">
|
||||
<div v-if="subtitle" class="mt-4 text-center">
|
||||
<div class="text-sm text-neutral-500">{{ subtitle }}</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
interface Props {
|
||||
imagePath?: string
|
||||
|
||||
@@ -64,7 +64,7 @@ import { computed } from 'vue'
|
||||
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { cn } from '@comfyorg/tailwind-utils'
|
||||
4
apps/website/.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
dist/
|
||||
.astro/
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
# Platform-specific Playwright snapshots (CI runs Linux)
|
||||
*-win32.png
|
||||
|
||||
@@ -6,9 +6,15 @@ import tailwindcss from '@tailwindcss/vite'
|
||||
export default defineConfig({
|
||||
site: 'https://comfy.org',
|
||||
output: 'static',
|
||||
devToolbar: { enabled: !process.env.NO_TOOLBAR },
|
||||
integrations: [vue(), sitemap()],
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
plugins: [tailwindcss()],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/playwright-report/**']
|
||||
}
|
||||
}
|
||||
},
|
||||
i18n: {
|
||||
locales: ['en', 'zh-CN'],
|
||||
|
||||
BIN
apps/website/e2e/assets/placeholder-1x1.webp
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
apps/website/e2e/assets/placeholder.webm
Normal file
@@ -1,4 +1,6 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
test.describe('Cloud page @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -41,13 +43,11 @@ test.describe('Cloud page @smoke', () => {
|
||||
test('AIModelsSection heading and 5 model cards are visible', async ({
|
||||
page
|
||||
}) => {
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /leading AI models/i })
|
||||
).toBeVisible()
|
||||
const heading = page.getByRole('heading', { name: /leading AI models/i })
|
||||
await expect(heading).toBeVisible()
|
||||
|
||||
const grid = page.locator('.grid', {
|
||||
has: page.getByText('Grok Imagine')
|
||||
})
|
||||
const section = heading.locator('xpath=ancestor::section')
|
||||
const grid = section.locator('.grid')
|
||||
const modelCards = grid.locator('a[href="https://comfy.org/workflows"]')
|
||||
await expect(modelCards).toHaveCount(5)
|
||||
})
|
||||
@@ -100,38 +100,44 @@ test.describe('Cloud FAQ accordion @interaction', () => {
|
||||
await page.goto('/cloud')
|
||||
})
|
||||
|
||||
test('all FAQs are expanded by default', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('clicking an expanded FAQ collapses it', async ({ page }) => {
|
||||
const firstQuestion = page.getByRole('button', {
|
||||
name: /What is Comfy Cloud/i
|
||||
})
|
||||
await firstQuestion.scrollIntoViewIfNeeded()
|
||||
await firstQuestion.click()
|
||||
|
||||
test('all FAQs are collapsed by default', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('clicking a collapsed FAQ expands it again', async ({ page }) => {
|
||||
test('clicking a collapsed FAQ expands it', async ({ page }) => {
|
||||
const firstQuestion = page.getByRole('button', {
|
||||
name: /What is Comfy Cloud/i
|
||||
})
|
||||
await firstQuestion.scrollIntoViewIfNeeded()
|
||||
|
||||
// Gate: wait for Vue hydration to bind aria-expanded
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
|
||||
await firstQuestion.click()
|
||||
await expect(
|
||||
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
|
||||
).toBeHidden()
|
||||
|
||||
await firstQuestion.click()
|
||||
await expect(
|
||||
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('clicking an expanded FAQ collapses it again', async ({ page }) => {
|
||||
const firstQuestion = page.getByRole('button', {
|
||||
name: /What is Comfy Cloud/i
|
||||
})
|
||||
await firstQuestion.scrollIntoViewIfNeeded()
|
||||
// Gate: wait for Vue hydration to bind aria-expanded
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
|
||||
|
||||
await firstQuestion.click()
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'true')
|
||||
await expect(
|
||||
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
|
||||
).toBeVisible()
|
||||
|
||||
await firstQuestion.click()
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
|
||||
await expect(
|
||||
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
|
||||
).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { devices, expect } from '@playwright/test'
|
||||
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
const WINDOWS_UA =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
||||
|
||||
test.describe('Download page @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -22,7 +27,11 @@ test.describe('Download page @smoke', () => {
|
||||
await expect(page.getByText(/The full ComfyUI engine/)).toBeVisible()
|
||||
})
|
||||
|
||||
test('HeroSection has download and GitHub buttons', async ({ page }) => {
|
||||
test('HeroSection has download and GitHub buttons', async ({ browser }) => {
|
||||
const context = await browser.newContext({ userAgent: WINDOWS_UA })
|
||||
const page = await context.newPage()
|
||||
await page.goto('/download')
|
||||
|
||||
const hero = page.locator('section', {
|
||||
has: page.getByRole('heading', {
|
||||
name: /Run on your hardware/i,
|
||||
@@ -39,6 +48,8 @@ test.describe('Download page @smoke', () => {
|
||||
'href',
|
||||
'https://github.com/Comfy-Org/ComfyUI'
|
||||
)
|
||||
|
||||
await context.close()
|
||||
})
|
||||
|
||||
test('ReasonSection heading and reasons are visible', async ({ page }) => {
|
||||
@@ -93,40 +104,46 @@ test.describe('FAQ accordion @interaction', () => {
|
||||
await page.goto('/download')
|
||||
})
|
||||
|
||||
test('all FAQs are expanded by default', async ({ page }) => {
|
||||
test('all FAQs are collapsed by default', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByText(/A dedicated GPU is strongly recommended/i)
|
||||
).toBeVisible()
|
||||
await expect(page.getByText(/ComfyUI is lightweight/i)).toBeVisible()
|
||||
).toBeHidden()
|
||||
await expect(page.getByText(/ComfyUI is lightweight/i)).toBeHidden()
|
||||
})
|
||||
|
||||
test('clicking an expanded FAQ collapses it', async ({ page }) => {
|
||||
test('clicking a collapsed FAQ expands it', async ({ page }) => {
|
||||
const firstQuestion = page.getByRole('button', {
|
||||
name: /Do I need a GPU/i
|
||||
})
|
||||
await firstQuestion.scrollIntoViewIfNeeded()
|
||||
// Gate: wait for Vue hydration to bind aria-expanded
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
|
||||
await firstQuestion.click()
|
||||
|
||||
await expect(
|
||||
page.getByText(/A dedicated GPU is strongly recommended/i)
|
||||
).toBeHidden()
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('clicking a collapsed FAQ expands it again', async ({ page }) => {
|
||||
test('clicking an expanded FAQ collapses it again', async ({ page }) => {
|
||||
const firstQuestion = page.getByRole('button', {
|
||||
name: /Do I need a GPU/i
|
||||
})
|
||||
await firstQuestion.scrollIntoViewIfNeeded()
|
||||
// Gate: wait for Vue hydration to bind aria-expanded
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
|
||||
|
||||
await firstQuestion.click()
|
||||
await expect(
|
||||
page.getByText(/A dedicated GPU is strongly recommended/i)
|
||||
).toBeHidden()
|
||||
|
||||
await firstQuestion.click()
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'true')
|
||||
await expect(
|
||||
page.getByText(/A dedicated GPU is strongly recommended/i)
|
||||
).toBeVisible()
|
||||
|
||||
await firstQuestion.click()
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
|
||||
await expect(
|
||||
page.getByText(/A dedicated GPU is strongly recommended/i)
|
||||
).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -145,7 +162,14 @@ test.describe('Download page mobile @mobile', () => {
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('download buttons are stacked vertically', async ({ page }) => {
|
||||
test('download buttons are stacked vertically', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
...devices['Pixel 5'],
|
||||
userAgent: WINDOWS_UA
|
||||
})
|
||||
const page = await context.newPage()
|
||||
await page.goto('/download')
|
||||
|
||||
const hero = page.locator('section', {
|
||||
has: page.getByRole('heading', {
|
||||
name: /Run on your hardware/i,
|
||||
@@ -155,13 +179,18 @@ test.describe('Download page mobile @mobile', () => {
|
||||
const downloadBtn = hero.getByRole('link', { name: /DOWNLOAD LOCAL/i })
|
||||
const githubBtn = hero.getByRole('link', { name: /INSTALL FROM GITHUB/i })
|
||||
|
||||
await downloadBtn.scrollIntoViewIfNeeded()
|
||||
await expect(downloadBtn).toBeVisible()
|
||||
await expect(githubBtn).toBeVisible()
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const downloadBox = await downloadBtn.boundingBox()
|
||||
const githubBox = await githubBtn.boundingBox()
|
||||
if (!downloadBox || !githubBox) return false
|
||||
return githubBox.y > downloadBox.y
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
expect(downloadBox, 'download button bounding box').not.toBeNull()
|
||||
expect(githubBox, 'github button bounding box').not.toBeNull()
|
||||
expect(githubBox!.y).toBeGreaterThan(downloadBox!.y)
|
||||
await context.close()
|
||||
})
|
||||
})
|
||||
|
||||
47
apps/website/e2e/fixtures/blockExternalMedia.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import type { Route } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
function assetPath(relativePath: string) {
|
||||
return fileURLToPath(new URL(relativePath, import.meta.url))
|
||||
}
|
||||
|
||||
const IMAGE_PLACEHOLDER = assetPath('../assets/placeholder-1x1.webp')
|
||||
const VIDEO_PLACEHOLDER = assetPath('../assets/placeholder.webm')
|
||||
|
||||
const ANALYTICS_PATTERN = '**/va.vercel-scripts.com/**' as const
|
||||
const MEDIA_PATTERN =
|
||||
/^https:\/\/media\.comfy\.org\/.*\.(webp|webm|mp4|png|jpg|jpeg|vtt)(\?.*)?$/i
|
||||
const VIDEO_PATTERN = /\.(webm|mp4)(\?|$)/i
|
||||
const SUBTITLE_PATTERN = /\.vtt(\?|$)/i
|
||||
|
||||
function blockAnalytics(route: Route) {
|
||||
return route.abort('blockedbyclient')
|
||||
}
|
||||
|
||||
async function fulfillMedia(route: Route) {
|
||||
const url = route.request().url()
|
||||
if (VIDEO_PATTERN.test(url))
|
||||
return route.fulfill({ path: VIDEO_PLACEHOLDER, status: 200 })
|
||||
|
||||
if (SUBTITLE_PATTERN.test(url))
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/vtt',
|
||||
body: 'WEBVTT\n'
|
||||
})
|
||||
|
||||
await route.fulfill({ path: IMAGE_PLACEHOLDER, status: 200 })
|
||||
}
|
||||
|
||||
export const test = base.extend<{ blockExternalMedia: void }>({
|
||||
blockExternalMedia: [
|
||||
async ({ page }, use) => {
|
||||
await page.route(ANALYTICS_PATTERN, blockAnalytics)
|
||||
await page.route(MEDIA_PATTERN, fulfillMedia)
|
||||
await use()
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
})
|
||||
@@ -1,4 +1,15 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
const caseStudyVideoPath = fileURLToPath(
|
||||
new URL(
|
||||
'../../../public/assets/images/cloud-subscription.webm',
|
||||
import.meta.url
|
||||
)
|
||||
)
|
||||
|
||||
test.describe('Homepage @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -83,17 +94,56 @@ test.describe('Product showcase accordion @interaction', () => {
|
||||
.first()
|
||||
|
||||
await secondFeature.scrollIntoViewIfNeeded()
|
||||
|
||||
await expect(async () => {
|
||||
await secondFeature.click()
|
||||
await expect(
|
||||
page.getByText(/If you are new to ComfyUI/).first()
|
||||
).toBeVisible({ timeout: 1000 })
|
||||
}).toPass({ timeout: 10000 })
|
||||
|
||||
await expect(
|
||||
page.getByText(/Build powerful AI pipelines by connecting nodes/).first()
|
||||
).toBeHidden()
|
||||
secondFeature.getByText(/If you are new to ComfyUI/)
|
||||
).toBeVisible()
|
||||
|
||||
const firstFeature = page
|
||||
.getByRole('button', { name: /Full Control with Nodes/i })
|
||||
.first()
|
||||
|
||||
await expect(firstFeature).not.toHaveClass(/bg-primary-comfy-yellow/)
|
||||
await expect(secondFeature).toHaveClass(/bg-primary-comfy-yellow/)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Video player @interaction', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.route(
|
||||
'https://media.comfy.org/website/customers/blackmath/video.webm',
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
contentType: 'video/webm',
|
||||
path: caseStudyVideoPath
|
||||
})
|
||||
)
|
||||
|
||||
await page.goto('/')
|
||||
})
|
||||
|
||||
test('clicking play advances playback', async ({ page }) => {
|
||||
const section = page.locator('section', {
|
||||
has: page.getByText('Customer Stories')
|
||||
})
|
||||
const video = section.locator('video')
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
video.evaluate((element: HTMLVideoElement) => element.duration),
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBeGreaterThan(0)
|
||||
|
||||
await section.getByRole('button', { name: 'Play' }).click()
|
||||
|
||||
await expect
|
||||
.poll(async () =>
|
||||
video.evaluate((element: HTMLVideoElement) => element.currentTime)
|
||||
)
|
||||
.toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -125,6 +175,6 @@ test.describe('Get started section links @smoke', () => {
|
||||
|
||||
const cloudLink = section.getByRole('link', { name: 'Launch Cloud' })
|
||||
await expect(cloudLink).toBeVisible()
|
||||
await expect(cloudLink).toHaveAttribute('href', 'https://app.comfy.org')
|
||||
await expect(cloudLink).toHaveAttribute('href', 'https://cloud.comfy.org')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
test.describe('Desktop navigation @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
test.describe('Desktop layout @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
||||
6
apps/website/e2e/viewports.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const VIEWPORTS = [
|
||||
{ name: '1-sm', width: 393, height: 851 },
|
||||
{ name: '2-md', width: 768, height: 1024 },
|
||||
{ name: '3-lg', width: 1280, height: 800 },
|
||||
{ name: '4-xl', width: 1536, height: 864 }
|
||||
] as const
|
||||
159
apps/website/e2e/visual-responsive.spec.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
import { VIEWPORTS } from './viewports'
|
||||
|
||||
test.describe.configure({ timeout: 60_000 })
|
||||
|
||||
const SMALL_VIEWPORTS = VIEWPORTS.filter(
|
||||
(v) => v.name === '1-sm' || v.name === '2-md'
|
||||
)
|
||||
|
||||
async function assertNoOverflow(page: Page) {
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
document.documentElement.scrollWidth >
|
||||
document.documentElement.clientWidth
|
||||
),
|
||||
{ message: 'page has horizontal overflow', timeout: 5_000 }
|
||||
)
|
||||
.toBe(false)
|
||||
}
|
||||
|
||||
async function navigateAndSettle(page: Page, url: string) {
|
||||
await page.goto(url)
|
||||
await page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
test.describe('Home', { tag: '@visual' }, () => {
|
||||
for (const vp of VIEWPORTS) {
|
||||
test.describe(vp.name, () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: vp.width, height: vp.height })
|
||||
await navigateAndSettle(page, '/')
|
||||
})
|
||||
|
||||
test('product-cards screenshot', async ({ page }) => {
|
||||
const section = page.locator('section', {
|
||||
has: page.getByRole('heading', { name: /The AI creation/i })
|
||||
})
|
||||
await expect(section).toBeVisible()
|
||||
await section.scrollIntoViewIfNeeded()
|
||||
await expect(page).toHaveScreenshot(`home-product-cards-${vp.name}.png`)
|
||||
})
|
||||
|
||||
test('get-started screenshot', async ({ page }) => {
|
||||
const section = page.locator('section', {
|
||||
has: page.getByRole('heading', { name: /Get started/i })
|
||||
})
|
||||
await expect(section).toBeVisible()
|
||||
await section.scrollIntoViewIfNeeded()
|
||||
await expect(page).toHaveScreenshot(`home-get-started-${vp.name}.png`)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('Pricing', { tag: '@visual' }, () => {
|
||||
for (const vp of VIEWPORTS) {
|
||||
test(`pricing-tiers-${vp.name}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: vp.width, height: vp.height })
|
||||
await navigateAndSettle(page, '/cloud/pricing')
|
||||
await assertNoOverflow(page)
|
||||
|
||||
const section = page.locator('section', {
|
||||
has: page.getByRole('heading', { name: /Pricing/i })
|
||||
})
|
||||
await expect(section).toBeVisible()
|
||||
await section.scrollIntoViewIfNeeded()
|
||||
await expect(page).toHaveScreenshot(`pricing-tiers-${vp.name}.png`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('Contact', { tag: '@visual' }, () => {
|
||||
for (const vp of SMALL_VIEWPORTS) {
|
||||
test(`form-${vp.name}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: vp.width, height: vp.height })
|
||||
await navigateAndSettle(page, '/contact')
|
||||
|
||||
const section = page.locator('section', {
|
||||
has: page.getByRole('heading', { name: /Create powerful workflows/i })
|
||||
})
|
||||
await expect(section).toBeVisible()
|
||||
await section.scrollIntoViewIfNeeded()
|
||||
await expect(page).toHaveScreenshot(`contact-form-${vp.name}.png`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('Gallery', { tag: '@visual' }, () => {
|
||||
for (const vp of SMALL_VIEWPORTS) {
|
||||
test(`gallery-grid-${vp.name}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: vp.width, height: vp.height })
|
||||
await navigateAndSettle(page, '/gallery')
|
||||
|
||||
const section = page.getByTestId('gallery-grid')
|
||||
await expect(section).toBeVisible()
|
||||
await section.scrollIntoViewIfNeeded()
|
||||
await expect(page).toHaveScreenshot(`gallery-grid-${vp.name}.png`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('About', { tag: '@visual' }, () => {
|
||||
for (const vp of SMALL_VIEWPORTS) {
|
||||
test(`hero-${vp.name}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: vp.width, height: vp.height })
|
||||
await navigateAndSettle(page, '/about')
|
||||
|
||||
const hero = page.locator('section', {
|
||||
has: page.getByRole('heading', { name: /Build the tools/i })
|
||||
})
|
||||
await expect(hero).toBeVisible()
|
||||
await hero.scrollIntoViewIfNeeded()
|
||||
await expect(page).toHaveScreenshot(`about-hero-${vp.name}.png`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const OVERFLOW_SKIP = new Set([
|
||||
'/ 3-lg',
|
||||
'/ 4-xl',
|
||||
'/cloud 2-md',
|
||||
'/cloud 3-lg',
|
||||
'/cloud 4-xl',
|
||||
'/download 2-md',
|
||||
'/download 3-lg',
|
||||
'/download 4-xl'
|
||||
])
|
||||
|
||||
test.describe('Overflow guards', { tag: '@visual' }, () => {
|
||||
const pages = [
|
||||
'/',
|
||||
'/cloud',
|
||||
'/cloud/pricing',
|
||||
'/contact',
|
||||
'/download',
|
||||
'/gallery',
|
||||
'/about',
|
||||
'/careers'
|
||||
]
|
||||
for (const url of pages) {
|
||||
for (const vp of VIEWPORTS) {
|
||||
const key = `${url} ${vp.name}`
|
||||
|
||||
test(`${url} ${vp.name} no overflow`, async ({ page }) => {
|
||||
test.skip(OVERFLOW_SKIP.has(key), 'Known overflow bug at this viewport')
|
||||
await page.setViewportSize({ width: vp.width, height: vp.height })
|
||||
await page.goto(url)
|
||||
await assertNoOverflow(page)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 103 KiB |
@@ -5,17 +5,22 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:no-toolbar": "cross-env NO_TOOLBAR=1 astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"typecheck": "astro check",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:local": "PLAYWRIGHT_LOCAL=1 playwright test"
|
||||
"test:e2e:local": "cross-env PLAYWRIGHT_LOCAL=1 playwright test",
|
||||
"test:visual": "playwright test --project visual",
|
||||
"test:visual:update": "playwright test --project visual --update-snapshots"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "catalog:",
|
||||
"@comfyorg/design-system": "workspace:*",
|
||||
"@comfyorg/tailwind-utils": "workspace:*",
|
||||
"@vercel/analytics": "catalog:",
|
||||
"@vueuse/core": "catalog:",
|
||||
"cva": "catalog:",
|
||||
"gsap": "catalog:",
|
||||
"lenis": "catalog:",
|
||||
"vue": "catalog:"
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test'
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
const maybeLocalOptions: PlaywrightTestConfig = process.env.PLAYWRIGHT_LOCAL
|
||||
? {
|
||||
timeout: 30_000,
|
||||
retries: 0,
|
||||
workers: 1,
|
||||
use: {
|
||||
baseURL: 'http://localhost:4321',
|
||||
trace: 'on',
|
||||
video: 'on'
|
||||
}
|
||||
}
|
||||
: {
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
use: {
|
||||
baseURL: 'http://localhost:4321',
|
||||
trace: 'on-first-retry'
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: 'http://localhost:4321',
|
||||
trace: process.env.CI ? 'on-first-retry' : 'on'
|
||||
reporter: process.env.CI
|
||||
? [['html'], ['json', { outputFile: 'results.json' }]]
|
||||
: 'html',
|
||||
expect: {
|
||||
toHaveScreenshot: { maxDiffPixels: 50 }
|
||||
},
|
||||
...maybeLocalOptions,
|
||||
webServer: {
|
||||
command: 'pnpm preview',
|
||||
port: 4321,
|
||||
@@ -19,12 +40,18 @@ export default defineConfig({
|
||||
{
|
||||
name: 'desktop',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
grepInvert: /@mobile/
|
||||
grepInvert: /@mobile|@visual/
|
||||
},
|
||||
{
|
||||
name: 'mobile',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
grep: /@mobile/
|
||||
},
|
||||
{
|
||||
name: 'visual',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
grep: /@visual/,
|
||||
fullyParallel: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
BIN
apps/website/public/fonts/PPFormula-NarrowSemibold.woff2
Normal file
3
apps/website/public/icons/check.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 11.5811L10.2582 18.0581L20 6.05811" stroke="#F2FF59" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 234 B |
11
apps/website/public/icons/clock.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2871_8492)">
|
||||
<path d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="#F2FF59" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 6.75V12H17.25" stroke="#F2FF59" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2871_8492">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 567 B |
19
apps/website/public/icons/investors/abstract.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 595.3 130">
|
||||
<!-- Generator: Adobe Illustrator 28.7.1, SVG Export Plug-In . SVG Version: 1.2.0 Build 142) -->
|
||||
<g>
|
||||
<g id="Layer_1">
|
||||
<g id="Group_458" fill="#C2BFB9">
|
||||
<path id="Path_3625" d="M16.4,109.7c4.4,0,8.6-.3,12.8-11L62.4,16.5h9.6l31.8,79.8c4.1,10.2,7.6,13.5,13,13.5v6.9h-36.7v-6.4c9-.5,12.8-3.5,9.2-13l-6.3-16.2h-36.8l-6,15.4c-3.4,8.7-1.8,13.8,7.6,13.8v6.4h-31.5v-6.9ZM79.8,72.6l-15.1-39.6-15.3,39.6h30.4Z"/>
|
||||
<path id="Path_3626" d="M126,113.8V26.4l-11.8-2.1v-4.9l20.8-8.3h3.7v47.2c7.8-9.3,13.9-13,22.8-13,16.7,0,28,13.1,28,32.7s-13.9,40.2-38.5,40.2c-8.5-.1-16.9-1.6-24.9-4.4M155.1,111.6c12.2,0,20.5-11.8,20.5-28.7s-7.5-27.1-20.6-27.1c-6.4,0-12.4,3-16.2,8.1v31.5c-.4,8.6,6.3,15.8,14.8,16.2.5,0,1,0,1.5,0"/>
|
||||
<path id="Path_3627" d="M209,110.9l-7.9,6.7h-2.8l-1.7-25.2h6.1c5.8,12.8,13.8,18.3,23.7,18.3s12.8-3.7,12.8-10.2-2.1-8.3-9.6-11.2l-12.4-4.7c-11.9-4.6-17.7-10.4-17.7-19.7s10.1-19.7,22.3-19.7c6-.2,11.8,2.3,15.7,6.9l7.9-6.4h2.6v22.6h-6.1c-5-12.5-11.2-15.6-18.2-15.6s-12.5,4-12.5,9.6,2.6,7.5,9.3,9.9l13.6,4.9c11,4,17,10.4,17,20s-9.2,21.1-23.2,21.1c-7,0-13.8-2.6-18.9-7.3"/>
|
||||
<path id="Path_3628" d="M271.6,97.6l.5-41.4h-11.9v-8.4h7.9l13.3-16.7h2.9v16.7h21.9v8.4h-21.9v38.5c0,8.7,4,13.3,11.6,13.3,4.4-.1,8.6-1.9,11.8-4.9l2.3,2.7c-3.8,7.1-10.9,11.7-18.9,12.2-11.6,0-19.6-6.3-19.4-20.5"/>
|
||||
<path id="Path_3629" d="M327.4,100.5v-39l-10.4-2.1v-4.6l18.2-8.3h4.9v13.1c9.6-10.7,14.7-13.6,19.9-13.6,4.2-.4,7.9,2.7,8.2,6.9,0,.2,0,.4,0,.6-.3,4.1-2.7,7.8-6.4,9.6l-11-6.3c-4.1,1.9-7.7,4.7-10.7,8.1v37c0,6.7,2.1,8.4,10.7,8.4h2.3v6.1h-36.8v-6.1c8.9,0,11.2-1.7,11.2-9.9"/>
|
||||
<path id="Path_3630" d="M409.6,106.5c-6.6,8.6-12.5,11.8-19.3,11.8-8.2.4-15.2-6-15.6-14.2,0-.5,0-.9,0-1.4,0-9,5.7-14.4,16.7-18.8l18-7v-6c0-9.3-4.4-14.8-13.1-14.8s-11.6,3.1-17.6,11.2l-5-3.4c8.6-13.9,18-18.5,27.4-18.5s21.1,8.9,21.1,23.7v33.3c0,3.5,1.2,6,5.2,6,2.6-.1,5-1.4,6.6-3.4l.9,3.8c-5.3,7.2-9,9.5-13.6,9.5-6.4-.2-11.5-5.4-11.6-11.8M396.6,107.9c4.8-.2,9.4-2.2,12.8-5.7v-17.3l-11.5,2.6c-7.3,1.8-10.7,6-10.7,11.6-.2,4.7,3.5,8.6,8.2,8.8.4,0,.7,0,1.1,0"/>
|
||||
<path id="Path_3631" d="M436.7,84.5c-.3-21.1,16.6-38.5,37.7-38.8,0,0,0,0,0,0,11.6,0,18.9,5,18.9,11.2-.5,4.3-4,7.6-8.3,7.8l-13.1-12.7c-12.4,1.5-22.5,12.1-22.5,28.3s7,26.1,23.2,26.1c8.7.3,16.7-4.7,20.3-12.5l3.8,3.2c-6.7,15.3-17.6,21.3-29.5,21.3-18.3,0-30.7-15.6-30.7-33.8"/>
|
||||
<path id="Path_3632" d="M513.3,97.6l.5-41.4h-11.9v-8.4h7.9l13.3-16.7h2.9v16.7h21.9v8.4h-21.9v38.5c0,8.7,4,13.3,11.6,13.3,4.4-.1,8.6-1.9,11.8-4.9l2.3,2.7c-3.8,7.1-10.9,11.7-18.9,12.2-11.6,0-19.6-6.3-19.4-20.5"/>
|
||||
<path id="Path_3633" d="M562.1,110.5c0-4.6,3.8-8.4,8.4-8.4,4.6,0,8.4,3.8,8.4,8.4,0,4.6-3.7,8.3-8.3,8.4-4.6.1-8.4-3.5-8.6-8.1,0-.1,0-.2,0-.3"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1,18 +0,0 @@
|
||||
<svg width="114" height="23" viewBox="0 0 114 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M59.7261 19.7973C59.7261 21.2999 60.3252 22.0063 61.7621 22.0063C63.1991 22.0063 63.7905 21.2999 63.7905 19.7973V16.8824H64.7098V19.5511C64.7098 21.9979 63.6425 22.7617 61.7621 22.7617C59.8817 22.7617 58.8063 21.9979 58.8063 19.5511V16.8824H59.7261V19.7973Z" fill="#C2BFB9"/>
|
||||
<path d="M88.2206 16.7512C90.0927 16.7513 91.3324 17.5068 91.349 18.6973H90.4045C90.4127 17.9584 89.5177 17.5146 88.2696 17.5146C86.9233 17.5146 86.2004 17.835 86.2004 18.4097C86.2004 19.8713 91.4883 18.3443 91.4883 20.98C91.4881 22.154 90.1824 22.7617 88.2943 22.7617C86.3566 22.7616 85.125 21.9733 85.1494 20.8155H86.0853C86.0771 21.5463 86.9724 21.9978 88.2287 21.9978C89.7391 21.9896 90.5603 21.6778 90.5604 21.0209C90.5604 19.3295 85.2561 20.9633 85.2558 18.5167C85.2558 17.359 86.4717 16.7512 88.2206 16.7512Z" fill="#C2BFB9"/>
|
||||
<path d="M25.8672 22.3676L28.6832 16.8824H29.8165L26.7865 22.63H24.9393L21.9093 16.8824H23.0426L25.8672 22.3676Z" fill="#C2BFB9"/>
|
||||
<path d="M37.4926 17.6463H33.174V19.3624H37.4189V20.0602H33.174V21.8666H37.4926V22.63H32.2541V16.8824H37.4926V17.6463Z" fill="#C2BFB9"/>
|
||||
<path d="M46.0695 21.5134V16.8824H46.9889V22.63H46.0448L41.4633 18.0076V22.63H40.5435V16.8824H41.4794L46.0695 21.5134Z" fill="#C2BFB9"/>
|
||||
<path d="M56.2693 17.6463H53.3463V22.63H52.427V17.6463H49.5121V16.8824H56.2693V17.6463Z" fill="#C2BFB9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M72.3161 16.8824C73.5067 16.8824 74.1638 17.5231 74.1638 18.4017C74.1637 19.3212 73.4244 19.8468 72.3161 19.8468H71.7081C73.0301 19.937 73.5643 20.758 74.5821 22.63H73.5805C72.5131 20.7169 72.2256 20.1258 71.1338 20.1258H68.7694L68.777 22.63H67.8577V16.8824H72.3161ZM68.7694 19.3376H72.0285C72.8987 19.3376 73.2439 19.0175 73.244 18.5495C73.244 17.9255 72.8827 17.6544 72.0285 17.6544H68.7608L68.7694 19.3376Z" fill="#C2BFB9"/>
|
||||
<path d="M82.4683 17.6463H78.1492V19.3624H82.3942V20.0602H78.1492V21.8666H82.4683V22.63H77.2299V16.8824H82.4683V17.6463Z" fill="#C2BFB9"/>
|
||||
<path d="M6.82474 0C10.6948 4.35571e-05 12.5148 1.24639 12.5148 1.24639V3.24338C12.5148 3.24338 10.6948 1.65668 6.9683 1.65663C4.07267 1.65663 2.17477 2.33495 2.17477 3.61749C2.17519 7.145 13.5522 3.35015 13.5525 9.15639C13.5525 11.688 10.9865 13.041 6.95119 13.041C2.69336 13.041 0.0167388 11.2525 0 11.2413V9.33892C0 9.33892 3.01305 11.2367 6.75677 11.2385C9.99295 11.2385 11.5455 10.7091 11.5455 9.2486C11.5453 5.47073 0.14838 9.46271 0.148312 3.86753C0.148312 1.33587 3.03945 0 6.82474 0Z" fill="#C2BFB9"/>
|
||||
<path d="M113.4 1.95088H103.178V5.3516H111.851V7.061H103.178V11.1101H113.4V12.7677H101.186V0.293297H113.4V1.95088Z" fill="#C2BFB9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.5959 0.293297C44.1736 0.293325 45.7748 1.89222 45.7748 3.82142C45.7721 6.16157 43.9928 7.33195 41.5931 7.33195H35.2204V12.7667H33.2476V0.293297H41.5959ZM35.2175 5.62256H41.1476C42.9965 5.62256 43.7788 5.08577 43.7788 3.80954C43.7796 2.53432 42.9973 1.96799 41.1476 1.96799H35.2175V5.62256Z" fill="#C2BFB9"/>
|
||||
<path d="M65.2223 5.34162H74.1286V0.284741H76.1208V12.7577H74.1286V6.99778H65.2223V12.7577H63.2295V0.284741H65.2223V5.34162Z" fill="#C2BFB9"/>
|
||||
<path d="M81.6388 12.7577H79.6466V0.284741H81.6388V12.7577Z" fill="#C2BFB9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M94.0372 0.270955C96.6238 0.270955 98.0511 1.66293 98.0511 3.57138C98.051 5.56913 96.3736 6.71066 94.0372 6.71066L92.7171 6.72872V6.7639C95.589 6.95987 96.7305 8.69061 98.9424 12.7577H96.7125C94.3941 8.60131 93.5165 7.31681 91.1617 7.31674H87.1444L87.1611 12.7577H85.1627V0.270955H94.0372ZM87.1444 5.6045H93.4135C95.304 5.60445 96.0536 4.90875 96.0536 3.89177C96.0536 2.53619 95.2684 1.94711 93.4135 1.94707H87.1264L87.1444 5.6045Z" fill="#C2BFB9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.5869 0.284741C59.1648 0.284762 60.7658 1.88318 60.7658 3.81239C60.7633 6.15432 58.9847 7.32241 56.584 7.32245H50.2113V12.7568H48.2386V0.284741H56.5869ZM50.2113 5.61495H56.1415C57.9902 5.61495 58.7726 5.07773 58.7726 3.80241C58.7726 2.52713 57.9902 1.96087 56.1415 1.96086H50.2113V5.61495Z" fill="#C2BFB9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.3638 12.7506H29.1187L27.2182 9.18919H18.8124L16.9076 12.7506H14.6458L17.5712 7.56773V7.55632H17.5783L21.6874 0.284741H24.3399L31.3638 12.7506ZM19.6847 7.55632H26.3468L23.0203 1.31675L19.6847 7.55632Z" fill="#C2BFB9"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 648 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 463 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
@@ -1,4 +0,0 @@
|
||||
WEBVTT
|
||||
|
||||
00:00:00.000 --> 00:00:06.000
|
||||
AI-generated video showcasing Grok Imagine image generation capabilities
|
||||
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 128 KiB |
@@ -1,4 +0,0 @@
|
||||
WEBVTT
|
||||
|
||||
00:00:00.000 --> 00:00:05.000
|
||||
AI-generated video showcasing Seedance 2.0 video generation capabilities
|
||||
@@ -1,4 +0,0 @@
|
||||
WEBVTT
|
||||
|
||||
00:00:00.000 --> 00:00:05.000
|
||||
AI-generated video showcasing Wan 2.2 image-to-video generation capabilities
|
||||
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 124 KiB |
@@ -1,110 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 900" fill="none">
|
||||
<!-- Background geometric lines -->
|
||||
<g stroke="#49378B" stroke-width="1.5" fill="none" opacity="0.4">
|
||||
<!-- Outer hexagonal frame layers -->
|
||||
<path d="M400 80 L600 200 L600 440 L400 560 L200 440 L200 200 Z" />
|
||||
<path d="M400 120 L570 220 L570 420 L400 520 L230 420 L230 220 Z" />
|
||||
<!-- Connector lines going up -->
|
||||
<line x1="300" y1="160" x2="300" y2="60" />
|
||||
<line x1="400" y1="120" x2="400" y2="20" />
|
||||
<line x1="500" y1="160" x2="500" y2="60" />
|
||||
<!-- Bottom platform layers -->
|
||||
<path d="M250 520 L550 520 L600 560 L600 600 L400 700 L200 600 L200 560 Z" opacity="0.3" />
|
||||
<path d="M280 620 L520 620 L560 650 L560 680 L400 760 L240 680 L240 650 Z" opacity="0.2" />
|
||||
<path d="M320 700 L480 700 L510 720 L510 740 L400 800 L290 740 L290 720 Z" opacity="0.15" />
|
||||
</g>
|
||||
|
||||
<!-- 3D Isometric cube cluster -->
|
||||
<g transform="translate(400, 380)">
|
||||
<!-- Back layer cubes (purple/dark) -->
|
||||
<!-- Top back -->
|
||||
<g transform="translate(0, -100)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
|
||||
<!-- Middle row - left back -->
|
||||
<g transform="translate(-70, -55)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
<!-- Middle row - right back -->
|
||||
<g transform="translate(70, -55)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
|
||||
<!-- Yellow accent cubes - front facing -->
|
||||
<!-- Top -->
|
||||
<g transform="translate(0, -65)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
|
||||
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
|
||||
</g>
|
||||
|
||||
<!-- Middle left yellow -->
|
||||
<g transform="translate(-70, -20)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
|
||||
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
|
||||
</g>
|
||||
<!-- Middle right yellow -->
|
||||
<g transform="translate(70, -20)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
|
||||
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
|
||||
</g>
|
||||
|
||||
<!-- Center purple -->
|
||||
<g transform="translate(0, -20)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
|
||||
<!-- Bottom row -->
|
||||
<g transform="translate(-70, 25)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
<g transform="translate(70, 25)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
|
||||
<!-- Front bottom yellow -->
|
||||
<g transform="translate(0, 25)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
|
||||
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
|
||||
</g>
|
||||
|
||||
<!-- Outer corner yellow accents -->
|
||||
<g transform="translate(-105, 5)">
|
||||
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
|
||||
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
|
||||
<polygon points="0,-2 20,-12 20,12 0,25" fill="#d4e04e" />
|
||||
</g>
|
||||
<g transform="translate(105, 5)">
|
||||
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
|
||||
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
|
||||
<polygon points="0,-2 -20,-12 -20,12 0,25" fill="#e0ec50" />
|
||||
</g>
|
||||
<g transform="translate(0, -135)">
|
||||
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
|
||||
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Bottom arrow/chevron shape -->
|
||||
<path d="M340 780 L400 820 L460 780 L460 850 L400 890 L340 850 Z" fill="#211927" stroke="#49378B" stroke-width="1" opacity="0.5" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 866 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 80 KiB |
19
apps/website/public/svg/404-title.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="title404" fill="currentColor" transform="matrix(0.9346 -0.02737 -0.00901 1.0422 -597.31 -135.88)">
|
||||
<path d="M1436.44 522.556V439.749L1462.05 454.536C1478.8 464.21 1489.92 479.348 1489.92 505.313V518.746C1489.92 544.611 1478.72 546.966 1462.05 537.342L1436.44 522.556ZM1442.77 519.899L1461.87 530.926C1477.15 539.748 1483.32 535.087 1483.32 515.137V501.303C1483.32 481.253 1477.15 469.573 1461.87 460.751L1442.77 449.724V519.899Z"/>
|
||||
<path d="M1369.77 484.067V401.261L1376.02 404.87L1416.65 498.303V428.328L1422.73 431.837V514.643L1416.48 511.034L1375.85 417.601V487.576L1369.77 484.067Z"/>
|
||||
<path d="M1330.66 462.491C1311.56 451.464 1304.62 438.231 1304.62 417.68V363.645L1311.04 367.354V421.69C1311.04 440.737 1318.34 448.857 1330.66 455.975C1342.99 463.093 1350.2 463.343 1350.2 444.296V389.96L1356.62 393.67V447.704C1356.62 468.757 1349.24 473.218 1330.66 462.491Z"/>
|
||||
<path d="M1265.37 424.794C1245.75 413.466 1236.11 392.263 1236.11 372.614V358.278C1236.11 338.83 1245.75 328.654 1265.37 339.983C1285.08 351.361 1294.72 372.664 1294.72 392.113V406.448C1294.72 426.097 1285.08 436.173 1265.37 424.794ZM1242.71 376.724C1242.71 394.068 1250.53 409.506 1265.37 418.077C1280.3 426.699 1288.12 420.283 1288.12 402.94V388.002C1288.12 370.96 1280.3 355.321 1265.37 346.699C1250.53 338.128 1242.71 344.745 1242.71 361.787V376.724Z"/>
|
||||
<path d="M1187.24 378.685V295.879L1231.78 321.593V328.009L1193.58 305.954V341.643L1229.79 362.545V368.961L1193.58 348.059V382.344L1187.24 378.685Z"/>
|
||||
<path d="M1129.17 345.153V268.763L1106.33 255.58V249.164L1158.34 279.189V285.605L1135.5 272.422V348.812L1129.17 345.153Z"/>
|
||||
<path d="M1076.63 315.825C1057.01 304.497 1047.37 283.294 1047.37 263.645V249.309C1047.37 229.861 1057.01 219.685 1076.63 231.014C1096.34 242.392 1105.98 263.695 1105.98 283.144V297.479C1105.98 317.128 1096.34 327.204 1076.63 315.825ZM1053.97 267.755C1053.97 285.099 1061.79 300.537 1076.63 309.108C1091.56 317.73 1099.38 311.314 1099.38 293.971V279.033C1099.38 261.991 1091.56 246.352 1076.63 237.73C1061.79 229.159 1053.97 235.776 1053.97 252.818V267.755Z"/>
|
||||
<path d="M983.922 261.297V178.49L990.173 182.099L1030.8 275.532V205.558L1036.88 209.066V291.873L1030.63 288.264L989.999 194.831V264.805L983.922 261.297Z"/>
|
||||
<path d="M905.279 215.892V133.086L949.557 158.65V165.066L911.617 143.161V174.94L947.994 195.943V202.359L911.617 181.356V213.135L949.557 235.04V241.456L905.279 215.892Z"/>
|
||||
<path d="M864.552 193.381C846.059 182.704 836.422 161.702 836.422 141.852V127.517C836.422 107.767 845.972 97.8425 865.507 109.121C883.131 119.296 892.855 137.642 893.723 154.083L893.81 158.243L887.038 154.333V149.822C886.343 135.687 878.356 123.156 865.68 115.837C850.313 106.965 842.934 113.832 842.934 130.875V145.912C842.934 163.356 850.4 178.594 865.073 187.065C878.096 194.584 886.864 191.225 887.125 175.436V170.123L860.732 154.885V148.669L893.81 167.767V209.27L889.208 206.614L888.601 191.426C884.52 198.193 876.706 200.398 864.552 193.381Z"/>
|
||||
<path d="M777.479 142.107L801.615 73.2351L809.515 77.7965L833.738 174.588L826.966 170.678L821.93 150.027L789.286 131.18L784.251 146.017L777.479 142.107ZM791.11 125.816L820.107 142.558L805.869 84.2124L805.522 83.0094L805.261 83.8616L791.11 125.816Z"/>
|
||||
<path d="M727.161 113.056V30.2494L756.853 47.3921C772.047 56.164 779.513 69.2967 779.513 86.9407V88.1437C779.513 105.687 772.047 110.6 756.853 101.828L733.499 88.3442V116.715L727.161 113.056ZM733.499 82.0285L756.419 95.2615C767.792 101.828 772.915 98.67 772.915 84.7352V82.63C772.915 68.7955 767.792 60.0236 756.419 53.4572L733.499 40.2242V82.0285Z"/>
|
||||
<path d="M582.85 5.5V67.98L592.376 62.48V72.38L582.85 77.88V96.36L572.942 102.08V83.6L532.075 107.195V96.855L573.133 11.11L582.85 5.5ZM541.601 91.795L572.942 73.7V27.61L541.601 91.795Z"/>
|
||||
<path d="M495.102 148.121C475.096 159.671 464.903 150.816 464.903 126.286V111.546C464.903 87.2364 475.192 66.5564 495.102 55.0614C514.631 43.7864 524.824 52.6414 524.824 76.9514V91.6914C524.824 116.221 514.631 136.846 495.102 148.121ZM475.001 121.556C475.001 127.936 475.763 132.666 477.287 136.076L509.296 63.2564C506.057 60.8364 501.294 61.3864 495.102 64.9614C481.765 72.6614 475.001 86.0264 475.001 104.726V121.556ZM480.812 140.311C484.146 142.456 488.91 141.796 495.102 138.221C508.057 130.741 514.726 117.431 514.726 98.5114V81.6814C514.726 75.7414 514.059 71.1764 512.725 67.8764L480.812 140.311Z"/>
|
||||
<path d="M447.443 83.6773V146.157L456.969 140.657V150.557L447.443 156.057V174.537L437.535 180.257V161.777L396.668 185.372V175.032L437.726 89.2873L447.443 83.6773ZM406.194 169.972L437.535 151.877V105.787L406.194 169.972Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |