mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
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
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -0,0 +1,46 @@
|
||||
# Audit Report Format
|
||||
|
||||
Produce both `design-audit.json` and `design-audit.md`.
|
||||
|
||||
## JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"designSystemVersion": "v0.0.1",
|
||||
"consumerSha": "<sha>",
|
||||
"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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -27,6 +27,7 @@
|
||||
"coverage/",
|
||||
"dist/",
|
||||
"node_modules/",
|
||||
".agents/skills/openclaw-*/**",
|
||||
"public/",
|
||||
"src/routeTree.gen.ts",
|
||||
"test-results/",
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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`
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <url> --output <path> --screenshots <directory>",
|
||||
);
|
||||
}
|
||||
|
||||
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<HTMLElement>(
|
||||
"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();
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
};
|
||||
|
||||
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<string, string>;
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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<AuditSeverity, number> = {
|
||||
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<AuditFinding>;
|
||||
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<string, AuditFinding>();
|
||||
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<AuditSummary>(
|
||||
(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`);
|
||||
}
|
||||
@@ -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<void>((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 <path> --browser <path> --screenshots <dir> --design-system <dir> --output <path> --events <path>",
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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<string, string> = {
|
||||
"#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<string[]> {
|
||||
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<AuditFinding[]> {
|
||||
const sourceFiles = await listSourceFiles("src");
|
||||
const packageFiles = await listSourceFiles(designSystemStyles);
|
||||
const definitions = new Set<string>();
|
||||
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<string, (typeof references)[number]>();
|
||||
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 <sha> | --working-tree) --output <path> [--design-system-styles <dir>] [--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();
|
||||
}
|
||||
@@ -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(", ")}`,
|
||||
);
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -69,7 +69,7 @@ function DeploymentDriftBannerContent() {
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="mx-auto mt-4 w-[min(1100px,calc(100vw-32px))] rounded-[14px] border border-status-warning-fg/40 bg-status-warning-bg px-4 py-3 text-[0.95rem] leading-[1.4] text-status-warning-fg"
|
||||
className="mx-auto mt-4 w-[min(1100px,calc(100vw-32px))] rounded-[var(--oc-radius-surface)] border border-status-warning-fg/40 bg-status-warning-bg px-4 py-3 text-[var(--oc-font-size-md)] leading-[1.4] text-status-warning-fg"
|
||||
>
|
||||
Deploy mismatch detected. Frontend expects backend build <code>{drift.expectedBuildSha}</code>{" "}
|
||||
but Convex reports <code>{drift.actualBuildSha}</code>.
|
||||
|
||||
@@ -153,7 +153,7 @@ export function DevPersonaFab() {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-14 w-14 shrink-0 cursor-pointer items-center justify-center rounded-[9999px] border border-[color:var(--line)] bg-[color:var(--surface)] p-0 text-[color:var(--ink)] shadow-[var(--shadow)] transition-colors hover:bg-[color:var(--surface-subtle)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]/35 focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bg)]"
|
||||
className="flex h-14 w-14 shrink-0 cursor-pointer items-center justify-center rounded-[var(--oc-radius-round)] border border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-bg-elevated)] p-0 text-[color:var(--oc-text-primary)] shadow-[var(--oc-shadow-md)] transition-colors hover:bg-[color:var(--oc-surface-interactive)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--oc-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--oc-bg-page)]"
|
||||
aria-label="Open local dev personas"
|
||||
>
|
||||
<Wrench size={22} aria-hidden="true" />
|
||||
|
||||
@@ -23,8 +23,8 @@ export function EmptyState({
|
||||
children,
|
||||
}: EmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 rounded-[var(--radius-md)] border border-dashed border-[color:var(--line)] bg-[color:var(--surface)] px-6 py-14 text-center">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-[color:var(--surface-muted)]">
|
||||
<div className="flex flex-col items-center justify-center gap-4 rounded-[var(--oc-radius-surface)] border border-dashed border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-surface-card)] px-6 py-14 text-center">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-[var(--oc-radius-inset)] bg-[color:var(--oc-bg-surface)]">
|
||||
<Icon className="h-7 w-7 text-[color:var(--ink-soft)]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -73,7 +73,7 @@ function extractErrorMessage(error: unknown): string {
|
||||
function ErrorFallback({ error, onRetry }: { error: unknown; onRetry?: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 rounded-[var(--radius-md)] border border-[color:var(--line)] bg-[color:var(--surface)] p-10 text-center">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[rgba(255,107,74,0.12)]">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-[var(--oc-radius-inset)] bg-[color:var(--oc-surface-accent-soft)]">
|
||||
<AlertTriangle className="h-6 w-6 text-[color:var(--accent)]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -60,7 +60,7 @@ function HomeAppsCompactSkill({ app }: { app: HomeSkillApp }) {
|
||||
<Link
|
||||
to="/skills"
|
||||
search={{ ...SKILLS_BROWSE_SEARCH, q: app.browseQuery }}
|
||||
className="home-v2-apps-tile"
|
||||
className="home-v2-apps-tile oc-card oc-card-interactive"
|
||||
title={app.description}
|
||||
>
|
||||
<span className="home-v2-apps-tile-icon" aria-hidden="true">
|
||||
@@ -79,7 +79,7 @@ function HomeAppsCompactPlugin({ plugin: shortcut }: { plugin: HomePluginShortcu
|
||||
return (
|
||||
<Link
|
||||
to={buildPluginDetailHref(shortcut.packageName)}
|
||||
className="home-v2-apps-tile"
|
||||
className="home-v2-apps-tile oc-card oc-card-interactive"
|
||||
title={shortcut.description}
|
||||
>
|
||||
<span className="home-v2-apps-tile-icon" aria-hidden="true">
|
||||
@@ -255,12 +255,14 @@ export function HomeAppsSection() {
|
||||
}, [activeCategoryId]);
|
||||
|
||||
return (
|
||||
<section className="home-v2-apps" aria-labelledby="home-v2-apps-title">
|
||||
<section className="home-v2-apps oc-section" aria-labelledby="home-v2-apps-title">
|
||||
<div className="home-v2-apps-stage">
|
||||
<div className="home-v2-apps-workflow-header">
|
||||
<div className="home-v2-apps-workflow-copy">
|
||||
<h2 id="home-v2-apps-title">Skills for the apps you already use</h2>
|
||||
<p>
|
||||
<div className="home-v2-apps-workflow-copy oc-section-heading">
|
||||
<h2 id="home-v2-apps-title" className="oc-section-title">
|
||||
Skills for the apps you already use
|
||||
</h2>
|
||||
<p className="oc-section-copy">
|
||||
Ready-made skills and gateway plugins that plug OpenClaw straight into your everyday
|
||||
tools.
|
||||
</p>
|
||||
@@ -275,7 +277,11 @@ export function HomeAppsSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="home-v2-apps-categories" role="tablist" aria-label="App categories">
|
||||
<div
|
||||
className="home-v2-apps-categories oc-segmented"
|
||||
role="tablist"
|
||||
aria-label="App categories"
|
||||
>
|
||||
{appCategories.map((category) => {
|
||||
const Icon = category.icon;
|
||||
return (
|
||||
@@ -284,7 +290,7 @@ export function HomeAppsSection() {
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={category.id === activeCategoryId}
|
||||
className="home-v2-apps-category-tab"
|
||||
className="home-v2-apps-category-tab oc-segmented-item"
|
||||
onClick={() => setActiveCategoryId(category.id)}
|
||||
>
|
||||
<Icon className="home-v2-apps-category-tab-icon" size={14} aria-hidden="true" />
|
||||
@@ -305,7 +311,11 @@ export function HomeAppsSection() {
|
||||
</div>
|
||||
|
||||
<div className="home-v2-apps-see-all-row">
|
||||
<Link to="/skills" search={SKILLS_BROWSE_SEARCH} className="home-v2-apps-see-all">
|
||||
<Link
|
||||
to="/skills"
|
||||
search={SKILLS_BROWSE_SEARCH}
|
||||
className="home-v2-apps-see-all oc-action oc-action-ghost"
|
||||
>
|
||||
Browse all skills
|
||||
<ArrowRight size={14} aria-hidden="true" />
|
||||
</Link>
|
||||
|
||||
@@ -178,19 +178,21 @@ export function HomeBringSkillsSection() {
|
||||
|
||||
return (
|
||||
<section
|
||||
className="home-v2-byos"
|
||||
className="home-v2-byos oc-section"
|
||||
aria-labelledby="home-v2-byos-title"
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerLeave={handlePointerLeave}
|
||||
>
|
||||
<ByosRevealBackdrop />
|
||||
<div className="home-v2-byos-content">
|
||||
<header className="home-v2-byos-head">
|
||||
<span className="home-v2-byos-eyebrow">ClawHub CLI</span>
|
||||
<h2 id="home-v2-byos-title" className="home-v2-byos-title">
|
||||
<header className="home-v2-byos-head oc-section-heading">
|
||||
<span className="home-v2-byos-eyebrow oc-eyebrow">ClawHub CLI</span>
|
||||
<h2 id="home-v2-byos-title" className="home-v2-byos-title oc-section-title">
|
||||
Bring your skills to ClawHub
|
||||
</h2>
|
||||
<p className="home-v2-byos-lede">Publish and sync your skills to ClawHub, your way.</p>
|
||||
<p className="home-v2-byos-lede oc-section-copy">
|
||||
Publish and sync your skills to ClawHub, your way.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="home-v2-byos-tabs" role="tablist" aria-label="Choose an audience">
|
||||
@@ -224,7 +226,11 @@ export function HomeBringSkillsSection() {
|
||||
</div>
|
||||
|
||||
<div className="home-v2-byos-foot">
|
||||
<Link to="/import" search={{ ownerHandle: undefined }} className="home-v2-byos-import">
|
||||
<Link
|
||||
to="/import"
|
||||
search={{ ownerHandle: undefined }}
|
||||
className="home-v2-byos-import oc-action oc-action-ghost"
|
||||
>
|
||||
<GitHubGlyph />
|
||||
or import from your GitHub
|
||||
<ArrowRight size={15} aria-hidden="true" />
|
||||
|
||||
@@ -258,7 +258,9 @@ function HomeListingSkillCard({ entry, showStats }: { entry: SkillPageEntry; sho
|
||||
return (
|
||||
<Link
|
||||
to={skillLink(entry)}
|
||||
className={`home-v2-listing-card${showStats ? "" : " has-no-stats"}`}
|
||||
className={`home-v2-listing-card oc-card oc-card-interactive${
|
||||
showStats ? "" : " has-no-stats"
|
||||
}`}
|
||||
>
|
||||
<div className="home-v2-listing-card-head">
|
||||
<span className="home-v2-listing-card-icon" aria-hidden="true">
|
||||
@@ -293,7 +295,7 @@ function HomeListingPluginCard({ plugin }: { plugin: PackageListItem }) {
|
||||
const pluginHref = buildPluginDetailHref(plugin.name, { ownerHandle: plugin.ownerHandle });
|
||||
|
||||
return (
|
||||
<Link to={pluginHref} className="home-v2-listing-card">
|
||||
<Link to={pluginHref} className="home-v2-listing-card oc-card oc-card-interactive">
|
||||
<div className="home-v2-listing-card-head">
|
||||
<span className="home-v2-listing-card-icon" aria-hidden="true">
|
||||
<MarketplaceIcon kind="plugin" label={name} size="sm" />
|
||||
@@ -660,13 +662,19 @@ export function HomeListingSection({ initialListing = null }: HomeListingSection
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="home-v2-listing" className="home-v2-listing" aria-label="Browse catalog">
|
||||
<section
|
||||
id="home-v2-listing"
|
||||
className="home-v2-listing oc-section"
|
||||
aria-label="Browse catalog"
|
||||
>
|
||||
<div className="home-v2-listing-controls">
|
||||
<div className="home-v2-listing-toolbar">
|
||||
<div className="home-v2-listing-kind" role="group" aria-label="Content type">
|
||||
<div className="home-v2-listing-kind oc-segmented" role="group" aria-label="Content type">
|
||||
<button
|
||||
type="button"
|
||||
className={`home-v2-listing-kind-btn${kind === "skills" ? " is-active" : ""}`}
|
||||
className={`home-v2-listing-kind-btn oc-segmented-item${
|
||||
kind === "skills" ? " is-active" : ""
|
||||
}`}
|
||||
aria-pressed={kind === "skills"}
|
||||
onClick={() => handleKindChange("skills")}
|
||||
>
|
||||
@@ -674,7 +682,9 @@ export function HomeListingSection({ initialListing = null }: HomeListingSection
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`home-v2-listing-kind-btn${kind === "plugins" ? " is-active" : ""}`}
|
||||
className={`home-v2-listing-kind-btn oc-segmented-item${
|
||||
kind === "plugins" ? " is-active" : ""
|
||||
}`}
|
||||
aria-pressed={kind === "plugins"}
|
||||
onClick={() => handleKindChange("plugins")}
|
||||
>
|
||||
@@ -713,7 +723,9 @@ export function HomeListingSection({ initialListing = null }: HomeListingSection
|
||||
<div className="home-v2-listing-actions-rail has-category">
|
||||
<button
|
||||
type="button"
|
||||
className={`home-v2-listing-search-trigger${searchOpen ? " is-active" : ""}`}
|
||||
className={`home-v2-listing-search-trigger oc-action oc-action-ghost oc-action-icon${
|
||||
searchOpen ? " is-active" : ""
|
||||
}`}
|
||||
aria-label="Search catalog"
|
||||
aria-expanded={searchOpen}
|
||||
aria-controls="home-v2-listing-search-panel"
|
||||
@@ -729,10 +741,12 @@ export function HomeListingSection({ initialListing = null }: HomeListingSection
|
||||
onChange={setCategorySlugs}
|
||||
/>
|
||||
|
||||
<div className="home-v2-listing-view" role="group" aria-label="Layout">
|
||||
<div className="home-v2-listing-view oc-segmented" role="group" aria-label="Layout">
|
||||
<button
|
||||
type="button"
|
||||
className={`home-v2-listing-view-btn${view === "list" ? " is-active" : ""}`}
|
||||
className={`home-v2-listing-view-btn oc-segmented-item${
|
||||
view === "list" ? " is-active" : ""
|
||||
}`}
|
||||
aria-pressed={view === "list"}
|
||||
aria-label="List view"
|
||||
onClick={() => setView("list")}
|
||||
@@ -741,7 +755,9 @@ export function HomeListingSection({ initialListing = null }: HomeListingSection
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`home-v2-listing-view-btn${view === "grid" ? " is-active" : ""}`}
|
||||
className={`home-v2-listing-view-btn oc-segmented-item${
|
||||
view === "grid" ? " is-active" : ""
|
||||
}`}
|
||||
aria-pressed={view === "grid"}
|
||||
aria-label="Grid view"
|
||||
onClick={() => setView("grid")}
|
||||
@@ -790,7 +806,7 @@ export function HomeListingSection({ initialListing = null }: HomeListingSection
|
||||
<button
|
||||
key={category.slug}
|
||||
type="button"
|
||||
className="home-v2-listing-filter-chip"
|
||||
className="home-v2-listing-filter-chip oc-pill"
|
||||
onClick={() => removeCategory(category.slug)}
|
||||
aria-label={`Remove ${category.label} category filter`}
|
||||
>
|
||||
@@ -800,7 +816,7 @@ export function HomeListingSection({ initialListing = null }: HomeListingSection
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<span className="home-v2-listing-filter-chip is-summary">
|
||||
<span className="home-v2-listing-filter-chip oc-pill is-summary">
|
||||
{selectedCategories.length} categories
|
||||
</span>
|
||||
<button
|
||||
|
||||
@@ -42,7 +42,7 @@ function PopularPublisherCard({
|
||||
<Link
|
||||
to="/$slug"
|
||||
params={{ slug: pinned.handle }}
|
||||
className="home-v2-popular-publisher-card"
|
||||
className="home-v2-popular-publisher-card oc-card oc-card-interactive"
|
||||
aria-label={`${name}, @${pinned.handle}`}
|
||||
draggable={false}
|
||||
>
|
||||
@@ -140,13 +140,18 @@ export function HomePopularPublishersSection() {
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="home-v2-popular-publishers" aria-labelledby="popular-publishers-title">
|
||||
<header className="home-v2-popular-publishers-header">
|
||||
<div className="home-v2-popular-publishers-heading">
|
||||
<h2 id="popular-publishers-title">Popular creators</h2>
|
||||
<p>Explore skills and plugins from standout builders.</p>
|
||||
<section
|
||||
className="home-v2-popular-publishers oc-section"
|
||||
aria-labelledby="popular-publishers-title"
|
||||
>
|
||||
<header className="home-v2-popular-publishers-header oc-section-header">
|
||||
<div className="home-v2-popular-publishers-heading oc-section-heading">
|
||||
<h2 id="popular-publishers-title" className="oc-section-title">
|
||||
Popular creators
|
||||
</h2>
|
||||
<p className="oc-section-copy">Explore skills and plugins from standout builders.</p>
|
||||
</div>
|
||||
<Link to="/creators" className="home-v2-popular-publishers-link">
|
||||
<Link to="/creators" className="home-v2-popular-publishers-link oc-action oc-action-ghost">
|
||||
Browse creators <ArrowRight size={14} aria-hidden="true" />
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
@@ -75,7 +75,7 @@ function PromotionCard({ promotion }: { promotion: PublicPromotion }) {
|
||||
const isTencentPromotion = isTencentHyPromotion(promotion.title);
|
||||
|
||||
return (
|
||||
<article className="home-v2-promotion-card">
|
||||
<article className="home-v2-promotion-card oc-card">
|
||||
<div className="home-v2-promotion-content">
|
||||
<div className="home-v2-promotion-stack">
|
||||
<h3 className="home-v2-promotion-title">
|
||||
@@ -109,7 +109,7 @@ function PromotionCard({ promotion }: { promotion: PublicPromotion }) {
|
||||
</span>
|
||||
{ctaUrl ? (
|
||||
<a
|
||||
className="home-v2-promotion-link"
|
||||
className="home-v2-promotion-link oc-action oc-action-primary"
|
||||
href={ctaUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -167,7 +167,7 @@ export function HomePromotionsSection() {
|
||||
if (promotions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="home-v2-promotions" aria-labelledby="home-promotions-title">
|
||||
<section className="home-v2-promotions oc-section" aria-labelledby="home-promotions-title">
|
||||
<h2 id="home-promotions-title" className="sr-only">
|
||||
Active promotions
|
||||
</h2>
|
||||
|
||||
@@ -5,7 +5,11 @@ export function OfficialTag({ className }: { className?: string }) {
|
||||
return (
|
||||
<Badge
|
||||
variant="official"
|
||||
className={className ? `official-tag rounded-full ${className}` : "official-tag rounded-full"}
|
||||
className={
|
||||
className
|
||||
? `official-tag rounded-[var(--oc-radius-control)] ${className}`
|
||||
: "official-tag rounded-[var(--oc-radius-control)]"
|
||||
}
|
||||
aria-label="Verified"
|
||||
>
|
||||
<BadgeCheck size={15} aria-hidden="true" className="official-badge-icon" />
|
||||
|
||||
@@ -138,15 +138,15 @@ export function PackageSourceChooser(props: {
|
||||
props.family === "code-plugin" && props.codePluginFieldIssues.length > 0;
|
||||
const hasPackagePanelFooter = props.ignoredPaths.length > 0 || Boolean(props.validationError);
|
||||
const selectedPackagePanelToneClass = isDragging
|
||||
? "border-[color:var(--accent)] bg-[rgba(255,107,74,0.06)]"
|
||||
? "border-[color:var(--oc-border-accent)] bg-[color:var(--oc-surface-accent-soft)]"
|
||||
: isMetadataLocked
|
||||
? "border-[color:var(--line)] bg-[color:var(--surface-muted)]"
|
||||
: "border-emerald-300/45 bg-emerald-50/80 dark:border-emerald-500/30 dark:bg-emerald-950/25";
|
||||
: "border-status-success-fg/35 bg-status-success-bg";
|
||||
const selectedPackagePanelBgClass = isDragging
|
||||
? "bg-[rgba(255,107,74,0.06)]"
|
||||
? "bg-[color:var(--oc-surface-accent-soft)]"
|
||||
: isMetadataLocked
|
||||
? "bg-[color:var(--surface-muted)]"
|
||||
: "bg-emerald-50/80 dark:bg-emerald-950/25";
|
||||
: "bg-status-success-bg";
|
||||
|
||||
const setDirectoryInputRef = (node: HTMLInputElement | null) => {
|
||||
directoryInputRef.current = node;
|
||||
@@ -292,9 +292,9 @@ export function PackageSourceChooser(props: {
|
||||
) : (
|
||||
<Card className="mb-5">
|
||||
<div
|
||||
className={`relative flex flex-col items-center gap-4 overflow-hidden rounded-[var(--radius-md)] border-2 border-dashed p-8 text-center transition-colors ${
|
||||
className={`relative flex flex-col items-center gap-4 overflow-hidden rounded-[var(--oc-radius-inset)] border-2 border-dashed p-8 text-center transition-colors ${
|
||||
isDragging
|
||||
? "border-[color:var(--accent)] bg-[rgba(255,107,74,0.06)]"
|
||||
? "border-[color:var(--oc-border-accent)] bg-[color:var(--oc-surface-accent-soft)]"
|
||||
: "border-[color:var(--line)] bg-[color:var(--surface-muted)]"
|
||||
}`}
|
||||
onDragOver={(event) => {
|
||||
|
||||
@@ -235,7 +235,7 @@ export function PluginVersionsPanel({
|
||||
})}
|
||||
</div>
|
||||
{loadMoreError ? (
|
||||
<p className="mt-3 text-sm font-medium text-red-600 dark:text-red-400" role="alert">
|
||||
<p className="mt-3 text-sm font-medium text-status-error-fg" role="alert">
|
||||
{loadMoreError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function SignInPrompt({
|
||||
}}
|
||||
/>
|
||||
<section className="relative z-10 mx-auto w-full max-w-[980px]">
|
||||
<div className="relative isolate flex min-w-0 flex-col gap-6 overflow-hidden rounded-[var(--radius-md)] border border-[color:var(--line)] bg-[color:var(--surface)] px-5 pb-10 pt-7 shadow-[0_18px_50px_rgba(0,0,0,0.16)] sm:flex-row sm:items-center sm:justify-between sm:px-8 sm:py-8 sm:pb-10">
|
||||
<div className="relative isolate flex min-w-0 flex-col gap-6 overflow-hidden rounded-[var(--oc-radius-surface)] border border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-bg-elevated)] px-5 pb-10 pt-7 shadow-[var(--oc-shadow-lg)] sm:flex-row sm:items-center sm:justify-between sm:px-8 sm:py-8 sm:pb-10">
|
||||
<div className="min-w-0">
|
||||
<span className="mb-4 inline-flex h-11 w-11 items-center justify-center rounded-[var(--radius-md)] border border-[color:var(--line)] bg-[color:var(--surface-muted)] text-[color:var(--ink-soft)] sm:h-12 sm:w-12">
|
||||
<Icon size={21} />
|
||||
@@ -92,7 +92,7 @@ export function SignInPrompt({
|
||||
) : null}
|
||||
{visibleError ? (
|
||||
<p
|
||||
className="mt-3 rounded-[var(--radius-sm)] border border-red-300/40 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-500/30 dark:bg-red-950/50 dark:text-red-300"
|
||||
className="mt-3 rounded-[var(--oc-radius-control)] border border-[color:color-mix(in_srgb,var(--oc-status-error-fg)_30%,var(--oc-border-subtle))] bg-[color:var(--oc-status-error-bg)] px-3 py-2 text-sm text-[color:var(--oc-status-error-fg)]"
|
||||
role="alert"
|
||||
>
|
||||
<AuthErrorMessage message={visibleError} />{" "}
|
||||
|
||||
@@ -716,17 +716,17 @@ function buildDiffOptions(
|
||||
|
||||
function applyMonacoTheme(monaco: NonNullable<ReturnType<typeof useMonaco>>) {
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const surface = normalizeHex(styles.getPropertyValue("--surface").trim() || "#ffffff");
|
||||
const surfaceMuted = styles.getPropertyValue("--surface-muted").trim() || "#f6f1ec";
|
||||
const ink = styles.getPropertyValue("--ink").trim() || "#1d1a17";
|
||||
const inkSoft = styles.getPropertyValue("--ink-soft").trim() || "#4c463f";
|
||||
const line = styles.getPropertyValue("--line").trim() || "rgba(29, 26, 23, 0.12)";
|
||||
const accent = styles.getPropertyValue("--accent").trim() || "#e65c46";
|
||||
const seafoam = styles.getPropertyValue("--seafoam").trim() || "#2bc6a4";
|
||||
const diffAdded = styles.getPropertyValue("--diff-added").trim() || "#9bb955";
|
||||
const diffAddedStrong = styles.getPropertyValue("--diff-added-strong").trim() || seafoam;
|
||||
const diffRemoved = styles.getPropertyValue("--diff-removed").trim() || "#e47866";
|
||||
const diffRemovedStrong = styles.getPropertyValue("--diff-removed-strong").trim() || accent;
|
||||
const surface = normalizeHex(styles.getPropertyValue("--oc-bg-elevated").trim() || "#ffffff");
|
||||
const surfaceMuted = styles.getPropertyValue("--oc-bg-surface").trim() || "#f6f1ec";
|
||||
const ink = styles.getPropertyValue("--oc-text-primary").trim() || "#1d1a17";
|
||||
const inkSoft = styles.getPropertyValue("--oc-text-secondary").trim() || "#4c463f";
|
||||
const line = styles.getPropertyValue("--oc-border-subtle").trim() || "rgba(29, 26, 23, 0.12)";
|
||||
const accent = styles.getPropertyValue("--oc-accent-primary").trim() || "#e65c46";
|
||||
const seafoam = styles.getPropertyValue("--oc-accent-secondary").trim() || "#2bc6a4";
|
||||
const diffAdded = styles.getPropertyValue("--oc-diff-added").trim() || "#9bb955";
|
||||
const diffAddedStrong = styles.getPropertyValue("--oc-diff-added-strong").trim() || seafoam;
|
||||
const diffRemoved = styles.getPropertyValue("--oc-diff-removed").trim() || "#e47866";
|
||||
const diffRemovedStrong = styles.getPropertyValue("--oc-diff-removed-strong").trim() || accent;
|
||||
const diffDiagonal = toMonacoColor(
|
||||
styles.getPropertyValue("--diff-diagonal").trim() || "#22222233",
|
||||
);
|
||||
|
||||
@@ -313,14 +313,14 @@ export function SkillOwnershipPanel({
|
||||
</SettingsActionRow>
|
||||
|
||||
{canDeleteSkill ? (
|
||||
<div className="mt-8 rounded-[var(--radius-md)] border border-red-300/50 bg-red-500/[0.035] p-4 dark:border-red-500/35 dark:bg-red-500/[0.045] sm:p-5">
|
||||
<div className="mt-8 rounded-[var(--oc-radius-surface)] border border-status-error-fg/30 bg-status-error-bg p-4 sm:p-5">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-[var(--radius-sm)] border border-red-300/40 bg-red-500/10 text-red-700 dark:border-red-500/30 dark:text-red-300">
|
||||
<span className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-[var(--oc-radius-inset)] border border-status-error-fg/30 bg-status-error-bg text-status-error-fg">
|
||||
<ShieldAlert size={17} aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-bold text-red-700 dark:text-red-300">Delete skill</h3>
|
||||
<h3 className="text-sm font-bold text-status-error-fg">Delete skill</h3>
|
||||
<p className="text-sm text-[color:var(--ink-soft)]">
|
||||
Hide this skill from search, browse, and public install surfaces.
|
||||
</p>
|
||||
@@ -340,9 +340,7 @@ export function SkillOwnershipPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<p className="text-sm font-medium text-red-600 dark:text-red-400">{error}</p>
|
||||
) : null}
|
||||
{error ? <p className="text-sm font-medium text-status-error-fg">{error}</p> : null}
|
||||
</div>
|
||||
|
||||
{/* Rename confirmation dialog */}
|
||||
|
||||
@@ -142,7 +142,7 @@ export function SkillPublishSuccessDialog({
|
||||
onInteractOutside={() => {
|
||||
dismiss();
|
||||
}}
|
||||
className="[--discord-accent:#5865F2] [--publish-accent:var(--status-success-fg)] [display:block] w-[min(calc(100vw-2rem),620px)] overflow-hidden rounded-[calc(var(--radius-md)+8px)] border-[color:color-mix(in_srgb,var(--line)_88%,transparent)] bg-[color:var(--surface)] p-0 shadow-[0_34px_90px_-36px_rgba(0,0,0,0.62),0_0_0_1px_color-mix(in_srgb,var(--ink)_7%,transparent)] focus:outline-none sm:p-0"
|
||||
className="[--discord-accent:#5865F2] [--publish-accent:var(--oc-status-success-fg)] [display:block] w-[min(calc(100vw-2rem),620px)] overflow-hidden rounded-[var(--oc-radius-surface)] border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-bg-elevated)] p-0 shadow-[var(--oc-shadow-lg)] focus:outline-none sm:p-0"
|
||||
style={{ display: "block" }}
|
||||
>
|
||||
<div className="relative w-full overflow-hidden">
|
||||
@@ -234,7 +234,7 @@ export function SkillPublishSuccessDialog({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mt-2 flex min-w-0 items-center gap-2 rounded-b-[var(--radius-md)] border border-t-0 border-[color:color-mix(in_srgb,var(--line)_74%,transparent)] bg-[color:color-mix(in_srgb,var(--surface)_82%,black)] px-3.5 pb-2 pt-4">
|
||||
<div className="-mt-2 flex min-w-0 items-center gap-2 rounded-b-[var(--oc-radius-surface)] border border-t-0 border-[color:color-mix(in_srgb,var(--oc-border-subtle)_74%,transparent)] bg-[color:var(--oc-bg-surface)] px-3.5 pb-2 pt-4">
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-full bg-[color:color-mix(in_srgb,var(--publish-accent)_42%,transparent)]"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -63,7 +63,7 @@ export function SkillReportDialog({
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
{reportError ? (
|
||||
<p className="text-sm font-medium text-red-600 dark:text-red-400" role="alert">
|
||||
<p className="text-sm font-medium text-status-error-fg" role="alert">
|
||||
{reportError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -289,7 +289,7 @@ export function ScanResultBadge({
|
||||
return (
|
||||
<Badge
|
||||
variant={variant as BadgeProps["variant"]}
|
||||
className={`min-h-0 rounded-[4px] px-2.5 py-0.5 text-[0.78rem] leading-[1.3] ${statusInfo.className}${className ? ` ${className}` : ""}`}
|
||||
className={`min-h-0 rounded-[var(--oc-radius-control)] px-2.5 py-0.5 text-[var(--oc-font-size-xs)] leading-[1.3] ${statusInfo.className}${className ? ` ${className}` : ""}`}
|
||||
>
|
||||
{label ?? statusInfo.label}
|
||||
</Badge>
|
||||
|
||||
@@ -43,10 +43,15 @@ export function SkillShortSummaryField({
|
||||
className="mr-auto flex min-w-0 items-center gap-1.5 text-xs leading-snug text-[color:var(--ink-soft)]"
|
||||
role="note"
|
||||
>
|
||||
<Lightbulb className="size-3.5 shrink-0 text-[#0099ff]" aria-hidden />
|
||||
<Lightbulb
|
||||
className="size-3.5 shrink-0 text-[color:var(--oc-status-info-fg)]"
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="min-w-0">
|
||||
<span className="font-semibold text-[#0099ff]">Pulled from your SKILL.md.</span> This
|
||||
is what people see in cards and search.
|
||||
<span className="font-semibold text-[color:var(--oc-status-info-fg)]">
|
||||
Pulled from your SKILL.md.
|
||||
</span>{" "}
|
||||
This is what people see in cards and search.
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -59,7 +59,7 @@ function DecorIcon({ active, icon: Icon, className }: DecorIconProps) {
|
||||
return (
|
||||
<span
|
||||
data-active={active ? "true" : undefined}
|
||||
className={`absolute hidden h-11 w-11 items-center justify-center rounded-[var(--radius-md)] border border-[color:var(--line)] bg-[color:var(--surface)]/70 text-[color:var(--ink-soft)] opacity-45 shadow-sm data-[active=true]:animate-[upload-decor-jiggle_0.44s_ease-in-out_infinite] sm:flex ${className}`}
|
||||
className={`absolute hidden h-11 w-11 items-center justify-center rounded-[var(--oc-radius-inset)] border border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-bg-elevated)]/70 text-[color:var(--oc-text-secondary)] opacity-45 shadow-[var(--oc-shadow-sm)] data-[active=true]:animate-[upload-decor-jiggle_0.44s_ease-in-out_infinite] sm:flex ${className}`}
|
||||
>
|
||||
<Icon className="h-5 w-5" strokeWidth={1.8} />
|
||||
</span>
|
||||
|
||||
@@ -23,7 +23,7 @@ export function BrowseResultsSkeleton({
|
||||
className="card skill-card skill-card-spaced-footer"
|
||||
>
|
||||
<div className="skill-card-header">
|
||||
<Skeleton className="h-[34px] w-[34px] rounded-[9px]" />
|
||||
<Skeleton className="h-[34px] w-[34px] rounded-[var(--oc-radius-inset)]" />
|
||||
<div className="skill-card-identity">
|
||||
<Skeleton className="h-5 w-40 max-w-full" />
|
||||
<Skeleton className="h-4 w-24 max-w-full" />
|
||||
@@ -62,7 +62,7 @@ export function BrowseResultsSkeleton({
|
||||
key={i}
|
||||
className="skill-list-item skill-list-item-has-creator browse-results-skeleton-row"
|
||||
>
|
||||
<Skeleton className="browse-results-skeleton-icon h-9 w-9 shrink-0 rounded-[var(--r-sm)]" />
|
||||
<Skeleton className="browse-results-skeleton-icon h-9 w-9 shrink-0 rounded-[var(--oc-radius-inset)]" />
|
||||
<div className="skill-list-item-body">
|
||||
<div className="skill-list-item-main">
|
||||
<Skeleton className="h-5 w-32 max-w-[45%]" />
|
||||
|
||||
@@ -3,14 +3,14 @@ import * as React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-[var(--radius-sm)] border px-4 py-3 text-sm [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg+*]:pl-7",
|
||||
"relative w-full rounded-[var(--oc-radius-control)] border px-4 py-3 text-sm [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg+*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-[color:var(--line)] bg-[color:var(--surface-muted)] text-[color:var(--ink)]",
|
||||
info: "border-[color:color-mix(in_srgb,#6aa9ff_36%,var(--line))] bg-[color:color-mix(in_srgb,#6aa9ff_14%,transparent)] text-[#8fbdff]",
|
||||
warn: "border-[color:color-mix(in_srgb,#f5c84b_38%,var(--line))] bg-[color:color-mix(in_srgb,#f5c84b_14%,transparent)] text-[#f5c84b]",
|
||||
info: "border-[color:color-mix(in_srgb,var(--oc-status-info-fg)_32%,var(--oc-border-subtle))] bg-[color:var(--oc-status-info-bg)] text-[color:var(--oc-status-info-fg)]",
|
||||
warn: "border-[color:color-mix(in_srgb,var(--oc-status-warning-fg)_32%,var(--oc-border-subtle))] bg-[color:var(--oc-status-warning-bg)] text-[color:var(--oc-status-warning-fg)]",
|
||||
destructive:
|
||||
"border-[color:var(--status-error-border,var(--line))] bg-[color:var(--status-error-bg)] text-[color:var(--status-error-fg)]",
|
||||
},
|
||||
|
||||
@@ -8,7 +8,10 @@ const Avatar = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-[var(--oc-radius-round)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -33,7 +36,7 @@ const AvatarFallback = React.forwardRef<
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-[color:var(--surface-muted)] text-sm font-semibold text-[color:var(--ink-soft)]",
|
||||
"flex h-full w-full items-center justify-center rounded-[var(--oc-radius-round)] bg-[color:var(--oc-bg-surface)] text-sm font-semibold text-[color:var(--oc-text-secondary)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -35,7 +35,7 @@ const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
variant === "warning" &&
|
||||
"bg-status-warning-bg px-3 py-1 text-status-warning-fg border border-line",
|
||||
variant === "review" &&
|
||||
"bg-[color:color-mix(in_srgb,#6aa9ff_16%,transparent)] px-3 py-1 text-[#6aa9ff] border border-[color:color-mix(in_srgb,#6aa9ff_24%,var(--line))]",
|
||||
"bg-[color:var(--oc-status-info-bg)] px-3 py-1 text-[color:var(--oc-status-info-fg)] border border-[color:color-mix(in_srgb,var(--oc-status-info-fg)_28%,var(--oc-border-subtle))]",
|
||||
variant === "destructive" &&
|
||||
"bg-status-error-bg px-3 py-1 text-status-error-fg border border-line",
|
||||
size === "sm" &&
|
||||
|
||||
@@ -5,8 +5,8 @@ import { cn } from "../../lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
[
|
||||
"inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap font-semibold transition-all duration-200 ease-out",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]/35 focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bg)]",
|
||||
"inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap font-semibold transition-all duration-[var(--oc-duration-ui)] ease-[var(--oc-ease-out)]",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--oc-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--oc-bg-page)]",
|
||||
"cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-60",
|
||||
"!no-underline hover:!no-underline",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
|
||||
@@ -7,7 +7,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
|
||||
ref={ref}
|
||||
className={cn(
|
||||
// Matches .card
|
||||
"flex w-full flex-col gap-3 rounded-[var(--radius-md)] border border-[color:var(--line)] bg-[color:var(--surface)] p-space-5 transition-all duration-200 ease-out",
|
||||
"flex w-full flex-col gap-3 rounded-[var(--oc-radius-surface)] border border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-surface-card)] p-space-5 transition-all duration-[var(--oc-duration-ui)] ease-[var(--oc-ease-out)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -17,7 +17,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
align={align}
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[180px] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-[var(--radius-md)] border border-[color:var(--line)] bg-[color:var(--surface)] p-2 text-[color:var(--ink)] shadow-[var(--shadow)]",
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[180px] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-[var(--oc-radius-surface)] border border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-bg-elevated)] p-2 text-[color:var(--oc-text-primary)] shadow-[var(--oc-shadow-lg)]",
|
||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
@@ -42,7 +42,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center gap-2 rounded-[var(--radius-sm)] px-3 py-2 text-sm font-semibold text-[color:var(--ink)] outline-none transition-colors focus-visible:outline-none data-[highlighted]:bg-[color:var(--surface-muted)] data-[highlighted]:text-[color:var(--ink)] focus:bg-[color:var(--surface-muted)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset=true]:pl-8 data-[variant=destructive]:text-status-error-fg",
|
||||
"relative flex cursor-pointer select-none items-center gap-2 rounded-[var(--oc-radius-control)] px-3 py-2 text-sm font-semibold text-[color:var(--oc-text-primary)] outline-none transition-colors focus-visible:outline-none data-[highlighted]:bg-[color:var(--oc-surface-interactive)] data-[highlighted]:text-[color:var(--oc-text-primary)] focus:bg-[color:var(--oc-surface-interactive)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset=true]:pl-8 data-[variant=destructive]:text-status-error-fg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -58,7 +58,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-[var(--radius-sm)] py-2 pr-3 pl-8 text-sm font-semibold text-[color:var(--ink)] outline-none transition-colors focus:bg-[color:var(--surface-muted)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex cursor-pointer select-none items-center rounded-[var(--oc-radius-control)] py-2 pr-3 pl-8 text-sm font-semibold text-[color:var(--oc-text-primary)] outline-none transition-colors focus:bg-[color:var(--oc-surface-interactive)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -8,11 +8,11 @@ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLI
|
||||
type={type}
|
||||
className={cn(
|
||||
// Base styles
|
||||
"w-full min-h-[44px] rounded-[var(--radius-sm)] border px-3.5 py-space-3 text-[color:var(--ink)] transition-all duration-[180ms] ease-out",
|
||||
"w-full min-h-[44px] rounded-[var(--oc-radius-control)] border px-3.5 py-space-3 text-[color:var(--oc-text-primary)] transition-all duration-[var(--oc-duration-fast)] ease-[var(--oc-ease-out)]",
|
||||
"border-input-border bg-input-bg",
|
||||
"placeholder:text-input-placeholder",
|
||||
// Focus
|
||||
"focus:outline-none focus:border-input-focus-border focus:shadow-[0_0_0_3px_var(--input-focus-ring)]",
|
||||
"focus:outline-none focus:border-input-focus-border focus:shadow-[0_0_0_3px_var(--oc-input-focus-ring)]",
|
||||
// Disabled
|
||||
"disabled:cursor-not-allowed disabled:opacity-60",
|
||||
className,
|
||||
|
||||
@@ -19,11 +19,11 @@ const SelectTrigger = React.forwardRef<
|
||||
data-size={size}
|
||||
className={cn(
|
||||
// Matches form input token styling
|
||||
"flex w-full min-h-[44px] cursor-pointer items-center justify-between rounded-[var(--radius-sm)] border px-3.5 py-space-3 text-sm text-[color:var(--ink)] transition-all duration-[180ms] ease-out",
|
||||
"flex w-full min-h-[44px] cursor-pointer items-center justify-between rounded-[var(--oc-radius-control)] border px-3.5 py-space-3 text-sm text-[color:var(--oc-text-primary)] transition-all duration-[var(--oc-duration-fast)] ease-[var(--oc-ease-out)]",
|
||||
"data-[size=sm]:min-h-8 data-[size=sm]:px-2.5 data-[size=sm]:py-1 data-[size=sm]:text-xs",
|
||||
"border-input-border bg-input-bg",
|
||||
"placeholder:text-input-placeholder",
|
||||
"focus:outline-none focus:border-input-focus-border focus:shadow-[0_0_0_3px_var(--input-focus-ring)]",
|
||||
"focus:outline-none focus:border-input-focus-border focus:shadow-[0_0_0_3px_var(--oc-input-focus-ring)]",
|
||||
"disabled:cursor-not-allowed disabled:opacity-60",
|
||||
className,
|
||||
)}
|
||||
@@ -74,7 +74,7 @@ const SelectContent = React.forwardRef<
|
||||
ref={ref}
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"relative z-[90] max-h-[var(--radix-select-content-available-height)] min-w-[8rem] origin-[var(--radix-select-content-transform-origin)] overflow-hidden rounded-[var(--radius-md)] border border-[color:var(--line)] bg-[color:var(--surface)] text-[color:var(--ink)] shadow-[var(--shadow)]",
|
||||
"relative z-[90] max-h-[var(--radix-select-content-available-height)] min-w-[8rem] origin-[var(--radix-select-content-transform-origin)] overflow-hidden rounded-[var(--oc-radius-surface)] border border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-bg-elevated)] text-[color:var(--oc-text-primary)] shadow-[var(--oc-shadow-lg)]",
|
||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
|
||||
@@ -41,7 +41,7 @@ const SheetContent = React.forwardRef<
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
className={cn(
|
||||
"fixed z-80 flex flex-col gap-4 border-[color:var(--line)] bg-[color:var(--bg)] p-6 shadow-[var(--shadow)] transition-transform duration-300 ease-out",
|
||||
"fixed z-80 flex flex-col gap-4 border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-bg-page)] p-6 shadow-[var(--oc-shadow-lg)] transition-transform duration-[var(--oc-duration-ui)] ease-[var(--oc-ease-out)]",
|
||||
side === "right" &&
|
||||
"inset-y-0 right-0 h-full w-3/4 max-w-sm border-l data-[state=open]:animate-in data-[state=open]:slide-in-from-right data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right",
|
||||
side === "left" &&
|
||||
|
||||
@@ -9,11 +9,11 @@ const Textarea = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
// Base styles
|
||||
"w-full min-h-[100px] resize-y rounded-[var(--radius-sm)] border px-3.5 py-space-3 text-[color:var(--ink)] transition-all duration-[180ms] ease-out",
|
||||
"w-full min-h-[100px] resize-y rounded-[var(--oc-radius-control)] border px-3.5 py-space-3 text-[color:var(--oc-text-primary)] transition-all duration-[var(--oc-duration-fast)] ease-[var(--oc-ease-out)]",
|
||||
"border-input-border bg-input-bg",
|
||||
"placeholder:text-input-placeholder",
|
||||
// Focus
|
||||
"focus:outline-none focus:border-input-focus-border focus:shadow-[0_0_0_3px_var(--input-focus-ring)]",
|
||||
"focus:outline-none focus:border-input-focus-border focus:shadow-[0_0_0_3px_var(--oc-input-focus-ring)]",
|
||||
// Disabled
|
||||
"disabled:cursor-not-allowed disabled:opacity-60",
|
||||
className,
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const toggleGroupItemVariants = cva(
|
||||
"inline-flex shrink-0 items-center justify-center rounded-[var(--radius-pill)] text-[color:var(--ink-soft)] transition-colors hover:text-[color:var(--ink)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)] disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-fg [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"inline-flex shrink-0 items-center justify-center rounded-[var(--oc-radius-control)] text-[color:var(--oc-text-secondary)] transition-colors hover:text-[color:var(--oc-text-primary)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--oc-focus-ring)] disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-fg [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -48,7 +48,7 @@ const ToggleGroup = React.forwardRef<
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"inline-flex h-[38px] items-center gap-0.5 rounded-[var(--radius-pill)] border border-[color:var(--line)] bg-[color:var(--surface)] p-[3px]",
|
||||
"inline-flex h-[38px] items-center gap-0.5 rounded-[var(--oc-radius-control)] border border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-bg-elevated)] p-[3px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -15,7 +15,7 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-[var(--radius-sm)] border border-[color:var(--line)] bg-[color:var(--surface)] px-3 py-1.5 text-xs font-medium text-[color:var(--ink)] shadow-[var(--shadow)]",
|
||||
"z-50 overflow-hidden rounded-[var(--oc-radius-control)] border border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-bg-elevated)] px-3 py-1.5 text-xs font-medium text-[color:var(--oc-text-primary)] shadow-[var(--oc-shadow-md)]",
|
||||
"animate-in fade-in-0 zoom-in-95",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
@import "@openclaw/design-system/tokens.css";
|
||||
@import "@openclaw/design-system/themes.css";
|
||||
@import "@openclaw/design-system/typography.css";
|
||||
@import "@openclaw/design-system/themes/product.css";
|
||||
@import "@openclaw/design-system/components.css";
|
||||
@import "@openclaw/design-system/compat/clawhub.css";
|
||||
|
||||
/*
|
||||
* Temporary aliases for legacy ClawHub styles that have not yet moved to the
|
||||
* canonical token names. Values live here so old selectors cannot drift into
|
||||
* a second theme system while the route CSS is migrated incrementally.
|
||||
*/
|
||||
:root {
|
||||
--accent-soft: var(--oc-surface-accent-soft);
|
||||
--card-border: var(--oc-border-subtle);
|
||||
--color-muted: var(--oc-text-muted);
|
||||
--color-text: var(--oc-text-primary);
|
||||
--danger: var(--oc-status-error-fg);
|
||||
--font-sans: var(--oc-font-body);
|
||||
--ink-faint: var(--oc-text-muted);
|
||||
--official-border: color-mix(in srgb, var(--oc-status-info-fg) 28%, var(--oc-border-subtle));
|
||||
--shadow: var(--oc-shadow-md);
|
||||
--shadow-card: var(--oc-shadow-sm);
|
||||
--shadow-dialog: var(--oc-shadow-lg);
|
||||
--shadow-hover: var(--oc-shadow-md);
|
||||
--shadow-lg: var(--oc-shadow-lg);
|
||||
--status-pending-bg: var(--oc-surface-interactive);
|
||||
--status-pending-fg: var(--oc-text-muted);
|
||||
--surface-raised: var(--oc-surface-interactive-hover);
|
||||
--surface-subtle: var(--oc-surface-interactive);
|
||||
--transition-fast: var(--oc-duration-fast) var(--oc-ease-out);
|
||||
}
|
||||
|
||||
/*
|
||||
* The landing page predates the shared package and still references --hv2-*
|
||||
* variables throughout its layout CSS. Keep those aliases during migration,
|
||||
* but make the canonical design system their only source of visual values.
|
||||
*/
|
||||
.home-v2-main.oc-app-surface {
|
||||
--hv2-bg: var(--oc-bg-page);
|
||||
--hv2-surface: var(--oc-surface-card);
|
||||
--hv2-surface-hover: var(--oc-surface-interactive);
|
||||
--hv2-border: var(--oc-border-subtle);
|
||||
--hv2-border-hover: color-mix(in srgb, var(--oc-text-primary) 28%, var(--oc-border-subtle));
|
||||
--hv2-border-strong: color-mix(in srgb, var(--oc-text-primary) 20%, var(--oc-border-subtle));
|
||||
--hv2-text: var(--oc-text-primary);
|
||||
--hv2-text-secondary: var(--oc-text-secondary);
|
||||
--hv2-text-tertiary: var(--oc-text-muted);
|
||||
--hv2-accent: var(--oc-accent-primary);
|
||||
--hv2-accent-fill: var(--oc-surface-accent-soft);
|
||||
--hv2-accent-fill-hover: color-mix(
|
||||
in srgb,
|
||||
var(--oc-surface-accent-soft) 78%,
|
||||
var(--oc-accent-primary)
|
||||
);
|
||||
--hv2-accent-glow: color-mix(in srgb, var(--oc-accent-primary) 12%, transparent);
|
||||
--hv2-accent-border: var(--oc-border-accent);
|
||||
--hv2-accent-border-hover: color-mix(in srgb, var(--oc-accent-primary) 68%, transparent);
|
||||
--hv2-green: var(--oc-accent-secondary);
|
||||
--hv2-green-fill: var(--oc-surface-secondary-soft);
|
||||
--hv2-green-border: color-mix(in srgb, var(--oc-accent-secondary) 38%, transparent);
|
||||
--hv2-green-border-hover: color-mix(in srgb, var(--oc-accent-secondary) 62%, transparent);
|
||||
--hv2-green-fill-hover: color-mix(
|
||||
in srgb,
|
||||
var(--oc-surface-secondary-soft) 72%,
|
||||
var(--oc-accent-secondary)
|
||||
);
|
||||
--hv2-radius-sm: var(--oc-radius-control);
|
||||
--hv2-radius-md: var(--oc-radius-surface);
|
||||
--hv2-radius-lg: var(--oc-radius-surface);
|
||||
--hv2-ease-out: var(--oc-ease-out);
|
||||
--hv2-duration-fast: var(--oc-duration-fast);
|
||||
--hv2-duration-ui: var(--oc-duration-ui);
|
||||
}
|
||||
|
||||
.home-v2-main .oc-card {
|
||||
border-color: var(--oc-component-border);
|
||||
border-radius: var(--oc-radius-surface);
|
||||
background: var(--oc-component-surface);
|
||||
color: var(--oc-text-primary);
|
||||
box-shadow: var(--oc-component-shadow);
|
||||
}
|
||||
|
||||
.home-v2-main .oc-card-interactive:hover,
|
||||
.home-v2-main .oc-card-interactive:focus-visible {
|
||||
border-color: var(--oc-component-border-hover);
|
||||
background: var(--oc-component-surface-hover);
|
||||
box-shadow: var(--oc-component-shadow-hover);
|
||||
}
|
||||
|
||||
.home-v2-main .oc-action-primary {
|
||||
border-color: var(--oc-accent-primary);
|
||||
background: var(--oc-accent-primary);
|
||||
color: var(--oc-text-on-accent);
|
||||
}
|
||||
|
||||
.home-v2-main .oc-action-primary:hover,
|
||||
.home-v2-main .oc-action-primary:focus-visible {
|
||||
border-color: var(--oc-accent-primary-hover);
|
||||
background: var(--oc-accent-primary-hover);
|
||||
color: var(--oc-text-on-accent);
|
||||
}
|
||||
|
||||
.home-v2-main .oc-segmented {
|
||||
border-color: var(--oc-border-subtle);
|
||||
border-radius: var(--oc-radius-control);
|
||||
background: var(--oc-surface-card);
|
||||
}
|
||||
|
||||
.home-v2-main .oc-segmented-item {
|
||||
border-radius: var(--oc-radius-inset);
|
||||
}
|
||||
|
||||
.home-v2-main .oc-segmented-item[aria-selected="true"],
|
||||
.home-v2-main .oc-segmented-item[aria-pressed="true"],
|
||||
.home-v2-main .oc-segmented-item.is-active {
|
||||
background: var(--oc-surface-interactive-hover);
|
||||
color: var(--oc-text-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.home-v2-main .home-v2-popular-publishers-header.oc-section-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: var(--oc-space-3);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The legacy light selectors in styles.css are more specific than the
|
||||
* compatibility aliases' :root rule. Rebind those aliases at the same
|
||||
* specificity while keeping every value owned by the shared package.
|
||||
*/
|
||||
[data-theme-family="claw"][data-theme-resolved="light"] {
|
||||
--bg: var(--oc-bg-page);
|
||||
--bg-soft: var(--oc-bg-surface);
|
||||
--surface: var(--oc-bg-elevated);
|
||||
--surface-muted: var(--oc-bg-surface);
|
||||
--nav-bg: var(--oc-bg-page);
|
||||
--ink: var(--oc-text-primary);
|
||||
--ink-soft: var(--oc-text-secondary);
|
||||
--accent: var(--oc-accent-primary);
|
||||
--accent-fg: var(--oc-text-on-accent);
|
||||
--accent-deep: var(--oc-accent-primary-hover);
|
||||
--accent-subtle: var(--oc-surface-accent-soft);
|
||||
--line: var(--oc-border-subtle);
|
||||
--border-ui: var(--oc-border-subtle);
|
||||
--border-ui-hover: color-mix(in srgb, var(--oc-text-primary) 20%, transparent);
|
||||
--border-ui-active: var(--oc-border-accent);
|
||||
--status-success-bg: var(--oc-status-success-bg);
|
||||
--status-success-fg: var(--oc-status-success-fg);
|
||||
--status-warning-bg: var(--oc-status-warning-bg);
|
||||
--status-warning-fg: var(--oc-status-warning-fg);
|
||||
--status-error-bg: var(--oc-status-error-bg);
|
||||
--status-error-fg: var(--oc-status-error-fg);
|
||||
--official-bg: var(--oc-status-info-bg);
|
||||
--official-fg: var(--oc-status-info-fg);
|
||||
--input-border: var(--oc-input-border);
|
||||
--input-bg: var(--oc-input-bg);
|
||||
--input-placeholder: var(--oc-input-placeholder);
|
||||
--input-focus-border: var(--oc-input-focus-border);
|
||||
--input-focus-ring: var(--oc-input-focus-ring);
|
||||
--label-fg: var(--oc-text-secondary);
|
||||
--hover-bg: var(--oc-surface-interactive);
|
||||
--active-bg: var(--oc-surface-accent-soft);
|
||||
--overlay-bg: var(--oc-surface-overlay);
|
||||
--diff-added: var(--oc-diff-added);
|
||||
--diff-added-strong: var(--oc-diff-added-strong);
|
||||
--diff-removed: var(--oc-diff-removed);
|
||||
--diff-removed-strong: var(--oc-diff-removed-strong);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
[data-theme-family="claw"][data-theme-mode="system"] {
|
||||
--bg: var(--oc-bg-page);
|
||||
--bg-soft: var(--oc-bg-surface);
|
||||
--surface: var(--oc-bg-elevated);
|
||||
--surface-muted: var(--oc-bg-surface);
|
||||
--nav-bg: var(--oc-bg-page);
|
||||
--ink: var(--oc-text-primary);
|
||||
--ink-soft: var(--oc-text-secondary);
|
||||
--accent: var(--oc-accent-primary);
|
||||
--accent-fg: var(--oc-text-on-accent);
|
||||
--accent-deep: var(--oc-accent-primary-hover);
|
||||
--accent-subtle: var(--oc-surface-accent-soft);
|
||||
--line: var(--oc-border-subtle);
|
||||
--border-ui: var(--oc-border-subtle);
|
||||
--border-ui-hover: color-mix(in srgb, var(--oc-text-primary) 20%, transparent);
|
||||
--border-ui-active: var(--oc-border-accent);
|
||||
--status-success-bg: var(--oc-status-success-bg);
|
||||
--status-success-fg: var(--oc-status-success-fg);
|
||||
--status-warning-bg: var(--oc-status-warning-bg);
|
||||
--status-warning-fg: var(--oc-status-warning-fg);
|
||||
--status-error-bg: var(--oc-status-error-bg);
|
||||
--status-error-fg: var(--oc-status-error-fg);
|
||||
--official-bg: var(--oc-status-info-bg);
|
||||
--official-fg: var(--oc-status-info-fg);
|
||||
--input-border: var(--oc-input-border);
|
||||
--input-bg: var(--oc-input-bg);
|
||||
--input-placeholder: var(--oc-input-placeholder);
|
||||
--input-focus-border: var(--oc-input-focus-border);
|
||||
--input-focus-ring: var(--oc-input-focus-ring);
|
||||
--label-fg: var(--oc-text-secondary);
|
||||
--hover-bg: var(--oc-surface-interactive);
|
||||
--active-bg: var(--oc-surface-accent-soft);
|
||||
--overlay-bg: var(--oc-surface-overlay);
|
||||
--diff-added: var(--oc-diff-added);
|
||||
--diff-added-strong: var(--oc-diff-added-strong);
|
||||
--diff-removed: var(--oc-diff-removed);
|
||||
--diff-removed-strong: var(--oc-diff-removed-strong);
|
||||
}
|
||||
}
|
||||
@@ -176,12 +176,12 @@
|
||||
.publisher-profile-member-avatar-stack-item .marketplace-icon,
|
||||
.publisher-profile-member-menu-row .marketplace-icon,
|
||||
.publisher-profile-meta-chip .marketplace-icon-user {
|
||||
border-radius: 999px;
|
||||
border-radius: var(--oc-radius-round);
|
||||
}
|
||||
|
||||
.publisher-profile-chrome-identity .publisher-profile-avatar .marketplace-icon-org,
|
||||
.publisher-profile-meta-chip .marketplace-icon-org {
|
||||
border-radius: var(--r-md);
|
||||
border-radius: var(--oc-radius-inset);
|
||||
}
|
||||
|
||||
.publisher-profile-chrome-identity
|
||||
@@ -536,17 +536,13 @@
|
||||
max-height: min(20rem, 56vh);
|
||||
overflow: hidden !important;
|
||||
border: 1px solid var(--line) !important;
|
||||
border-radius: 16px !important;
|
||||
border-radius: var(--oc-radius-surface) !important;
|
||||
background: var(--surface) !important;
|
||||
box-shadow:
|
||||
0 18px 48px rgba(0, 0, 0, 0.24),
|
||||
0 0 0 1px color-mix(in srgb, var(--ink) 4%, transparent) !important;
|
||||
box-shadow: var(--oc-shadow-lg) !important;
|
||||
}
|
||||
|
||||
[data-theme-resolved="light"] .publisher-profile-members-menu {
|
||||
box-shadow:
|
||||
0 14px 36px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 1px var(--line) !important;
|
||||
box-shadow: var(--oc-shadow-lg) !important;
|
||||
}
|
||||
|
||||
.publisher-profile-members-menu-list {
|
||||
|
||||
@@ -425,7 +425,9 @@ export function PromotionsPage({
|
||||
/>
|
||||
</PromotionField>
|
||||
</div>
|
||||
{formError ? <p className="section-subtitle m-0 mt-3 text-red-500">{formError}</p> : null}
|
||||
{formError ? (
|
||||
<p className="section-subtitle m-0 mt-3 text-status-error-fg">{formError}</p>
|
||||
) : null}
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Button type="button" onClick={submit} disabled={submitting}>
|
||||
{editingSlug ? "Save changes" : "Create draft"}
|
||||
|
||||
@@ -501,7 +501,8 @@ describe("Settings", () => {
|
||||
expect(screen.getByRole("heading", { name: "Delete synced repo & skills" })).toBeTruthy();
|
||||
expect(screen.getByText(/This will delete the sync job for this repo/i)).toBeTruthy();
|
||||
const deleteButton = screen.getByRole("button", { name: "Delete" });
|
||||
expect(deleteButton.className).toMatch(/border-red/);
|
||||
expect(deleteButton.className).toContain("bg-status-error-bg");
|
||||
expect(deleteButton.className).toContain("text-status-error-fg");
|
||||
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
|
||||
+10
-5
@@ -24,6 +24,7 @@ import {
|
||||
} from "../lib/authErrorMessage";
|
||||
import { getClawHubSiteUrl, SITE_DESCRIPTION, SITE_NAME } from "../lib/site";
|
||||
import { getThemeModeFromCookieHeader, normalizeThemeMode } from "../lib/themeCookie";
|
||||
import designSystemCss from "../design-system.css?url";
|
||||
import appCss from "../styles.css?url";
|
||||
|
||||
const OG_IMAGE_VERSION = "20260624-1";
|
||||
@@ -120,6 +121,10 @@ export const Route = createRootRoute({
|
||||
rel: "stylesheet",
|
||||
href: appCss,
|
||||
},
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: designSystemCss,
|
||||
},
|
||||
{
|
||||
rel: "icon",
|
||||
href: "/favicon.ico",
|
||||
@@ -200,11 +205,11 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: "var(--surface)",
|
||||
color: "var(--ink)",
|
||||
border: "1px solid var(--line)",
|
||||
borderRadius: "var(--radius-md)",
|
||||
fontFamily: "var(--font-body)",
|
||||
background: "var(--oc-bg-elevated)",
|
||||
color: "var(--oc-text-primary)",
|
||||
border: "1px solid var(--oc-border-subtle)",
|
||||
borderRadius: "var(--oc-radius-control)",
|
||||
fontFamily: "var(--oc-font-body)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
+2
-2
@@ -139,7 +139,7 @@ export function AddPage() {
|
||||
type="button"
|
||||
className={`flex min-h-11 items-center justify-center gap-2 rounded-[var(--radius-sm)] px-3 text-sm font-semibold transition-colors ${
|
||||
kind === "skill"
|
||||
? "bg-[color:var(--surface)] text-[color:var(--ink)] shadow-sm"
|
||||
? "bg-[color:var(--oc-bg-elevated)] text-[color:var(--oc-text-primary)] shadow-[var(--oc-shadow-sm)]"
|
||||
: "text-[color:var(--ink-soft)] hover:text-[color:var(--ink)]"
|
||||
}`}
|
||||
aria-pressed={kind === "skill"}
|
||||
@@ -152,7 +152,7 @@ export function AddPage() {
|
||||
type="button"
|
||||
className={`flex min-h-11 items-center justify-center gap-2 rounded-[var(--radius-sm)] px-3 text-sm font-semibold transition-colors ${
|
||||
kind === "plugin"
|
||||
? "bg-[color:var(--surface)] text-[color:var(--ink)] shadow-sm"
|
||||
? "bg-[color:var(--oc-bg-elevated)] text-[color:var(--oc-text-primary)] shadow-[var(--oc-shadow-sm)]"
|
||||
: "text-[color:var(--ink-soft)] hover:text-[color:var(--ink)]"
|
||||
}`}
|
||||
aria-pressed={kind === "plugin"}
|
||||
|
||||
@@ -768,13 +768,13 @@ export function ImportGitHub() {
|
||||
) : null}
|
||||
|
||||
{repoListError ? (
|
||||
<div className="rounded-[var(--radius-sm)] border border-red-300/40 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-500/30 dark:bg-red-950/50 dark:text-red-300">
|
||||
<div className="rounded-[var(--oc-radius-control)] border border-status-error-fg/30 bg-status-error-bg px-3 py-2 text-sm text-status-error-fg">
|
||||
{repoListError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="rounded-[var(--radius-sm)] border border-red-300/40 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-500/30 dark:bg-red-950/50 dark:text-red-300">
|
||||
<div className="rounded-[var(--oc-radius-control)] border border-status-error-fg/30 bg-status-error-bg px-3 py-2 text-sm text-status-error-fg">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -873,7 +873,7 @@ export function ImportGitHub() {
|
||||
) : isReviewing && !isReviewLoading ? (
|
||||
<section className="flex flex-col gap-4">
|
||||
{error ? (
|
||||
<div className="rounded-[var(--radius-sm)] border border-red-300/40 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-500/30 dark:bg-red-950/50 dark:text-red-300">
|
||||
<div className="rounded-[var(--oc-radius-control)] border border-status-error-fg/30 bg-status-error-bg px-3 py-2 text-sm text-status-error-fg">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1390,7 +1390,7 @@ function ReviewSkillCard({
|
||||
</div>
|
||||
|
||||
{issues.length > 0 ? (
|
||||
<div className="rounded-[var(--radius-sm)] border border-red-300/40 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-500/30 dark:bg-red-950/50 dark:text-red-300">
|
||||
<div className="rounded-[var(--oc-radius-control)] border border-status-error-fg/30 bg-status-error-bg px-3 py-2 text-sm text-status-error-fg">
|
||||
{issues[0]}
|
||||
{slugCollision?.url ? (
|
||||
<>
|
||||
|
||||
@@ -337,12 +337,12 @@ function SkillsHome() {
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="home-v2-main">
|
||||
<main className="home-v2-main oc-app-surface">
|
||||
<HomeV2FoldBottomFade />
|
||||
<canvas ref={confettiRef} className="home-v2-confetti" style={{ display: "none" }} />
|
||||
|
||||
{/* ═══ HERO ═══ */}
|
||||
<section className="home-v2-hero">
|
||||
<section className="home-v2-hero oc-hero">
|
||||
<div className="home-v2-hero-bg">
|
||||
<div className="home-v2-glow" />
|
||||
<div className="home-v2-dots" />
|
||||
@@ -353,7 +353,7 @@ function SkillsHome() {
|
||||
|
||||
{slotState ? (
|
||||
<h1
|
||||
className={`home-v2-headline home-v2-headline-slots${
|
||||
className={`home-v2-headline home-v2-headline-slots oc-hero-title${
|
||||
slotState.phase === "stopped" && slotState.won
|
||||
? slotState.isHackJackpot
|
||||
? " home-v2-headline-jackpot home-v2-headline-hack"
|
||||
@@ -385,7 +385,7 @@ function SkillsHome() {
|
||||
</button>
|
||||
</h1>
|
||||
) : (
|
||||
<h1 className="home-v2-headline">
|
||||
<h1 className="home-v2-headline oc-hero-title">
|
||||
<button
|
||||
type="button"
|
||||
className="home-v2-headline-trigger"
|
||||
@@ -411,7 +411,7 @@ function SkillsHome() {
|
||||
</h1>
|
||||
)}
|
||||
|
||||
<p className="home-v2-sub">Discover skills and plugins from top creators</p>
|
||||
<p className="home-v2-sub oc-hero-lede">Discover skills and plugins from top creators</p>
|
||||
</section>
|
||||
|
||||
<HomePromotionsSection />
|
||||
|
||||
+37
-42
@@ -831,7 +831,7 @@ export function Settings() {
|
||||
</Field>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-end">
|
||||
{hasProfileChanges ? (
|
||||
<span className="text-sm font-semibold text-red-700 dark:text-red-300">
|
||||
<span className="text-sm font-semibold text-status-error-fg">
|
||||
You have unsaved changes.
|
||||
</span>
|
||||
) : null}
|
||||
@@ -1003,10 +1003,7 @@ export function Settings() {
|
||||
</Field>
|
||||
</div>
|
||||
{createOrgError ? (
|
||||
<p
|
||||
className="text-sm font-medium text-red-600 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
<p className="text-sm font-medium text-status-error-fg" role="alert">
|
||||
{createOrgError}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -1108,7 +1105,7 @@ export function Settings() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 lg:col-span-2 lg:flex-row lg:items-center lg:justify-end">
|
||||
{hasOrgProfileChanges ? (
|
||||
<span className="text-sm font-semibold text-red-700 dark:text-red-300">
|
||||
<span className="text-sm font-semibold text-status-error-fg">
|
||||
You have unsaved changes.
|
||||
</span>
|
||||
) : null}
|
||||
@@ -1204,7 +1201,7 @@ export function Settings() {
|
||||
</div>
|
||||
{inviteError ? (
|
||||
<p
|
||||
className="text-sm font-medium text-red-600 dark:text-red-400"
|
||||
className="text-sm font-medium text-status-error-fg"
|
||||
role="alert"
|
||||
>
|
||||
{inviteError}
|
||||
@@ -1310,11 +1307,11 @@ export function Settings() {
|
||||
<SettingsBlock tone="danger">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-[var(--radius-sm)] border border-red-300/40 bg-red-500/10 text-red-700 dark:border-red-500/30 dark:text-red-300">
|
||||
<span className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-[var(--oc-radius-inset)] border border-status-error-fg/30 bg-status-error-bg text-status-error-fg">
|
||||
<ShieldAlert size={17} />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-bold text-red-700 dark:text-red-300">
|
||||
<h3 className="text-sm font-bold text-status-error-fg">
|
||||
Delete organization
|
||||
</h3>
|
||||
<p className="text-sm text-[color:var(--ink-soft)]">
|
||||
@@ -1435,10 +1432,7 @@ export function Settings() {
|
||||
</Field>
|
||||
</div>
|
||||
{createOrgError ? (
|
||||
<p
|
||||
className="text-sm font-medium text-red-600 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
<p className="text-sm font-medium text-status-error-fg" role="alert">
|
||||
{createOrgError}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -1568,8 +1562,8 @@ export function Settings() {
|
||||
</SettingsBlock>
|
||||
|
||||
{newToken ? (
|
||||
<div className="flex flex-col gap-3 rounded-[var(--radius-md)] border border-amber-300/30 bg-amber-500/[0.06] p-4 dark:border-amber-500/25 dark:bg-amber-500/[0.08]">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-amber-800 dark:text-amber-300">
|
||||
<div className="flex flex-col gap-3 rounded-[var(--oc-radius-surface)] border border-status-warning-fg/30 bg-status-warning-bg p-4">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-status-warning-fg">
|
||||
<ShieldAlert size={16} />
|
||||
Copy this token now — it will not be shown again.
|
||||
</div>
|
||||
@@ -1714,13 +1708,10 @@ export function Settings() {
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-[var(--radius-sm)] border border-red-300/20 bg-red-500/[0.04] p-4 dark:border-red-500/20 dark:bg-red-500/[0.06]">
|
||||
<ShieldAlert
|
||||
size={18}
|
||||
className="mt-0.5 shrink-0 text-red-600 dark:text-red-400"
|
||||
/>
|
||||
<div className="flex items-start gap-3 rounded-[var(--oc-radius-control)] border border-status-error-fg/25 bg-status-error-bg p-4">
|
||||
<ShieldAlert size={18} className="mt-0.5 shrink-0 text-status-error-fg" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-semibold text-red-700 dark:text-red-300">
|
||||
<p className="text-sm font-semibold text-status-error-fg">
|
||||
This will permanently delete your account
|
||||
</p>
|
||||
<p className="text-sm text-[color:var(--ink-soft)]">
|
||||
@@ -1794,11 +1785,9 @@ function DeletionResourceSummary({
|
||||
: emptyLabel;
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[var(--radius-sm)] border border-red-300/40 bg-red-500/5 text-sm dark:border-red-500/30">
|
||||
<div className="border-b border-red-300/30 px-3 py-2 dark:border-red-500/25">
|
||||
<p className="font-semibold text-red-700 dark:text-red-300">
|
||||
Resources permanently deleted
|
||||
</p>
|
||||
<div className="overflow-hidden rounded-[var(--oc-radius-control)] border border-status-error-fg/30 bg-status-error-bg text-sm">
|
||||
<div className="border-b border-status-error-fg/25 px-3 py-2">
|
||||
<p className="font-semibold text-status-error-fg">Resources permanently deleted</p>
|
||||
<p className="mt-1 text-[color:var(--ink-soft)]">{summary}</p>
|
||||
</div>
|
||||
{resources.length ? (
|
||||
@@ -2055,8 +2044,8 @@ function SettingsSection({
|
||||
<span
|
||||
className={`mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--radius-sm)] border ${
|
||||
tone === "danger"
|
||||
? "border-red-300/40 bg-red-500/10 text-red-700 dark:border-red-500/30 dark:text-red-300"
|
||||
: "border-[color:var(--line)] bg-[color:var(--surface-muted)] text-[color:var(--ink)]"
|
||||
? "border-status-error-fg/30 bg-status-error-bg text-status-error-fg"
|
||||
: "border-[color:var(--oc-border-subtle)] bg-[color:var(--oc-bg-surface)] text-[color:var(--oc-text-primary)]"
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
@@ -2064,7 +2053,9 @@ function SettingsSection({
|
||||
<div className="min-w-0">
|
||||
<h2
|
||||
className={`font-display text-2xl font-black leading-none ${
|
||||
tone === "danger" ? "text-red-700 dark:text-red-300" : "text-[color:var(--ink)]"
|
||||
tone === "danger"
|
||||
? "text-status-error-fg"
|
||||
: "text-[color:var(--oc-text-primary)]"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
@@ -2093,7 +2084,7 @@ function SettingsBlock({
|
||||
<div
|
||||
className={`flex min-w-0 flex-col gap-4 rounded-[var(--radius-md)] border p-4 sm:p-5 ${
|
||||
tone === "danger"
|
||||
? "border-red-300/50 bg-red-500/[0.035] dark:border-red-500/35 dark:bg-red-500/[0.045]"
|
||||
? "border-status-error-fg/30 bg-status-error-bg"
|
||||
: "border-[color:var(--line)] bg-[color:var(--surface)]"
|
||||
} ${className}`}
|
||||
>
|
||||
@@ -2256,9 +2247,9 @@ function GitHubSourceList({
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="destructive"
|
||||
loading={deletingSourceId === source._id}
|
||||
className="shrink-0 border-red-500/45 text-red-700 hover:not-disabled:border-red-500 hover:not-disabled:bg-red-500/10 dark:border-red-500/35 dark:text-red-300"
|
||||
className="shrink-0"
|
||||
onClick={() => onDeleteSource(source)}
|
||||
>
|
||||
Delete
|
||||
@@ -2398,7 +2389,7 @@ function GitHubSourceHealth({ source }: { source: GitHubSkillSource }) {
|
||||
</GitHubSourceOverviewRow>
|
||||
</div>
|
||||
{needsAttention && latestError ? (
|
||||
<p className="border-t border-[color:var(--line)] px-3 py-2 text-sm text-red-700 dark:text-red-300">
|
||||
<p className="border-t border-[color:var(--oc-border-subtle)] px-3 py-2 text-sm text-status-error-fg">
|
||||
<span className="font-semibold">Latest error:</span> {latestError}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -2416,7 +2407,7 @@ function GitHubSourceSyncIssues({ source }: { source: GitHubSkillSource }) {
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.08em] text-[color:var(--ink-soft)]">
|
||||
Sync issues
|
||||
</span>
|
||||
<span className="inline-flex h-5 items-center rounded-full border border-red-500/35 bg-red-500/10 px-2 text-[11px] font-semibold text-red-700 dark:text-red-300">
|
||||
<span className="inline-flex h-5 items-center rounded-[var(--oc-radius-control)] border border-status-error-fg/30 bg-status-error-bg px-2 text-[11px] font-semibold text-status-error-fg">
|
||||
{issues.length}
|
||||
</span>
|
||||
</div>
|
||||
@@ -2435,7 +2426,7 @@ function GitHubSourceSyncIssues({ source }: { source: GitHubSkillSource }) {
|
||||
{formatGitHubSourceIssueKind(skill.kind)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 text-left text-xs font-semibold text-red-700 dark:text-red-300 sm:max-w-[40%] sm:text-right">
|
||||
<div className="shrink-0 text-left text-xs font-semibold text-status-error-fg sm:max-w-[40%] sm:text-right">
|
||||
{skill.message}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2489,10 +2480,10 @@ function GitHubSourceOverviewRow({ label, children }: { label: string; children:
|
||||
function GitHubSourceStatusPill({ needsAttention }: { needsAttention: boolean }) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex h-6 items-center rounded-full border px-2.5 text-xs font-semibold ${
|
||||
className={`inline-flex h-6 items-center rounded-[var(--oc-radius-control)] border px-2.5 text-xs font-semibold ${
|
||||
needsAttention
|
||||
? "border-red-500/35 bg-red-500/10 text-red-700 dark:text-red-300"
|
||||
: "border-emerald-500/35 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
|
||||
? "border-status-error-fg/30 bg-status-error-bg text-status-error-fg"
|
||||
: "border-status-success-fg/30 bg-status-success-bg text-status-success-fg"
|
||||
}`}
|
||||
>
|
||||
{needsAttention ? "Needs attention" : "Healthy"}
|
||||
@@ -2629,7 +2620,11 @@ function TokenList({
|
||||
<td className="py-4 align-middle">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
{token.revokedAt ? (
|
||||
<CircleX size={16} aria-hidden="true" className="shrink-0 text-red-500" />
|
||||
<CircleX
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
className="shrink-0 text-status-error-fg"
|
||||
/>
|
||||
) : (
|
||||
<Code
|
||||
size={16}
|
||||
@@ -2672,7 +2667,7 @@ function TokenList({
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => onRevoke?.(token._id)}
|
||||
className="h-8 gap-2 px-0 text-xs text-red-700 hover:bg-transparent hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
||||
className="h-8 gap-2 px-0 text-xs text-status-error-fg hover:bg-transparent hover:opacity-80"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
Revoke
|
||||
@@ -2693,7 +2688,7 @@ function TokenList({
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{token.revokedAt ? (
|
||||
<CircleX size={16} aria-hidden="true" className="shrink-0 text-red-500" />
|
||||
<CircleX size={16} aria-hidden="true" className="shrink-0 text-status-error-fg" />
|
||||
) : (
|
||||
<Code
|
||||
size={16}
|
||||
@@ -2746,7 +2741,7 @@ function TokenList({
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => onRevoke?.(token._id)}
|
||||
className="h-8 gap-2 px-0 text-xs text-red-700 hover:bg-transparent hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
||||
className="h-8 gap-2 px-0 text-xs text-status-error-fg hover:bg-transparent hover:opacity-80"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
Revoke
|
||||
|
||||
@@ -263,7 +263,7 @@ export function SkillsIndex() {
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="browse-page browse-page-borderless-header">
|
||||
<main className="browse-page browse-page-borderless-header skills-browse-page">
|
||||
<div className="browse-page-header">
|
||||
<div className="browse-page-header-main">
|
||||
<h1 className="browse-title">
|
||||
|
||||
@@ -1311,7 +1311,7 @@ export function Upload() {
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{error ? (
|
||||
<div className="text-sm font-medium text-red-600 dark:text-red-400" role="alert">
|
||||
<div className="text-sm font-medium text-status-error-fg" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1344,7 +1344,7 @@ export function Upload() {
|
||||
function InlineValidationMessage(props: { id: string; message?: string }) {
|
||||
if (!props.message) return null;
|
||||
return (
|
||||
<p id={props.id} className="text-sm font-medium text-red-600 dark:text-red-400">
|
||||
<p id={props.id} className="text-sm font-medium text-status-error-fg">
|
||||
{props.message}
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -199,7 +199,7 @@ export function Stars() {
|
||||
handleUnstar(skill);
|
||||
}}
|
||||
aria-label={`Unstar ${skill.displayName}`}
|
||||
className="stars-card-unstar text-[color:var(--gold)] hover:text-red-500"
|
||||
className="stars-card-unstar text-[color:var(--gold)] hover:text-status-error-fg"
|
||||
>
|
||||
<Star className="h-4 w-4 fill-current" />
|
||||
</Button>
|
||||
@@ -221,7 +221,7 @@ export function Stars() {
|
||||
handleUnstar(skill);
|
||||
}}
|
||||
aria-label={`Unstar ${skill.displayName}`}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-[color:var(--gold)] hover:text-red-500"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-[color:var(--gold)] hover:text-status-error-fg"
|
||||
>
|
||||
<Star className="h-4 w-4 fill-current" />
|
||||
</Button>
|
||||
|
||||
@@ -374,7 +374,7 @@ function PublisherProfileMembers({
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
sideOffset={6}
|
||||
className="publisher-profile-members-menu !overflow-hidden !rounded-2xl p-0 shadow-none"
|
||||
className="publisher-profile-members-menu !overflow-hidden !rounded-[var(--oc-radius-surface)] p-0 shadow-none"
|
||||
aria-label={`${members.length} members`}
|
||||
>
|
||||
<div className="publisher-profile-members-menu-list">
|
||||
|
||||
+357
-382
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user