Compare commits
61 Commits
sno-qa-743
...
test-cover
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
119c9764d2 | ||
|
|
4885ef856c | ||
|
|
873a75d607 | ||
|
|
ecb6fbe8fb | ||
|
|
52ccd9ed1a | ||
|
|
92ad6fc798 | ||
|
|
06686a1f50 | ||
|
|
693b8383d6 | ||
|
|
93fddf2343 | ||
|
|
033b3dad3a | ||
|
|
66e8d570e7 | ||
|
|
121ceda66d | ||
|
|
634d57809b | ||
|
|
5807e03c74 | ||
|
|
5c6be4ed66 | ||
|
|
6847c7ba2d | ||
|
|
988a546721 | ||
|
|
25ac047b58 | ||
|
|
165984fe4c | ||
|
|
34a02a29c9 | ||
|
|
4bd655f625 | ||
|
|
a09bb81b98 | ||
|
|
aeedb60628 | ||
|
|
43fb5a8b19 | ||
|
|
c484c3984f | ||
|
|
2524846f5c | ||
|
|
12f578870e | ||
|
|
72eed86cea | ||
|
|
719ed16d32 | ||
|
|
5899a9392e | ||
|
|
e39468567a | ||
|
|
a373633ab2 | ||
|
|
521019d173 | ||
|
|
bd82c855e0 | ||
|
|
5b7ef3fe21 | ||
|
|
85de833776 | ||
|
|
cab46567c0 | ||
|
|
63435bdb34 | ||
|
|
20255da61f | ||
|
|
c2dba8f4ee | ||
|
|
6f579c5992 | ||
|
|
e729e5edb8 | ||
|
|
3043b181d7 | ||
|
|
8c9328c1b2 | ||
|
|
577f373cde | ||
|
|
44f88027b6 | ||
|
|
5d07de1913 | ||
|
|
f0ae91de43 | ||
|
|
fb8025c49f | ||
|
|
5c14badc42 | ||
|
|
82fb8ce658 | ||
|
|
c3e823e55b | ||
|
|
ebc9025de5 | ||
|
|
0353524e6f | ||
|
|
f1bb756929 | ||
|
|
d70c3cbfc2 | ||
|
|
e38dd1efae | ||
|
|
62779d3c51 | ||
|
|
4cf160d66e | ||
|
|
c0871ba219 | ||
|
|
6d4fc1bb10 |
@@ -1,9 +1,86 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(pnpx vitest run --testPathPattern=\"draftCacheV2.property\")",
|
||||
"Bash(pnpx vitest run \"draftCacheV2.property\")",
|
||||
"Bash(node -e \"const fc = require\\(''fast-check''\\); console.log\\(Object.keys\\(fc\\).filter\\(k => k.includes\\(''string''\\)\\).join\\('', ''\\)\\)\")"
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(tsc *)",
|
||||
"command": "echo 'Use `pnpm typecheck` instead of running tsc directly.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(vue-tsc *)",
|
||||
"command": "echo 'Use `pnpm typecheck` instead of running vue-tsc directly.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(npx tsc *)",
|
||||
"command": "echo 'Use `pnpm typecheck` instead of running tsc via npx.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(pnpx tsc *)",
|
||||
"command": "echo 'Use `pnpm typecheck` instead of running tsc via pnpx.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(pnpm exec tsc *)",
|
||||
"command": "echo 'Use `pnpm typecheck` instead of `pnpm exec tsc`.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(npx vitest *)",
|
||||
"command": "echo 'Use `pnpm test:unit` (or `pnpm test:unit -- <path>`) instead of npx vitest.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(pnpx vitest *)",
|
||||
"command": "echo 'Use `pnpm test:unit` (or `pnpm test:unit -- <path>`) instead of pnpx vitest.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(npx eslint *)",
|
||||
"command": "echo 'Use `pnpm lint` or `pnpm lint:fix` instead of npx eslint.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(pnpx eslint *)",
|
||||
"command": "echo 'Use `pnpm lint` or `pnpm lint:fix` instead of pnpx eslint.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(npx prettier *)",
|
||||
"command": "echo 'This project uses oxfmt, not prettier. Use `pnpm format` or `pnpm format:check`.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(pnpx prettier *)",
|
||||
"command": "echo 'This project uses oxfmt, not prettier. Use `pnpm format` or `pnpm format:check`.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(npx oxlint *)",
|
||||
"command": "echo 'Use `pnpm oxlint` instead of npx oxlint.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(npx stylelint *)",
|
||||
"command": "echo 'Use `pnpm stylelint` instead of npx stylelint.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(npx knip *)",
|
||||
"command": "echo 'Use `pnpm knip` instead of npx knip.' >&2 && exit 2"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"if": "Bash(pnpx knip *)",
|
||||
"command": "echo 'Use `pnpm knip` instead of pnpx knip.' >&2 && exit 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
---
|
||||
name: ticket-intake
|
||||
description: 'Parse ticket URL (Notion or GitHub), extract all data, initialize pipeline run. Use when starting work on a new ticket or when asked to pick up a ticket.'
|
||||
---
|
||||
|
||||
# Ticket Intake
|
||||
|
||||
Parses a ticket URL from supported sources (Notion or GitHub), extracts all relevant information, and creates a ticket in the pipeline API.
|
||||
|
||||
> **🚨 CRITICAL REQUIREMENT**: This skill MUST register the ticket in the Pipeline API and update the source (Notion/GitHub). If these steps are skipped, the entire pipeline breaks. See [Mandatory API Calls](#mandatory-api-calls-execute-all-three) below.
|
||||
|
||||
## Supported Sources
|
||||
|
||||
| Source | URL Pattern | Provider File |
|
||||
| ------ | --------------------------------------------------- | --------------------- |
|
||||
| Notion | `https://notion.so/...` `https://www.notion.so/...` | `providers/notion.md` |
|
||||
| GitHub | `https://github.com/{owner}/{repo}/issues/{n}` | `providers/github.md` |
|
||||
|
||||
## Quick Start
|
||||
|
||||
When given a ticket URL:
|
||||
|
||||
1. **Detect source type** from URL pattern
|
||||
2. **Load provider-specific logic** from `providers/` directory
|
||||
3. Fetch ticket content via appropriate API
|
||||
4. Extract and normalize properties to common schema
|
||||
5. **Register ticket in pipeline API** ← MANDATORY
|
||||
6. **Update source** (Notion status / GitHub comment) ← MANDATORY
|
||||
7. **Run verification script** to confirm API registration
|
||||
8. Output summary and handoff to `research-orchestrator`
|
||||
|
||||
## Configuration
|
||||
|
||||
Uses the **production API** by default. No configuration needed for read operations.
|
||||
|
||||
**Defaults (no setup required):**
|
||||
|
||||
- API URL: `https://api-gateway-856475788601.us-central1.run.app`
|
||||
- Read-only endpoints at `/public/*` require no authentication
|
||||
|
||||
**For write operations** (transitions, creating tickets), set:
|
||||
|
||||
```bash
|
||||
export PIPELINE_API_KEY="..." # Get from GCP Secret Manager or ask admin
|
||||
```
|
||||
|
||||
**Optional (for local working artifacts):**
|
||||
|
||||
```bash
|
||||
PIPELINE_DIR="${PIPELINE_DIR:-$HOME/repos/ticket-to-pr-pipeline}"
|
||||
```
|
||||
|
||||
## Mandatory API Calls (Execute ALL Three)
|
||||
|
||||
**⚠️ These three API calls are the ENTIRE POINT of this skill. Without them, the ticket is invisible to the pipeline, downstream skills will fail, and Notion status won't update.**
|
||||
|
||||
**You MUST make these HTTP requests.** Use `curl` from bash — do not just read this as documentation.
|
||||
|
||||
### Call 1: Create Ticket
|
||||
|
||||
```bash
|
||||
API_URL="${PIPELINE_API_URL:-https://api-gateway-856475788601.us-central1.run.app}"
|
||||
API_KEY="${PIPELINE_API_KEY}"
|
||||
|
||||
curl -s -X POST "${API_URL}/v1/tickets" \
|
||||
-H "Authorization: Bearer ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Agent-ID: ${AGENT_ID:-amp-agent}" \
|
||||
-d '{
|
||||
"notion_page_id": "NOTION_PAGE_UUID_HERE",
|
||||
"title": "TICKET_TITLE_HERE",
|
||||
"source": "notion",
|
||||
"metadata": {
|
||||
"description": "DESCRIPTION_HERE",
|
||||
"priority": "High",
|
||||
"labels": [],
|
||||
"acceptanceCriteria": []
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Save the returned `id` — you need it for the next two calls.
|
||||
|
||||
### Call 2: Transition to RESEARCH
|
||||
|
||||
```bash
|
||||
TICKET_ID="id-from-step-1"
|
||||
|
||||
curl -s -X POST "${API_URL}/v1/tickets/${TICKET_ID}/transition" \
|
||||
-H "Authorization: Bearer ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Agent-ID: ${AGENT_ID:-amp-agent}" \
|
||||
-d '{
|
||||
"to_state": "RESEARCH",
|
||||
"reason": "Intake complete, starting research"
|
||||
}'
|
||||
```
|
||||
|
||||
### Call 3: Queue Source Update
|
||||
|
||||
```bash
|
||||
curl -s -X POST "${API_URL}/v1/sync/queue" \
|
||||
-H "Authorization: Bearer ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Agent-ID: ${AGENT_ID:-amp-agent}" \
|
||||
-d '{
|
||||
"ticket_id": "TICKET_ID_HERE",
|
||||
"action": "update_status",
|
||||
"payload": { "status": "In Progress" },
|
||||
"priority": "normal"
|
||||
}'
|
||||
```
|
||||
|
||||
> **Note:** The action MUST be `"update_status"` (not `"UPDATE_NOTION_STATUS"`). Valid actions: `update_status`, `update_pr_url`, `mark_done`.
|
||||
|
||||
### TypeScript Equivalent (if using pipeline client)
|
||||
|
||||
```typescript
|
||||
import { PipelineClient } from '@pipeline/client'
|
||||
|
||||
const client = new PipelineClient({
|
||||
apiUrl:
|
||||
process.env.PIPELINE_API_URL ||
|
||||
'https://api-gateway-856475788601.us-central1.run.app',
|
||||
agentId: process.env.AGENT_ID!
|
||||
})
|
||||
|
||||
const ticket = await client.createTicket({
|
||||
notion_page_id: pageId,
|
||||
title: ticketTitle,
|
||||
source: 'notion',
|
||||
metadata: { description, priority, labels, acceptanceCriteria }
|
||||
})
|
||||
|
||||
await client.transitionState(
|
||||
ticket.id,
|
||||
'RESEARCH',
|
||||
'Intake complete, starting research'
|
||||
)
|
||||
|
||||
await client.queueSync(ticket.id, 'update_status', { status: 'In Progress' })
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Detect Source Type
|
||||
|
||||
Parse the URL to determine source:
|
||||
|
||||
```javascript
|
||||
if (url.includes('notion.so')) {
|
||||
source = 'notion'
|
||||
// Load providers/notion.md
|
||||
} else if (url.match(/github\.com\/[^\/]+\/[^\/]+\/issues\/\d+/)) {
|
||||
source = 'github'
|
||||
// Load providers/github.md
|
||||
} else {
|
||||
// Error: Unsupported URL format
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Load Provider and Fetch Data
|
||||
|
||||
Read the appropriate provider file for source-specific instructions:
|
||||
|
||||
- **Notion**: `providers/notion.md` - Uses Notion MCP, handles Slack links
|
||||
- **GitHub**: `providers/github.md` - Uses `gh` CLI, handles Dosu comments
|
||||
|
||||
Follow the provider's instructions for:
|
||||
|
||||
- Fetching content
|
||||
- Extracting properties
|
||||
- **Updating the source** (Notion status → "In Progress", Assignee → pipeline owner)
|
||||
|
||||
### Step 3: Normalize to Common Schema
|
||||
|
||||
All providers must extract normalized ticket data following `schema.md`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "abc12345",
|
||||
"url": "https://...",
|
||||
"source": "notion | github",
|
||||
"title": "Ticket title",
|
||||
"description": "Full description",
|
||||
"status": "Not Started",
|
||||
"assignee": "username",
|
||||
"priority": "High",
|
||||
"area": "UI",
|
||||
"labels": ["bug", "frontend"],
|
||||
"acceptanceCriteria": ["Criterion 1", "Criterion 2"],
|
||||
"fetchedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Register Ticket in Pipeline API (MANDATORY — DO NOT SKIP)
|
||||
|
||||
**Execute all three API calls from [Mandatory API Calls](#mandatory-api-calls-execute-all-three) above.**
|
||||
|
||||
This is not optional. This is not documentation. You MUST make these HTTP requests right now.
|
||||
|
||||
1. `createTicket()` → save the returned ticket ID
|
||||
2. `transitionState(id, 'RESEARCH')` → confirm state changed
|
||||
3. `queueSync(id, 'update_status', { status: 'In Progress' })` → confirm queued
|
||||
|
||||
**If any call fails**, retry once. If it still fails, report the error prominently — do NOT silently continue.
|
||||
|
||||
### Step 5: Run Verification Script
|
||||
|
||||
After making the API calls, run the verification script to confirm everything worked:
|
||||
|
||||
```bash
|
||||
bash scripts/verify-intake.sh TICKET_ID_OR_NOTION_PAGE_ID
|
||||
```
|
||||
|
||||
**If the script is not available locally**, verify manually via the public API:
|
||||
|
||||
```bash
|
||||
curl -s "${API_URL}/public/tickets/${TICKET_ID}" | jq '{id, state, title, notion_page_id}'
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "...",
|
||||
"state": "RESEARCH",
|
||||
"title": "...",
|
||||
"notion_page_id": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**If `state` is not `RESEARCH`, go back to Step 4 and complete the missing calls.**
|
||||
|
||||
### Step 6: Output Summary and Handoff
|
||||
|
||||
Print a clear summary:
|
||||
|
||||
```markdown
|
||||
## Ticket Intake Complete
|
||||
|
||||
**Source:** Notion | GitHub
|
||||
**Title:** [Ticket title]
|
||||
**ID:** abc12345
|
||||
**Status:** In Progress (queued)
|
||||
**Priority:** High
|
||||
**Area:** UI
|
||||
|
||||
### Description
|
||||
|
||||
[Brief description or first 200 chars]
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- [ ] Criterion 1
|
||||
- [ ] Criterion 2
|
||||
|
||||
### Links
|
||||
|
||||
- **Ticket:** [Original URL]
|
||||
- **Slack:** [Slack thread content fetched via slackdump] (Notion only)
|
||||
|
||||
### Pipeline
|
||||
|
||||
- **API Ticket ID:** abc12345
|
||||
- **State:** RESEARCH
|
||||
- **Verified:** ✅ (via verify-intake.sh or public API)
|
||||
```
|
||||
|
||||
**After printing the summary, immediately handoff** to continue the pipeline. Use the `handoff` tool with all necessary context (ticket ID, source, title, description, slack context if any):
|
||||
|
||||
> **Handoff goal:** "Continue pipeline for ticket {ID} ({title}). Ticket is in RESEARCH state. Load skill: `research-orchestrator` to begin research phase. Ticket data: source={source}, notion_page_id={pageId}, priority={priority}. {slack context summary if available}"
|
||||
|
||||
**Do NOT wait for human approval to proceed.** The intake phase is complete — handoff immediately.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Unsupported URL
|
||||
|
||||
```
|
||||
❌ Unsupported ticket URL format.
|
||||
|
||||
Supported formats:
|
||||
- Notion: https://notion.so/... or https://www.notion.so/...
|
||||
- GitHub: https://github.com/{owner}/{repo}/issues/{number}
|
||||
|
||||
Received: [provided URL]
|
||||
```
|
||||
|
||||
### Provider-Specific Errors
|
||||
|
||||
See individual provider files for source-specific error handling:
|
||||
|
||||
- `providers/notion.md` - Authentication, page not found
|
||||
- `providers/github.md` - Auth, rate limits, issue not found
|
||||
|
||||
### Missing Properties
|
||||
|
||||
Continue with available data and note what's missing:
|
||||
|
||||
```
|
||||
⚠️ Some properties unavailable:
|
||||
- Priority: not found (using default: Medium)
|
||||
- Area: not found
|
||||
|
||||
Proceeding with available data...
|
||||
```
|
||||
|
||||
### API Call Failures
|
||||
|
||||
```
|
||||
❌ Pipeline API call failed: {method} {endpoint}
|
||||
Status: {status}
|
||||
Error: {message}
|
||||
|
||||
Retrying once...
|
||||
|
||||
❌ Retry also failed. INTAKE IS INCOMPLETE.
|
||||
The ticket was NOT registered in the pipeline.
|
||||
Downstream skills will not work until this is fixed.
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This skill focuses ONLY on intake — it does not do research
|
||||
- Slack thread content is fetched automatically via the `slackdump` skill — no manual copy-paste needed
|
||||
- ALL API calls (createTicket, transitionState, queueSync) are MANDATORY — never skip them
|
||||
- The `queueSync` action must be `"update_status"`, NOT `"UPDATE_NOTION_STATUS"`
|
||||
- Pipeline state is tracked via the API, not local files
|
||||
- Working artifacts (research-report.md, plan.md) can be saved locally to `$PIPELINE_DIR/runs/{ticket-id}/`
|
||||
- The `source` field in the ticket determines which research strategies to use
|
||||
|
||||
## API Client Reference
|
||||
|
||||
### Available Methods
|
||||
|
||||
| Method | Description |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `createTicket({ notion_page_id, title, source, metadata })` | Create a new ticket in the API |
|
||||
| `getTicket(id)` | Retrieve a ticket by ID |
|
||||
| `findByNotionId(notionPageId)` | Look up a ticket by its Notion page ID |
|
||||
| `listTickets({ state, agent_id, limit, offset })` | List tickets with optional filters |
|
||||
| `transitionState(id, state, reason)` | Move ticket to a new state (e.g., `'RESEARCH'`) |
|
||||
| `setPRCreated(id, prUrl)` | Mark ticket as having a PR created |
|
||||
| `queueSync(id, action, payload)` | Queue a sync action (`update_status`, `update_pr_url`, `mark_done`) |
|
||||
| `registerBranch(id, branch, repo)` | Register working branch for automatic PR detection |
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
import { PipelineClient, PipelineAPIError } from '@pipeline/client';
|
||||
|
||||
try {
|
||||
await client.createTicket({ ... });
|
||||
} catch (error) {
|
||||
if (error instanceof PipelineAPIError) {
|
||||
console.error(`API Error ${error.status}: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
@@ -1,194 +0,0 @@
|
||||
# GitHub Provider - Ticket Intake
|
||||
|
||||
Provider-specific logic for ingesting tickets from GitHub Issues.
|
||||
|
||||
## URL Pattern
|
||||
|
||||
```
|
||||
https://github.com/{owner}/{repo}/issues/{number}
|
||||
https://www.github.com/{owner}/{repo}/issues/{number}
|
||||
```
|
||||
|
||||
Extract: `owner`, `repo`, `issue_number` from URL.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `gh` CLI authenticated (`gh auth status`)
|
||||
- Access to the repository
|
||||
|
||||
## Fetch Issue Content
|
||||
|
||||
Use `gh` CLI to fetch issue details:
|
||||
|
||||
```bash
|
||||
# Get issue details in JSON
|
||||
gh issue view {number} --repo {owner}/{repo} --json title,body,state,labels,assignees,milestone,author,createdAt,comments,linkedPRs
|
||||
|
||||
# Get comments separately if needed
|
||||
gh issue view {number} --repo {owner}/{repo} --comments
|
||||
```
|
||||
|
||||
## Extract Ticket Data
|
||||
|
||||
Map GitHub issue fields to normalized ticket data (stored via API):
|
||||
|
||||
| GitHub Field | ticket.json Field | Notes |
|
||||
| ------------ | ----------------- | -------------------------- |
|
||||
| title | title | Direct mapping |
|
||||
| body | description | Issue body/description |
|
||||
| state | status | Map: open → "Not Started" |
|
||||
| labels | labels | Array of label names |
|
||||
| assignees | assignee | First assignee login |
|
||||
| author | author | Issue author login |
|
||||
| milestone | milestone | Milestone title if present |
|
||||
| comments | comments | Array of comment objects |
|
||||
| linkedPRs | linkedPRs | PRs linked to this issue |
|
||||
|
||||
### Priority Mapping
|
||||
|
||||
Infer priority from labels:
|
||||
|
||||
- `priority:critical`, `P0` → "Critical"
|
||||
- `priority:high`, `P1` → "High"
|
||||
- `priority:medium`, `P2` → "Medium"
|
||||
- `priority:low`, `P3` → "Low"
|
||||
- No priority label → "Medium" (default)
|
||||
|
||||
### Area Mapping
|
||||
|
||||
Infer area from labels:
|
||||
|
||||
- `area:ui`, `frontend`, `component:*` → "UI"
|
||||
- `area:api`, `backend` → "API"
|
||||
- `area:docs`, `documentation` → "Docs"
|
||||
- `bug`, `fix` → "Bug"
|
||||
- `enhancement`, `feature` → "Feature"
|
||||
|
||||
## Update Source
|
||||
|
||||
**For GitHub issues, update is optional but recommended.**
|
||||
|
||||
Add a comment to indicate work has started:
|
||||
|
||||
```bash
|
||||
gh issue comment {number} --repo {owner}/{repo} --body "🤖 Pipeline started processing this issue."
|
||||
```
|
||||
|
||||
Optionally assign to self:
|
||||
|
||||
```bash
|
||||
gh issue edit {number} --repo {owner}/{repo} --add-assignee @me
|
||||
```
|
||||
|
||||
Log any updates via the Pipeline API:
|
||||
|
||||
```typescript
|
||||
await client.updateTicket(ticketId, {
|
||||
metadata: {
|
||||
...ticket.metadata,
|
||||
githubWrites: [
|
||||
...(ticket.metadata?.githubWrites || []),
|
||||
{
|
||||
action: 'comment',
|
||||
issueNumber: 123,
|
||||
at: new Date().toISOString(),
|
||||
skill: 'ticket-intake',
|
||||
success: true
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## GitHub-Specific Ticket Fields
|
||||
|
||||
Store via API using `client.createTicket()`:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "github",
|
||||
"githubOwner": "Comfy-Org",
|
||||
"githubRepo": "ComfyUI_frontend",
|
||||
"githubIssueNumber": 123,
|
||||
"githubIssueUrl": "https://github.com/Comfy-Org/ComfyUI_frontend/issues/123",
|
||||
"labels": ["bug", "area:ui", "priority:high"],
|
||||
"linkedPRs": [456, 789],
|
||||
"dosuComment": "..." // Extracted Dosu bot analysis if present
|
||||
}
|
||||
```
|
||||
|
||||
## Dosu Bot Detection
|
||||
|
||||
Many repositories use Dosu bot for automated issue analysis. Check comments for Dosu:
|
||||
|
||||
```bash
|
||||
gh issue view {number} --repo {owner}/{repo} --comments | grep -A 100 "dosu"
|
||||
```
|
||||
|
||||
Look for comments from:
|
||||
|
||||
- `dosu[bot]`
|
||||
- `dosu-bot`
|
||||
|
||||
Extract Dosu analysis which typically includes:
|
||||
|
||||
- Root cause analysis
|
||||
- Suggested files to modify
|
||||
- Related issues/PRs
|
||||
- Potential solutions
|
||||
|
||||
Store in ticket data via API:
|
||||
|
||||
```json
|
||||
{
|
||||
"dosuComment": {
|
||||
"found": true,
|
||||
"analysis": "...",
|
||||
"suggestedFiles": ["src/file1.ts", "src/file2.ts"],
|
||||
"relatedIssues": [100, 101]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Extract Linked Issues/PRs
|
||||
|
||||
Parse issue body and comments for references:
|
||||
|
||||
- `#123` → Issue or PR reference
|
||||
- `fixes #123`, `closes #123` → Linked issue
|
||||
- `https://github.com/.../issues/123` → Full URL reference
|
||||
|
||||
Store in ticket data via API for research phase:
|
||||
|
||||
```json
|
||||
{
|
||||
"referencedIssues": [100, 101, 102],
|
||||
"referencedPRs": [200, 201]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Authentication Error
|
||||
|
||||
```
|
||||
⚠️ GitHub CLI not authenticated.
|
||||
Run: gh auth login
|
||||
```
|
||||
|
||||
### Issue Not Found
|
||||
|
||||
```
|
||||
❌ GitHub issue not found or inaccessible.
|
||||
- Check the URL is correct
|
||||
- Ensure you have access to this repository
|
||||
- Run: gh auth status
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```
|
||||
⚠️ GitHub API rate limited.
|
||||
Wait a few minutes and try again.
|
||||
Check status: gh api rate_limit
|
||||
```
|
||||
@@ -1,202 +0,0 @@
|
||||
# Notion Provider - Ticket Intake
|
||||
|
||||
Provider-specific logic for ingesting tickets from Notion.
|
||||
|
||||
## URL Pattern
|
||||
|
||||
```
|
||||
https://www.notion.so/workspace/Page-Title-abc123def456...
|
||||
https://notion.so/Page-Title-abc123def456...
|
||||
https://www.notion.so/abc123def456...
|
||||
```
|
||||
|
||||
Page ID is the 32-character hex string (with or without hyphens).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Notion MCP connected and authenticated
|
||||
- If not setup: `claude mcp add --transport http notion https://mcp.notion.com/mcp`
|
||||
- Authenticate via `/mcp` command if prompted
|
||||
|
||||
## Fetch Ticket Content
|
||||
|
||||
Use `Notion:notion-fetch` with the page URL or ID:
|
||||
|
||||
```
|
||||
Fetch the full page content including all properties
|
||||
```
|
||||
|
||||
## Extract Ticket Data
|
||||
|
||||
Extract these properties (names may vary):
|
||||
|
||||
| Property | Expected Name | Type |
|
||||
| ------------- | ------------------------- | ------------ |
|
||||
| Title | Name / Title | Title |
|
||||
| Status | Status | Select |
|
||||
| Assignee | Assignee / Assigned To | Person |
|
||||
| Description | - | Page content |
|
||||
| Slack Link | Slack Link / Slack Thread | URL |
|
||||
| GitHub PR | GitHub PR / PR Link | URL |
|
||||
| Priority | Priority | Select |
|
||||
| Area | Area / Category | Select |
|
||||
| Related Tasks | Related Tasks | Relation |
|
||||
|
||||
**If properties are missing**: Note what's unavailable and continue with available data.
|
||||
|
||||
## Update Source (REQUIRED)
|
||||
|
||||
**⚠️ DO NOT SKIP THIS STEP. This is a required action, not optional.**
|
||||
|
||||
**⚠️ Notion Write Safety rules apply (see `$PIPELINE_DIR/docs/notion-write-safety.md` for full reference):**
|
||||
|
||||
- **Whitelist**: Only `Status`, `GitHub PR`, and `Assignee` fields may be written
|
||||
- **Valid transitions**: Not Started → In Progress, In Progress → In Review, In Review → Done
|
||||
- **Logging**: Every write attempt MUST be logged with timestamp, field, value, previous value, skill name, and success status
|
||||
|
||||
Use `Notion:notion-update-page` to update the ticket:
|
||||
|
||||
1. **Status**: Set to "In Progress" (only valid from "Not Started")
|
||||
2. **Assignee**: Assign to pipeline owner (Notion ID: `175d872b-594c-81d4-ba5a-0002911c5966`)
|
||||
|
||||
```json
|
||||
{
|
||||
"page_id": "{page_id_from_ticket}",
|
||||
"command": "update_properties",
|
||||
"properties": {
|
||||
"Status": "In Progress",
|
||||
"Assignee": "175d872b-594c-81d4-ba5a-0002911c5966"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After the update succeeds**, log the write via the Pipeline API:
|
||||
|
||||
```typescript
|
||||
await client.updateTicket(ticketId, {
|
||||
metadata: {
|
||||
...ticket.metadata,
|
||||
notionWrites: [
|
||||
...(ticket.metadata?.notionWrites || []),
|
||||
{
|
||||
field: 'Status',
|
||||
value: 'In Progress',
|
||||
previousValue: 'Not Started',
|
||||
at: new Date().toISOString(),
|
||||
skill: 'ticket-intake',
|
||||
success: true
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
If update fails, log with `success: false` and continue.
|
||||
|
||||
## Notion-Specific Ticket Fields
|
||||
|
||||
Store via API using `client.createTicket()`:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "notion",
|
||||
"notionPageId": "abc123def456...",
|
||||
"slackLink": "https://slack.com/...",
|
||||
"relatedTasks": ["page-id-1", "page-id-2"]
|
||||
}
|
||||
```
|
||||
|
||||
## Slack Thread Handling
|
||||
|
||||
If a Slack link exists, use the `slackdump` skill to fetch the thread content programmatically.
|
||||
|
||||
### Slack URL Conversion
|
||||
|
||||
Notion stores Slack links in `slackMessage://` format:
|
||||
|
||||
```
|
||||
slackMessage://comfy-organization.slack.com/CHANNEL_ID/THREAD_TS/MESSAGE_TS
|
||||
```
|
||||
|
||||
Convert to browser-clickable format:
|
||||
|
||||
```
|
||||
https://comfy-organization.slack.com/archives/CHANNEL_ID/pMESSAGE_TS_NO_DOT
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
- Input: `slackMessage://comfy-organization.slack.com/C075ANWQ8KS/1766022478.450909/1764772881.854829`
|
||||
- Output: `https://comfy-organization.slack.com/archives/C075ANWQ8KS/p1764772881854829`
|
||||
|
||||
(Remove the dot from the last timestamp and prefix with `p`)
|
||||
|
||||
### Fetching Thread Content
|
||||
|
||||
Load the `slackdump` skill and use the **export-thread** workflow:
|
||||
|
||||
```bash
|
||||
# Export thread by URL
|
||||
slackdump dump "https://comfy-organization.slack.com/archives/CHANNEL_ID/pMESSAGE_TS"
|
||||
|
||||
# Or by colon notation (channel_id:thread_ts)
|
||||
slackdump dump CHANNEL_ID:THREAD_TS
|
||||
```
|
||||
|
||||
Save the thread content to `$RUN_DIR/slack-context.md` and include it in the ticket metadata.
|
||||
|
||||
> **No manual action required.** The slackdump CLI handles authentication via stored credentials at `~/.cache/slackdump/comfy-organization.bin`.
|
||||
|
||||
## Database Reference: Comfy Tasks
|
||||
|
||||
The "Comfy Tasks" database has these properties (verify via `notion-search`):
|
||||
|
||||
- **Status values**: Not Started, In Progress, In Review, Done
|
||||
- **Team assignment**: "Frontend Team" for unassigned tickets
|
||||
- **Filtering note**: Team filtering in Notion may have quirks - handle gracefully
|
||||
|
||||
### Pipeline Owner Details
|
||||
|
||||
When assigning tickets, use these identifiers:
|
||||
|
||||
| Platform | Identifier |
|
||||
| --------------- | -------------------------------------- |
|
||||
| Notion User ID | `175d872b-594c-81d4-ba5a-0002911c5966` |
|
||||
| Notion Name | Christian Byrne |
|
||||
| Notion Email | cbyrne@comfy.org |
|
||||
| Slack User ID | U087MJCDHHC |
|
||||
| GitHub Username | christian-byrne |
|
||||
|
||||
**To update Assignee**, use the Notion User ID (not name):
|
||||
|
||||
```
|
||||
properties: {"Assignee": "175d872b-594c-81d4-ba5a-0002911c5966"}
|
||||
```
|
||||
|
||||
### Finding Active Tickets
|
||||
|
||||
To list your active tickets:
|
||||
|
||||
```
|
||||
Use Notion:notion-search for "Comfy Tasks"
|
||||
Filter by Assignee = current user OR Team = "Frontend Team"
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Authentication Error
|
||||
|
||||
```
|
||||
⚠️ Notion authentication required.
|
||||
Run: claude mcp add --transport http notion https://mcp.notion.com/mcp
|
||||
Then authenticate via /mcp command.
|
||||
```
|
||||
|
||||
### Page Not Found
|
||||
|
||||
```
|
||||
❌ Notion page not found or inaccessible.
|
||||
- Check the URL is correct
|
||||
- Ensure you have access to this page
|
||||
- Try re-authenticating via /mcp
|
||||
```
|
||||
@@ -1,81 +0,0 @@
|
||||
# Ticket Schema
|
||||
|
||||
Common schema for normalized ticket data across all sources. This data is stored and retrieved via the Pipeline API, not local files.
|
||||
|
||||
## Ticket Data Schema
|
||||
|
||||
```json
|
||||
{
|
||||
// Required fields (all sources)
|
||||
"id": "string", // Unique identifier (short form)
|
||||
"url": "string", // Original URL
|
||||
"source": "notion | github", // Source type
|
||||
"title": "string", // Ticket title
|
||||
"description": "string", // Full description/body
|
||||
"fetchedAt": "ISO8601", // When ticket was fetched
|
||||
|
||||
// Common optional fields
|
||||
"status": "string", // Current status
|
||||
"assignee": "string", // Assigned user
|
||||
"priority": "string", // Priority level
|
||||
"area": "string", // Category/area
|
||||
"labels": ["string"], // Tags/labels
|
||||
"acceptanceCriteria": ["string"] // List of AC items
|
||||
|
||||
// Source-specific fields (see providers)
|
||||
// Notion: notionPageId, slackLink, relatedTasks, notionWrites
|
||||
// GitHub: githubOwner, githubRepo, githubIssueNumber, linkedPRs, dosuComment, referencedIssues
|
||||
}
|
||||
```
|
||||
|
||||
## Ticket State Schema (via API)
|
||||
|
||||
State is managed via the Pipeline API using `client.transitionState()`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ticketId": "string",
|
||||
"state": "intake | research | planning | implementation | pr_created | done | failed",
|
||||
"stateChangedAt": "ISO8601",
|
||||
|
||||
// Timestamps tracked by API
|
||||
"createdAt": "ISO8601",
|
||||
"updatedAt": "ISO8601"
|
||||
}
|
||||
```
|
||||
|
||||
## Priority Normalization
|
||||
|
||||
All sources should normalize to these values:
|
||||
|
||||
| Normalized | Description |
|
||||
| ---------- | ------------------------- |
|
||||
| Critical | Production down, security |
|
||||
| High | Blocking work, urgent |
|
||||
| Medium | Normal priority (default) |
|
||||
| Low | Nice to have, backlog |
|
||||
|
||||
## Status Normalization
|
||||
|
||||
Pipeline tracks these statuses internally:
|
||||
|
||||
| Status | Description |
|
||||
| -------------- | ---------------------------- |
|
||||
| research | Gathering context |
|
||||
| planning | Creating implementation plan |
|
||||
| implementation | Writing code |
|
||||
| review | Code review in progress |
|
||||
| qa | Quality assurance |
|
||||
| done | PR merged or completed |
|
||||
|
||||
## ID Generation
|
||||
|
||||
IDs are generated by the API when creating tickets. For reference:
|
||||
|
||||
- **Notion**: First 8 characters of page ID
|
||||
- **GitHub**: `gh-{owner}-{repo}-{issue_number}` (sanitized)
|
||||
|
||||
Examples:
|
||||
|
||||
- Notion: `abc12345`
|
||||
- GitHub: `gh-comfy-org-frontend-123`
|
||||
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
65
.github/actions/find-workflow-run/action.yaml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: Find Workflow Run
|
||||
description: Finds a workflow run for a given commit SHA and outputs its status and run ID.
|
||||
|
||||
inputs:
|
||||
workflow-id:
|
||||
description: The workflow filename (e.g., 'ci-size-data.yaml')
|
||||
required: true
|
||||
head-sha:
|
||||
description: The commit SHA to find runs for
|
||||
required: true
|
||||
not-found-status:
|
||||
description: Status to output when no run exists
|
||||
required: false
|
||||
default: pending
|
||||
token:
|
||||
description: GitHub token for API access
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
status:
|
||||
description: One of 'ready', 'pending', 'failed', or the not-found-status value
|
||||
value: ${{ steps.find.outputs.status }}
|
||||
run-id:
|
||||
description: The workflow run ID (only set when status is 'ready')
|
||||
value: ${{ steps.find.outputs.run-id }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Find workflow run
|
||||
id: find
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
WORKFLOW_ID: ${{ inputs.workflow-id }}
|
||||
HEAD_SHA: ${{ inputs.head-sha }}
|
||||
NOT_FOUND_STATUS: ${{ inputs.not-found-status }}
|
||||
with:
|
||||
github-token: ${{ inputs.token }}
|
||||
script: |
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: process.env.WORKFLOW_ID,
|
||||
head_sha: process.env.HEAD_SHA,
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
const run = runs.workflow_runs[0];
|
||||
if (!run) {
|
||||
core.setOutput('status', process.env.NOT_FOUND_STATUS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.status !== 'completed') {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.conclusion !== 'success') {
|
||||
core.setOutput('status', 'failed');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('status', 'ready');
|
||||
core.setOutput('run-id', String(run.id));
|
||||
31
.github/actions/lint-format-verify/action.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: 'Lint and format verify'
|
||||
description: >
|
||||
Runs the lint/format/knip verification suite plus a conditional
|
||||
browser-tests typecheck. Shared by ci-lint-format.yaml (PR) and
|
||||
ci-lint-format-queue.yaml (merge queue) so both paths run the exact
|
||||
same checks. The caller is responsible for checkout and frontend setup
|
||||
before invoking this action.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Detect browser_tests changes
|
||||
id: changed-paths
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
browser_tests:
|
||||
- 'browser_tests/**'
|
||||
|
||||
- name: Verify lint and format
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm lint
|
||||
pnpm stylelint
|
||||
pnpm format:check
|
||||
pnpm knip
|
||||
|
||||
- name: Typecheck browser tests
|
||||
if: steps.changed-paths.outputs.browser_tests == 'true'
|
||||
shell: bash
|
||||
run: pnpm typecheck:browser
|
||||
29
.github/workflows/ci-lint-format-queue.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Description: Lint and format verification for GitHub merge queue runs.
|
||||
# Paired with ci-lint-format.yaml — workflow name and job name must match
|
||||
# so branch protection resolves a single required check in both the
|
||||
# pull_request and merge_group contexts. This file runs verify-only steps
|
||||
# with a read-only token; auto-fix and PR comments live in the PR workflow.
|
||||
name: 'CI: Lint Format'
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint-and-format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout merge group ref
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Verify lint and format
|
||||
uses: ./.github/actions/lint-format-verify
|
||||
25
.github/workflows/ci-lint-format.yaml
vendored
@@ -1,4 +1,7 @@
|
||||
# Description: Linting and code formatting validation for pull requests
|
||||
# Description: Linting and code formatting validation for pull requests.
|
||||
# Paired with ci-lint-format-queue.yaml - workflow name and job name must
|
||||
# match so branch protection resolves a single required check in both the
|
||||
# pull_request and merge_group contexts.
|
||||
name: 'CI: Lint Format'
|
||||
|
||||
on:
|
||||
@@ -26,14 +29,6 @@ jobs:
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Detect browser_tests changes
|
||||
id: changed-paths
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
browser_tests:
|
||||
- 'browser_tests/**'
|
||||
|
||||
- name: Run ESLint with auto-fix
|
||||
run: pnpm lint:fix
|
||||
|
||||
@@ -77,16 +72,8 @@ jobs:
|
||||
echo "See CONTRIBUTING.md for more details."
|
||||
exit 1
|
||||
|
||||
- name: Final validation
|
||||
run: |
|
||||
pnpm lint
|
||||
pnpm stylelint
|
||||
pnpm format:check
|
||||
pnpm knip
|
||||
|
||||
- name: Typecheck browser tests
|
||||
if: steps.changed-paths.outputs.browser_tests == 'true'
|
||||
run: pnpm typecheck:browser
|
||||
- name: Verify lint and format
|
||||
uses: ./.github/actions/lint-format-verify
|
||||
|
||||
- name: Comment on PR about auto-fix
|
||||
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
73
.github/workflows/ci-tests-e2e-coverage.yaml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: 'CI: E2E Coverage'
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['CI: Tests E2E']
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: e2e-coverage-${{ github.event.workflow_run.head_sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
merge:
|
||||
if: >
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Download all shard coverage data
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: e2e-coverage-shard-.*
|
||||
name_is_regexp: true
|
||||
path: temp/coverage-shards
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Install lcov
|
||||
run: sudo apt-get install -y -qq lcov
|
||||
|
||||
- name: Merge shard coverage into single LCOV
|
||||
run: |
|
||||
mkdir -p coverage/playwright
|
||||
LCOV_FILES=$(find temp/coverage-shards -name 'coverage.lcov' -type f)
|
||||
if [ -z "$LCOV_FILES" ]; then
|
||||
echo "No coverage.lcov files found"
|
||||
touch coverage/playwright/coverage.lcov
|
||||
exit 0
|
||||
fi
|
||||
ADD_ARGS=""
|
||||
for f in $LCOV_FILES; do ADD_ARGS="$ADD_ARGS -a $f"; done
|
||||
lcov $ADD_ARGS -o coverage/playwright/coverage.lcov
|
||||
wc -l coverage/playwright/coverage.lcov
|
||||
|
||||
- name: Upload merged coverage data
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: e2e-coverage
|
||||
path: coverage/playwright/
|
||||
retention-days: 30
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload E2E coverage to Codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||
with:
|
||||
files: coverage/playwright/coverage.lcov
|
||||
flags: e2e
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
11
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
pull_request:
|
||||
branches-ignore: [wip/*, draft/*, temp/*]
|
||||
paths-ignore: ['**/*.md']
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -86,6 +87,7 @@ jobs:
|
||||
run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
|
||||
env:
|
||||
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
|
||||
COLLECT_COVERAGE: 'true'
|
||||
|
||||
- name: Upload blob report
|
||||
uses: actions/upload-artifact@v6
|
||||
@@ -95,6 +97,15 @@ jobs:
|
||||
path: blob-report/
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload shard coverage data
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: e2e-coverage-shard-${{ matrix.shardIndex }}
|
||||
path: coverage/playwright/
|
||||
retention-days: 1
|
||||
if-no-files-found: warn
|
||||
|
||||
playwright-tests:
|
||||
# Ideally, each shard runs test in 6 minutes, but allow up to 15 minutes
|
||||
timeout-minutes: 15
|
||||
|
||||
11
.github/workflows/ci-tests-unit.yaml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
pull_request:
|
||||
branches-ignore: [wip/*, draft/*, temp/*]
|
||||
paths-ignore: ['**/*.md']
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -26,10 +27,20 @@ jobs:
|
||||
- name: Run Vitest tests with coverage
|
||||
run: pnpm test:coverage
|
||||
|
||||
- name: Upload unit coverage artifact
|
||||
if: always() && github.event_name == 'push'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: unit-coverage
|
||||
path: coverage/lcov.info
|
||||
retention-days: 30
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||
with:
|
||||
files: coverage/lcov.info
|
||||
flags: unit
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
||||
149
.github/workflows/coverage-slack-notify.yaml
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
name: 'Coverage: Slack Notification'
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['CI: Tests Unit']
|
||||
branches: [main]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
if: >
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Download current unit coverage
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: unit-coverage
|
||||
path: coverage
|
||||
|
||||
- name: Download previous unit coverage baseline
|
||||
continue-on-error: true
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
branch: main
|
||||
workflow: coverage-slack-notify.yaml
|
||||
name: unit-coverage-baseline
|
||||
path: temp/coverage-baseline
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Download latest E2E coverage
|
||||
continue-on-error: true
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
branch: main
|
||||
workflow: ci-tests-e2e-coverage.yaml
|
||||
name: e2e-coverage
|
||||
path: temp/e2e-coverage
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Download previous E2E coverage baseline
|
||||
continue-on-error: true
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
branch: main
|
||||
workflow: coverage-slack-notify.yaml
|
||||
name: e2e-coverage-baseline
|
||||
path: temp/e2e-coverage-baseline
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Resolve merged PR metadata
|
||||
id: pr-meta
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const sha = context.payload.workflow_run.head_sha;
|
||||
const { data: commit } = await github.rest.repos.getCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: sha,
|
||||
});
|
||||
const message = commit.commit.message ?? '';
|
||||
const firstLine = message.split('\n')[0];
|
||||
const match = firstLine.match(/\(#(\d+)\)\s*$/);
|
||||
if (!match) {
|
||||
core.setOutput('skip', 'true');
|
||||
core.info('No PR number found in commit message — skipping.');
|
||||
return;
|
||||
}
|
||||
const prNumber = match[1];
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: Number(prNumber),
|
||||
});
|
||||
core.setOutput('skip', 'false');
|
||||
core.setOutput('number', prNumber);
|
||||
core.setOutput('url', pr.html_url);
|
||||
core.setOutput('author', pr.user.login);
|
||||
|
||||
- name: Generate Slack notification
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
id: slack-payload
|
||||
env:
|
||||
PR_URL: ${{ steps.pr-meta.outputs.url }}
|
||||
PR_NUMBER: ${{ steps.pr-meta.outputs.number }}
|
||||
PR_AUTHOR: ${{ steps.pr-meta.outputs.author }}
|
||||
run: |
|
||||
PAYLOAD=$(pnpm exec tsx scripts/coverage-slack-notify.ts \
|
||||
--pr-url="$PR_URL" \
|
||||
--pr-number="$PR_NUMBER" \
|
||||
--author="$PR_AUTHOR")
|
||||
if [ -n "$PAYLOAD" ]; then
|
||||
echo "has_payload=true" >> "$GITHUB_OUTPUT"
|
||||
DELIM="SLACK_PAYLOAD_$(date +%s)"
|
||||
echo "payload<<$DELIM" >> "$GITHUB_OUTPUT"
|
||||
printf '%s\n' "$PAYLOAD" >> "$GITHUB_OUTPUT"
|
||||
echo "$DELIM" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_payload=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Post to Slack
|
||||
if: steps.slack-payload.outputs.has_payload == 'true'
|
||||
continue-on-error: true
|
||||
env:
|
||||
SLACK_PAYLOAD: ${{ steps.slack-payload.outputs.payload }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
run: |
|
||||
# Channel: #p-deprecated-frontend-automated-testing
|
||||
BODY=$(echo "$SLACK_PAYLOAD" | jq --arg ch "C0AP09LKRDZ" '. + {channel: $ch}')
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY" \
|
||||
-o /dev/null \
|
||||
https://slack.com/api/chat.postMessage
|
||||
|
||||
- name: Save unit coverage baseline
|
||||
if: always() && hashFiles('coverage/lcov.info') != ''
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: unit-coverage-baseline
|
||||
path: coverage/lcov.info
|
||||
retention-days: 90
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Save E2E coverage baseline
|
||||
if: always() && hashFiles('temp/e2e-coverage/coverage.lcov') != ''
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: e2e-coverage-baseline
|
||||
path: temp/e2e-coverage/coverage.lcov
|
||||
retention-days: 90
|
||||
if-no-files-found: warn
|
||||
94
.github/workflows/pr-report.yaml
vendored
@@ -2,7 +2,7 @@ name: 'PR: Unified Report'
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['CI: Size Data', 'CI: Performance Report']
|
||||
workflows: ['CI: Size Data', 'CI: Performance Report', 'CI: E2E Coverage']
|
||||
types:
|
||||
- completed
|
||||
|
||||
@@ -67,73 +67,23 @@ jobs:
|
||||
core.setOutput('base', livePr.base.ref);
|
||||
core.setOutput('head-sha', livePr.head.sha);
|
||||
|
||||
- name: Find size workflow run for this commit
|
||||
- name: Find size workflow run
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
id: find-size
|
||||
uses: actions/github-script@v8
|
||||
uses: ./.github/actions/find-workflow-run
|
||||
with:
|
||||
script: |
|
||||
const headSha = '${{ steps.pr-meta.outputs.head-sha }}';
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'ci-size-data.yaml',
|
||||
head_sha: headSha,
|
||||
per_page: 1,
|
||||
});
|
||||
workflow-id: ci-size-data.yaml
|
||||
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
const run = runs.workflow_runs[0];
|
||||
if (!run) {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.status !== 'completed') {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.conclusion !== 'success') {
|
||||
core.setOutput('status', 'failed');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('status', 'ready');
|
||||
core.setOutput('run-id', String(run.id));
|
||||
|
||||
- name: Find perf workflow run for this commit
|
||||
- name: Find perf workflow run
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
id: find-perf
|
||||
uses: actions/github-script@v8
|
||||
uses: ./.github/actions/find-workflow-run
|
||||
with:
|
||||
script: |
|
||||
const headSha = '${{ steps.pr-meta.outputs.head-sha }}';
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'ci-perf-report.yaml',
|
||||
head_sha: headSha,
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
const run = runs.workflow_runs[0];
|
||||
if (!run) {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.status !== 'completed') {
|
||||
core.setOutput('status', 'pending');
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.conclusion !== 'success') {
|
||||
core.setOutput('status', 'failed');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('status', 'ready');
|
||||
core.setOutput('run-id', String(run.id));
|
||||
workflow-id: ci-perf-report.yaml
|
||||
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download size data (current)
|
||||
if: steps.pr-meta.outputs.skip != 'true' && steps.find-size.outputs.status == 'ready'
|
||||
@@ -154,6 +104,25 @@ jobs:
|
||||
path: temp/size-prev
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Find coverage workflow run
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
id: find-coverage
|
||||
uses: ./.github/actions/find-workflow-run
|
||||
with:
|
||||
workflow-id: ci-tests-e2e-coverage.yaml
|
||||
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
|
||||
not-found-status: skip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download coverage data
|
||||
if: steps.pr-meta.outputs.skip != 'true' && steps.find-coverage.outputs.status == 'ready'
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
name: e2e-coverage
|
||||
run_id: ${{ steps.find-coverage.outputs.run-id }}
|
||||
path: temp/coverage
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Download perf metrics (current)
|
||||
if: steps.pr-meta.outputs.skip != 'true' && steps.find-perf.outputs.status == 'ready'
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
@@ -189,9 +158,10 @@ jobs:
|
||||
- name: Generate unified report
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
run: >
|
||||
node scripts/unified-report.js
|
||||
pnpm exec tsx scripts/unified-report.ts
|
||||
--size-status=${{ steps.find-size.outputs.status }}
|
||||
--perf-status=${{ steps.find-perf.outputs.status }}
|
||||
--coverage-status=${{ steps.find-coverage.outputs.status }}
|
||||
> pr-report.md
|
||||
|
||||
- name: Remove legacy separate comments
|
||||
|
||||
24
.github/workflows/release-biweekly-comfyui.yaml
vendored
@@ -1,14 +1,23 @@
|
||||
# Automated bi-weekly workflow to bump ComfyUI frontend RC releases
|
||||
name: 'Release: Bi-weekly ComfyUI'
|
||||
# Release workflow for ComfyUI frontend: version bump → PyPI publish → ComfyUI PR.
|
||||
# Runs on a bi-weekly schedule for minor releases, or manually for patch/hotfix releases.
|
||||
name: 'Release: ComfyUI'
|
||||
|
||||
on:
|
||||
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
|
||||
# Bi-weekly schedule: Monday at 20:00 UTC
|
||||
schedule:
|
||||
- cron: '0 20 * * 1'
|
||||
|
||||
# Allow manual triggering (bypasses bi-weekly check)
|
||||
# Manual trigger for both on-demand minor and patch/hotfix releases
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_type:
|
||||
description: 'minor = next minor version (bi-weekly cadence), patch = hotfix for current production version'
|
||||
required: true
|
||||
default: 'minor'
|
||||
type: choice
|
||||
options:
|
||||
- minor
|
||||
- patch
|
||||
comfyui_fork:
|
||||
description: 'ComfyUI fork to use for PR (e.g., Comfy-Org/ComfyUI)'
|
||||
required: false
|
||||
@@ -41,10 +50,11 @@ jobs:
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## Bi-weekly Check" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Release Check" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Is release week: ${{ steps.check.outputs.is_release_week }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Manual trigger: ${{ github.event_name == 'workflow_dispatch' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Release type: ${{ inputs.release_type || 'minor (scheduled)' }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
resolve-version:
|
||||
needs: check-release-week
|
||||
@@ -76,6 +86,8 @@ jobs:
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
with:
|
||||
package_json_file: frontend/package.json
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
@@ -89,6 +101,8 @@ jobs:
|
||||
- name: Resolve release information
|
||||
id: resolve
|
||||
working-directory: frontend
|
||||
env:
|
||||
RELEASE_TYPE: ${{ inputs.release_type || 'minor' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
"typescript/no-unused-vars": "off",
|
||||
"unicorn/no-empty-file": "off",
|
||||
"vitest/require-mock-type-parameters": "off",
|
||||
"unicorn/no-new-array": "off",
|
||||
"unicorn/no-single-promise-in-promise-methods": "off",
|
||||
"unicorn/no-useless-fallback-in-spread": "off",
|
||||
@@ -116,13 +117,60 @@
|
||||
},
|
||||
{
|
||||
"files": ["browser_tests/**/*.ts"],
|
||||
"jsPlugins": ["eslint-plugin-playwright"],
|
||||
"rules": {
|
||||
"typescript/no-explicit-any": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-control-regex": "error",
|
||||
"no-useless-rename": "error",
|
||||
"no-unused-private-class-members": "error",
|
||||
"unicorn/no-empty-file": "error"
|
||||
"unicorn/no-empty-file": "error",
|
||||
"playwright/consistent-spacing-between-blocks": "error",
|
||||
"playwright/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
"assertFunctionNames": [
|
||||
"recordMeasurement",
|
||||
"logMeasurement",
|
||||
"builderSaveAs"
|
||||
],
|
||||
"assertFunctionPatterns": [
|
||||
"^expect",
|
||||
"^assert",
|
||||
"^verify",
|
||||
"^searchAndExpect",
|
||||
"waitForOpen",
|
||||
"waitForClosed",
|
||||
"waitForRequest"
|
||||
]
|
||||
}
|
||||
],
|
||||
"playwright/max-nested-describe": "error",
|
||||
"playwright/no-duplicate-hooks": "error",
|
||||
"playwright/no-element-handle": "error",
|
||||
"playwright/no-eval": "error",
|
||||
"playwright/no-focused-test": "error",
|
||||
"playwright/no-force-option": "error",
|
||||
"playwright/no-networkidle": "error",
|
||||
"playwright/no-page-pause": "error",
|
||||
"playwright/no-skipped-test": "error",
|
||||
"playwright/no-unsafe-references": "error",
|
||||
"playwright/no-unused-locators": "error",
|
||||
"playwright/no-useless-await": "error",
|
||||
"playwright/no-useless-not": "error",
|
||||
"playwright/no-wait-for-navigation": "error",
|
||||
"playwright/no-wait-for-selector": "error",
|
||||
"playwright/no-wait-for-timeout": "error",
|
||||
"playwright/prefer-hooks-on-top": "error",
|
||||
"playwright/prefer-locator": "error",
|
||||
"playwright/prefer-to-have-count": "error",
|
||||
"playwright/prefer-to-have-length": "error",
|
||||
"playwright/prefer-web-first-assertions": "error",
|
||||
"playwright/prefer-native-locators": "error",
|
||||
"playwright/require-to-pass-timeout": "error",
|
||||
"playwright/valid-expect": "error",
|
||||
"playwright/valid-expect-in-promise": "error",
|
||||
"playwright/valid-title": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -318,6 +318,9 @@ When referencing Comfy-Org repos:
|
||||
- Find existing `!important` classes that are interfering with the styling and propose corrections of those instead.
|
||||
- NEVER use arbitrary percentage values like `w-[80%]` when a Tailwind fraction utility exists
|
||||
- Use `w-4/5` instead of `w-[80%]`, `w-1/2` instead of `w-[50%]`, etc.
|
||||
- NEVER use font-size classes (`text-xs`, `text-sm`, etc.) to size `icon-[...]` (iconify) icons
|
||||
- Iconify icons size via `width`/`height: 1.2em`, so font-size produces unpredictable results
|
||||
- Use `size-*` classes for explicit sizing, or set font-size on the **parent** container and let `1.2em` scale naturally
|
||||
|
||||
## Agent-only rules
|
||||
|
||||
|
||||
1
apps/website/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
dist/
|
||||
.astro/
|
||||
test-results/
|
||||
|
||||
@@ -10,11 +10,6 @@ export default defineConfig({
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
},
|
||||
build: {
|
||||
assetsPrefix: process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: undefined
|
||||
},
|
||||
i18n: {
|
||||
locales: ['en', 'zh-CN'],
|
||||
defaultLocale: 'en',
|
||||
|
||||
130
apps/website/e2e/homepage.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.describe('Homepage @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Comfy — Professional Control of Visual AI')
|
||||
})
|
||||
|
||||
test('HeroSection heading is visible', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /Professional Control/i, level: 1 })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('SocialProofBar logos are visible', async ({ page }) => {
|
||||
await expect(
|
||||
page.locator('img[src*="/icons/clients/"]').first()
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('ProductShowcase section is visible', async ({ page }) => {
|
||||
await expect(page.getByText('HOW', { exact: true }).first()).toBeVisible()
|
||||
await expect(
|
||||
page.getByText(/Connect models, processing steps, and outputs/)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('UseCaseSection is visible', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByText('Industries that create with ComfyUI')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('GetStartedSection with heading is visible', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Get started in minutes' })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('ProductCardsSection has 4 product cards', async ({ page }) => {
|
||||
const section = page.locator('section', {
|
||||
has: page.getByRole('heading', { name: /The AI creation/ })
|
||||
})
|
||||
const cards = section.locator('a[href]')
|
||||
await expect(cards).toHaveCount(4)
|
||||
})
|
||||
|
||||
test('CaseStudySpotlight section is visible', async ({ page }) => {
|
||||
const section = page.locator('section', {
|
||||
has: page.getByText('Customer Stories')
|
||||
})
|
||||
await expect(section).toBeVisible()
|
||||
await expect(
|
||||
section.getByRole('heading', { name: /See Comfy/i })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('BuildWhatSection is visible', async ({ page }) => {
|
||||
// "DOESN'T EXIST" is the actual badge text rendered in the Build What section
|
||||
await expect(page.getByText("DOESN'T EXIST")).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Product showcase accordion @interaction', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
})
|
||||
|
||||
test('first feature is active by default', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByText(/Build powerful AI pipelines by connecting nodes/).first()
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('clicking inactive feature expands it and collapses previous', async ({
|
||||
page
|
||||
}) => {
|
||||
const secondFeature = page
|
||||
.getByRole('button', { name: /App mode/i })
|
||||
.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()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Product cards links @smoke', () => {
|
||||
test('cards have correct hrefs', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
||||
const section = page.locator('section', {
|
||||
has: page.getByRole('heading', { name: /The AI creation/ })
|
||||
})
|
||||
|
||||
for (const href of ['/download', '/cloud', '/api', '/cloud/enterprise']) {
|
||||
await expect(section.locator(`a[href="${href}"]`)).toBeVisible()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Get started section links @smoke', () => {
|
||||
test('has download and cloud links', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
||||
const section = page.locator('section', {
|
||||
has: page.getByRole('heading', { name: 'Get started in minutes' })
|
||||
})
|
||||
|
||||
const downloadLink = section.getByRole('link', { name: 'Download Local' })
|
||||
await expect(downloadLink).toBeVisible()
|
||||
await expect(downloadLink).toHaveAttribute('href', '/download')
|
||||
|
||||
const cloudLink = section.getByRole('link', { name: 'Launch Cloud' })
|
||||
await expect(cloudLink).toBeVisible()
|
||||
await expect(cloudLink).toHaveAttribute('href', 'https://app.comfy.org')
|
||||
})
|
||||
})
|
||||
161
apps/website/e2e/navigation.spec.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.describe('Desktop navigation @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
})
|
||||
|
||||
test('logo links to homepage', async ({ page }) => {
|
||||
const logo = page.getByRole('link', { name: 'Comfy home' })
|
||||
await expect(logo).toBeVisible()
|
||||
await expect(logo).toHaveAttribute('href', '/')
|
||||
})
|
||||
|
||||
test('has all top-level nav items', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
|
||||
for (const label of [
|
||||
'PRODUCTS',
|
||||
'PRICING',
|
||||
'COMMUNITY',
|
||||
'RESOURCES',
|
||||
'COMPANY'
|
||||
]) {
|
||||
await expect(desktopLinks.getByText(label).first()).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('CTA buttons are visible', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopCTA = nav.getByTestId('desktop-nav-cta')
|
||||
await expect(
|
||||
desktopCTA.getByRole('link', { name: 'DOWNLOAD LOCAL' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
desktopCTA.getByRole('link', { name: 'LAUNCH CLOUD' })
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Desktop dropdown @interaction', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
})
|
||||
|
||||
test('hovering PRODUCTS shows dropdown items', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
const productsButton = desktopLinks.getByRole('button', {
|
||||
name: /PRODUCTS/i
|
||||
})
|
||||
await productsButton.hover()
|
||||
|
||||
const dropdown = productsButton.locator('..').getByTestId('nav-dropdown')
|
||||
for (const item of [
|
||||
'Comfy Local',
|
||||
'Comfy Cloud',
|
||||
'Comfy API',
|
||||
'Comfy Enterprise'
|
||||
]) {
|
||||
await expect(dropdown.getByText(item)).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('moving mouse away closes dropdown', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
await desktopLinks.getByRole('button', { name: /PRODUCTS/i }).hover()
|
||||
|
||||
const comfyLocal = nav.getByRole('link', { name: 'Comfy Local' }).first()
|
||||
await expect(comfyLocal).toBeVisible()
|
||||
|
||||
await page.locator('main').hover()
|
||||
await expect(comfyLocal).toBeHidden()
|
||||
})
|
||||
|
||||
test('Escape key closes dropdown', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
await desktopLinks.getByRole('button', { name: /PRODUCTS/i }).hover()
|
||||
|
||||
const comfyLocal = nav.getByRole('link', { name: 'Comfy Local' }).first()
|
||||
await expect(comfyLocal).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(comfyLocal).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Mobile menu @mobile', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
})
|
||||
|
||||
test('hamburger button is visible', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Toggle menu' })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('clicking hamburger opens mobile menu with nav items', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.getByRole('button', { name: 'Toggle menu' }).click()
|
||||
|
||||
const menu = page.locator('#site-mobile-menu')
|
||||
await expect(menu).toBeVisible()
|
||||
|
||||
for (const label of ['PRODUCTS', 'PRICING', 'COMMUNITY']) {
|
||||
await expect(menu.getByText(label).first()).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('clicking section with subitems drills down and back works', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.getByRole('button', { name: 'Toggle menu' }).click()
|
||||
|
||||
const menu = page.locator('#site-mobile-menu')
|
||||
await menu.getByText('PRODUCTS').first().click()
|
||||
|
||||
await expect(menu.getByText('Comfy Local')).toBeVisible()
|
||||
await expect(menu.getByText('Comfy Cloud')).toBeVisible()
|
||||
|
||||
await menu.getByRole('button', { name: /BACK/i }).click()
|
||||
await expect(menu.getByText('PRODUCTS').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('CTA buttons visible in mobile menu', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Toggle menu' }).click()
|
||||
|
||||
const menu = page.locator('#site-mobile-menu')
|
||||
await expect(
|
||||
menu.getByRole('link', { name: 'DOWNLOAD LOCAL' })
|
||||
).toBeVisible()
|
||||
await expect(menu.getByRole('link', { name: 'LAUNCH CLOUD' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Footer @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
})
|
||||
|
||||
test('footer is visible with link sections', async ({ page }) => {
|
||||
const footer = page.locator('footer')
|
||||
await expect(footer).toBeVisible()
|
||||
|
||||
for (const heading of ['Products', 'Resources', 'Company']) {
|
||||
await expect(
|
||||
footer.getByRole('heading', { name: heading }).first()
|
||||
).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('copyright text is visible', async ({ page }) => {
|
||||
await expect(
|
||||
page.locator('footer').getByText(/© \d{4} Comfy Org/)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
48
apps/website/e2e/responsive.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.describe('Desktop layout @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
})
|
||||
|
||||
test('navigation links visible and hamburger hidden', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
await expect(desktopLinks.getByText('PRODUCTS').first()).toBeVisible()
|
||||
await expect(desktopLinks.getByText('PRICING').first()).toBeVisible()
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Toggle menu' })).toBeHidden()
|
||||
})
|
||||
|
||||
test('product cards in grid layout', async ({ page }) => {
|
||||
const section = page.locator('section', {
|
||||
has: page.getByRole('heading', { name: /The AI creation/ })
|
||||
})
|
||||
const cards = section.locator('a[href]')
|
||||
await expect(cards).toHaveCount(4)
|
||||
|
||||
const firstBox = await cards.nth(0).boundingBox()
|
||||
const secondBox = await cards.nth(1).boundingBox()
|
||||
|
||||
expect(firstBox, 'first card bounding box').not.toBeNull()
|
||||
expect(secondBox, 'second card bounding box').not.toBeNull()
|
||||
expect(firstBox!.y).toBeCloseTo(secondBox!.y, 0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Mobile layout @mobile', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
})
|
||||
|
||||
test('hamburger visible and desktop nav hidden', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Toggle menu' })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('SocialProofBar shows two marquee rows on mobile', async ({ page }) => {
|
||||
const mobileContainer = page.getByTestId('social-proof-mobile')
|
||||
await expect(mobileContainer).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -6,16 +6,24 @@
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
"preview": "astro preview",
|
||||
"typecheck": "astro check",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:local": "PLAYWRIGHT_LOCAL=1 playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "catalog:",
|
||||
"@comfyorg/design-system": "workspace:*",
|
||||
"@comfyorg/tailwind-utils": "workspace:*",
|
||||
"@vercel/analytics": "catalog:",
|
||||
"gsap": "catalog:",
|
||||
"lenis": "catalog:",
|
||||
"vue": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "catalog:",
|
||||
"@astrojs/vue": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"astro": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
@@ -75,6 +83,16 @@
|
||||
"cwd": "apps/website",
|
||||
"command": "astro check"
|
||||
}
|
||||
},
|
||||
"test:e2e": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/website",
|
||||
"command": "playwright test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
apps/website/playwright.config.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
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'
|
||||
},
|
||||
webServer: {
|
||||
command: 'pnpm preview',
|
||||
port: 4321,
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'desktop',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
grepInvert: /@mobile/
|
||||
},
|
||||
{
|
||||
name: 'mobile',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
grep: /@mobile/
|
||||
}
|
||||
]
|
||||
})
|
||||
BIN
apps/website/public/fonts/PPFormula-Bold.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-CondensedSemibold.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-Light.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-Medium.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-Regular.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-Semibold.woff2
Normal file
4
apps/website/public/icons/arrow-up-right.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f2ff59" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M7 17L17 7" />
|
||||
<path d="M7 7h10v10" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
3
apps/website/public/icons/breadthumb.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="12" viewBox="0 0 20 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 1C20 1.55228 19.5523 2 19 2H17.5C16.6716 2 16 2.67157 16 3.5C16 4.32843 16.6716 5 17.5 5H19C19.5523 5 20 5.44772 20 6C20 6.55228 19.5523 7 19 7H7.5C6.67157 7 6 7.67157 6 8.5C6 9.32843 6.67157 10 7.5 10H19C19.5523 10 20 10.4477 20 11C20 11.5523 19.5523 12 19 12H1C0.447715 12 0 11.5523 0 11C0 10.4477 0.447715 10 1 10H2.5C3.32843 10 4 9.32843 4 8.5C4 7.67157 3.32843 7 2.5 7H1C0.447715 7 0 6.55228 0 6C0 5.44772 0.447715 5 1 5H12.5C13.3284 5 14 4.32843 14 3.5C14 2.67157 13.3284 2 12.5 2H1C0.447716 2 0 1.55228 0 1C0 0.447715 0.447715 0 1 0H19C19.5523 0 20 0.447715 20 1Z" fill="#211927"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 705 B |
3
apps/website/public/icons/clients/Amazon Studios.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
3
apps/website/public/icons/clients/Apple.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="52" height="64" viewBox="0 0 52 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M50.3711 21.8353C46.1108 24.4706 43.4795 28.9882 43.4795 34.0078C43.4795 39.6549 46.8627 44.8 52 46.9333C50.9976 50.1961 49.494 53.2078 47.6145 55.9686C44.8578 59.8588 41.9759 63.8745 37.7157 63.8745C33.4554 63.8745 32.2024 61.3647 27.1904 61.3647C22.3036 61.3647 20.5494 64 16.5398 64C12.5301 64 9.77349 60.3608 6.64096 55.8431C2.50602 49.5686 0.125301 42.2902 0 34.6353C0 22.2118 8.01928 15.5608 16.0386 15.5608C20.2988 15.5608 23.8072 18.3216 26.4386 18.3216C28.9446 18.3216 32.9542 15.4353 37.7157 15.4353C42.7277 15.3098 47.4892 17.6941 50.3711 21.8353ZM35.4602 10.1647C37.5904 7.6549 38.7181 4.51765 38.8434 1.2549C38.8434 0.878431 38.8434 0.376471 38.7181 0C35.0843 0.376471 31.7012 2.13333 29.3205 4.89412C27.1904 7.27843 25.9373 10.2902 25.812 13.5529C25.812 13.9294 25.812 14.3059 25.9373 14.6824C26.188 14.6824 26.5639 14.8078 26.8145 14.8078C30.1976 14.5569 33.3301 12.8 35.4602 10.1647Z" fill="#4D3762"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
11
apps/website/public/icons/clients/Autodesk.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="162" height="17" viewBox="0 0 162 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.145075 16.3507L14.2657 7.49409H21.6162C21.858 7.49409 22.0514 7.68874 22.0514 7.93206C22.0514 8.12671 21.9547 8.22403 21.858 8.2727L14.8944 12.4577C14.4591 12.7497 14.2657 13.285 14.2657 13.6743V16.3021H23.1153V0.973258C23.1153 0.681281 22.8735 0.437966 22.535 0.437966H14.1206L0 9.24596V16.3507H0.145075Z" fill="#4D3762"/>
|
||||
<path d="M116.06 16.3507H128.778V12.8957H120.074V10.0246H127.086V6.56949H120.074V3.89303H128.778V0.437966H116.06V16.3507Z" fill="#4D3762"/>
|
||||
<path d="M36.172 0.437966L30.0789 16.3507H34.2377L35.3016 13.3823H42.0718L43.0873 16.3507H47.3912L41.298 0.437966H36.172ZM36.5106 9.9759L38.735 3.74705L40.9112 9.9759H36.5106Z" fill="#4D3762"/>
|
||||
<path d="M69.7811 3.89303V16.3507H73.7948V3.89303H78.8725V0.437966H64.6551V3.89303H69.7811Z" fill="#4D3762"/>
|
||||
<path d="M58.8521 10.2192C58.8521 12.2631 57.5948 13.139 55.4187 13.139C53.3393 13.139 51.9852 12.2631 51.9852 10.2192V0.389303H47.9715V10.7058C47.9715 15.2802 52.0336 16.74 55.467 16.74C58.8521 16.74 62.9142 15.3288 62.9142 10.7058V0.389303H58.9005L58.8521 10.2192Z" fill="#4D3762"/>
|
||||
<path d="M162 0.437966H157.213L151.458 6.91013V0.437966H147.445V16.3507H151.458V10.3165L156.923 16.3507H162L154.65 8.51601L162 0.437966Z" fill="#4D3762"/>
|
||||
<path d="M106.582 0.437966H99.183V16.3507H106.582C112.24 16.3507 113.787 14.2096 113.787 8.2727C113.787 2.67646 112.288 0.437966 106.582 0.437966ZM106.388 12.8957H103.197V3.89303H106.388C108.903 3.89303 109.677 4.57431 109.677 8.2727C109.677 11.6304 108.855 12.8957 106.388 12.8957Z" fill="#4D3762"/>
|
||||
<path d="M140.965 6.9588L137.289 6.52083C135.162 6.27752 134.581 5.74222 134.581 5.01228C134.581 4.18501 135.694 3.45507 137.821 3.45507C139.901 3.45507 140.965 4.23367 141.158 5.40158H145.123C144.93 2.09251 142.125 0 138.063 0C133.227 0 130.761 1.94652 130.761 5.40158C130.761 8.17537 132.405 9.53793 135.742 9.87857L139.465 10.2679C140.868 10.4139 141.545 10.9978 141.545 11.6791C141.545 12.5064 140.578 13.2363 138.305 13.2363C135.645 13.2363 134.581 12.555 134.436 11.1925H130.568C130.761 14.8422 133.131 16.74 138.208 16.74C142.851 16.74 145.414 14.6962 145.414 11.6304C145.365 8.90532 143.818 7.29944 140.965 6.9588Z" fill="#4D3762"/>
|
||||
<path d="M88.254 0C82.4993 0 79.5495 3.55239 79.5495 8.32136C79.5495 13.139 82.5477 16.6914 88.254 16.6914C94.0086 16.6914 97.0068 13.139 97.0068 8.32136C97.0068 3.55239 94.0086 0 88.254 0ZM88.254 13.139C85.2558 13.139 83.6599 11.2898 83.6599 8.32136C83.6599 5.40158 85.3041 3.55239 88.254 3.55239C91.2522 3.55239 92.848 5.40158 92.848 8.32136C92.8964 11.3385 91.2522 13.139 88.254 13.139Z" fill="#4D3762"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
3
apps/website/public/icons/clients/EA.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M32.0001 0C14.3391 0 0 14.3369 0 32.0001C0 49.6633 14.318 64.0001 32.0001 64.0001C49.6822 64.0001 64.0001 49.6842 64.0001 32.0001C64.0001 14.3158 49.6822 0 32.0001 0ZM19.3431 19.3685H37.5927L34.8175 23.8105H16.5677L19.3431 19.3685ZM49.8504 41.5369L47.075 37.1159H38.9804L41.7556 32.6737H44.3207L41.2301 27.7264L32.6097 41.5369H9.5874L15.138 32.6737H11.0592L13.8345 28.2317H31.6216L28.8462 32.6737H20.3522L17.5769 37.1159H30.1289L41.2091 19.3685L55.0646 41.558H49.8293L49.8504 41.5369Z" fill="#4D3762"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 615 B |
5
apps/website/public/icons/clients/Harman.svg
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
3
apps/website/public/icons/clients/Hp.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M32.0892 0.000172855L25.2041 19.3042L29.8845 19.3057C33.256 19.3067 34.6908 21.965 33.6291 24.942L26.783 44.1367H19.894L27.4262 23.0162H23.8791L16.3476 44.1367H9.39908L24.8539 0.800758C10.6215 4.04761 0 16.7825 0 31.9988C0 46.9783 10.2925 59.552 24.1908 63.0382L39.805 19.1966H51.1132C53.452 19.1966 56.3937 20.7737 55.1225 24.3631L48.7735 42.2905C47.9625 44.5804 46.0347 45.0445 44.499 45.0445H37.5473L30.8049 63.9755C31.2019 63.99 31.5996 64 31.9999 64C49.6729 64 64 49.6733 64 31.9994C64 14.3555 49.7209 0.048001 32.0882 0L32.0892 0.000172855ZM45.4109 22.9645L38.8528 41.3738H42.3451L48.9044 22.9645H45.4109Z" fill="#4D3762"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 742 B |
3
apps/website/public/icons/clients/Lucid.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="152" height="10" viewBox="0 0 152 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M147.888 4.54103C147.888 2.60302 147.032 1.78601 145.04 1.78601H126.894V7.35304H145.04C147.013 7.35304 147.888 6.46004 147.888 4.54103ZM152.001 4.54103C152.001 7.35304 149.544 8.93005 147.236 8.93005H123.004V0.228001H147.255C149.563 0.228001 152.001 1.72901 152.001 4.54103ZM78.9504 1.78601H100.987V0.228001H76.7542C74.4464 0.228001 71.9896 1.74801 71.9896 4.54103C71.9896 7.35304 74.4464 8.93005 76.7542 8.93005H101.005V7.37204H78.9504C76.9776 7.37204 76.1028 6.46004 76.1028 4.54103C76.1028 2.60302 76.9403 1.78601 78.9504 1.78601ZM6.49545 7.33404C4.69012 7.33404 3.88982 6.55504 3.88982 4.80703V0.209001H0V4.80703C0 7.46704 2.32645 8.91105 4.50401 8.91105H28.0849V7.35304L6.49545 7.33404ZM60.1527 0.228001V7.37204H41.7644C39.9591 7.37204 39.1588 6.59304 39.1588 4.84503V0.228001H35.269V4.82603C35.269 7.48604 37.5954 8.93005 39.773 8.93005H64.0425V0.228001H60.1527ZM110.106 9.12005H113.884V0H110.106V9.12005Z" fill="#4D3762"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
apps/website/public/icons/clients/Netflix.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="134" height="37" viewBox="0 0 134 37" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M126.84 17.2267L134.001 36.1802C131.902 35.8924 129.762 35.4813 127.622 35.1524L123.589 24.6683L119.432 34.289C117.415 33.9601 115.399 33.8367 113.341 33.5901L120.625 17.0212L114.041 0H120.173L123.877 9.49731L127.828 0H134.001L126.84 17.2267ZM109.226 0H103.67V32.85C105.48 32.9734 107.374 33.0556 109.226 33.2612V0ZM98.8956 32.48C93.8335 32.1511 88.7715 31.8222 83.6271 31.7399V0H89.2653V26.3951C92.5166 26.4774 95.7678 26.724 98.8956 26.8885V32.48ZM77.5773 18.5013H69.8401V31.3699H64.2431V0H80.0878V5.6326H69.8401V12.8275H77.5773V18.5013ZM54.7362 5.6326V31.6577C52.8431 31.6577 50.9088 31.6577 49.0568 31.7399V5.6326H43.2128V0H60.6214V5.6326H54.7362ZM37.1219 18.7891C34.6526 18.7891 31.6894 18.7891 29.5905 18.9124V27.2996C32.9241 27.053 36.2165 26.8063 39.5912 26.6829V32.11L23.9522 33.3434V0H39.5912V5.6326H29.5905V13.2387C31.7717 13.2387 35.1464 13.1153 37.1219 13.1153V18.7891ZM5.76171 15.6233V35.358C3.74511 35.6046 1.93429 35.8924 0 36.1802V0H5.39131L12.7581 20.557V0H18.4375V33.8367C16.4209 34.1657 14.4043 34.289 12.2642 34.5768L5.76171 15.6233Z" fill="#4D3762"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
apps/website/public/icons/clients/Nike.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="106" height="38" viewBox="0 0 106 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M105.999 0L28.4741 32.9616C22.0197 35.7266 16.5806 37.1091 12.2294 37.1091C7.2979 37.1091 3.74436 35.3628 1.4962 31.8702C-1.33213 27.3589 -0.0992643 20.1554 4.75966 12.5152C7.66051 8.0767 11.3591 4.00197 14.9852 0.072763C14.1149 1.45526 6.64521 13.9705 14.8401 19.8643C16.4356 21.0285 18.7563 21.6106 21.5846 21.6106C23.8328 21.6106 26.4435 21.2468 29.3444 20.5192L105.999 0Z" fill="#4D3762"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 548 B |
3
apps/website/public/icons/clients/Pixomondo.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="152" height="16" viewBox="0 0 152 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.43281 3.52349H8.38328C9.61244 3.52349 10.1515 4.08285 10.1515 5.38666C10.1515 6.69149 9.61244 7.25287 8.38328 7.25287H4.43281V3.52349ZM0 15.2522H4.43281V10.3194H10.0426C13.6161 10.3194 14.7353 7.83251 14.7353 5.38666C14.7353 2.58988 13.4499 0.456918 9.83538 0.456918H0V15.2522ZM16.6176 15.2522H21.0519V0.456918H16.6176V15.2522ZM22.5223 15.2522H28.0176L30.8508 10.7764L33.6836 15.2522H39.1758L33.4019 7.56323L38.5633 0.456918H33.2914L30.8508 4.43479L28.4097 0.456918H23.1379L28.2982 7.56323L22.5223 15.2522ZM39.4383 7.8528C39.4383 13.987 43.4065 15.7066 48.1002 15.7066C52.796 15.7066 56.7642 13.987 56.7642 7.8528C56.7637 1.72067 52.7955 0 48.1002 0C43.4065 0 39.4383 1.72067 39.4383 7.8528ZM44.1346 7.8528C44.1346 4.76594 45.7356 3.31557 48.0997 3.31557C50.4856 3.31557 52.0684 4.76594 52.0684 7.8528C52.0684 10.961 50.4856 12.3895 48.0997 12.3895C45.7361 12.3895 44.1346 10.9615 44.1346 7.8528ZM58.7579 15.2522H62.9288V4.18579H62.9683L66.4694 15.2522H70.1199L73.624 4.18579H73.6605V15.2522H77.8339V0.456918H71.3516L68.3136 10.2155H68.2771L65.2402 0.456918H58.7579V15.2522ZM79.8281 7.8528C79.8281 13.987 83.7943 15.7066 88.491 15.7066C93.1863 15.7066 97.154 13.987 97.154 7.8528C97.1535 1.72067 93.1858 0 88.4905 0C83.7943 0 79.8281 1.72067 79.8281 7.8528ZM84.5223 7.8528C84.5223 4.76594 86.1254 3.31557 88.4905 3.31557C90.8759 3.31557 92.4587 4.76594 92.4587 7.8528C92.4587 10.961 90.8759 12.3895 88.4905 12.3895C86.1254 12.3895 84.5223 10.9615 84.5223 7.8528ZM99.1457 15.2522H103.432V5.90646H103.47L109.84 15.2517H114.739V0.456918H110.455V9.718H110.417L104.251 0.456918H99.1457V15.2522ZM117.424 15.2522H126.459C130.277 15.2522 133.368 13.6776 133.368 7.8533C133.368 2.92255 131.153 0.457425 127.035 0.457425H117.424V15.2522ZM121.858 3.77198H125.21C127.578 3.77198 128.787 5.07782 128.787 7.83251C128.787 9.86253 128.098 11.9346 125.563 11.9346H121.858V3.77198ZM134.674 7.8528C134.674 13.987 138.641 15.7066 143.339 15.7066C148.033 15.7066 152 13.987 152 7.8528C151.999 1.72067 148.033 0 143.338 0C138.641 0 134.674 1.72067 134.674 7.8528ZM139.37 7.8528C139.37 4.76594 140.972 3.31557 143.338 3.31557C145.722 3.31557 147.305 4.76594 147.305 7.8528C147.305 10.961 145.722 12.3895 143.338 12.3895C140.972 12.3895 139.37 10.9615 139.37 7.8528Z" fill="#4D3762"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
11
apps/website/public/icons/clients/Tencent.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="140" height="19" viewBox="0 0 140 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M91.1909 5.7923H86.9427L88.6109 8.1785H90.7641L88.9601 18.3776H92.7233L94.5274 8.1785H99.2411L91.8116 2.25149L91.1909 5.7923Z" fill="#4D3762"/>
|
||||
<path d="M17.5748 0H0.465556L0 2.67485H6.65358L3.86024 18.3776H7.62349L10.4168 2.67485H17.1092L17.5748 0Z" fill="#4D3762"/>
|
||||
<path d="M22.3079 10.7571H15.7707C16.1587 9.46782 17.439 7.69741 19.7474 7.69741C22.2691 7.67817 22.5407 9.58328 22.3079 10.7571ZM20.3681 5.52289C15.5767 5.52289 12.5118 8.50564 11.9105 11.9887C11.2315 15.8374 13.6175 18.6662 18.4477 18.6662C20.6009 18.6662 22.6183 18.2621 24.2671 17.3577L24.7715 14.4904C23.1614 15.6257 21.1246 16.2608 19.3982 16.2608C16.9928 16.2608 15.0724 15.2601 15.3828 12.9894H25.334C25.4504 12.72 25.6056 12.2389 25.7026 11.6616C26.1875 8.87127 25.1982 5.52289 20.3681 5.52289Z" fill="#4D3762"/>
|
||||
<path d="M36.9923 5.52289C34.6452 5.52289 32.8799 6.48507 32.0458 7.46649H31.9488L28.6317 5.52289C28.5153 6.61977 28.3407 8.10153 28.0886 9.54479L26.5367 18.3776H30.3L31.5996 10.9688C31.6578 10.6032 31.7936 10.2183 31.9488 9.96815C32.5114 8.98673 33.6946 8.10153 35.1883 8.10153C37.2445 8.10153 37.8071 9.48706 37.4967 11.2767L36.2746 18.3776H40.0378L41.3569 10.9111C42.0553 6.92767 39.5917 5.52289 36.9923 5.52289Z" fill="#4D3762"/>
|
||||
<path d="M82.1707 5.52289C79.8235 5.52289 78.0583 6.48507 77.2242 7.46649H77.1272L73.8101 5.52289C73.6937 6.61977 73.5191 8.10153 73.2669 9.54479L71.7151 18.3776H75.4783L76.8168 10.9688C76.875 10.6032 77.0108 10.2183 77.166 9.96815C77.7285 8.98673 78.9118 8.10153 80.4055 8.10153C82.4617 8.10153 83.0242 9.48706 82.7139 11.2767L81.453 18.3776H85.2162L86.5353 10.9111C87.2336 6.92767 84.7701 5.52289 82.1707 5.52289Z" fill="#4D3762"/>
|
||||
<path d="M67.5057 10.7571H60.9491C61.3371 9.46782 62.6173 7.69741 64.9257 7.69741C67.4475 7.67817 67.7191 9.58328 67.5057 10.7571ZM65.5465 5.52289C60.7551 5.52289 57.6902 8.50564 57.0889 11.9887C56.4099 15.8374 58.7959 18.6662 63.626 18.6662C65.7792 18.6662 67.7967 18.2621 69.4455 17.3577L69.9499 14.4904C68.3398 15.6257 66.303 16.2608 64.5766 16.2608C62.1712 16.2608 60.2508 15.2601 60.5611 12.9894H70.5124C70.6288 12.72 70.784 12.2389 70.881 11.6616C71.3659 8.87127 70.3766 5.52289 65.5465 5.52289Z" fill="#4D3762"/>
|
||||
<path d="M42.9476 11.9117C42.2686 15.8182 45.0232 18.647 49.6787 18.647C51.541 18.647 53.7524 18.1274 54.5865 17.8003L55.1878 14.4326C53.8882 15.8374 52.1035 16.1261 50.8426 16.1261C48.2821 16.1261 46.3423 14.6443 46.7884 12.0657C47.1764 9.75647 49.3878 7.98606 52.2587 7.98606C53.4226 7.98606 55.0132 8.37094 56.0219 9.6795L56.6039 6.25415C55.7892 5.94625 54.4895 5.50365 52.5691 5.50365C47.351 5.52289 43.6071 8.15926 42.9476 11.9117Z" fill="#4D3762"/>
|
||||
<path d="M116.35 8.46715C116.001 8.06304 115.613 7.428 115.38 6.96616H114.604C114.178 7.48573 113.557 8.06304 113.014 8.46715H116.35ZM115.574 10.0259H110.977L112.354 10.6417L112.102 11.8925H115.148L115.574 10.0259ZM117.766 9.73723L117.281 11.8925H118.31C119.008 11.8925 119.512 12.0465 119.376 12.8932C119.144 14.4326 118.717 16.5687 118.387 18.3776H112.8L112.335 16.7419H116.447C116.641 15.8759 116.874 14.7983 116.99 13.9323C117.029 13.6437 116.913 13.5282 116.622 13.5282H109.658L110.279 10.1798C109.774 10.4108 109.018 10.6609 108.281 10.7956L108.727 8.77505C110.026 8.46715 111.346 7.83212 112.16 6.94691H109.057L109.406 5.36895H113.247C113.46 5.08029 113.615 4.69542 113.712 4.42601H109.561L109.871 2.79031H111.365L110.841 0.384871H112.956L113.46 2.8288H114.197C114.43 2.02057 114.604 0.942933 114.624 0.0384871H116.777C116.738 0.942933 116.602 2.00133 116.408 2.8288H117.262C117.592 2.0783 117.941 1.1931 118.213 0.384871H120.366C120.075 1.13537 119.687 2.05906 119.338 2.8288H120.851L120.443 4.4645H115.943C115.865 4.73391 115.691 5.13802 115.555 5.40743H121.083L120.715 6.9854H117.495C118.193 8.00531 119.842 8.77505 121.122 9.04446L120.405 10.8341C119.493 10.6417 118.542 10.2183 117.766 9.73723ZM115.419 15.9336H108.3L108.009 14.3364H115.768V14.3557L115.419 15.9336ZM107.039 2.27074H105.992C105.817 3.2714 105.662 4.31055 105.449 5.38819H106.865L107.388 2.65561C107.466 2.34771 107.33 2.27074 107.039 2.27074ZM106.593 7.04313H105.138C104.925 8.12077 104.692 9.25614 104.459 10.3723H105.972L106.593 7.04313ZM109.522 1.69343C108.494 7.2933 107.563 12.8162 106.515 18.3968H104.401C104.828 16.2993 105.293 14.067 105.662 12.008H104.149C103.683 14.1632 103.198 16.3378 102.752 18.3968H100.638C102.422 12.7585 103.295 6.94691 104.304 0.538819H108.261C109.289 0.538819 109.697 0.712011 109.522 1.69343Z" fill="#4D3762"/>
|
||||
<path d="M138.639 0.558062H128.843L129.27 2.40544H136.583C137.068 2.40544 137.3 2.46317 137.184 3.00199C136.912 4.50299 136.621 6.02323 136.33 7.50498H133.149L133.692 4.34904L131.462 3.05972L130.647 7.50498H128.28L127.97 9.35236H130.298C129.813 12.3543 129.231 15.4141 128.61 18.3968H131.151C131.733 15.3756 132.334 12.3543 132.839 9.35236H136.001C135.477 12.3736 134.895 15.4718 134.294 18.3968H138.775L139.551 16.5302H137.165C138.115 11.7578 139.085 6.79297 139.939 1.98208C140.132 0.962177 139.919 0.558062 138.639 0.558062ZM127.873 4.38753C127.562 2.92502 127.155 1.40478 126.825 0H124.459C124.769 1.44326 125.118 2.98275 125.39 4.38753H127.873ZM122.694 5.36895H127.097C126.651 8.27472 126.146 11.2382 125.584 14.1825C126.418 13.5667 127.31 12.8932 128.106 12.2004L127.64 14.8945C126.146 16.1453 124.09 17.6078 122.209 18.6662C123.023 14.8945 123.78 10.9111 124.323 7.21632H122.344L122.694 5.36895Z" fill="#4D3762"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
3
apps/website/public/icons/clients/Ubisoft.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="151" height="48" viewBox="0 0 151 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.3983 15.028V25.7062C54.3983 30.042 57.3539 32.8056 61.6585 32.8056C66.1863 32.8056 68.9577 29.8912 68.9577 25.4138V15.028H65.1254V25.2187C65.1254 27.6601 63.8959 29.1741 61.6993 29.1741C59.292 29.1741 58.2308 27.433 58.2308 25.4138V15.028H54.3983ZM70.821 15.1755V32.6314H76.9251C80.3058 32.6314 83.1785 30.9071 83.1785 27.4872C83.1785 23.9175 80.0548 23.1263 80.0548 23.1263C80.0548 23.1263 82.3678 22.4838 82.3678 19.4298C82.3678 16.3778 79.4442 15.1755 76.9251 15.1755H70.821ZM74.6821 18.4911H76.535C77.821 18.4911 78.6438 19.1684 78.6438 20.347C78.6438 21.4013 77.9457 22.1968 76.6325 22.1968H74.6822L74.6821 18.4911ZM74.6821 24.9274H76.73C78.6558 24.9274 79.3235 25.9166 79.3235 27.0971C79.3235 28.4708 78.4597 29.2761 76.9251 29.2761H74.6822V24.9275L74.6821 24.9274ZM84.629 15.1743H88.481V32.6301H84.629V15.1743ZM126.66 32.6301H122.808V15.1743H134.9V18.7825H126.66V22.7808H134.168V25.7062H126.66V32.6301ZM136.314 15.1743V18.7825H141.287V32.6301H145.188V18.7825H150.064V15.1743H136.314ZM92.8785 26.8259L89.8184 29.0632C89.8184 29.0632 90.8758 32.9305 96.1768 32.9305C99.7069 32.9305 102.304 30.8704 102.304 27.2487C102.304 21.4121 94.1433 22.6503 94.1433 19.6487C94.1433 18.8587 94.8457 18.161 96.1422 18.161C98.2224 18.161 99.0129 20.2015 99.0129 20.2015L102.074 18.0533C102.074 18.0533 100.878 14.8664 96.2959 14.8664C92.53 14.8664 90.5296 17.2825 90.5296 19.7755C90.5296 25.7235 98.4532 24.4325 98.4532 27.5541C98.4532 28.6075 97.6231 29.3815 96.3303 29.3726C93.7688 29.3726 92.8785 26.8259 92.8785 26.8259ZM112.251 14.8584C107.715 14.8584 105.693 17.3207 105.693 17.3207L106.278 17.9058C106.278 17.9058 103.343 19.9904 103.343 24.467C103.343 29.6181 107.316 32.9267 112.272 32.9267C118.031 32.9267 121.299 28.6774 121.299 23.9001C121.299 18.8059 117.568 14.8584 112.251 14.8584ZM112.333 18.4818C115.409 18.4818 117.404 20.8326 117.404 23.8849C117.404 26.9056 115.433 29.3216 112.333 29.3216C109.271 29.3216 107.259 27.1833 107.259 24.3451C107.259 21.762 109.009 20.6363 109.009 20.6363L108.375 20.0024C108.375 20.0024 109.724 18.4818 112.333 18.4818ZM21.5432 0C6.78898 0 0.420489 13.1328 0.420489 13.1328L2.29511 14.4748C2.29511 14.4748 -0.0639514 18.961 0.00133032 24.7151C0.00133032 36.4616 9.35181 47.5268 23.1165 47.5268C35.6212 47.5268 45.9508 37.3488 45.9508 24.744C45.9508 8.30461 33.2024 0 21.5432 0ZM21.6712 4.00737C33.1987 4.00737 42.1437 13.2966 42.1437 24.0958C42.1437 35.5436 33.5336 43.5049 23.5005 43.5049C16.1163 43.5049 9.45504 37.8817 9.45504 30.1718C9.45504 25.7188 11.8219 22.6143 14.4741 20.9597L14.8622 21.3809C14.1364 21.9488 12.0668 25.2733 12.0668 28.5409C12.0668 34.6069 16.8237 38.808 22.8171 38.808C30.4516 38.808 35.5432 32.2965 35.5432 24.7358C35.5432 15.9391 27.7784 8.47099 18.4339 8.47099C13.2655 8.47099 8.87885 10.6937 6.3582 12.6559L5.96594 12.3009C9.26362 7.21623 14.9744 4.00737 21.6712 4.00737ZM18.2316 12.4226C24.1947 12.4226 30.0166 16.3676 31.4697 21.9672L30.9412 22.1674C28.332 18.7219 24.0566 15.8664 19.0079 15.8664C9.71774 15.8664 4.83881 24.3083 5.55505 31.359L5.00797 31.5593C5.00797 31.5593 3.98187 29.2952 3.98187 26.6105C3.98187 18.8359 10.3665 12.4226 18.2316 12.4226ZM22.1729 22.1798C24.9961 22.1798 27.2539 24.4681 27.2539 27.1844C27.2539 29.4323 25.8107 30.7644 25.8107 30.7644L27.5202 31.9598C27.5202 31.9598 25.8251 34.621 22.3649 34.621C19.0551 34.621 16.1278 31.9496 16.1278 28.3571C16.1278 24.6554 19.3119 22.1798 22.1729 22.1798Z" fill="#4D3762"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
4
apps/website/public/icons/close.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f2ff59" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6L6 18" />
|
||||
<path d="M6 6l12 12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
5
apps/website/public/icons/cubic.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="213" height="252" viewBox="0 0 213 252" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M113.891 241.07C114.663 240.74 115.378 240.339 116.018 239.858C119.563 237.196 119.19 233.305 115.185 231.166L82.5103 213.57C80.7481 212.649 80.5683 210.948 82.1141 209.787C82.3951 209.576 82.7135 209.405 83.0339 209.255L118.439 193.433C119.996 192.739 122.105 192.742 123.552 193.515L156.203 210.912C163.084 214.586 172.995 214.602 180.347 211.325L197.65 203.799C203.417 201.279 204.58 194.765 199.211 191.703L161.926 171.792C155.081 168.137 145.219 168.107 137.894 171.318L124.319 177.251C122.76 177.923 120.684 177.918 119.237 177.145L109.763 172.086C102.971 168.459 93.2401 168.399 85.9246 171.535L40.2587 191.485C34.7284 194.099 32.5998 199.828 38.9002 204.268L45.2838 207.677C47.0303 208.61 47.2102 210.311 45.6488 211.484C45.3833 211.683 45.0826 211.864 44.7446 212.004L31.2805 217.876C30.5753 218.202 29.9073 218.567 29.3296 219.001C25.8007 221.651 26.1718 225.521 30.1589 227.65L70.1782 249.187C77.041 252.852 86.9186 252.87 94.2571 249.626L113.858 241.072L113.891 241.07Z" fill="#F2FF59"/>
|
||||
<path d="M104.261 118.957C104.273 117.985 104.096 117.016 103.841 116.059C102.429 110.759 97.068 108.263 92.0768 110.485L51.5756 128.509C49.405 129.501 47.1266 128.426 46.511 126.114C46.3991 125.694 46.3594 125.268 46.3478 124.855L46.5093 79.6575C46.4985 77.6776 47.6894 75.6852 49.4924 74.8828L90.1351 56.7436C98.7103 52.9274 104.316 43.5678 104.261 34.2174V9.01374C104.247 8.13187 104.245 7.21909 104.015 6.35524C102.609 1.07876 97.4477 -1.40313 92.4785 0.808285L43.2922 22.6974C34.7611 26.494 29.172 35.7971 29.1585 45.0737L29.11 62.2525C29.0926 64.2188 27.9174 66.1781 26.1144 66.9804L14.3071 72.235C5.84186 76.0022 0.104287 85.151 2.96039e-05 94.3635V156.109C-0.00222287 157.024 0.124117 157.961 0.360394 158.848C1.77184 164.148 6.96164 166.643 11.9528 164.422L23.5623 159.255C25.7391 158.287 28.0175 159.362 28.6393 161.697C28.745 162.093 28.8067 162.51 28.7962 162.932L28.7324 179.961C28.7521 180.867 28.844 181.766 29.0741 182.63C30.4793 187.906 35.6409 190.388 40.6101 188.177L89.8059 166.231C98.3591 162.425 104.282 153.098 104.261 143.785V118.99V118.957Z" fill="#F2FF59"/>
|
||||
<path d="M212.999 155.785C213.007 154.849 212.925 153.795 212.663 152.613C211.209 146.063 205.925 138.562 200.86 135.857L159.759 113.908C157.556 112.757 155.236 109.484 154.603 106.628C154.487 106.108 154.445 105.654 154.432 105.24L154.436 61.2966C154.418 59.3542 155.621 58.5727 157.451 59.5498L198.696 81.525C207.398 86.1723 213.089 82.505 212.999 73.3274V48.857C212.982 47.9834 212.86 46.9755 212.624 45.908C211.177 39.3878 205.921 31.9276 200.879 29.2345L150.963 2.57821C142.306 -2.04523 136.658 1.57626 136.677 10.6146L136.689 27.3292C136.678 29.2308 135.491 29.9954 133.661 29.0182L121.679 22.6193C113.088 18.0316 107.475 21.5446 107.402 30.4318L107.435 90.4925C107.436 91.3831 107.568 92.4198 107.811 93.5161C109.265 100.065 114.548 107.566 119.614 110.271L131.395 116.563C133.604 117.743 135.924 121.016 136.564 123.901C136.673 124.391 136.737 124.858 136.728 125.26L136.724 141.813C136.747 142.716 136.844 143.683 137.08 144.75C138.528 151.271 143.783 158.731 148.825 161.424L198.75 188.034C207.43 192.67 213.053 189.019 212.999 179.911V155.802V155.785Z" fill="#F2FF59"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
3
apps/website/public/icons/logo.svg
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
3
apps/website/public/icons/logomark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="45" height="48" viewBox="0 0 45 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M35.6487 36.021C35.733 35.7387 35.7791 35.4411 35.7791 35.1283C35.7791 33.3963 34.3675 31.9924 32.6262 31.9924H18.4956C17.7361 32 17.1147 31.3896 17.1147 30.6342C17.1147 30.4969 17.1377 30.3672 17.1684 30.2451L20.9734 17.0606C21.1345 16.4807 21.6715 16.0534 22.3005 16.0534L36.4848 16.0382C39.4766 16.0382 42.0005 14.0315 42.76 11.2923L44.8926 3.94468C44.9616 3.68526 45 3.40296 45 3.12065C45 1.39628 43.5961 0 41.8624 0L24.7017 0C21.7252 0 19.209 1.99142 18.4342 4.70005L16.992 9.71292C16.8232 10.2852 16.2939 10.7048 15.6648 10.7048H11.5453C8.59189 10.7048 6.0987 12.6581 5.30089 15.3362L0.11507 33.3505C0.0383566 33.6175 0 33.9075 0 34.1974C0 35.9294 1.41152 37.3333 3.15292 37.3333H7.20338C7.96284 37.3333 8.58421 37.9437 8.58421 38.7067C8.58421 38.8364 8.56887 38.9661 8.53051 39.0882L7.09598 44.0553C7.02694 44.3224 6.98091 44.597 6.98091 44.8794C6.98091 46.6037 8.38476 48 10.1185 48L27.2869 47.9847C30.2711 47.9847 32.7873 45.9857 33.5544 43.2618L35.641 36.0286L35.6487 36.021Z" fill="#F2FF59"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
apps/website/public/icons/node-left.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="42.66" height="94.14" viewBox="0 0 42.66 94.14" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.5464 94.1386H42.6566V0C30.658 0 18.2687 9.66676 14.9543 21.5781L0.813667 72.5605C-2.48629 84.4719 4.54783 94.1386 16.5464 94.1386Z" fill="#F2FF59"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 304 B |
3
apps/website/public/icons/node-link.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="32" viewBox="0 0 20 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 32V0C20 5.39616 15.5172 9.78053 10 9.78053C4.48276 9.78053 0 5.416 0 0V32C0 26.6038 4.48276 22.2195 10 22.2195C15.5172 22.2195 20 26.6038 20 32Z" fill="#F2FF59"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
3
apps/website/public/icons/node-right.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="42.66" height="94.14" viewBox="0 0 42.66 94.14" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M26.1101 0H0V94.1387C11.9985 94.1387 24.3878 84.4719 27.7022 72.5606L41.8428 21.5781C45.1428 9.66678 38.1087 0 26.1101 0Z" fill="#F2FF59"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 291 B |
3
apps/website/public/icons/node-union.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="87.7" height="95.23" viewBox="0 0 87.7 95.23" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M87.6679 0C75.187 0 62.2689 9.77836 58.8171 21.8272L54.0387 38.5685C53.1343 41.7321 50.2553 43.8966 46.9843 43.8966C42.1156 43.8966 38.5883 39.2042 39.9299 34.4967L43.5475 21.8272C47.0145 9.77836 39.6736 0 27.1927 0H0V95.2254C12.4809 95.2254 25.399 85.4471 28.8508 73.3982L33.5839 56.7932C34.4883 53.6296 37.3674 51.465 40.6383 51.465C45.5222 51.465 49.0343 56.1574 47.6928 60.865L44.1053 73.3982C40.6685 85.4471 47.9942 95.2254 60.4902 95.2254H87.6829V0H87.6679Z" fill="#F2FF59"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 631 B |
4
apps/website/public/icons/plus.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="M3.71973 11.5878H20.2197" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.9697 3.33777V19.8378" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 335 B |
BIN
apps/website/public/videos/footer-logo-seq/Logo00.webp
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo01.webp
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo02.webp
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo03.webp
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo04.webp
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo05.webp
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo06.webp
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo07.webp
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo08.webp
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo09.webp
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo10.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo11.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo12.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo13.webp
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo14.webp
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo15.webp
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo16.webp
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo17.webp
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo18.webp
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo19.webp
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo20.webp
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo21.webp
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo22.webp
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo23.webp
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo24.webp
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo25.webp
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo26.webp
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo27.webp
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo28.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo29.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo30.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo31.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo32.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo33.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo34.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo35.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo36.webp
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo37.webp
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo38.webp
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo39.webp
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo40.webp
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo41.webp
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo42.webp
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
apps/website/public/videos/footer-logo-seq/Logo43.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |