Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
165 changes: 165 additions & 0 deletions .claude/commands/enforce-charter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Graphic Charter Enforcement

Audit and fix graphic charter violations for the package: **$ARGUMENTS**

## Style System Architecture

The design system uses a **3-layer token architecture**:

1. **Primitives** (`variables.scss`) — OKLCH-derived shade scales. Components rarely reference these directly.
2. **Semantic tokens** (`variables.scss`) — Purpose-based names (`--color-bg-app`, `--color-text`, `--color-error`). **This is what components use.**
3. **Domain tokens** (package SCSS files) — Module-specific colors defined in the package that owns them.

### Which layer to use?

| Situation | Layer | Example |
| ----------------------------------------- | --------- | ---------------------------------------------- |
| Standard background, text, border | Semantic | `var(--color-bg-surface)`, `var(--color-text)` |
| Status indication | Semantic | `var(--color-error)`, `var(--color-success)` |
| Module-specific color (kernel, chat, LED) | Domain | `var(--color-kernel)`, `var(--c-led-blue)` |
| Building a new semantic/domain token | Primitive | `var(--primary-500)`, `var(--surface-800)` |

## Token Reference

Design tokens are defined in `packages/ui-base/src/lib/assets/css/variables.scss`.
Domain tokens are in each package's SCSS files (see `doc/guides/STYLE_SYSTEM.md`).

### Color Tokens — Primitives

| Scale | Shades | Role |
| ----------------------------- | --------- | ----------------------- |
| `--primary-100..900` | 9 shades | Brand purple/magenta |
| `--surface-100..900` | 9 shades | Dark theme surfaces |
| `--cyan-100..900` | 9 shades | Data/interactive accent |
| `--red-100..900` | 9 shades | Error/danger |
| `--orange-100..900` | 9 shades | Warning |
| `--yellow-100..900` | 9 shades | Debug/info |
| `--green-100..900` | 9 shades | Success |
| `--neutral-1..12` | 12 shades | Grays (Radix mauve) |
| `--alpha-white-*` | 5 values | White transparency |
| `--alpha-black-*` | 8 values | Black transparency |
| `--white`, `--black-600..900` | 5 values | Solids |

### Color Tokens — Semantic

| Token | Purpose |
| -------------------------- | --------------------------- |
| `--color-bg-app` | App background |
| `--color-bg-surface` | Surface/card background |
| `--color-bg-elevated` | Elevated surface |
| `--color-bg-input` | Input background |
| `--color-bg-hover` | Hover state background |
| `--color-bg-node` | Whiteboard node background |
| `--color-text` | Primary text |
| `--color-text-muted` | Secondary text |
| `--color-text-faint` | Tertiary text |
| `--color-text-on-color` | Text on colored backgrounds |
| `--color-text-placeholder` | Placeholder text |
| `--color-border` | Default border |
| `--color-border-muted` | Subtle border |
| `--color-border-focus` | Focus ring / form border |
| `--color-accent` | Primary accent |
| `--color-accent-hover` | Accent hover state |
| `--color-accent-muted` | Muted accent |
| `--color-accent-strong` | Strong accent |
| `--color-success` | Success state |
| `--color-error` | Error state |
| `--color-warning` | Warning state |
| `--color-info` | Info state |
| `--color-selection` | Selection highlight |
| `--color-link` | Link color |
| `--color-debug` | Debug overlay |
| `--color-button-blue` | Blue button accent |
| `--color-vault` | Vault/sensitive indicator |

### Font Size Tokens

| Hardcoded | Token |
| --------- | ---------------------- |
| 9px | `var(--font-size-2xs)` |
| 11px | `var(--font-size-xs)` |
| 12px | `var(--font-size-sm)` |
| 13px-14px | `var(--font-size-md)` |
| 15px-16px | `var(--font-size-lg)` |
| 17px-18px | `var(--font-size-xl)` |
| 20px-21px | `var(--font-size-2xl)` |
| 24px-25px | `var(--font-size-3xl)` |
| 28px-30px | `var(--font-size-4xl)` |

### Shadow Tokens

| Hardcoded Pattern | Token |
| ------------------- | ------------------ |
| Small/subtle shadow | `var(--shadow-sm)` |
| Medium shadow | `var(--shadow-md)` |
| Large shadow | `var(--shadow-lg)` |
| Extra-large shadow | `var(--shadow-xl)` |

### Z-Index Tokens

| Hardcoded Range | Token |
| --------------- | ------------------- |
| 0-10 | `var(--z-base)` |
| 50-150 | `var(--z-dropdown)` |
| 150-250 | `var(--z-sticky)` |
| 250-350 | `var(--z-modal)` |
| 350-450 | `var(--z-tooltip)` |
| 450+ | `var(--z-toast)` |

### Spacing Tokens

| Value | Token |
| ----- | ------------------- |
| 0px | `var(--spacing-0)` |
| 2px | `var(--spacing-1)` |
| 4px | `var(--spacing-2)` |
| 6px | `var(--spacing-3)` |
| 8px | `var(--spacing-4)` |
| 12px | `var(--spacing-5)` |
| 16px | `var(--spacing-6)` |
| 20px | `var(--spacing-7)` |
| 24px | `var(--spacing-8)` |
| 32px | `var(--spacing-10)` |
| 40px | `var(--spacing-12)` |
| 48px | `var(--spacing-14)` |
| 64px | `var(--spacing-16)` |

## Workflow

1. **Audit** the package for violations:

