Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 295b20a

Browse files
committed
Freeze Radix UI imports to Box/Flex/Text; new UI comes from @posthog/quill
Add scripts/check-radix-imports.mjs (pnpm radix), run in the code-quality workflow: it diffs each changed file's Radix imports against the PR base, so any import added beyond Box, Flex, and Text from @radix-ui/themes — including any other @radix-ui/* package — fails CI. Existing imports stay untouched until migrated; no baseline file needed. Document the freeze in AGENTS.md and add the missing CLAUDE.md -> AGENTS.md symlinks in browser-tabs and canvas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MDJ4nfqaXqzioZtx5Ct3q4
1 parent 49b9e67 commit 295b20a

6 files changed

Lines changed: 152 additions & 1 deletion

File tree

.github/workflows/code-quality.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,8 @@ jobs:
6262

6363
- name: Check host boundaries (apps/code must stay a thin Electron host)
6464
run: node scripts/check-host-boundaries.mjs
65+
66+
- name: Check Radix imports (frozen; only Box/Flex/Text from @radix-ui/themes)
67+
run: |
68+
git fetch --no-tags --depth=1 origin ${{ github.event.pull_request.base.sha }}
69+
node scripts/check-radix-imports.mjs ${{ github.event.pull_request.base.sha }}

AGENTS.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ For each new file or meaningful change:
113113
- Bespoke clients that wrap `trpcClient.x` one-to-one.
114114
- `*Port`, `*_PORT`, or `ports.ts` naming.
115115
- Business logic in `apps/<host>`.
116+
- New Radix imports. Radix is frozen: only `Box`, `Flex`, and `Text` from `@radix-ui/themes` may be added; every other Radix component and every other `@radix-ui/*` package is denied for new code — use the `@posthog/quill` equivalent. See "Radix Freeze" below.
117+
118+
## Radix Freeze
119+
120+
Radix UI is legacy and being migrated to `@posthog/quill`. New code may import only the layout/typography primitives `Box`, `Flex`, and `Text` from `@radix-ui/themes`. Everything else — every other `@radix-ui/themes` component (`Button`, `Dialog`, `Tooltip`, `Select`, ...) and every other `@radix-ui/*` package — is denied for new usage. Reach for the `@posthog/quill` equivalent instead.
121+
122+
`scripts/check-radix-imports.mjs` (`pnpm radix`) enforces this in CI by diffing each changed file's Radix imports against `main`: existing imports may stay until migrated, but a changed file must not gain any. When you touch a file that still uses frozen Radix components, prefer swapping them to quill.
116123

117124
## Host Boundary
118125

@@ -199,6 +206,7 @@ await boot(container);
199206
- `pnpm --filter <pkg> typecheck|test|build`: run a scoped task.
200207
- `pnpm --filter code package|make`: package the Electron app.
201208
- `node scripts/check-host-boundaries.mjs`: verify host boundary allowlist.
209+
- `node scripts/check-radix-imports.mjs`: fail on Radix imports added relative to `main` (`pnpm radix`).
202210

203211
## Merging PRs
204212

@@ -232,7 +240,7 @@ See [docs/conventions.md](./docs/conventions.md).
232240

233241
## Key Libraries
234242

235-
- React 19, Radix UI Themes, Tailwind CSS, `@posthog/quill`
243+
- React 19, Tailwind CSS, `@posthog/quill` (Radix UI Themes is legacy — frozen to `Box`/`Flex`/`Text` for new code; see "Radix Freeze")
236244
- TanStack Query, TanStack Router
237245
- Zustand, InversifyJS (with `@inversifyjs/strongly-typed`), Zod
238246
- xterm.js, CodeMirror, Tiptap

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"rebuild:sqlite-electron": "node scripts/rebuild-better-sqlite3-electron.mjs",
3030
"typecheck": "turbo typecheck",
3131
"boundaries": "node scripts/check-host-boundaries.mjs",
32+
"radix": "node scripts/check-radix-imports.mjs",
3233
"optimize:onboarding-videos": "node scripts/optimize-onboarding-videos.mjs",
3334
"lint": "biome check --write --unsafe",
3435
"format": "biome format --write",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

scripts/check-radix-imports.mjs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env node
2+
import { execSync } from "node:child_process";
3+
import { readFileSync } from "node:fs";
4+
import { dirname, join } from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
7+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
8+
const SCAN_ROOTS = ["packages", "apps"];
9+
const ALLOWED_THEMES_IMPORTS = new Set(["Box", "Flex", "Text"]);
10+
11+
const USAGE = `check-radix-imports — Radix is frozen; new UI comes from @posthog/quill.
12+
13+
node scripts/check-radix-imports.mjs [base] fail on Radix imports added since <base>
14+
(default: merge-base with origin/main)
15+
16+
Only Box, Flex, and Text from @radix-ui/themes are permitted in new code. Every
17+
other Radix component and every other @radix-ui/* package is frozen: existing
18+
imports may stay until migrated, but a changed file must not gain any.`;
19+
20+
// Matches static imports, re-exports, and dynamic imports of Radix packages.
21+
const IMPORT_RE =
22+
/(?:import|export)\s+(?:type\s+)?([\w$]+|\*\s+as\s+[\w$]+|\{[^}]*\}|[\w$]+\s*,\s*\{[^}]*\}|\*)?\s*(?:from\s*)?["'](@radix-ui\/[^"']+|radix-ui(?:\/[^"']*)?)["']/g;
23+
const DYNAMIC_RE =
24+
/import\s*\(\s*["'](@radix-ui\/[^"']+|radix-ui(?:\/[^"']*)?)["']\s*\)/g;
25+
26+
function git(cmd) {
27+
return execSync(`git -C "${ROOT}" ${cmd}`, { encoding: "utf8" });
28+
}
29+
30+
function importedNames(clause) {
31+
if (!clause) return ["*"]; // bare `import "pkg"` — side-effect import
32+
const names = [];
33+
const braces = clause.match(/\{([^}]*)\}/);
34+
if (braces) {
35+
for (let n of braces[1].split(",")) {
36+
n = n.trim().replace(/^type\s+/, "");
37+
if (!n) continue;
38+
names.push(n.split(/\s+as\s+/)[0].trim());
39+
}
40+
}
41+
const outside = clause
42+
.replace(/\{[^}]*\}/, "")
43+
.replace(/,\s*$/, "")
44+
.trim();
45+
if (outside) names.push("*"); // default, namespace, or star import — all names reachable
46+
return names.length ? names : ["*"];
47+
}
48+
49+
function frozenImports(src) {
50+
const hits = new Set();
51+
IMPORT_RE.lastIndex = 0;
52+
for (let m = IMPORT_RE.exec(src); m; m = IMPORT_RE.exec(src)) {
53+
const [, clause, spec] = m;
54+
if (spec === "@radix-ui/themes") {
55+
for (const name of importedNames(clause)) {
56+
if (!ALLOWED_THEMES_IMPORTS.has(name)) hits.add(`${spec}#${name}`);
57+
}
58+
} else {
59+
hits.add(spec);
60+
}
61+
}
62+
DYNAMIC_RE.lastIndex = 0;
63+
for (let m = DYNAMIC_RE.exec(src); m; m = DYNAMIC_RE.exec(src)) {
64+
hits.add(m[1] === "@radix-ui/themes" ? `${m[1]}#*` : m[1]);
65+
}
66+
return hits;
67+
}
68+
69+
const arg = process.argv[2];
70+
if (arg === "--help" || arg === "-h") {
71+
console.log(USAGE);
72+
process.exit(0);
73+
}
74+
75+
let base = arg;
76+
if (!base) {
77+
try {
78+
base = git("merge-base origin/main HEAD").trim();
79+
} catch {
80+
base = "origin/main";
81+
}
82+
}
83+
84+
const isSource = (f) =>
85+
/\.tsx?$/.test(f) &&
86+
!f.endsWith(".d.ts") &&
87+
!f.includes("/generated") &&
88+
SCAN_ROOTS.some((r) => f.startsWith(`${r}/`));
89+
90+
const changed = new Set(
91+
[
92+
...git(`diff --name-only --diff-filter=ACMR ${base}`).split("\n"),
93+
...git("ls-files --others --exclude-standard").split("\n"),
94+
]
95+
.map((f) => f.trim())
96+
.filter(Boolean)
97+
.filter(isSource),
98+
);
99+
100+
const fresh = [];
101+
for (const file of changed) {
102+
let head;
103+
try {
104+
head = readFileSync(join(ROOT, file), "utf8");
105+
} catch {
106+
continue; // deleted in worktree
107+
}
108+
if (!head.includes("radix-ui")) continue;
109+
let baseSrc = "";
110+
try {
111+
baseSrc = git(`show ${base}:"${file}"`);
112+
} catch {
113+
// new file — everything counts as added
114+
}
115+
const before = frozenImports(baseSrc);
116+
for (const entry of frozenImports(head)) {
117+
if (!before.has(entry)) fresh.push({ file, entry });
118+
}
119+
}
120+
121+
if (fresh.length) {
122+
console.error(
123+
`\n✗ ${fresh.length} NEW Radix import(s) since ${base.slice(0, 12)} — Radix is frozen; new UI comes from @posthog/quill:\n`,
124+
);
125+
for (const { file, entry } of fresh) console.error(` ${file}\n ${entry}`);
126+
console.error(
127+
`\nOnly Box, Flex, and Text from @radix-ui/themes are allowed in new code. Use the
128+
@posthog/quill equivalent (Button, Dialog*, Tooltip*, DropdownMenu*, ...) instead.`,
129+
);
130+
process.exit(1);
131+
}
132+
133+
console.log(
134+
`✓ No new Radix imports in ${changed.size} changed file(s) vs ${base.slice(0, 12)}.`,
135+
);

0 commit comments

Comments
 (0)