From de29369ba7a24e9bade32d29e5f8316e249553da Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Thu, 9 Jul 2026 11:05:59 -0700 Subject: [PATCH] feat: adopt shared design system across ClawHub UI (#3026) * chore: install OpenClaw design system * feat: adopt shared design system palette * chore: automate design system updates * feat: add weekly design system audit * fix: prevent mobile skills tab overlap * fix: harden design audit automation * fix: validate audit changes before execution * fix: scope design system clone credentials * fix: align audit with installed design release * fix: preserve audit artifacts and access * feat: adopt shared design system on landing page * fix: align icon geometry with design tokens * chore: pin design system to v0.0.1 * chore: pin design system to v0.0.1 * chore: pin design system to v0.0.1 * chore: pin design system to v0.0.1 * chore: pin design system to v0.0.1 * fix: migrate ClawHub UI to design tokens * fix: use public design system installs * fix: use public design system distribution --- .agents/skills/openclaw-brand/SKILL.md | 32 + .../openclaw-brand/references/asset-rights.md | 25 + .../openclaw-brand/references/identity.md | 42 + .agents/skills/openclaw-design-audit/SKILL.md | 45 ++ .../references/fix-policy.md | 25 + .../references/github-pr-delivery.md | 55 ++ .../references/report-format.md | 46 ++ .../references/rubric.md | 37 + .../skills/openclaw-design-system/SKILL.md | 41 + .../references/consumer-adapters.md | 55 ++ .../references/tokens.md | 51 ++ .agents/skills/openclaw-design/SKILL.md | 31 + .../skills/openclaw-marketing-pages/SKILL.md | 32 + .../references/page-patterns.md | 36 + .github/dependabot.yml | 6 +- .github/workflows/design-system-audit.yml | 271 +++++++ .../workflows/update-design-system-skills.yml | 117 +++ .oxfmtrc.jsonc | 1 + bun.lock | 3 + design-audits/latest/design-audit.json | 47 ++ design-audits/latest/design-audit.md | 38 + package.json | 6 + scripts/design-audit/browser-check.ts | 147 ++++ scripts/design-audit/codex-output.schema.json | 43 + .../design-audit-workflow.test.ts | 105 +++ scripts/design-audit/design-audit.test.ts | 115 +++ scripts/design-audit/finalize.ts | 152 ++++ scripts/design-audit/report.ts | 224 ++++++ scripts/design-audit/run-codex.ts | 117 +++ scripts/design-audit/source-check.ts | 212 +++++ scripts/design-audit/validate-changes.ts | 8 + skills-lock.json | 35 + src/__tests__/ui-design-contract.test.ts | 128 ++- src/components/DeploymentDriftBanner.tsx | 2 +- src/components/DevPersonaFab.tsx | 2 +- src/components/EmptyState.tsx | 4 +- src/components/ErrorBoundary.tsx | 2 +- src/components/HomeAppsSection.tsx | 28 +- src/components/HomeBringSkillsSection.tsx | 18 +- src/components/HomeListingSection.tsx | 40 +- .../HomePopularPublishersSection.tsx | 19 +- src/components/HomePromotionsSection.tsx | 6 +- src/components/OfficialBadge.tsx | 6 +- src/components/PackageSourceChooser.tsx | 12 +- src/components/PluginVersionsPanel.tsx | 2 +- src/components/SignInPrompt.tsx | 4 +- src/components/SkillDiffCard.tsx | 22 +- src/components/SkillOwnershipPanel.tsx | 10 +- src/components/SkillPublishSuccessDialog.tsx | 4 +- src/components/SkillReportDialog.tsx | 2 +- src/components/SkillSecurityScanResults.tsx | 2 +- src/components/SkillShortSummaryField.tsx | 11 +- src/components/UploadDropzoneDecor.tsx | 2 +- .../skeletons/BrowseResultsSkeleton.tsx | 4 +- src/components/ui/alert.tsx | 6 +- src/components/ui/avatar.tsx | 7 +- src/components/ui/badge.tsx | 2 +- src/components/ui/button.tsx | 4 +- src/components/ui/card.tsx | 2 +- src/components/ui/dropdown-menu.tsx | 6 +- src/components/ui/input.tsx | 4 +- src/components/ui/select.tsx | 6 +- src/components/ui/sheet.tsx | 2 +- src/components/ui/textarea.tsx | 4 +- src/components/ui/toggle-group.tsx | 4 +- src/components/ui/tooltip.tsx | 2 +- src/design-system.css | 212 +++++ src/publisher-profile.css | 14 +- src/routes/-management/PromotionsPage.tsx | 4 +- src/routes/-settings.test.tsx | 3 +- src/routes/__root.tsx | 15 +- src/routes/add.tsx | 4 +- src/routes/import.tsx | 8 +- src/routes/index.tsx | 10 +- src/routes/settings.tsx | 79 +- src/routes/skills/index.tsx | 2 +- src/routes/skills/publish.tsx | 4 +- src/routes/stars.tsx | 4 +- src/routes/user/$handle.tsx | 2 +- src/styles.css | 739 +++++++++--------- 80 files changed, 3110 insertions(+), 569 deletions(-) create mode 100644 .agents/skills/openclaw-brand/SKILL.md create mode 100644 .agents/skills/openclaw-brand/references/asset-rights.md create mode 100644 .agents/skills/openclaw-brand/references/identity.md create mode 100644 .agents/skills/openclaw-design-audit/SKILL.md create mode 100644 .agents/skills/openclaw-design-audit/references/fix-policy.md create mode 100644 .agents/skills/openclaw-design-audit/references/github-pr-delivery.md create mode 100644 .agents/skills/openclaw-design-audit/references/report-format.md create mode 100644 .agents/skills/openclaw-design-audit/references/rubric.md create mode 100644 .agents/skills/openclaw-design-system/SKILL.md create mode 100644 .agents/skills/openclaw-design-system/references/consumer-adapters.md create mode 100644 .agents/skills/openclaw-design-system/references/tokens.md create mode 100644 .agents/skills/openclaw-design/SKILL.md create mode 100644 .agents/skills/openclaw-marketing-pages/SKILL.md create mode 100644 .agents/skills/openclaw-marketing-pages/references/page-patterns.md create mode 100644 .github/workflows/design-system-audit.yml create mode 100644 .github/workflows/update-design-system-skills.yml create mode 100644 design-audits/latest/design-audit.json create mode 100644 design-audits/latest/design-audit.md create mode 100644 scripts/design-audit/browser-check.ts create mode 100644 scripts/design-audit/codex-output.schema.json create mode 100644 scripts/design-audit/design-audit-workflow.test.ts create mode 100644 scripts/design-audit/design-audit.test.ts create mode 100644 scripts/design-audit/finalize.ts create mode 100644 scripts/design-audit/report.ts create mode 100644 scripts/design-audit/run-codex.ts create mode 100644 scripts/design-audit/source-check.ts create mode 100644 scripts/design-audit/validate-changes.ts create mode 100644 src/design-system.css diff --git a/.agents/skills/openclaw-brand/SKILL.md b/.agents/skills/openclaw-brand/SKILL.md new file mode 100644 index 00000000..9366d4e4 --- /dev/null +++ b/.agents/skills/openclaw-brand/SKILL.md @@ -0,0 +1,32 @@ +--- +name: openclaw-brand +description: Apply OpenClaw visual identity to logos, typography, imagery, voice, documents, presentations, social graphics, and launch materials. Use when the task changes brand identity rather than ordinary product UI or public-page composition. +--- + +# OpenClaw Brand + +Use the canonical identity without turning every surface into a marketing page. + +## Workflow + +1. Read [identity.md](references/identity.md) for palette, typography, logo, and voice. +2. Read [asset-rights.md](references/asset-rights.md) before copying or redistributing assets. +3. Identify the artifact's audience and whether it is product, documentation, or marketing. +4. Use semantic design tokens when the artifact is code. +5. Keep status colors functional; do not use them as arbitrary decoration. +6. Verify contrast, responsive cropping, and text legibility. +7. Verify logo clearspace only when approved consumer-local guidance defines it; + otherwise report that check as unavailable rather than inventing a measurement. + +## Rules + +- Use sentence case for headings, labels, buttons, and navigation. +- Let OpenClaw coral carry primary brand emphasis. +- Use sea-glass green as a restrained secondary accent. +- Use neutral ink and warm-paper surfaces as the foundation. +- Prefer Switzer-compatible sans-serif stacks for interface and body copy. +- Reserve Sentient-compatible serif stacks for editorial accents and quotations. +- Use system monospace for code unless the consumer already licenses another face. +- Preserve logo proportions and colors. Do not rotate, distort, outline, or add effects. +- Use real product, interface, community, or mascot imagery when imagery is needed. +- Avoid generic technology gradients, decorative blobs, and ornamental glow as a substitute for content. diff --git a/.agents/skills/openclaw-brand/references/asset-rights.md b/.agents/skills/openclaw-brand/references/asset-rights.md new file mode 100644 index 00000000..24c34b9f --- /dev/null +++ b/.agents/skills/openclaw-brand/references/asset-rights.md @@ -0,0 +1,25 @@ +# Asset Rights + +This repository distributes CSS and guidance, not brand asset binaries. + +## Current Rule + +- Do not copy Switzer or Sentient font files into a consumer or release artifact. +- Do not redistribute logos, lobster artwork, mascot files, photos, or + illustrations without a repository-local license or explicit recorded grant. +- A public URL or an asset already committed to a site is not proof of + redistribution rights. +- System and fallback font stacks are always acceptable. + +## Before Adding An Asset + +Record: + +1. source and owner +2. license or written permission +3. allowed uses and redistribution terms +4. required attribution +5. the repository path where that evidence lives + +If any item is unknown, keep the asset in the existing consumer and reference it +only from that consumer. diff --git a/.agents/skills/openclaw-brand/references/identity.md b/.agents/skills/openclaw-brand/references/identity.md new file mode 100644 index 00000000..4f601316 --- /dev/null +++ b/.agents/skills/openclaw-brand/references/identity.md @@ -0,0 +1,42 @@ +# OpenClaw Identity + +## Color + +Use semantic variables in code. The palette names below explain the identity; +they are not permission to replace semantic tokens with raw values. + +| Role | Dark | Light | +| --- | --- | --- | +| Page | Ink `#101012` | Warm paper `#f6f5f3` | +| Surface | Ink `#19191c` | Warm paper `#eceae6` | +| Primary text | `#ededed` | `#17171a` | +| Secondary text | `#bcbcc4` | `#46464e` | +| Primary coral | `#f5654a` | `#d84a31` | +| Secondary sea glass | `#4fc8ae` | `#14806e` | + +Coral is the primary brand and action color. Sea glass is a secondary accent for +focus, contrast, and occasional supporting emphasis. Neither replaces functional +success, warning, error, or information colors. + +## Typography + +- Display and body: Switzer when the consumer holds a license, otherwise the + `--oc-font-display` and `--oc-font-body` fallback stack. +- Editorial accent: Sentient when licensed, otherwise `--oc-font-serif`. +- Code: the consumer's licensed monospace or `--oc-font-mono`. +- Use sentence case. Keep headings direct, concrete, and proportional to the + surface that contains them. + +## Voice + +OpenClaw should sound capable, direct, curious, and human. Prefer plain verbs, +specific nouns, and short explanations. Avoid inflated futurism, vague claims, +and novelty language that obscures what the product does. + +## Marks And Imagery + +- Preserve the supplied logo's aspect ratio and colors. Apply clearspace only + from approved consumer-local guidance; do not invent a measurement. +- Do not reconstruct the logo from screenshots. +- Use product interfaces, real community work, or approved mascot imagery. +- Keep backgrounds useful to the subject; avoid generic gradients and glow. diff --git a/.agents/skills/openclaw-design-audit/SKILL.md b/.agents/skills/openclaw-design-audit/SKILL.md new file mode 100644 index 00000000..064aecbf --- /dev/null +++ b/.agents/skills/openclaw-design-audit/SKILL.md @@ -0,0 +1,45 @@ +--- +name: openclaw-design-audit +description: Audit OpenClaw frontend code and rendered interfaces for design-system drift, token misuse, primitive reimplementation, accessibility problems, responsive defects, and off-brand copy. Use for design reviews, compliance checks, or scheduled audit-and-fix workflows. +--- + +# OpenClaw Design Audit + +Separate mechanical violations from judgment. Report suggestions as suggestions +unless a documented rule makes them violations. + +## Workflow + +1. Read [rubric.md](references/rubric.md) and run every applicable category. +2. Read the consumer's installed design-system version and current commit SHA. +3. Read the version-matched + [token contract](../openclaw-design-system/references/tokens.md) and + [consumer adapters](../openclaw-design-system/references/consumer-adapters.md). +4. Read the brand or marketing references when those categories apply. +5. Run deterministic source checks before judgment-based review. +6. Inspect representative rendered routes at desktop and mobile sizes. +7. Check light and dark themes where supported. +8. Emit the JSON and Markdown defined in [report-format.md](references/report-format.md). +9. When asked to fix findings, apply only narrow changes allowed by [fix-policy.md](references/fix-policy.md). +10. For scheduled ClawHub delivery, follow [github-pr-delivery.md](references/github-pr-delivery.md). + +## Evidence + +Each finding must include: + +- file and line +- category and severity +- stable rule ID +- concise remediation +- design-system reference +- whether the finding is mechanical or judgment-based + +## Curation + +- Include every error. +- Rank warnings before informational findings, then by affected-file count. +- Surface at most five non-error findings in the concise report. +- Summarize remaining non-error findings by count. +- Treat zero errors, zero warnings, and five or fewer informational findings as + no significant drift. +- Never invent source locations or visual evidence. diff --git a/.agents/skills/openclaw-design-audit/references/fix-policy.md b/.agents/skills/openclaw-design-audit/references/fix-policy.md new file mode 100644 index 00000000..6e1a4fcc --- /dev/null +++ b/.agents/skills/openclaw-design-audit/references/fix-policy.md @@ -0,0 +1,25 @@ +# Audit Fix Policy + +An audit may automatically fix a finding only when the change is narrow, +deterministic, and covered by an existing rule. + +## Allowed + +- replace a raw value with an equivalent canonical semantic token +- replace a new legacy alias with its canonical token +- use an established local primitive instead of a duplicate raw control +- add a missing accessible label when intent is unambiguous +- repair clipping or overflow without changing information architecture +- update the pinned design-system tag in a dedicated dependency change + +## Requires Human Review + +- copy, hierarchy, navigation, or information-architecture changes +- new components or abstractions +- broad visual redesign +- deletion of compatibility aliases +- asset or license interpretation +- changes that intentionally alter current rendered behavior + +Do not combine unrelated dependency, redesign, and audit fixes in one pull +request. Preserve tests and include real browser evidence for rendered changes. diff --git a/.agents/skills/openclaw-design-audit/references/github-pr-delivery.md b/.agents/skills/openclaw-design-audit/references/github-pr-delivery.md new file mode 100644 index 00000000..0bb1091f --- /dev/null +++ b/.agents/skills/openclaw-design-audit/references/github-pr-delivery.md @@ -0,0 +1,55 @@ +# GitHub Pull Request Delivery + +The scheduled ClawHub audit opens a pull request directly against +`openclaw/clawhub`. It does not create or update a tracker issue. + +The schedule and credentials live in the consumer repository's GitHub Actions +workflow. This design-system skill defines the audit and delivery contract; it +does not schedule itself. + +## Branch And Scope + +- Use a stable automation branch such as `automation/design-audit`. +- Start from current remote `main`. +- Commit only the report and allowed deterministic fixes. +- Do not overwrite unrelated human work on an existing branch. + +## Procedure + +1. Checkout `openclaw/clawhub` with full history and fetch remote `main`. +2. Reset only the dedicated automation branch to `origin/main`. +3. Install the design system at the workflow's pinned Git tag. +4. Run source checks, browser checks, and report generation. +5. Apply only fixes allowed by `fix-policy.md`. +6. Write reports under the consumer's established audit-artifact path. +7. If the decision table says `artifact only`, upload the reports and job + summary without pushing a branch. +8. Otherwise commit, force-push the dedicated automation branch with + `--force-with-lease`, then use `gh pr create` or `gh pr edit` for the single + open pull request owned by that branch. + +## Pull Request + +The title must identify the audit and date. The body includes: + +- design-system version +- audited ClawHub SHA +- count by severity +- commands and routes checked +- concise expanded findings +- whether fixes are included +- paths to JSON, Markdown, and screenshot artifacts + +If an open audit pull request exists, update it only when it owns the same stable +automation branch. Close it without merge when a later clean run makes its +findings obsolete. + +## Decision Table + +| Findings | Delivery | +| --- | --- | +| One or more errors | Open or update the pull request | +| Zero errors and one or more warnings | Open or update the pull request | +| Zero errors, zero warnings, more than five informational findings | Open or update the pull request | +| Zero errors, zero warnings, five or fewer informational findings | Artifact and job summary only | +| No findings | Artifact and job summary only; close an obsolete open audit PR | diff --git a/.agents/skills/openclaw-design-audit/references/report-format.md b/.agents/skills/openclaw-design-audit/references/report-format.md new file mode 100644 index 00000000..225a1793 --- /dev/null +++ b/.agents/skills/openclaw-design-audit/references/report-format.md @@ -0,0 +1,46 @@ +# Audit Report Format + +Produce both `design-audit.json` and `design-audit.md`. + +## JSON + +```json +{ + "designSystemVersion": "v0.0.1", + "consumerSha": "", + "summary": { + "errors": 0, + "warnings": 0, + "info": 0 + }, + "findings": [ + { + "id": "token/raw-color", + "severity": "warning", + "kind": "mechanical", + "file": "src/example.css", + "line": 12, + "message": "Use the semantic accent token.", + "remediation": "Replace the raw coral value with var(--oc-accent-primary).", + "reference": "openclaw-design-system/references/tokens.md" + } + ] +} +``` + +Sort findings by severity, rule ID, file, then line. Keep stable IDs so recurring +automation can compare runs. + +## Markdown + +Include: + +1. audited design-system version and consumer SHA +2. validation commands and rendered routes +3. count by severity +4. every error +5. at most five warning or informational findings +6. count of additional non-error findings not expanded + +Use repository-relative file links. State explicitly when no significant drift +was found. diff --git a/.agents/skills/openclaw-design-audit/references/rubric.md b/.agents/skills/openclaw-design-audit/references/rubric.md new file mode 100644 index 00000000..c803b7a8 --- /dev/null +++ b/.agents/skills/openclaw-design-audit/references/rubric.md @@ -0,0 +1,37 @@ +# Design Audit Rubric + +## Mechanical Rules + +| ID | Check | +| --- | --- | +| `token/raw-color` | New raw colors where a semantic token exists | +| `token/undefined` | Custom properties used but not defined by package or consumer | +| `token/legacy-alias` | New code depends on a migration-only alias | +| `component/duplicate` | Raw control or primitive duplicates an established local primitive | +| `component/state` | Missing hover, focus, disabled, loading, invalid, or selected state | +| `layout/overflow` | Text or fixed-format UI clips or causes accidental horizontal scroll | +| `a11y/name` | Interactive control lacks an accessible name | +| `a11y/focus` | Keyboard focus is hidden, trapped, or incoherent | +| `theme/parity` | Light or dark theme loses content, hierarchy, or contrast | +| `asset/rights` | New distributable asset has no recorded rights | + +## Judgment Checks + +| ID | Check | +| --- | --- | +| `hierarchy/primary-action` | Competing primary actions obscure the decision | +| `layout/card-overuse` | Sections or cards are unnecessarily nested or floated | +| `typography/scale` | Type scale does not match its container or task density | +| `brand/accent` | Coral, sea glass, or status colors are used without their intended role | +| `marketing/subject` | First viewport hides the actual product, place, person, or offer | +| `copy/clarity` | Interface text is vague, inflated, or does not name the action | + +## Severity + +- `error`: broken interaction, accessibility barrier, illegible theme, accidental + overflow, missing asset rights, or deterministic contract violation. +- `warning`: likely drift or inconsistency with meaningful user impact. +- `info`: improvement with limited current impact. + +Do not mark aesthetic preference as a violation. A finding needs source or +rendered evidence and a documented rule. diff --git a/.agents/skills/openclaw-design-system/SKILL.md b/.agents/skills/openclaw-design-system/SKILL.md new file mode 100644 index 00000000..2b6677e6 --- /dev/null +++ b/.agents/skills/openclaw-design-system/SKILL.md @@ -0,0 +1,41 @@ +--- +name: openclaw-design-system +description: Build or modify OpenClaw application UI using canonical semantic tokens, themes, shared CSS foundations, consumer adapters, and established local primitives. Use for product interfaces, component styling, theme work, or design-token integration. +--- + +# OpenClaw Design System + +Use the shared package for foundations and framework-neutral visual primitives. +Keep consumer-specific behavior, data, routes, and layout composition local. + +## Workflow + +1. Read [tokens.md](references/tokens.md) before choosing colors, spacing, type, radii, or shadows. +2. Read [consumer-adapters.md](references/consumer-adapters.md) for the current framework. +3. Inspect the consumer's existing shared primitives before creating a component. +4. Use semantic tokens for UI intent; use palette primitives only for documented exceptions. +5. Keep application behavior, routes, and information architecture unchanged unless the task says otherwise. +6. Validate the affected routes with existing tests and real browser screenshots. + +## Interface Rules + +- Import the complete CSS contract or its focused exported entry points. +- Compose shared classes from `components.css` before adding a one-off visual implementation. +- Use local shared primitives before raw controls or one-off component implementations. +- Keep one primary action per decision area. +- Use familiar icons for icon-only commands and provide accessible names. +- Use status colors for status, warning, success, error, and informational meaning. +- Keep cards, controls, and repeated fixed-format elements dimensionally stable. +- Avoid nested decorative cards and page sections styled as floating cards. +- Keep surfaces, controls, and insets square through their semantic radius tokens. +- Reserve round geometry for avatars, status dots, and other truly circular indicators. +- Keep focus, hover, active, disabled, loading, and invalid states coherent. +- Keep text within its container at supported viewport sizes. +- Prefer dense, scan-friendly composition for operational product surfaces. + +## Ownership + +Move visual implementation into this repository when its interface is +framework-neutral and useful across consumers. Keep runtime behavior and +framework adapters local until at least two consumers need the same interface +and behavior. diff --git a/.agents/skills/openclaw-design-system/references/consumer-adapters.md b/.agents/skills/openclaw-design-system/references/consumer-adapters.md new file mode 100644 index 00000000..bfa8ac0b --- /dev/null +++ b/.agents/skills/openclaw-design-system/references/consumer-adapters.md @@ -0,0 +1,55 @@ +# Consumer Adapters + +## Plain CSS And Astro + +Use the complete contract when the global reset is desired: + +```css +@import "@openclaw/design-system"; +``` + +For a controlled migration, import `tokens.css`, `themes.css`, and +`typography.css`, then `components.css`. Retain consumer-specific layout CSS. +Theme switching remains application-owned. The canonical public-site selector is +`html[data-theme="light"|"dark"]`. + +## Tailwind 4 + +Import in this order: + +```css +@import "@openclaw/design-system/tokens.css"; +@import "@openclaw/design-system/themes.css"; +@import "@openclaw/design-system/typography.css"; +@import "@openclaw/design-system/components.css"; +@import "@openclaw/design-system/themes/product.css"; +@import "@openclaw/design-system/compat/clawhub.css"; +@import "@openclaw/design-system/tailwind.css"; +``` + +The Tailwind adapter exposes theme utilities. `components.css` provides +framework-neutral classes; keep Radix, React, route, and product behavior in the +consumer. + +The ClawHub compatibility adapter understands: + +- `data-theme-family="claw"` +- `data-theme-resolved="light"|"dark"` +- `data-theme-mode="system"` +- the existing unprefixed token aliases + +Remove aliases only after source search and browser validation prove that no +consumer uses them. + +## Static Documentation Builders + +Copy or resolve the focused CSS exports as build inputs. Import tokens, themes, +and typography before the docs shell CSS. Do not import `base.css` until the +generated navigation, prose, search, code, and Mermaid views have been compared +in a real browser. + +## Versioning + +Install an immutable Git tag. Runtime CSS and skill guidance use the same tag. +Dependabot or a scheduled update workflow may propose a newer tag, but migration +and visual validation remain consumer responsibilities. diff --git a/.agents/skills/openclaw-design-system/references/tokens.md b/.agents/skills/openclaw-design-system/references/tokens.md new file mode 100644 index 00000000..32303f22 --- /dev/null +++ b/.agents/skills/openclaw-design-system/references/tokens.md @@ -0,0 +1,51 @@ +# Token Contract + +Import `@openclaw/design-system` for the complete foundation or use focused +exports when the consumer must control reset and adapter order. + +## Layers + +| Layer | Prefix | Purpose | +| --- | --- | --- | +| Palette | `--oc-palette-*` | Fixed source colors; rare direct use | +| Semantic | `--oc-bg-*`, `--oc-text-*`, `--oc-accent-*` | Theme-aware UI intent | +| Scale | `--oc-space-*`, `--oc-font-size-*`, `--oc-radius-*` | Shared dimensions | +| Motion | `--oc-duration-*`, `--oc-ease-*` | Shared interaction timing | +| Product | `--oc-status-*`, `--oc-input-*`, `--oc-diff-*` | Opt-in operational UI | +| Consumer alias | Unprefixed legacy names | Migration compatibility only | + +## Semantic Choices + +- Page background: `--oc-bg-page` +- Ordinary surface: `--oc-bg-surface` +- Elevated surface: `--oc-bg-elevated` +- Primary, secondary, muted text: `--oc-text-primary`, + `--oc-text-secondary`, `--oc-text-muted` +- Primary action: `--oc-accent-primary`; hover: + `--oc-accent-primary-hover` +- Secondary accent: `--oc-accent-secondary` +- Subtle and accent borders: `--oc-border-subtle`, + `--oc-border-accent` +- Focus: `--oc-focus-ring` + +Use `color-mix()` from semantic variables for a local translucent state. Add a +new shared semantic token only when the same intent recurs across consumers. + +## Radius + +Use semantic geometry roles in product UI: + +- `--oc-radius-surface`: cards, panels, and framed sections +- `--oc-radius-control`: buttons, fields, chips, and segmented controls +- `--oc-radius-inset`: nested interactive or decorative surfaces +- `--oc-radius-round`: avatars, status dots, and genuinely circular indicators + +The first three roles are square in the canonical OpenClaw system. Raw +`--oc-radius-*` scale values remain available for documented exceptions, but +must not replace the semantic defaults. + +## Ownership + +Consumer repositories own page composition and application states. This package +owns stable visual foundations, framework-neutral component primitives, and +thin migration aliases. diff --git a/.agents/skills/openclaw-design/SKILL.md b/.agents/skills/openclaw-design/SKILL.md new file mode 100644 index 00000000..967777e9 --- /dev/null +++ b/.agents/skills/openclaw-design/SKILL.md @@ -0,0 +1,31 @@ +--- +name: openclaw-design +description: Route OpenClaw design work to the canonical brand, product design-system, marketing-page, or design-audit guidance. Use when a task touches OpenClaw visual identity, shared CSS tokens, product UI, public web pages, or design-system compliance. +--- + +# OpenClaw Design + +Choose one focused branch before changing an interface. Load multiple branches +only when the task genuinely crosses them. + +| Skill | Use for | +| --- | --- | +| `openclaw-brand` | Identity decisions, typography, logos, imagery, voice, and non-product brand artifacts | +| `openclaw-design-system` | Application UI, semantic tokens, themes, component reuse, and framework adapters | +| `openclaw-marketing-pages` | Public-page composition, landing/content pages, navigation, SEO, and responsive layout | +| `openclaw-design-audit` | Design drift, token misuse, component substitution, accessibility, and recurring audits | + +For a public website change, start with `openclaw-marketing-pages` and add +`openclaw-brand` only when the task changes identity, logo, imagery, typography, +or voice. For a product application, start with `openclaw-design-system`. + +## Shared Contract + +- Treat this repository's release tag as the version of both runtime CSS and agent guidance. +- Prefer semantic tokens over raw palette values. +- Keep product-specific components and layouts in their consumer repository. +- Add shared implementation only after at least two consumers demonstrate the same interface. +- Preserve consumer behavior while changing the visual foundation. +- Validate rendered pages in a real browser at desktop and mobile sizes. +- Check both light and dark themes where the consumer supports them. +- Do not redistribute fonts, logos, or artwork without recorded permission. diff --git a/.agents/skills/openclaw-marketing-pages/SKILL.md b/.agents/skills/openclaw-marketing-pages/SKILL.md new file mode 100644 index 00000000..de15f61b --- /dev/null +++ b/.agents/skills/openclaw-marketing-pages/SKILL.md @@ -0,0 +1,32 @@ +--- +name: openclaw-marketing-pages +description: Create or update OpenClaw public-page composition for websites, landing pages, content pages, ecosystem pages, and campaigns. Use for page structure, navigation, responsive layout, media, SEO presentation, or public-site visual polish. +--- + +# OpenClaw Marketing Pages + +Build the real public experience first. Do not replace it with a generic landing-page +template or an explanatory feature tour. + +## Workflow + +1. Read [page-patterns.md](references/page-patterns.md). +2. Inspect the consumer's existing header, footer, sections, and page primitives. +3. Reuse local public-site patterns before adding another composition. +4. Use the canonical tokens and typography contract. +5. Make the brand, product, place, or object visible in the first viewport. +6. Keep a hint of the next section visible at common desktop and mobile heights. +7. Validate navigation, content hierarchy, media, light/dark themes, and responsive behavior in a browser. + +## Rules + +- Use the literal brand, product, place, person, or offer as the primary headline. +- Put value propositions in supporting copy rather than an abstract headline. +- Use relevant product or community imagery instead of atmospheric stock media. +- Keep page sections unframed; use cards only for genuinely repeated items. +- Avoid repetitive social-proof, feature-grid, and CTA sections without a content need. +- Keep headings proportionate to their container. +- Use icons for tools and familiar actions; do not use decorative icon boxes. +- Preserve readable line length and clear section rhythm. +- Use motion to explain state or progression, not as ambient decoration. +- Preserve SEO metadata, semantic heading order, and accessible navigation. diff --git a/.agents/skills/openclaw-marketing-pages/references/page-patterns.md b/.agents/skills/openclaw-marketing-pages/references/page-patterns.md new file mode 100644 index 00000000..bc4d566a --- /dev/null +++ b/.agents/skills/openclaw-marketing-pages/references/page-patterns.md @@ -0,0 +1,36 @@ +# Public Page Patterns + +## First Viewport + +- Show the literal product, project, venue, person, or offer immediately. +- Use the name or literal category as the primary heading. +- Keep supporting copy specific: what it is, who it helps, and the next action. +- Let the next section remain partially visible at common viewport heights. +- Use an approved real image or an actual product state when media helps. + +## Structure + +- Reuse the consumer's header, footer, content width, and navigation behavior. +- Build sections as full-width bands or unframed layouts with constrained inner + content. +- Use cards for repeated comparable items, not as wrappers around every section. +- Keep one primary action per decision area and avoid repeated CTA blocks. +- Preserve semantic heading order, metadata, canonical URLs, and readable prose + widths. + +## Responsive Checks + +Check at least one narrow mobile viewport and one desktop viewport. Verify: + +- navigation and menus remain reachable +- headings and buttons wrap without clipping +- media shows the important subject +- fixed controls do not cover content +- horizontal scrolling is intentional +- light and dark themes preserve hierarchy and contrast + +## Local Ownership + +Marketing layouts, decorative textures, film grain, dot grids, site headers, +footers, article prose, and route-specific media remain in the consumer unless +multiple sites prove the same reusable interface. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8e34a85e..2f4ebe0a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,8 +3,7 @@ updates: - package-ecosystem: "bun" directory: "/" schedule: - interval: "weekly" - day: "monday" + interval: "daily" time: "09:00" timezone: "America/Los_Angeles" # Preserve the old total Bun capacity: 10 general updates plus the @@ -37,8 +36,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" - day: "monday" + interval: "daily" time: "09:00" timezone: "America/Los_Angeles" groups: diff --git a/.github/workflows/design-system-audit.yml b/.github/workflows/design-system-audit.yml new file mode 100644 index 00000000..d5b534a8 --- /dev/null +++ b/.github/workflows/design-system-audit.yml @@ -0,0 +1,271 @@ +name: Weekly Design System Audit + +on: + schedule: + # Monday at 08:17 Pacific during daylight saving time. GitHub cron uses UTC. + - cron: "17 15 * * 1" + workflow_dispatch: + +permissions: {} + +concurrency: + group: clawhub-design-system-audit + cancel-in-progress: false + +env: + AUDIT_BRANCH: automation/design-system-audit + ARTIFACT_DIRECTORY: artifacts/design-audit + BUN_VERSION: "1.3.11" + CODEX_VERSION: "0.142.3" + +jobs: + audit: + runs-on: ubuntu-latest + timeout-minutes: 75 + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout current main + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + ref: main + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Install browser + run: bunx playwright install --with-deps chromium + + - name: Resolve installed design-system release + id: release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + release="v$(node -p "require('./node_modules/@openclaw/design-system/package.json').version")" + [[ "$release" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] + gh api "repos/openclaw/design-system/releases/tags/${release}" >/dev/null + echo "tag=$release" >> "$GITHUB_OUTPUT" + mkdir -p "$ARTIFACT_DIRECTORY" + git clone \ + --branch "$release" \ + --depth 1 \ + https://github.com/openclaw/design-system.git \ + "$ARTIFACT_DIRECTORY/design-system" + + - name: Record audit commits + id: commits + run: | + set -euo pipefail + consumer_sha="$(git rev-parse HEAD)" + base_sha="$(git rev-list -1 --before='8 days ago' origin/main || true)" + if [[ -z "$base_sha" ]]; then + base_sha="$(git rev-list --max-parents=0 HEAD | tail -n 1)" + fi + echo "consumer_sha=$consumer_sha" >> "$GITHUB_OUTPUT" + echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT" + { + echo "### Design audit inputs" + echo + echo "- ClawHub SHA: \`$consumer_sha\`" + echo "- Comparison base: \`$base_sha\`" + echo "- Design system: \`${{ steps.release.outputs.tag }}\`" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Run deterministic source checks + run: | + mkdir -p "$ARTIFACT_DIRECTORY" + bun scripts/design-audit/source-check.ts \ + --base "${{ steps.commits.outputs.base_sha }}" \ + --design-system-styles "$ARTIFACT_DIRECTORY/design-system/styles" \ + --output "$ARTIFACT_DIRECTORY/deterministic.json" + + - name: Build ClawHub + env: + VITE_CONVEX_SITE_URL: https://wry-manatee-359.convex.site + VITE_CONVEX_URL: https://wry-manatee-359.convex.cloud + run: bun run build + + - name: Capture real-browser evidence + env: + VITE_CONVEX_SITE_URL: https://wry-manatee-359.convex.site + VITE_CONVEX_URL: https://wry-manatee-359.convex.cloud + run: | + set -euo pipefail + HOST=127.0.0.1 PORT=4173 bun .output/server/index.mjs \ + > "$ARTIFACT_DIRECTORY/preview.log" 2>&1 & + preview_pid=$! + trap 'kill "$preview_pid" 2>/dev/null || true' EXIT + for attempt in $(seq 1 60); do + if curl --fail --silent http://127.0.0.1:4173/favicon.ico >/dev/null; then + break + fi + if [[ "$attempt" == "60" ]]; then + cat "$ARTIFACT_DIRECTORY/preview.log" + exit 1 + fi + sleep 1 + done + bun scripts/design-audit/browser-check.ts \ + --base-url http://127.0.0.1:4173 \ + --output "$ARTIFACT_DIRECTORY/browser-evidence.json" \ + --screenshots "$ARTIFACT_DIRECTORY/screenshots" + + - name: Install Codex CLI + run: | + set -euo pipefail + npm install -g "@openai/codex@${CODEX_VERSION}" + codex --version + + - name: Authenticate Codex CLI + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: printf '%s' "$OPENAI_API_KEY" | codex login --with-api-key + + - name: Run judgment audit and safe fixes + id: codex + continue-on-error: true + run: | + bun scripts/design-audit/run-codex.ts \ + --deterministic "$ARTIFACT_DIRECTORY/deterministic.json" \ + --browser "$ARTIFACT_DIRECTORY/browser-evidence.json" \ + --screenshots "$ARTIFACT_DIRECTORY/screenshots" \ + --design-system "$ARTIFACT_DIRECTORY/design-system" \ + --output "$ARTIFACT_DIRECTORY/codex-report.json" \ + --events "$ARTIFACT_DIRECTORY/codex-events.jsonl" + + - name: Validate agent change boundary + id: change_boundary + if: steps.codex.outcome == 'success' + continue-on-error: true + run: bun scripts/design-audit/validate-changes.ts + + - name: Re-run deterministic checks on agent patch + id: post_source + if: steps.codex.outcome == 'success' && steps.change_boundary.outcome == 'success' + continue-on-error: true + run: | + bun scripts/design-audit/source-check.ts \ + --working-tree \ + --fail-on-findings \ + --design-system-styles "$ARTIFACT_DIRECTORY/design-system/styles" \ + --output "$ARTIFACT_DIRECTORY/post-agent-deterministic.json" + + - name: Validate proposed source fixes + id: validation + if: steps.codex.outcome == 'success' && steps.change_boundary.outcome == 'success' && steps.post_source.outcome == 'success' + continue-on-error: true + env: + VITE_CONVEX_SITE_URL: https://wry-manatee-359.convex.site + VITE_CONVEX_URL: https://wry-manatee-359.convex.cloud + run: | + set -euo pipefail + { + bun run test:ui-contract + bun run ci:static + bun run ci:unit + bun run ci:types-build + bun run ci:playwright-smoke + } 2>&1 | tee "$ARTIFACT_DIRECTORY/validation.log" + + - name: Finalize reports + id: finalize + if: steps.codex.outcome == 'success' && steps.change_boundary.outcome == 'success' && steps.post_source.outcome == 'success' && !cancelled() + run: | + bun scripts/design-audit/finalize.ts \ + --deterministic "$ARTIFACT_DIRECTORY/deterministic.json" \ + --codex "$ARTIFACT_DIRECTORY/codex-report.json" \ + --browser "$ARTIFACT_DIRECTORY/browser-evidence.json" \ + --output-directory "$ARTIFACT_DIRECTORY" \ + --release "${{ steps.release.outputs.tag }}" \ + --base-sha "${{ steps.commits.outputs.base_sha }}" \ + --consumer-sha "${{ steps.commits.outputs.consumer_sha }}" \ + --validation-passed "${{ steps.validation.outcome == 'success' }}" \ + --run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + - name: Commit audit branch + if: steps.finalize.outputs.open_pr == 'true' + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$AUDIT_BRANCH" + git add src design-audits/latest + git commit -m "chore: apply weekly design audit" + git push --force-with-lease origin "HEAD:$AUDIT_BRANCH" + + - name: Open or update draft pull request + if: steps.finalize.outputs.open_pr == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + open_pr="$( + gh pr list \ + --repo "$GITHUB_REPOSITORY" \ + --state open \ + --head "$AUDIT_BRANCH" \ + --base main \ + --json number \ + --jq '.[0].number' + )" + title="[automation] Weekly design audit $(date -u +%F)" + if [[ -n "$open_pr" ]]; then + gh pr edit "$open_pr" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$title" \ + --body-file "$ARTIFACT_DIRECTORY/pull-request-body.md" + gh pr ready "$open_pr" --repo "$GITHUB_REPOSITORY" --undo || true + else + gh pr create \ + --repo "$GITHUB_REPOSITORY" \ + --base main \ + --head "$AUDIT_BRANCH" \ + --title "$title" \ + --body-file "$ARTIFACT_DIRECTORY/pull-request-body.md" \ + --draft + fi + + - name: Close obsolete clean audit pull request + if: steps.validation.outcome == 'success' && steps.finalize.outputs.open_pr != 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + open_pr="$( + gh pr list \ + --repo "$GITHUB_REPOSITORY" \ + --state open \ + --head "$AUDIT_BRANCH" \ + --base main \ + --json number \ + --jq '.[0].number' + )" + if [[ -n "$open_pr" ]]; then + gh pr close "$open_pr" \ + --repo "$GITHUB_REPOSITORY" \ + --comment "Closing without merge: the latest validated audit found no significant drift." + fi + + - name: Upload audit artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: design-system-audit-${{ github.run_id }} + path: ${{ env.ARTIFACT_DIRECTORY }} + if-no-files-found: warn + retention-days: 30 + + - name: Fail unsuccessful audit + if: steps.codex.outcome == 'failure' || steps.change_boundary.outcome == 'failure' || steps.post_source.outcome == 'failure' || steps.validation.outcome == 'failure' + run: | + echo "The audit or validation failed. Artifacts were preserved and no PR was created." >&2 + exit 1 diff --git a/.github/workflows/update-design-system-skills.yml b/.github/workflows/update-design-system-skills.yml new file mode 100644 index 00000000..116bf32d --- /dev/null +++ b/.github/workflows/update-design-system-skills.yml @@ -0,0 +1,117 @@ +name: Update design system skills + +on: + schedule: + - cron: "17 15 * * *" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: update-design-system-skills + cancel-in-progress: false + +jobs: + update: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + fetch-depth: 0 + + - name: Resolve latest design-system release + id: release + env: + GH_TOKEN: ${{ github.token }} + run: | + tag="$(gh api repos/openclaw/design-system/releases/latest --jq .tag_name)" + test -n "$tag" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Reinstall released design skills + env: + DESIGN_SYSTEM_TAG: ${{ steps.release.outputs.tag }} + run: | + npm exec --yes --package=skills@1.5.15 -- skills add \ + "https://github.com/openclaw/design-system.git#${DESIGN_SYSTEM_TAG}" \ + --skill "*" \ + --agent codex \ + --copy \ + --yes \ + --full-depth + + - name: Validate and stage expected changes + id: changes + run: | + allowed='^(\.agents/skills/openclaw-(brand|design|design-audit|design-system|marketing-pages)(/|$)|skills-lock\.json)$' + unexpected="$( + git status --short --untracked-files=all \ + | sed -E 's/^.. //' \ + | grep -Ev "$allowed" \ + || true + )" + if [[ -n "$unexpected" ]]; then + echo "Unexpected paths changed:" + echo "$unexpected" + exit 1 + fi + + git add \ + .agents/skills/openclaw-brand \ + .agents/skills/openclaw-design \ + .agents/skills/openclaw-design-audit \ + .agents/skills/openclaw-design-system \ + .agents/skills/openclaw-marketing-pages \ + skills-lock.json + + if git diff --cached --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Open or update pull request + if: steps.changes.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} + DESIGN_SYSTEM_TAG: ${{ steps.release.outputs.tag }} + run: | + branch="automation/design-system-skills" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git fetch origin "refs/heads/${branch}:refs/remotes/origin/${branch}" || true + git switch -C "$branch" + git commit -m "chore: update design system skills to ${DESIGN_SYSTEM_TAG}" + git push --force-with-lease origin "$branch" + + body="$RUNNER_TEMP/design-system-skills-pr.md" + { + echo "## Summary" + echo + echo "- refresh committed OpenClaw design skills from release ${DESIGN_SYSTEM_TAG}" + echo "- update only the five design-system skill directories and skills-lock.json" + echo + echo "Generated by the daily design-system skill updater." + } > "$body" + + number="$( + gh pr list \ + --head "$branch" \ + --state open \ + --json number \ + --jq '.[0].number // empty' + )" + if [[ -n "$number" ]]; then + gh pr edit "$number" \ + --title "chore: update design system skills to ${DESIGN_SYSTEM_TAG}" \ + --body-file "$body" + else + gh pr create \ + --base main \ + --head "$branch" \ + --title "chore: update design system skills to ${DESIGN_SYSTEM_TAG}" \ + --body-file "$body" + fi diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index a53f7ebe..5484ca86 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -27,6 +27,7 @@ "coverage/", "dist/", "node_modules/", + ".agents/skills/openclaw-*/**", "public/", "src/routeTree.gen.ts", "test-results/", diff --git a/bun.lock b/bun.lock index 112fb4c6..937dde58 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "@fontsource/manrope": "5.2.8", "@fontsource/noto-sans-sc": "5.2.9", "@monaco-editor/react": "4.7.0", + "@openclaw/design-system": "git+https://github.com/openclaw/design-system.git#v0.0.1", "@openclaw/plugin-inspector": "0.3.17", "@radix-ui/react-avatar": "1.2.2", "@radix-ui/react-dialog": "1.1.19", @@ -428,6 +429,8 @@ "@openclaw/clawhub-admin": ["@openclaw/clawhub-admin@workspace:packages/clawhub-admin"], + "@openclaw/design-system": ["@openclaw/design-system@github:openclaw/design-system#b1774c3", {}, "openclaw-design-system-b1774c3", "sha512-MDWcUclOCIb2eX8MAxQfZoVIDdKTwo5qM084NR0bnKPmuNiNdzcIT+GpWGt9e/VUHFhJFttoL8cLlQN5eHjjnw=="], + "@openclaw/plugin-inspector": ["@openclaw/plugin-inspector@0.3.17", "", { "bin": { "plugin-inspector": "src/cli.js" } }, "sha512-JPPHPhiXMsIvrV8UR8RQjhflMjRZX/uIhy9meE81dup7MMSnRJcsTGOXYACohv6e4z2P95z2QuE7nZkWT6Ysuw=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/design-audits/latest/design-audit.json b/design-audits/latest/design-audit.json new file mode 100644 index 00000000..f6f9b070 --- /dev/null +++ b/design-audits/latest/design-audit.json @@ -0,0 +1,47 @@ +{ + "designSystemVersion": "v0.0.1", + "consumerSha": "274c5b71ef2abef87dcf7442a086629a6ea71992", + "auditBaseSha": "735e1c4d", + "generatedAt": "2026-07-08T23:59:54.797Z", + "summary": { + "errors": 0, + "warnings": 1, + "info": 0 + }, + "findings": [ + { + "id": "layout/overflow", + "severity": "warning", + "kind": "mechanical", + "file": "src/styles.css", + "line": 29470, + "message": "Mobile /skills used the shared equal-width browse-tab rule with min-width: 0; in the supplied light/dark mobile screenshots, “Most starred” visually runs into “Featured”.", + "remediation": "Fixed by adding a skills-browse-page route class and route-scoped mobile CSS so skills tabs size to their content within the existing horizontal-scroll tab strip.", + "reference": "openclaw-design-audit/references/rubric.md layout/overflow; openclaw-marketing-pages/references/page-patterns.md Responsive Checks" + } + ], + "changedFiles": ["src/routes/skills/index.tsx", "src/styles.css"], + "validationCommands": [ + "bun run test:ui-contract", + "bun run ci:static", + "bun run ci:unit", + "bun run ci:types-build", + "bun run ci:playwright-smoke" + ], + "renderedRoutes": ["/", "/skills", "/plugins"], + "screenshotPaths": [ + ".artifacts/design-audit-local/post-fix/screenshots/home-dark-desktop.png", + ".artifacts/design-audit-local/post-fix/screenshots/skills-dark-desktop.png", + ".artifacts/design-audit-local/post-fix/screenshots/plugins-dark-desktop.png", + ".artifacts/design-audit-local/post-fix/screenshots/home-light-desktop.png", + ".artifacts/design-audit-local/post-fix/screenshots/skills-light-desktop.png", + ".artifacts/design-audit-local/post-fix/screenshots/plugins-light-desktop.png", + ".artifacts/design-audit-local/post-fix/screenshots/home-dark-mobile.png", + ".artifacts/design-audit-local/post-fix/screenshots/skills-dark-mobile.png", + ".artifacts/design-audit-local/post-fix/screenshots/plugins-dark-mobile.png", + ".artifacts/design-audit-local/post-fix/screenshots/home-light-mobile.png", + ".artifacts/design-audit-local/post-fix/screenshots/skills-light-mobile.png", + ".artifacts/design-audit-local/post-fix/screenshots/plugins-light-mobile.png" + ], + "validationPassed": true +} diff --git a/design-audits/latest/design-audit.md b/design-audits/latest/design-audit.md new file mode 100644 index 00000000..2763768f --- /dev/null +++ b/design-audits/latest/design-audit.md @@ -0,0 +1,38 @@ +# ClawHub design audit + +- Design system: `v0.0.1` +- ClawHub commit: `274c5b71ef2abef87dcf7442a086629a6ea71992` +- Comparison base: `735e1c4d` +- Generated: 2026-07-08T23:59:54.797Z +- Validation: passed + +## Summary + +- Errors: 0 +- Warnings: 1 +- Informational: 0 +- Safe source fixes: 2 + +## Validation + +- `bun run test:ui-contract` +- `bun run ci:static` +- `bun run ci:unit` +- `bun run ci:types-build` +- `bun run ci:playwright-smoke` + +## Rendered routes + +- `/` +- `/skills` +- `/plugins` + +## Findings + +### WARNING: `layout/overflow` + +- Evidence: [src/styles.css](../../src/styles.css#L29470) +- Kind: mechanical +- Finding: Mobile /skills used the shared equal-width browse-tab rule with min-width: 0; in the supplied light/dark mobile screenshots, “Most starred” visually runs into “Featured”. +- Remediation: Fixed by adding a skills-browse-page route class and route-scoped mobile CSS so skills tabs size to their content within the existing horizontal-scroll tab strip. +- Contract: `openclaw-design-audit/references/rubric.md layout/overflow; openclaw-marketing-pages/references/page-patterns.md Responsive Checks` diff --git a/package.json b/package.json index 3e08dfdf..1f57f1ea 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,10 @@ "deadcode:exports": "KNIP_INCLUDE_TESTS=1 bunx knip@6.8.0 --config knip.config.ts --no-progress --reporter compact --exports --no-config-hints", "deadcode:files": "bunx knip@6.8.0 --config knip.config.ts --production --no-progress --reporter compact --files --no-config-hints", "deadcode:knip": "bun run deadcode:files && bun run deadcode:dependencies && bun run deadcode:exports", + "design:audit:browser": "bun scripts/design-audit/browser-check.ts", + "design:audit:codex": "bun scripts/design-audit/run-codex.ts", + "design:audit:finalize": "bun scripts/design-audit/finalize.ts", + "design:audit:source": "bun scripts/design-audit/source-check.ts", "dev": "bun run llms:generate && bun --bun vite dev --port 3000", "dev:workers": "bun scripts/dev-workers.ts", "dev:worktree": "wt --yes dev", @@ -62,6 +66,7 @@ "seed:public-corpus": "bun run setup:worktree -- --quiet && bunx convex dev --once --typecheck=disable && bun scripts/public-corpus/seed-public-corpus.ts", "setup:worktree": "bun scripts/setup-worktree.ts", "skill-cards:worker": "bun scripts/skill-cards/run-skill-card-worker.ts", + "skills:install": "npm exec --yes --package=skills@1.5.15 -- skills add 'openclaw/design-system#v0.0.1' --skill '*' --agent codex --copy --yes --full-depth", "test": "vitest run", "test:e2e": "vitest run -c vitest.e2e.config.ts", "test:e2e:local": "bash scripts/run-playwright-local.sh", @@ -84,6 +89,7 @@ "@fontsource/manrope": "5.2.8", "@fontsource/noto-sans-sc": "5.2.9", "@monaco-editor/react": "4.7.0", + "@openclaw/design-system": "git+https://github.com/openclaw/design-system.git#v0.0.1", "@openclaw/plugin-inspector": "0.3.17", "@radix-ui/react-avatar": "1.2.2", "@radix-ui/react-dialog": "1.1.19", diff --git a/scripts/design-audit/browser-check.ts b/scripts/design-audit/browser-check.ts new file mode 100644 index 00000000..35c7cb22 --- /dev/null +++ b/scripts/design-audit/browser-check.ts @@ -0,0 +1,147 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { chromium, type BrowserContext } from "@playwright/test"; + +type BrowserEvidence = { + route: string; + theme: "dark" | "light"; + viewport: "desktop" | "mobile"; + screenshot: string; + horizontalOverflow: number; + unnamedInteractiveElements: string[]; + pageErrors: string[]; +}; + +const routes = ["/", "/skills", "/plugins"]; +const viewports = { + desktop: { width: 1440, height: 900 }, + mobile: { width: 390, height: 844 }, +} as const; + +function argument(name: string) { + const index = process.argv.indexOf(name); + return index >= 0 ? process.argv[index + 1] : undefined; +} + +async function setTheme(context: BrowserContext, theme: "dark" | "light") { + await context.addInitScript((resolvedTheme) => { + window.localStorage.setItem( + "clawhub-theme-selection", + JSON.stringify({ theme: "claw", mode: resolvedTheme }), + ); + window.localStorage.setItem("clawhub-theme", resolvedTheme); + window.localStorage.setItem("clawhub-theme-name", "claw"); + document.cookie = `clawhub-theme=${resolvedTheme}; path=/`; + }, theme); +} + +async function main() { + const baseUrl = argument("--base-url"); + const output = argument("--output"); + const screenshotDir = argument("--screenshots"); + if (!baseUrl || !output || !screenshotDir) { + throw new Error( + "usage: browser-check.ts --base-url --output --screenshots ", + ); + } + + await mkdir(screenshotDir, { recursive: true }); + const browser = await chromium.launch({ headless: true }); + const evidence: BrowserEvidence[] = []; + + try { + for (const [viewportName, viewport] of Object.entries(viewports)) { + for (const theme of ["dark", "light"] as const) { + const context = await browser.newContext({ + colorScheme: theme, + viewport, + }); + await setTheme(context, theme); + const page = await context.newPage(); + const pageErrors: string[] = []; + page.on("pageerror", (error) => pageErrors.push(error.message)); + + for (const route of routes) { + await page.goto(new URL(route, baseUrl).toString(), { + waitUntil: "domcontentloaded", + timeout: 60_000, + }); + await page.waitForTimeout(1_500); + const actualTheme = await page.locator("html").getAttribute("data-theme-resolved"); + if (actualTheme !== theme) { + throw new Error(`${route} resolved ${actualTheme ?? "no theme"} instead of ${theme}`); + } + + const routeName = route === "/" ? "home" : route.slice(1).replaceAll("/", "-"); + const screenshot = join(screenshotDir, `${routeName}-${theme}-${viewportName}.png`); + await page.screenshot({ path: screenshot, fullPage: true }); + + const pageEvidence = await page.evaluate(() => { + const interactive = [ + ...document.querySelectorAll( + "button, a[href], input, select, textarea, [role=button], [role=link]", + ), + ]; + const unnamedInteractiveElements = interactive + .filter((element) => { + const text = element.textContent?.trim(); + const label = element.getAttribute("aria-label")?.trim(); + const labelledBy = element.getAttribute("aria-labelledby")?.trim(); + const title = element.getAttribute("title")?.trim(); + const alt = element.querySelector("img")?.getAttribute("alt")?.trim(); + return !text && !label && !labelledBy && !title && !alt; + }) + .slice(0, 20) + .map((element) => { + const id = element.id ? `#${element.id}` : ""; + const classes = + typeof element.className === "string" && element.className.trim() + ? `.${element.className.trim().split(/\s+/).slice(0, 3).join(".")}` + : ""; + return `${element.tagName.toLowerCase()}${id}${classes}`; + }); + return { + horizontalOverflow: Math.max( + 0, + document.documentElement.scrollWidth - window.innerWidth, + ), + unnamedInteractiveElements, + }; + }); + + evidence.push({ + route, + theme, + viewport: viewportName as keyof typeof viewports, + screenshot, + horizontalOverflow: pageEvidence.horizontalOverflow, + unnamedInteractiveElements: pageEvidence.unnamedInteractiveElements, + pageErrors: [...pageErrors], + }); + pageErrors.length = 0; + } + await context.close(); + } + } + } finally { + await browser.close(); + } + + await writeFile( + output, + `${JSON.stringify( + { + baseUrl, + routes, + evidence, + }, + null, + 2, + )}\n`, + ); + console.log(`captured ${evidence.length} route/theme/viewport combinations`); +} + +if (import.meta.main) { + await main(); +} diff --git a/scripts/design-audit/codex-output.schema.json b/scripts/design-audit/codex-output.schema.json new file mode 100644 index 00000000..d1769e3c --- /dev/null +++ b/scripts/design-audit/codex-output.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["findings", "changedFiles", "deferredSuggestions"], + "properties": { + "findings": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "severity", + "kind", + "file", + "line", + "message", + "remediation", + "reference" + ], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "severity": { "enum": ["error", "warning", "info"] }, + "kind": { "enum": ["mechanical", "judgment"] }, + "file": { "type": "string", "minLength": 1 }, + "line": { "type": "integer", "minimum": 1 }, + "message": { "type": "string", "minLength": 1 }, + "remediation": { "type": "string", "minLength": 1 }, + "reference": { "type": "string", "minLength": 1 } + } + } + }, + "changedFiles": { + "type": "array", + "items": { "type": "string" } + }, + "deferredSuggestions": { + "type": "array", + "items": { "type": "string" } + } + } +} diff --git a/scripts/design-audit/design-audit-workflow.test.ts b/scripts/design-audit/design-audit-workflow.test.ts new file mode 100644 index 00000000..f496aeab --- /dev/null +++ b/scripts/design-audit/design-audit-workflow.test.ts @@ -0,0 +1,105 @@ +/* @vitest-environment node */ +import { readFile } from "node:fs/promises"; +import { describe, expect, it } from "vitest"; +import { parse as parseYaml } from "yaml"; + +type Step = { + name?: string; + if?: string; + run?: string; + uses?: string; + with?: Record; +}; + +describe("weekly design-system audit workflow", () => { + it("runs Monday and manually, with one guarded draft PR branch", async () => { + const source = await readFile(".github/workflows/design-system-audit.yml", "utf8"); + const workflow = parseYaml(source) as { + on: { + schedule: Array<{ cron: string }>; + workflow_dispatch: unknown; + }; + env: Record; + jobs: { audit: { steps: Step[] } }; + }; + const steps = workflow.jobs.audit.steps; + + expect(workflow.on.schedule).toEqual([{ cron: "17 15 * * 1" }]); + expect(workflow.on.workflow_dispatch).toBeDefined(); + expect(workflow.env.AUDIT_BRANCH).toBe("automation/design-system-audit"); + expect(workflow.env.ARTIFACT_DIRECTORY).toBe("artifacts/design-audit"); + expect(source.toLowerCase()).not.toContain("linear"); + expect(source).not.toContain("gh pr merge"); + + const createPr = steps.find((step) => step.name === "Open or update draft pull request"); + expect(createPr?.run).toContain("--draft"); + expect(createPr?.run).toContain("gh pr edit"); + expect(createPr?.run).toContain('--head "$AUDIT_BRANCH"'); + + const closeClean = steps.find( + (step) => step.name === "Close obsolete clean audit pull request", + ); + expect(closeClean?.if).toContain("steps.validation.outcome == 'success'"); + expect(closeClean?.run).toContain("gh pr close"); + + expect(steps.some((step) => step.name === "Create repository token")).toBe(false); + expect(source).not.toContain("DESIGN_SYSTEM_READ_TOKEN"); + expect(source).not.toContain("steps.app-token.outputs.token"); + }); + + it("preserves artifacts and suppresses PRs when validation fails", async () => { + const workflow = parseYaml( + await readFile(".github/workflows/design-system-audit.yml", "utf8"), + ) as { jobs: { audit: { steps: Step[] } } }; + const steps = workflow.jobs.audit.steps; + const upload = steps.find((step) => step.name === "Upload audit artifacts"); + const commit = steps.find((step) => step.name === "Commit audit branch"); + const failure = steps.find((step) => step.name === "Fail unsuccessful audit"); + + expect(upload?.if).toBe("always()"); + expect(upload?.uses).toMatch(/^actions\/upload-artifact@[0-9a-f]{40}$/); + expect(commit?.if).toBe("steps.finalize.outputs.open_pr == 'true'"); + expect(failure?.if).toContain("steps.validation.outcome == 'failure'"); + }); + + it("enforces the agent change boundary before running repository scripts", async () => { + const workflow = parseYaml( + await readFile(".github/workflows/design-system-audit.yml", "utf8"), + ) as { jobs: { audit: { steps: Step[] } } }; + const steps = workflow.jobs.audit.steps; + const boundaryIndex = steps.findIndex((step) => step.name === "Validate agent change boundary"); + const deterministicIndex = steps.findIndex( + (step) => step.name === "Re-run deterministic checks on agent patch", + ); + const validationIndex = steps.findIndex( + (step) => step.name === "Validate proposed source fixes", + ); + + expect(boundaryIndex).toBeGreaterThan(-1); + expect(boundaryIndex).toBeLessThan(deterministicIndex); + expect(deterministicIndex).toBeLessThan(validationIndex); + expect(steps[boundaryIndex]?.run).toContain("validate-changes.ts"); + expect(steps[deterministicIndex]?.run).toContain("--working-tree"); + expect(steps[deterministicIndex]?.run).toContain("--fail-on-findings"); + expect(steps[validationIndex]?.if).toContain("steps.change_boundary.outcome == 'success'"); + expect(steps[validationIndex]?.if).toContain("steps.post_source.outcome == 'success'"); + }); + + it("pins the design release and audit inputs in every report", async () => { + const source = await readFile(".github/workflows/design-system-audit.yml", "utf8"); + expect(source).toContain( + "require('./node_modules/@openclaw/design-system/package.json').version", + ); + expect(source).toContain("repos/openclaw/design-system/releases/tags/${release}"); + expect(source).toContain("https://github.com/openclaw/design-system.git"); + expect(source).toContain('clone \\\n --branch "$release"'); + expect(source).toContain("--consumer-sha"); + expect(source).toContain("--base-sha"); + expect(source).toContain("--release"); + expect(source).toContain("browser-check.ts"); + expect(source).toContain("run-codex.ts"); + expect(source).toContain( + "set -euo pipefail\n {\n bun run test:ui-contract", + ); + }); +}); diff --git a/scripts/design-audit/design-audit.test.ts b/scripts/design-audit/design-audit.test.ts new file mode 100644 index 00000000..a65b1c56 --- /dev/null +++ b/scripts/design-audit/design-audit.test.ts @@ -0,0 +1,115 @@ +/* @vitest-environment node */ +import { describe, expect, it } from "vitest"; +import { validateSafeChanges } from "./finalize"; +import { + curatedFindings, + renderAuditMarkdown, + shouldOpenAuditPullRequest, + sortAndDedupeFindings, + summarizeFindings, + type AuditFinding, + type AuditReport, +} from "./report"; +import { changedLineFindings, parseAddedLines } from "./source-check"; + +const finding = (severity: AuditFinding["severity"], id = "token/raw-color"): AuditFinding => ({ + id, + severity, + kind: "mechanical", + file: "src/example.css", + line: 12, + message: "Example finding.", + remediation: "Use a semantic token.", + reference: "openclaw-design-system/references/tokens.md", +}); + +describe("design audit reporting", () => { + it("uses the released skill significance threshold", () => { + expect(shouldOpenAuditPullRequest({ errors: 1, warnings: 0, info: 0 })).toBe(true); + expect(shouldOpenAuditPullRequest({ errors: 0, warnings: 1, info: 0 })).toBe(true); + expect(shouldOpenAuditPullRequest({ errors: 0, warnings: 0, info: 5 })).toBe(false); + expect(shouldOpenAuditPullRequest({ errors: 0, warnings: 0, info: 6 })).toBe(true); + }); + + it("includes every error and at most five non-errors in the concise report", () => { + const findings = [ + finding("error", "a"), + finding("error", "b"), + ...Array.from({ length: 8 }, (_, index) => finding("info", `info-${index}`)), + ]; + const curated = curatedFindings(findings); + expect(curated.expanded.filter((item) => item.severity === "error")).toHaveLength(2); + expect(curated.expanded.filter((item) => item.severity !== "error")).toHaveLength(5); + expect(curated.omittedNonErrors).toBe(3); + }); + + it("deduplicates and sorts findings by severity", () => { + const warning = finding("warning", "warning"); + const findings = sortAndDedupeFindings([ + finding("info", "info"), + warning, + warning, + finding("error", "error"), + ]); + expect(findings.map((item) => item.severity)).toEqual(["error", "warning", "info"]); + expect(summarizeFindings(findings)).toEqual({ errors: 1, warnings: 1, info: 1 }); + }); + + it("renders immutable audit provenance", () => { + const report: AuditReport = { + designSystemVersion: "v0.0.3", + consumerSha: "consumer", + auditBaseSha: "base", + generatedAt: "2026-07-08T00:00:00.000Z", + summary: { errors: 0, warnings: 0, info: 0 }, + findings: [], + changedFiles: [], + validationCommands: ["bun run ci:static"], + renderedRoutes: ["/"], + screenshotPaths: ["home.png"], + validationPassed: true, + }; + const markdown = renderAuditMarkdown(report); + expect(markdown).toContain("`v0.0.3`"); + expect(markdown).toContain("`consumer`"); + expect(markdown).toContain("No significant design-system drift was found."); + }); +}); + +describe("deterministic source checks", () => { + it("tracks exact added source line numbers", () => { + const added = parseAddedLines( + [ + "diff --git a/src/example.css b/src/example.css", + "--- a/src/example.css", + "+++ b/src/example.css", + "@@ -10,0 +11,2 @@", + "+color: #f5654a;", + "+background: var(--accent);", + ].join("\n"), + ); + expect(added).toEqual([ + { file: "src/example.css", line: 11, text: "color: #f5654a;" }, + { file: "src/example.css", line: 12, text: "background: var(--accent);" }, + ]); + expect(changedLineFindings(added).map((item) => item.id)).toEqual([ + "token/raw-color", + "token/legacy-alias", + ]); + }); +}); + +describe("safe fix boundary", () => { + it("allows bounded existing frontend changes", () => { + expect(() => validateSafeChanges(["src/example.css"], "10\t5\tsrc/example.css")).not.toThrow(); + }); + + it("rejects backend, configuration, and broad changes", () => { + expect(() => validateSafeChanges(["convex/schema.ts"], "1\t0\tconvex/schema.ts")).toThrow( + "outside existing frontend source", + ); + expect(() => validateSafeChanges(["src/example.css"], "401\t0\tsrc/example.css")).toThrow( + "maximum is 400", + ); + }); +}); diff --git a/scripts/design-audit/finalize.ts b/scripts/design-audit/finalize.ts new file mode 100644 index 00000000..2fffc544 --- /dev/null +++ b/scripts/design-audit/finalize.ts @@ -0,0 +1,152 @@ +import { execFileSync } from "node:child_process"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { + parseFindings, + renderAuditMarkdown, + renderPullRequestBody, + shouldOpenAuditPullRequest, + sortAndDedupeFindings, + summarizeFindings, + type AuditReport, +} from "./report"; + +const validationCommands = [ + "bun run test:ui-contract", + "bun run ci:static", + "bun run ci:unit", + "bun run ci:types-build", + "bun run ci:playwright-smoke", +]; + +function argument(name: string) { + const index = process.argv.indexOf(name); + return index >= 0 ? process.argv[index + 1] : undefined; +} + +function git(...args: string[]) { + return execFileSync("git", args, { encoding: "utf8" }).trim(); +} + +export function validateSafeChanges(paths: string[], numstat: string) { + if (paths.length > 12) throw new Error(`audit changed ${paths.length} files; maximum is 12`); + for (const path of paths) { + if (!/^src\/.+\.(css|ts|tsx)$/.test(path)) { + throw new Error(`audit attempted a change outside existing frontend source: ${path}`); + } + } + const changedLines = numstat + .split("\n") + .filter(Boolean) + .reduce((total, line) => { + const [added, removed] = line.split("\t"); + return total + Number(added || 0) + Number(removed || 0); + }, 0); + if (changedLines > 400) { + throw new Error(`audit changed ${changedLines} lines; maximum is 400`); + } +} + +export function validateCurrentSafeChanges() { + const changedFiles = git("diff", "--name-only", "HEAD").split("\n").filter(Boolean); + const untracked = git("ls-files", "--others", "--exclude-standard").split("\n").filter(Boolean); + if (untracked.length > 0) { + throw new Error(`audit created untracked files: ${untracked.join(", ")}`); + } + validateSafeChanges(changedFiles, git("diff", "--numstat", "HEAD")); + return changedFiles; +} + +async function main() { + const deterministicPath = argument("--deterministic"); + const codexPath = argument("--codex"); + const browserPath = argument("--browser"); + const outputDirectory = argument("--output-directory"); + const release = argument("--release"); + const baseSha = argument("--base-sha"); + const consumerSha = argument("--consumer-sha"); + const validationPassed = argument("--validation-passed") === "true"; + const runUrl = argument("--run-url") ?? "local"; + if ( + !deterministicPath || + !codexPath || + !browserPath || + !outputDirectory || + !release || + !baseSha || + !consumerSha + ) { + throw new Error("missing required design-audit finalize argument"); + } + + const changedFiles = validateCurrentSafeChanges(); + + const deterministic = JSON.parse(await readFile(deterministicPath, "utf8")); + const codex = JSON.parse(await readFile(codexPath, "utf8")); + const browser = JSON.parse(await readFile(browserPath, "utf8")) as { + routes: string[]; + evidence: Array<{ screenshot: string }>; + }; + const findings = sortAndDedupeFindings([ + ...parseFindings(deterministic), + ...parseFindings(codex), + ]); + const report: AuditReport = { + designSystemVersion: release, + consumerSha, + auditBaseSha: baseSha, + generatedAt: new Date().toISOString(), + summary: summarizeFindings(findings), + findings, + changedFiles, + validationCommands, + renderedRoutes: browser.routes, + screenshotPaths: browser.evidence.map((entry) => entry.screenshot), + validationPassed, + }; + + await mkdir(outputDirectory, { recursive: true }); + await writeFile( + join(outputDirectory, "design-audit.json"), + `${JSON.stringify(report, null, 2)}\n`, + ); + await writeFile(join(outputDirectory, "design-audit.md"), renderAuditMarkdown(report)); + await writeFile( + join(outputDirectory, "pull-request-body.md"), + renderPullRequestBody(report, runUrl), + ); + + const openPullRequest = validationPassed && shouldOpenAuditPullRequest(report.summary); + if (openPullRequest) { + const committedDirectory = "design-audits/latest"; + await mkdir(committedDirectory, { recursive: true }); + const committedJson = join(committedDirectory, "design-audit.json"); + await writeFile(committedJson, `${JSON.stringify(report, null, 2)}\n`); + await writeFile(join(committedDirectory, "design-audit.md"), renderAuditMarkdown(report)); + execFileSync("bunx", ["oxfmt", "--write", committedJson], { stdio: "inherit" }); + } + + const githubOutput = process.env.GITHUB_OUTPUT; + if (githubOutput) { + await mkdir(dirname(githubOutput), { recursive: true }); + await writeFile( + githubOutput, + [ + `open_pr=${openPullRequest}`, + `has_fixes=${changedFiles.length > 0}`, + `errors=${report.summary.errors}`, + `warnings=${report.summary.warnings}`, + `info=${report.summary.info}`, + "", + ].join("\n"), + { flag: "a" }, + ); + } + console.log( + `finalized audit: ${report.summary.errors} errors, ${report.summary.warnings} warnings, ${report.summary.info} info; PR=${openPullRequest}`, + ); +} + +if (import.meta.main) { + await main(); +} diff --git a/scripts/design-audit/report.ts b/scripts/design-audit/report.ts new file mode 100644 index 00000000..4a1e1fd8 --- /dev/null +++ b/scripts/design-audit/report.ts @@ -0,0 +1,224 @@ +import { readFile, writeFile } from "node:fs/promises"; + +export type AuditSeverity = "error" | "warning" | "info"; +export type AuditKind = "mechanical" | "judgment"; + +export type AuditFinding = { + id: string; + severity: AuditSeverity; + kind: AuditKind; + file: string; + line: number; + message: string; + remediation: string; + reference: string; +}; + +export type AuditSummary = { + errors: number; + warnings: number; + info: number; +}; + +export type AuditReport = { + designSystemVersion: string; + consumerSha: string; + auditBaseSha: string; + generatedAt: string; + summary: AuditSummary; + findings: AuditFinding[]; + changedFiles: string[]; + validationCommands: string[]; + renderedRoutes: string[]; + screenshotPaths: string[]; + validationPassed: boolean; +}; + +const severityRank: Record = { + error: 0, + warning: 1, + info: 2, +}; + +function findingKey(finding: AuditFinding) { + return [finding.id, finding.file, finding.line, finding.message].join("\u0000"); +} + +export function isAuditFinding(value: unknown): value is AuditFinding { + if (!value || typeof value !== "object") return false; + const finding = value as Partial; + return ( + typeof finding.id === "string" && + ["error", "warning", "info"].includes(finding.severity ?? "") && + ["mechanical", "judgment"].includes(finding.kind ?? "") && + typeof finding.file === "string" && + Number.isInteger(finding.line) && + Number(finding.line) > 0 && + typeof finding.message === "string" && + typeof finding.remediation === "string" && + typeof finding.reference === "string" + ); +} + +export function parseFindings(value: unknown): AuditFinding[] { + if (!value || typeof value !== "object") throw new Error("audit result must be an object"); + const findings = (value as { findings?: unknown }).findings; + if (!Array.isArray(findings) || !findings.every(isAuditFinding)) { + throw new Error("audit result findings do not match the report contract"); + } + return findings; +} + +export function sortAndDedupeFindings(findings: AuditFinding[]) { + const unique = new Map(); + for (const finding of findings) { + const key = findingKey(finding); + const existing = unique.get(key); + if (!existing || severityRank[finding.severity] < severityRank[existing.severity]) { + unique.set(key, finding); + } + } + return [...unique.values()].sort( + (left, right) => + severityRank[left.severity] - severityRank[right.severity] || + left.id.localeCompare(right.id) || + left.file.localeCompare(right.file) || + left.line - right.line, + ); +} + +export function summarizeFindings(findings: AuditFinding[]): AuditSummary { + return findings.reduce( + (summary, finding) => { + if (finding.severity === "error") summary.errors += 1; + else if (finding.severity === "warning") summary.warnings += 1; + else summary.info += 1; + return summary; + }, + { errors: 0, warnings: 0, info: 0 }, + ); +} + +export function shouldOpenAuditPullRequest(summary: AuditSummary) { + return summary.errors > 0 || summary.warnings > 0 || summary.info > 5; +} + +export function curatedFindings(findings: AuditFinding[]) { + const sorted = sortAndDedupeFindings(findings); + const errors = sorted.filter((finding) => finding.severity === "error"); + const nonErrors = sorted.filter((finding) => finding.severity !== "error"); + return { + expanded: [...errors, ...nonErrors.slice(0, 5)], + omittedNonErrors: Math.max(0, nonErrors.length - 5), + }; +} + +function findingMarkdown(finding: AuditFinding) { + return [ + `### ${finding.severity.toUpperCase()}: \`${finding.id}\``, + "", + `- Evidence: [${finding.file}](../../${finding.file}#L${finding.line})`, + `- Kind: ${finding.kind}`, + `- Finding: ${finding.message}`, + `- Remediation: ${finding.remediation}`, + `- Contract: \`${finding.reference}\``, + ].join("\n"); +} + +export function renderAuditMarkdown(report: AuditReport) { + const { expanded, omittedNonErrors } = curatedFindings(report.findings); + const lines = [ + "# ClawHub design audit", + "", + `- Design system: \`${report.designSystemVersion}\``, + `- ClawHub commit: \`${report.consumerSha}\``, + `- Comparison base: \`${report.auditBaseSha}\``, + `- Generated: ${report.generatedAt}`, + `- Validation: ${report.validationPassed ? "passed" : "failed"}`, + "", + "## Summary", + "", + `- Errors: ${report.summary.errors}`, + `- Warnings: ${report.summary.warnings}`, + `- Informational: ${report.summary.info}`, + `- Safe source fixes: ${report.changedFiles.length}`, + "", + "## Validation", + "", + ...report.validationCommands.map((command) => `- \`${command}\``), + "", + "## Rendered routes", + "", + ...report.renderedRoutes.map((route) => `- \`${route}\``), + "", + "## Findings", + "", + ]; + + if (expanded.length === 0) { + lines.push("No significant design-system drift was found."); + } else { + lines.push(...expanded.flatMap((finding) => [findingMarkdown(finding), ""])); + } + + if (omittedNonErrors > 0) { + lines.push(`${omittedNonErrors} additional non-error findings are retained in JSON.`); + } + + return `${lines.join("\n").trim()}\n`; +} + +export function renderPullRequestBody(report: AuditReport, runUrl: string) { + const { expanded, omittedNonErrors } = curatedFindings(report.findings); + const lines = [ + "## Audit", + "", + `- Design system: \`${report.designSystemVersion}\``, + `- Audited ClawHub SHA: \`${report.consumerSha}\``, + `- Comparison base: \`${report.auditBaseSha}\``, + `- Findings: ${report.summary.errors} errors, ${report.summary.warnings} warnings, ${report.summary.info} informational`, + `- Safe fixes included: ${report.changedFiles.length > 0 ? report.changedFiles.join(", ") : "none"}`, + `- Workflow run: ${runUrl}`, + "", + "## Validation", + "", + ...report.validationCommands.map((command) => `- \`${command}\``), + "", + "## Curated findings", + "", + ]; + + if (expanded.length === 0) { + lines.push("No significant design-system drift was found."); + } else { + lines.push( + ...expanded.map( + (finding) => + `- **${finding.severity}** \`${finding.id}\` at \`${finding.file}:${finding.line}\`: ${finding.message}`, + ), + ); + } + if (omittedNonErrors > 0) { + lines.push(`- ${omittedNonErrors} additional non-error findings are in the JSON report.`); + } + + lines.push( + "", + "## Artifacts", + "", + "- `design-audits/latest/design-audit.json`", + "- `design-audits/latest/design-audit.md`", + "- screenshots and full logs are attached to the workflow run", + "", + "This pull request is intentionally draft. The workflow never merges, deploys, publishes, or mutates backend data.", + ); + return `${lines.join("\n")}\n`; +} + +export async function readFindings(path: string) { + return parseFindings(JSON.parse(await readFile(path, "utf8"))); +} + +export async function writeReport(path: string, report: AuditReport) { + await writeFile(path, `${JSON.stringify(report, null, 2)}\n`); +} diff --git a/scripts/design-audit/run-codex.ts b/scripts/design-audit/run-codex.ts new file mode 100644 index 00000000..49d36128 --- /dev/null +++ b/scripts/design-audit/run-codex.ts @@ -0,0 +1,117 @@ +import { spawn } from "node:child_process"; +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { parseFindings } from "./report"; + +function argument(name: string) { + const index = process.argv.indexOf(name); + return index >= 0 ? process.argv[index + 1] : undefined; +} + +function run(command: string, args: string[], input: string, logPath: string) { + return new Promise((resolvePromise, reject) => { + const child = spawn(command, args, { + cwd: process.cwd(), + env: process.env, + stdio: ["pipe", "pipe", "pipe"], + }); + let stderr = ""; + const chunks: Buffer[] = []; + child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk)); + child.stderr.on("data", (chunk: Buffer) => { + stderr += chunk.toString(); + process.stderr.write(chunk); + }); + child.on("error", reject); + child.on("close", async (code) => { + await writeFile(logPath, Buffer.concat(chunks)); + if (code === 0) resolvePromise(); + else reject(new Error(`${command} exited ${code}: ${stderr.slice(-2_000)}`)); + }); + child.stdin.end(input); + }); +} + +async function main() { + const deterministicPath = argument("--deterministic"); + const browserPath = argument("--browser"); + const outputPath = argument("--output"); + const eventsPath = argument("--events"); + const screenshotDir = argument("--screenshots"); + const designSystemDirectory = argument("--design-system"); + if ( + !deterministicPath || + !browserPath || + !outputPath || + !eventsPath || + !screenshotDir || + !designSystemDirectory + ) { + throw new Error( + "usage: run-codex.ts --deterministic --browser --screenshots --design-system --output --events ", + ); + } + + await mkdir(dirname(outputPath), { recursive: true }); + const screenshots = (await readdir(screenshotDir)) + .filter((path) => path.endsWith(".png")) + .sort() + .map((path) => resolve(screenshotDir, path)); + const deterministic = await readFile(deterministicPath, "utf8"); + const browser = await readFile(browserPath, "utf8"); + const prompt = `You are running the scheduled ClawHub design audit. + +Read and follow: +- ${designSystemDirectory}/openclaw-design-audit/SKILL.md and every referenced file +- ${designSystemDirectory}/openclaw-design-system/references/tokens.md +- ${designSystemDirectory}/openclaw-design-system/references/consumer-adapters.md + +Audit the current repository. Deterministic checks ran first and produced: +${deterministic} + +Real-browser checks and screenshot paths: +${browser} + +Inspect the attached screenshots for desktop/mobile and light/dark parity. Verify every finding against source. Never invent a source file or line. Cover semantic tokens, shared primitive usage, deprecated aliases, accessibility, copy clarity, responsive behavior, and recurring pattern candidates. + +You may apply only narrow, high-confidence fixes allowed by fix-policy.md. Edit only existing frontend files under src/ with .ts, .tsx, or .css extensions. Do not edit backend behavior, dependencies, workflows, configuration, generated files, reports, or design-audit scripts. Do not add files. Do not make broad redesign, navigation, hierarchy, or information-architecture changes. Keep subjective ideas as informational findings without source edits. + +Return every confirmed error. Return warnings before informational suggestions. Include precise repository-relative source locations and canonical design-system references. Report the exact files you changed.`; + const args = [ + "exec", + "--cd", + process.cwd(), + "--model", + process.env.DESIGN_AUDIT_CODEX_MODEL ?? "gpt-5.5", + "--sandbox", + "workspace-write", + "--ignore-user-config", + "-c", + "approval_policy=never", + "-c", + `model_reasoning_effort=${process.env.DESIGN_AUDIT_REASONING_EFFORT ?? "high"}`, + "-c", + `service_tier=${process.env.DESIGN_AUDIT_SERVICE_TIER ?? "fast"}`, + "-c", + 'shell_environment_policy.inherit="core"', + "-c", + "shell_environment_policy.ignore_default_excludes=false", + "--output-schema", + join(process.cwd(), "scripts/design-audit/codex-output.schema.json"), + "--output-last-message", + outputPath, + "--ephemeral", + "--json", + ]; + for (const screenshot of screenshots) args.push("--image", screenshot); + args.push("-"); + + await run("codex", args, prompt, eventsPath); + const parsed = JSON.parse(await readFile(outputPath, "utf8")); + parseFindings(parsed); + console.log(`Codex returned ${parsed.findings.length} findings`); +} + +if (import.meta.main) { + await main(); +} diff --git a/scripts/design-audit/source-check.ts b/scripts/design-audit/source-check.ts new file mode 100644 index 00000000..aa9c612e --- /dev/null +++ b/scripts/design-audit/source-check.ts @@ -0,0 +1,212 @@ +import { execFileSync } from "node:child_process"; +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; +import { dirname, extname, join, relative } from "node:path"; +import type { AuditFinding } from "./report"; + +type AddedLine = { + file: string; + line: number; + text: string; +}; + +const rawPaletteTokens: Record = { + "#101012": "--oc-bg-page", + "#19191c": "--oc-bg-surface", + "#202024": "--oc-bg-elevated", + "#ededed": "--oc-text-primary", + "#bcbcc4": "--oc-text-secondary", + "#9a9aa2": "--oc-text-muted", + "#f5654a": "--oc-accent-primary", + "#d84a31": "--oc-accent-primary", + "#4fc8ae": "--oc-accent-secondary", + "#14806e": "--oc-accent-secondary", +}; + +const legacyAliases = new Set([ + "--bg", + "--bg-soft", + "--surface", + "--surface-muted", + "--ink", + "--ink-soft", + "--ink-muted", + "--accent", + "--accent-fg", + "--accent-deep", + "--seafoam", + "--line", + "--border-ui", + "--input-bg", + "--input-border", +]); + +function git(...args: string[]) { + return execFileSync("git", args, { encoding: "utf8" }).trim(); +} + +export function parseAddedLines(diff: string): AddedLine[] { + const lines = diff.split("\n"); + const added: AddedLine[] = []; + let file = ""; + let nextLine = 0; + + for (const line of lines) { + if (line.startsWith("+++ b/")) { + file = line.slice(6); + continue; + } + const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line); + if (hunk) { + nextLine = Number(hunk[1]); + continue; + } + if (!file || line.startsWith("---")) continue; + if (line.startsWith("+")) { + added.push({ file, line: nextLine, text: line.slice(1) }); + nextLine += 1; + } else if (!line.startsWith("-")) { + nextLine += 1; + } + } + return added; +} + +async function listSourceFiles(directory: string): Promise { + const entries = await readdir(directory, { withFileTypes: true }); + const nested = await Promise.all( + entries.map(async (entry) => { + const path = join(directory, entry.name); + if (entry.isDirectory()) return listSourceFiles(path); + return [".css", ".ts", ".tsx"].includes(extname(entry.name)) ? [path] : []; + }), + ); + return nested.flat(); +} + +function lineForOffset(source: string, offset: number) { + return source.slice(0, offset).split("\n").length; +} + +async function undefinedTokenFindings(designSystemStyles: string): Promise { + const sourceFiles = await listSourceFiles("src"); + const packageFiles = await listSourceFiles(designSystemStyles); + const definitions = new Set(); + const references: Array<{ file: string; line: number; token: string }> = []; + + for (const path of [...sourceFiles, ...packageFiles]) { + const source = await readFile(path, "utf8"); + for (const match of source.matchAll(/(--oc-[\w-]+)\s*:/g)) { + definitions.add(match[1]!); + } + if (!path.startsWith("src/")) continue; + for (const match of source.matchAll(/var\((--oc-[\w-]+)/g)) { + references.push({ + file: relative(".", path), + line: lineForOffset(source, match.index), + token: match[1]!, + }); + } + } + + const firstByToken = new Map(); + for (const reference of references) { + if (!definitions.has(reference.token) && !firstByToken.has(reference.token)) { + firstByToken.set(reference.token, reference); + } + } + + return [...firstByToken.values()].map((reference) => ({ + id: "token/undefined", + severity: "error", + kind: "mechanical", + file: reference.file, + line: reference.line, + message: `${reference.token} is referenced but is not defined by ClawHub or the installed design system.`, + remediation: + "Define the semantic token in the owning layer or replace it with an existing token.", + reference: "openclaw-design-system/references/tokens.md", + })); +} + +export function changedLineFindings(addedLines: AddedLine[]): AuditFinding[] { + const findings: AuditFinding[] = []; + for (const added of addedLines) { + if (!added.file.startsWith("src/")) continue; + const normalized = added.text.toLowerCase(); + for (const [raw, token] of Object.entries(rawPaletteTokens)) { + if (!normalized.includes(raw)) continue; + findings.push({ + id: "token/raw-color", + severity: "warning", + kind: "mechanical", + file: added.file, + line: added.line, + message: `A newly added raw OpenClaw palette value (${raw}) bypasses semantic theme intent.`, + remediation: `Replace it with var(${token}) when that token matches the UI role.`, + reference: "openclaw-design-system/references/tokens.md", + }); + } + for (const match of added.text.matchAll(/var\((--[\w-]+)/g)) { + if (!legacyAliases.has(match[1]!)) continue; + findings.push({ + id: "token/legacy-alias", + severity: "warning", + kind: "mechanical", + file: added.file, + line: added.line, + message: `New code depends on migration-only alias ${match[1]}.`, + remediation: "Use the equivalent canonical --oc-* semantic token.", + reference: "openclaw-design-system/references/consumer-adapters.md", + }); + } + } + return findings; +} + +async function main() { + const outputIndex = process.argv.indexOf("--output"); + const baseIndex = process.argv.indexOf("--base"); + const stylesIndex = process.argv.indexOf("--design-system-styles"); + const workingTree = process.argv.includes("--working-tree"); + const failOnFindings = process.argv.includes("--fail-on-findings"); + const output = outputIndex >= 0 ? process.argv[outputIndex + 1] : undefined; + const base = baseIndex >= 0 ? process.argv[baseIndex + 1] : undefined; + const designSystemStyles = + stylesIndex >= 0 + ? process.argv[stylesIndex + 1] + : "node_modules/@openclaw/design-system/styles"; + if (!output || (!base && !workingTree) || !designSystemStyles) { + throw new Error( + "usage: source-check.ts (--base | --working-tree) --output [--design-system-styles ] [--fail-on-findings]", + ); + } + + const diff = workingTree + ? git("diff", "--unified=0", "--no-ext-diff", "HEAD", "--", "src") + : git("diff", "--unified=0", "--no-ext-diff", `${base}..HEAD`, "--", "src"); + const findings = [ + ...(await undefinedTokenFindings(designSystemStyles)), + ...changedLineFindings(parseAddedLines(diff)), + ]; + await mkdir(dirname(output), { recursive: true }); + await writeFile( + output, + `${JSON.stringify( + { + baseSha: workingTree ? git("rev-parse", "HEAD") : base, + consumerSha: git("rev-parse", "HEAD"), + findings, + }, + null, + 2, + )}\n`, + ); + console.log(`wrote ${findings.length} deterministic findings to ${output}`); + if (failOnFindings && findings.length > 0) { + throw new Error(`post-agent deterministic checks found ${findings.length} violation(s)`); + } +} + +if (import.meta.main) { + await main(); +} diff --git a/scripts/design-audit/validate-changes.ts b/scripts/design-audit/validate-changes.ts new file mode 100644 index 00000000..7403d29f --- /dev/null +++ b/scripts/design-audit/validate-changes.ts @@ -0,0 +1,8 @@ +import { validateCurrentSafeChanges } from "./finalize"; + +const changedFiles = validateCurrentSafeChanges(); +console.log( + changedFiles.length === 0 + ? "audit proposed no source changes" + : `audit proposed ${changedFiles.length} safe frontend change(s): ${changedFiles.join(", ")}`, +); diff --git a/skills-lock.json b/skills-lock.json index d426f0fb..a37e7f2f 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -25,6 +25,41 @@ "skillPath": "skills/controlling-costs/SKILL.md", "computedHash": "73ed9a6417283ee4029a9e61d11ee3cc9ae64268d131150c4c0f4a71f87cd549" }, + "openclaw-brand": { + "source": "openclaw/design-system", + "ref": "v0.0.1", + "sourceType": "github", + "skillPath": "openclaw-brand/SKILL.md", + "computedHash": "d4bf50bb9d512dbfbcd4abde7a75d8399b0187ca9486dd7854844aab0cd02e6a" + }, + "openclaw-design": { + "source": "openclaw/design-system", + "ref": "v0.0.1", + "sourceType": "github", + "skillPath": "SKILL.md", + "computedHash": "8de963d26ec19a920da5e2a5d029764bba7c941d5cb051c67a877d0e5d91c6a4" + }, + "openclaw-design-audit": { + "source": "openclaw/design-system", + "ref": "v0.0.1", + "sourceType": "github", + "skillPath": "openclaw-design-audit/SKILL.md", + "computedHash": "12fec90fd4aa1aa569de1beb5a3c85d06887a5e548704dbd094355edc0f52ad3" + }, + "openclaw-design-system": { + "source": "openclaw/design-system", + "ref": "v0.0.1", + "sourceType": "github", + "skillPath": "openclaw-design-system/SKILL.md", + "computedHash": "475604f08f37186237fa62605fbfe16ea04367af81f4c3a8c59bcb395ae70cca" + }, + "openclaw-marketing-pages": { + "source": "openclaw/design-system", + "ref": "v0.0.1", + "sourceType": "github", + "skillPath": "openclaw-marketing-pages/SKILL.md", + "computedHash": "7da44d258d5a1e87b26535effa13cf20931fd04a884fd95225a4ff603932ef84" + }, "query-metrics": { "source": "axiomhq/skills", "sourceType": "github", diff --git a/src/__tests__/ui-design-contract.test.ts b/src/__tests__/ui-design-contract.test.ts index a3427971..64dd6d41 100644 --- a/src/__tests__/ui-design-contract.test.ts +++ b/src/__tests__/ui-design-contract.test.ts @@ -68,8 +68,109 @@ describe("restored UI design contract", () => { const publicRegistry = () => read("src/lib/publicRegistry.ts"); const settings = () => read("src/routes/settings.tsx"); const styles = () => read("src/styles.css"); + const designSystemStyles = () => read("src/design-system.css"); const theme = () => read("src/lib/theme.ts"); + it("loads the shared OpenClaw token adapter after the legacy application stylesheet", () => { + const rootSource = rootRoute(); + const sharedCss = designSystemStyles(); + + expect(sharedCss).toContain('@import "@openclaw/design-system/tokens.css";'); + expect(sharedCss).toContain('@import "@openclaw/design-system/typography.css";'); + expect(sharedCss).toContain('@import "@openclaw/design-system/themes/product.css";'); + expect(sharedCss).toContain('@import "@openclaw/design-system/components.css";'); + expect(sharedCss).toContain('@import "@openclaw/design-system/compat/clawhub.css";'); + expect(sharedCss).toContain(".home-v2-main.oc-app-surface"); + expect(sharedCss).toContain("--hv2-bg: var(--oc-bg-page)"); + expect(sharedCss).toContain("--hv2-text: var(--oc-text-primary)"); + expect(sharedCss).toContain("--hv2-accent: var(--oc-accent-primary)"); + expect(sharedCss).toContain("--hv2-radius-md: var(--oc-radius-surface)"); + expect(sharedCss).toContain("border-radius: var(--oc-radius-surface)"); + expect(sharedCss).toContain("border-radius: var(--oc-radius-control)"); + expect(sharedCss).toContain("border-radius: var(--oc-radius-inset)"); + expect(sharedCss).toContain("@media (max-width: 760px)"); + expect(sharedCss).toContain( + ".home-v2-main .home-v2-popular-publishers-header.oc-section-header", + ); + expect(sharedCss).toContain('[data-theme-family="claw"][data-theme-resolved="light"]'); + expect(sharedCss).toContain("--accent: var(--oc-accent-primary)"); + expect(sharedCss).toContain('[data-theme-family="claw"][data-theme-mode="system"]'); + expect(rootSource.indexOf("href: designSystemCss")).toBeGreaterThan( + rootSource.indexOf("href: appCss"), + ); + }); + + it("uses semantic design-system geometry across landing controls and surfaces", () => { + const css = styles(); + + for (const selector of [ + ".home-v2-headline-trigger", + ".home-v2-listing-search-bar", + ".home-v2-listing-search-close", + ".home-v2-listing-category-trigger", + ]) { + expect(cssRule(css, selector)).toContain("border-radius: var(--oc-radius-control)"); + } + expect(cssRule(css, ".home-v2-promotion-title-icon")).toContain( + "border-radius: var(--oc-radius-inset)", + ); + expect(cssRule(css, ".home-v2-apps-workflow-tile")).toContain( + "border-radius: var(--oc-radius-surface)", + ); + expect(cssRule(css, ".home-v2-apps-tile-icon")).toContain( + "border-radius: var(--oc-radius-inset)", + ); + for (const selector of [ + "\n.marketplace-icon", + ".browse-page .browse-results-grid .skill-card-header .marketplace-icon", + ".home-v2-listing-card-icon .marketplace-icon", + ".home-v2-listing-row-icon .marketplace-icon", + ".home-v2-popular-publisher-card .marketplace-icon", + ".navbar-search-typeahead-icon .marketplace-icon", + ".browse-page .dashboard-catalog-row-icon .marketplace-icon", + ]) { + expect(cssRule(css, selector)).toContain("border-radius: var(--oc-radius-inset)"); + } + expect(cssRule(css, ".marketplace-icon-user")).toContain( + "border-radius: var(--oc-radius-round)", + ); + }); + + it("keeps migrated application surfaces on canonical semantic tokens", () => { + const css = styles(); + const sharedCss = designSystemStyles(); + + for (const legacyReference of [ + "var(--danger)", + "var(--font-sans)", + "var(--ink-faint)", + "var(--surface-raised)", + "var(--transition-fast)", + "var(--card-border)", + "var(--color-muted)", + "var(--color-text)", + ]) { + expect(css).not.toContain(legacyReference); + } + + expect(cssRule(css, ".dashboard-route")).toContain("--hv2-bg: var(--oc-bg-page)"); + expect(cssRule(css, ".dashboard-route")).toContain("--hv2-radius-md: var(--oc-radius-surface)"); + expect(sharedCss).toContain("--status-pending-bg: var(--oc-surface-interactive)"); + expect(sharedCss).toContain("--status-pending-fg: var(--oc-text-muted)"); + + for (const sourcePath of [ + "src/components/SignInPrompt.tsx", + "src/components/SkillOwnershipPanel.tsx", + "src/routes/import.tsx", + "src/routes/settings.tsx", + "src/routes/skills/publish.tsx", + ]) { + expect(read(sourcePath)).not.toMatch( + /(?:text|bg|border)-(?:red|amber|emerald)-(?:\d+|\[[^\]]+\])/, + ); + } + }); + it("keeps Vercel browser instrumentation mounted outside local dev", () => { const rootSource = rootRoute(); @@ -141,6 +242,8 @@ describe("restored UI design contract", () => { expect(moreMenu).toContain("border-radius: var(--r-md)"); expect(css).toContain(".navbar-theme-switcher {\n --navbar-theme-ease"); expect(css).toContain("--navbar-theme-pad: 3px"); + expect(css).toContain("--navbar-theme-outer-r: var(--oc-radius-control)"); + expect(css).toContain("--navbar-theme-inner-r: var(--oc-radius-inset)"); expect(css).toContain("--navbar-theme-seg: 26px"); expect(css).toContain("height: var(--navbar-theme-collapsed-w)"); const mobileDrawerTheme = cssRule(css, ".mobile-nav-appearance-section .navbar-theme-switcher"); @@ -181,8 +284,20 @@ describe("restored UI design contract", () => { it("requires the experiment hero and canonical home catalog without later sections", () => { const homeSource = home(); const listingSource = read("src/components/HomeListingSection.tsx"); + const appsSource = read("src/components/HomeAppsSection.tsx"); + const publishersSource = read("src/components/HomePopularPublishersSection.tsx"); const css = styles(); + expect(homeSource).toContain('className="home-v2-main oc-app-surface"'); + expect(homeSource).toContain("home-v2-headline oc-hero-title"); + expect(listingSource).toContain("home-v2-listing-card oc-card oc-card-interactive"); + expect(listingSource).toContain("home-v2-listing-kind oc-segmented"); + expect(appsSource).toContain("home-v2-apps-tile oc-card oc-card-interactive"); + expect(appsSource).toContain('className="home-v2-apps-workflow-header"'); + expect(appsSource).not.toContain('className="home-v2-apps-workflow-header oc-card"'); + expect(publishersSource).toContain( + "home-v2-popular-publisher-card oc-card oc-card-interactive", + ); expect(homeSource).not.toContain("BUILT BY THE COMMUNITY"); expect(homeSource).not.toContain("Unleash."); expect(homeSource).not.toContain("Ship."); @@ -252,6 +367,7 @@ describe("restored UI design contract", () => { it("keeps runtime requirement text high contrast in both themes", () => { const css = styles(); + const designTokens = read("node_modules/@openclaw/design-system/styles/tokens.css"); const installCardSource = read("src/components/SkillInstallCard.tsx"); expect(installCardSource).toContain("requirements-env-row"); @@ -262,12 +378,12 @@ describe("restored UI design contract", () => { ); const darkRatio = contrastRatio( - tokenValue(css, ":root", "--ink"), - tokenValue(css, ":root", "--surface-muted"), + tokenValue(designTokens, ":root", "--oc-palette-ink-50"), + tokenValue(designTokens, ":root", "--oc-palette-ink-900"), ); const lightRatio = contrastRatio( - tokenValue(css, '[data-theme-family="claw"][data-theme-resolved="light"]', "--ink"), - tokenValue(css, '[data-theme-family="claw"][data-theme-resolved="light"]', "--surface-muted"), + tokenValue(designTokens, ":root", "--oc-palette-paper-950"), + tokenValue(designTokens, ":root", "--oc-palette-paper-200"), ); expect(darkRatio).toBeGreaterThanOrEqual(7); @@ -302,10 +418,10 @@ describe("restored UI design contract", () => { const css = styles(); expect(cssRule(css, ".navbar-search-typeahead-icon .marketplace-icon-user")).toContain( - "border-radius: 999px", + "border-radius: var(--oc-radius-round)", ); expect(cssRule(css, ".navbar-search-typeahead-icon .marketplace-icon-org")).toContain( - "border-radius: 8px", + "border-radius: var(--oc-radius-inset)", ); expect( cssRule(css, ".navbar-search-typeahead-icon .marketplace-icon-user .marketplace-icon-image"), diff --git a/src/components/DeploymentDriftBanner.tsx b/src/components/DeploymentDriftBanner.tsx index 6e4b360d..eaeaa24a 100644 --- a/src/components/DeploymentDriftBanner.tsx +++ b/src/components/DeploymentDriftBanner.tsx @@ -69,7 +69,7 @@ function DeploymentDriftBannerContent() { return (
Deploy mismatch detected. Frontend expects backend build {drift.expectedBuildSha}{" "} but Convex reports {drift.actualBuildSha}. diff --git a/src/components/DevPersonaFab.tsx b/src/components/DevPersonaFab.tsx index 6463aa29..5c7a8df8 100644 --- a/src/components/DevPersonaFab.tsx +++ b/src/components/DevPersonaFab.tsx @@ -153,7 +153,7 @@ export function DevPersonaFab() {
{hasOrgProfileChanges ? ( - + You have unsaved changes. ) : null} @@ -1204,7 +1201,7 @@ export function Settings() {
{inviteError ? (

{inviteError} @@ -1310,11 +1307,11 @@ export function Settings() {

- +
-

+

Delete organization

@@ -1435,10 +1432,7 @@ export function Settings() {

{createOrgError ? ( -

+

{createOrgError}

) : null} @@ -1568,8 +1562,8 @@ export function Settings() { {newToken ? ( -
-
+
+
Copy this token now — it will not be shown again.
@@ -1714,13 +1708,10 @@ export function Settings() {
-
- +
+
-

+

This will permanently delete your account

@@ -1794,11 +1785,9 @@ function DeletionResourceSummary({ : emptyLabel; return ( -

-
-

- Resources permanently deleted -

+
+
+

Resources permanently deleted

{summary}

{resources.length ? ( @@ -2055,8 +2044,8 @@ function SettingsSection({ {icon} @@ -2064,7 +2053,9 @@ function SettingsSection({

{title} @@ -2093,7 +2084,7 @@ function SettingsBlock({
@@ -2256,9 +2247,9 @@ function GitHubSourceList({

{needsAttention && latestError ? ( -

+

Latest error: {latestError}

) : null} @@ -2416,7 +2407,7 @@ function GitHubSourceSyncIssues({ source }: { source: GitHubSkillSource }) { Sync issues - + {issues.length}
@@ -2435,7 +2426,7 @@ function GitHubSourceSyncIssues({ source }: { source: GitHubSkillSource }) { {formatGitHubSourceIssueKind(skill.kind)}
-
+
{skill.message}
@@ -2489,10 +2480,10 @@ function GitHubSourceOverviewRow({ label, children }: { label: string; children: function GitHubSourceStatusPill({ needsAttention }: { needsAttention: boolean }) { return ( {needsAttention ? "Needs attention" : "Healthy"} @@ -2629,7 +2620,11 @@ function TokenList({
{token.revokedAt ? ( -