- Search all `.scss` and `.css` files in `packages/$ARGUMENTS/` for hardcoded hex colors, font-size values, z-index values, and box-shadow values
- Search all `.tsx` and `.ts` files for inline `style={{}}` props with hardcoded color/font-size/z-index/shadow values
- Check for references to old token names (`--c-gray-*`, `--c-pink-*`, `--c-blue-gray-*`, `--c-alt-blue-*`, `--ca-white-*`, `--ca-black-*`, `--c-white-1`, `--c-black-*`, `--mauve-*`)
- Count violations by category

2. **Fix** violations:

- Replace hardcoded values with the closest matching token
- For colors: prefer semantic tokens first, then domain tokens, then primitives
- For font-size: use the mapping table above
- For z-index: use the layer that matches the component's purpose
- For box-shadow: use the closest shadow token
- For inline styles in TSX: prefer SCSS class-based approaches where possible
- For old token names: use the mapping in `doc/guides/STYLE_SYSTEM.md`

3. **Validate** fixes:

- Run `npx nx run $ARGUMENTS:lint`
- Run `npx nx run $ARGUMENTS:typecheck`
- Run `npx nx run $ARGUMENTS:test` (if tests exist)
- Run `npm run lint:tokens` to check for undefined/unused variables
- Fix any errors introduced

4. **Report** results:
- List files changed
- Count violations fixed per category
- List any remaining violations that need manual review

## Important Notes

- `variables.scss` and `utilities.scss` are EXCLUDED from enforcement — they define the raw values
- Third-party library styles are excluded
- Some values are legitimate exceptions (e.g., CSS calculations, SVG-specific values, animation keyframes, conic-gradient colors)
- All values must map to a token — no `/* charter-exception */` needed if the color is genuinely unique (use a `/* charter-exception: <reason> */` comment only for truly one-off values)
- Domain tokens belong in their package's SCSS file, not in `variables.scss`
153 changes: 153 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ jobs:
- name: Validate frontend build
run: npm run test:build

- name: Run Stylelint (graphic charter)
run: npm run stylelint

- name: Run all tests
run: npx nx run-many -t test --parallel=3
env:
Expand Down Expand Up @@ -97,3 +100,153 @@ jobs:
**/coverage
**/test-results
retention-days: 7

visual-regression:
name: Visual Regression Tests
runs-on: ubuntu-24.04
needs: validate

permissions:
contents: read
actions: read
pull-requests: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24.x'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Install Playwright Chromium
run: npx playwright install chromium --with-deps

- name: Build library packages
run: npx nx run-many -t build --parallel=5
env:
NODE_OPTIONS: --max_old_space_size=4096

- name: Build all Storybooks
run: npx nx run-many -t build-storybook --parallel=3
env:
NODE_OPTIONS: --max_old_space_size=4096

- name: Run visual regression tests
id: visual-tests
run: |
FAILED_PACKAGES=""
DIFF_FILES=""
HAS_FAILURES="false"

for dir in packages/*/storybook-static packages/modules/*/storybook-static; do
if [ -d "$dir" ]; then
pkg=$(echo "$dir" | sed 's|/storybook-static||')
pkg_name=$(basename "$pkg")
echo "Testing $pkg..."
npx http-server "$dir" -p 6006 --silent &
SERVER_PID=$!
sleep 3

if ! npx test-storybook --url http://127.0.0.1:6006 --config-dir "$pkg/.storybook" 2>&1; then
HAS_FAILURES="true"
FAILED_PACKAGES="$FAILED_PACKAGES- **$pkg_name**\n"

# Collect diff images for this package
if [ -d "$pkg/__diff_output__" ]; then
for diff in "$pkg"/__diff_output__/*.png; do
if [ -f "$diff" ]; then
DIFF_FILES="$DIFF_FILES - \`$(basename "$diff")\`\n"
fi
done
fi
fi

kill $SERVER_PID 2>/dev/null || true
wait $SERVER_PID 2>/dev/null || true
fi
done

echo "has_failures=$HAS_FAILURES" >> $GITHUB_OUTPUT

# Write failures to file for the comment step
{
echo "FAILED_PACKAGES<<EOF"
echo -e "$FAILED_PACKAGES"
echo "EOF"
} >> $GITHUB_OUTPUT

{
echo "DIFF_FILES<<EOF"
echo -e "$DIFF_FILES"
echo "EOF"
} >> $GITHUB_OUTPUT

- name: Upload visual diff artifacts
if: steps.visual-tests.outputs.has_failures == 'true'
uses: actions/upload-artifact@v4
with:
name: visual-diffs
path: |
**/__diff_output__
retention-days: 7

- name: Comment on PR with visual regression results
if: github.event_name == 'pull_request' && steps.visual-tests.outputs.has_failures == 'true'
uses: actions/github-script@v7
with:
script: |
const failedPackages = `${{ steps.visual-tests.outputs.FAILED_PACKAGES }}`;
const diffFiles = `${{ steps.visual-tests.outputs.DIFF_FILES }}`;
const runUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`;

const body = `## 📸 Visual Regression Detected

The following packages have screenshot differences:

${failedPackages}
<details>
<summary>Changed screenshots</summary>

${diffFiles}
</details>

**[Download visual-diffs artifact](${runUrl})** to review the differences.

If these changes are intentional, update the baselines locally:
\`\`\`bash
npx nx run <package>:test-storybook -- -u
\`\`\`
`;

// Find and update existing comment or create new one
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('📸 Visual Regression Detected')
);

if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ test-output


storybook-static
__diff_output__

.env

Expand Down
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
legacy-peer-deps=true
Loading
Loading