mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 16:02:37 +00:00
feat(design-system): gate renderer UI with @shadcn/lint
Wires shadcn-ui/lint's Oxlint plugin into the two places this repo already ratchets: the changed-lines PR gate for rules the renderer can't satisfy today, and `pnpm lint` for the one that is already at zero. - config/oxlint-design-system.json: no-restyle (layout allowed), no-raw-colors, require-static-classes -- scoped to src/renderer/**/*.tsx, run over added lines only. Measured at 10 findings across the last 60 commits (771 changed files), so it holds the line without a migration. - config/oxlint-dead-classes.json: no-unknown-classes repo-wide, with the renderer's plain-CSS hook namespaces allow-listed. Now at zero. - no-inline-styles and no-arbitrary-values stay off; STYLEGUIDE says why. Fixes the three live bugs the linter found: - `--editor-surface` never reached `@theme inline`, so `bg-editor-surface` generated no CSS -- 12 editor/artifact/notebook panes fell through to the page background instead of #1e1e1e in dark mode. - `scrollbar-none` is not a Tailwind utility and was declared nowhere, so the remote file browser breadcrumbs showed the scrollbar they meant to hide. Declared as a real `@utility`. - Notebook markdown cells used `markdown-preview-body`, which no stylesheet defines; the styled class is `markdown-body`. They rendered unstyled.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Design System
|
||||
|
||||
All UI work — layout, color, typography, spacing, component selection, UX behavior — must follow [`docs/STYLEGUIDE.md`](./docs/STYLEGUIDE.md). Use the tokens defined in `src/renderer/src/assets/main.css` (the canonical source) and the shadcn primitives in `src/renderer/src/components/ui/`. Don't invent new color values, font sizes, or shadow tiers when a documented one already covers the role. When STYLEGUIDE.md is silent, follow the resolution order in its final section.
|
||||
All UI work — layout, color, typography, spacing, component selection, UX behavior — must follow [`docs/STYLEGUIDE.md`](./docs/STYLEGUIDE.md). Most of it is linted: `pnpm run check:code-quality:changed` fails on new restyles of a `components/ui/` primitive, raw palette colors, and computed `className` strings; `pnpm lint` fails on any class Tailwind cannot generate. See the Enforcement section of the style guide before suppressing either. Use the tokens defined in `src/renderer/src/assets/main.css` (the canonical source) and the shadcn primitives in `src/renderer/src/components/ui/`. Don't invent new color values, font sizes, or shadow tiers when a documented one already covers the role. When STYLEGUIDE.md is silent, follow the resolution order in its final section.
|
||||
|
||||
## Electron UI Validation
|
||||
|
||||
@@ -46,6 +46,7 @@ Avoid type assertions except `as const`. Unavoidable casts need a line-specific
|
||||
- **Typecheck**: `pnpm tc` (or `tc:node` / `tc:cli` / `tc:web`)
|
||||
- **Test**: `pnpm test [path/to/file.test.ts]`
|
||||
- **Lint**: `oxlint`, or `pnpm run check:code-quality:changed` for changed files (full `pnpm lint` is slow); format with `pnpm format`
|
||||
- **Design system**: `pnpm run lint:design-system` for the full renderer report (not a gate); the changed-lines gate above is what CI enforces
|
||||
|
||||
# Considerations
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"$schema": "../node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [],
|
||||
"categories": {
|
||||
"correctness": "off",
|
||||
"suspicious": "off",
|
||||
"pedantic": "off",
|
||||
"perf": "off",
|
||||
"style": "off",
|
||||
"restriction": "off",
|
||||
"nursery": "off"
|
||||
},
|
||||
"jsPlugins": [
|
||||
{
|
||||
"name": "shadcn",
|
||||
"specifier": "@shadcn/lint"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"shadcn": {
|
||||
"note": "See docs/STYLEGUIDE.md for the role each token and primitive plays."
|
||||
}
|
||||
},
|
||||
"rules": {},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/src/renderer/**/*.tsx"],
|
||||
"rules": {
|
||||
"shadcn/no-unknown-classes": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"agent-map-*",
|
||||
"comment-md-*",
|
||||
"compact-agent-*",
|
||||
"feature-wall-*",
|
||||
"is-*",
|
||||
"markdown-annotation-*",
|
||||
"markdown-body",
|
||||
"markdown-dark",
|
||||
"markdown-doc-link*",
|
||||
"markdown-light",
|
||||
"markdown-preview",
|
||||
"markdown-preview-search*",
|
||||
"markdown-preview-shell",
|
||||
"markdown-review-*",
|
||||
"markdown-toc-*",
|
||||
"mobile-browser-driver-banner",
|
||||
"mobile-driver-banner",
|
||||
"native-chat-*",
|
||||
"orca-*",
|
||||
"pdfViewer",
|
||||
"popover-scroll-content",
|
||||
"popover-wheel-scroll",
|
||||
"ravpr-*",
|
||||
"ravs-*",
|
||||
"scrollbar-editor",
|
||||
"scrollbar-sleek",
|
||||
"scrollbar-sleek-lg",
|
||||
"scrollbar-sleek-parent",
|
||||
"toaster",
|
||||
"worktree-sidebar-scrollbar",
|
||||
"xterm-*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.test.tsx"],
|
||||
"rules": {
|
||||
"shadcn/no-unknown-classes": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/out", "cloud/**", "mobile/**"]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"$schema": "../node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [],
|
||||
"categories": {
|
||||
"correctness": "off",
|
||||
"suspicious": "off",
|
||||
"pedantic": "off",
|
||||
"perf": "off",
|
||||
"style": "off",
|
||||
"restriction": "off",
|
||||
"nursery": "off"
|
||||
},
|
||||
"jsPlugins": [
|
||||
{
|
||||
"name": "shadcn",
|
||||
"specifier": "@shadcn/lint"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"shadcn": {
|
||||
"note": "See docs/STYLEGUIDE.md for the role each token and primitive plays."
|
||||
}
|
||||
},
|
||||
"rules": {},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/src/renderer/**/*.tsx"],
|
||||
"rules": {
|
||||
"shadcn/no-restyle": [
|
||||
"error",
|
||||
{
|
||||
"allow": ["layout"]
|
||||
}
|
||||
],
|
||||
"shadcn/no-raw-colors": [
|
||||
"error",
|
||||
{
|
||||
"allow": ["shadow-floating"]
|
||||
}
|
||||
],
|
||||
"shadcn/require-static-classes": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.test.tsx"],
|
||||
"rules": {
|
||||
"shadcn/no-restyle": "off",
|
||||
"shadcn/no-raw-colors": "off",
|
||||
"shadcn/require-static-classes": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/out", "cloud/**", "mobile/**"]
|
||||
}
|
||||
@@ -29,6 +29,12 @@ export const OXLINT_SCANS = [
|
||||
{
|
||||
label: 'React Doctor',
|
||||
args: ['--config', 'config/oxlint-react-doctor.json']
|
||||
},
|
||||
{
|
||||
// Why changed-lines only: the renderer carries ~4.7k pre-existing restyle/raw-color
|
||||
// findings. Gating added lines holds the line without a repo-wide migration.
|
||||
label: 'design system',
|
||||
args: ['--config', 'config/oxlint-design-system.json']
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -312,3 +312,29 @@ If you have a UI question this doc doesn't answer:
|
||||
2. Check `src/renderer/src/components/ui/` for a primitive that already encodes the pattern.
|
||||
3. If it's a token question, `main.css` is canonical — use what's there, or add a new one in both light and dark.
|
||||
4. If none of those resolve it, ask the user before inventing.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Most of this guide is checked by [`@shadcn/lint`](https://github.com/shadcn-ui/lint), an Oxlint JS plugin that reads `components.json` and `main.css` and knows which Tailwind classes each primitive already owns. Its diagnostics name the fix — the variant, size, or token to use instead — rather than only the violation.
|
||||
|
||||
| Command | Scope | What it does |
|
||||
| ------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------ |
|
||||
| `pnpm run check:dead-classes` | Whole renderer, in `pnpm lint` | Fails on any class Tailwind can't generate. Currently at zero — keep it there. |
|
||||
| `pnpm run check:code-quality:changed` | Lines a PR adds | Fails on new restyles, raw palette colors, and computed `className` strings. |
|
||||
| `pnpm run lint:design-system` | Whole renderer, report only | The full picture, including the pre-existing backlog. Not a gate. |
|
||||
|
||||
Rules live in `config/oxlint-design-system.json` (the PR gate) and `config/oxlint-dead-classes.json` (the repo-wide one). Both are scoped to `src/renderer/**/*.tsx`.
|
||||
|
||||
**What's enforced on new code:**
|
||||
|
||||
- `no-restyle` — don't reach into a primitive's own spacing, typography, color, shape, or motion through `className`. Layout classes (flex, grid, position, sizing) are allowed, because layout belongs to the parent. If a primitive genuinely lacks the treatment you need, add a variant or size to the file in `components/ui/` rather than patching it at one call site.
|
||||
- `no-raw-colors` — no `bg-pink-500`. Use a token from the tables above, or add one to `main.css` in both `:root` and `.dark`.
|
||||
- `require-static-classes` — a `className` built at runtime can't be checked by anything, including this linter. Enumerate the variants instead.
|
||||
- `no-unknown-classes` — a Tailwind-shaped name that Tailwind doesn't generate is dead text. Plain CSS hook classes (`ravs-*`, `agent-map-*`, `comment-md-*`, …) are allow-listed by namespace in `config/oxlint-dead-classes.json`; adding a new namespace means adding it there. A name that _looks_ like a utility should be a real `@utility` in `main.css`.
|
||||
|
||||
**What's deliberately off:**
|
||||
|
||||
- `no-inline-styles` — the renderer computes real geometry inline (virtualized row offsets, terminal metrics, graph lane positions) and the rule can't distinguish that from a hardcoded color. Inline styles still owe you a reason; `main.css` is still canonical for anything static.
|
||||
- `no-arbitrary-values` — `main.css` defines no `--text-*` scale, so the 11px and 13px sizes this guide documents have no token to point at. Turning this on means adding that scale first.
|
||||
|
||||
Neither gate rewrites existing code: the PR gate only looks at lines a change adds, so the renderer's pre-existing findings stay put until someone touches them.
|
||||
|
||||
+4
-1
@@ -14,13 +14,15 @@
|
||||
"audit:perf": "oxlint --config config/oxlint-performance-audit.json --format json src",
|
||||
"test:perf:contracts": "vitest run --config config/vitest.performance.config.ts",
|
||||
"format": "oxfmt --write .",
|
||||
"lint": "oxlint && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:readme-local-links && pnpm run verify:rpc-params-catalog && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalog && pnpm run verify:localization-runtime-catalog && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage",
|
||||
"lint": "oxlint && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:readme-local-links && pnpm run verify:rpc-params-catalog && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalog && pnpm run verify:localization-runtime-catalog && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage",
|
||||
"audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor",
|
||||
"audit:code-quality:native": "oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings",
|
||||
"audit:code-quality:type-aware": "oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings",
|
||||
"audit:react-doctor": "pnpm dlx react-doctor@0.9.1 . --yes --no-supply-chain --no-telemetry --blocking none",
|
||||
"audit:dead-code": "pnpm dlx knip@5.88.1 --config config/knip.json",
|
||||
"check:code-quality:changed": "node config/scripts/check-changed-code-quality.mjs",
|
||||
"check:dead-classes": "oxlint --config config/oxlint-dead-classes.json src/renderer",
|
||||
"lint:design-system": "oxlint --config config/oxlint-design-system.json src/renderer",
|
||||
"check:react-doctor:changed": "node config/scripts/check-react-doctor-changed.mjs",
|
||||
"check:zustand-selector-fanout": "node config/scripts/zustand-selector-fanout-benchmark.mjs --check",
|
||||
"doctor": "pnpm dlx react-doctor@0.9.1 . --no-telemetry",
|
||||
@@ -199,6 +201,7 @@
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@sanity/diff-match-patch": "^3.2.0",
|
||||
"@shadcn/lint": "^0.1.0",
|
||||
"@stablyai/playwright-test": "^2.1.14",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@tanstack/react-virtual": "^3.14.10",
|
||||
|
||||
Generated
+774
-2
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ minimumReleaseAgeExclude:
|
||||
- pdfjs-dist@6.3.289
|
||||
- zod@4.5.4
|
||||
- electron@43.7.0
|
||||
- '@shadcn/lint@0.1.0'
|
||||
shamefullyHoist: true
|
||||
|
||||
# Orca always launches the user's own resolved Claude CLI via
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-editor-surface: var(--editor-surface);
|
||||
--color-agent-question: var(--agent-question);
|
||||
--color-agent-question-text: var(--agent-question-text);
|
||||
--color-chart-1: var(--chart-1);
|
||||
@@ -509,6 +510,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Why @utility, not a plain class: this is a Tailwind-shaped name, so it has to be
|
||||
one Tailwind generates or `scrollbar-none` silently produces no CSS. */
|
||||
@utility scrollbar-none {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Sleek scrollbar (VS Code-like) ─────────────────── */
|
||||
|
||||
.scrollbar-sleek {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import fs from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const mainCss = fs.readFileSync(new URL('./main.css', import.meta.url), 'utf8')
|
||||
const themeBlock = /@theme inline\s*{([\s\S]*?)\n}/.exec(mainCss)?.[1] ?? ''
|
||||
|
||||
// Why: a token that never reaches `@theme inline`, and a Tailwind-shaped name that is only a
|
||||
// plain CSS selector, both generate no CSS at all -- the utility silently does nothing.
|
||||
describe('main.css utility generation', () => {
|
||||
it('exposes --editor-surface to Tailwind so bg-editor-surface generates', () => {
|
||||
expect(mainCss).toMatch(/--editor-surface:/)
|
||||
expect(themeBlock).toMatch(/--color-editor-surface:\s*var\(--editor-surface\)/)
|
||||
})
|
||||
|
||||
it('declares scrollbar-none as a utility rather than a plain class', () => {
|
||||
expect(mainCss).toMatch(/@utility scrollbar-none\s*{/)
|
||||
expect(mainCss).not.toMatch(/^\.scrollbar-none\b/m)
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import Markdown from 'react-markdown'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import rehypeSanitize from 'rehype-sanitize'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { monaco } from '@/lib/monaco-setup'
|
||||
import { computeEditorFontSize, resolveEditorFontFamily } from '@/lib/editor-font-zoom'
|
||||
import { resolveDocumentTheme } from '@/lib/document-theme'
|
||||
@@ -14,8 +15,12 @@ import type { IpynbCell } from './ipynb-parse'
|
||||
import MonacoCodeExcerpt from './MonacoCodeExcerpt'
|
||||
|
||||
export function IpynbMarkdownCell({ source }: { source: string }): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const isDark = resolveDocumentTheme(settings?.theme ?? 'system')
|
||||
return (
|
||||
<div className="markdown-preview-body px-4 py-3 text-sm">
|
||||
<div
|
||||
className={cn('markdown-body px-4 py-3 text-sm', isDark ? 'markdown-dark' : 'markdown-light')}
|
||||
>
|
||||
<Markdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw, rehypeSanitize]}>
|
||||
{source || '\u00a0'}
|
||||
</Markdown>
|
||||
|
||||
Reference in New Issue
Block a user