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

Commit b52d131

Browse files
committed
Freeze Radix UI imports to Box/Flex/Text; new UI comes from @posthog/quill
Add scripts/check-radix-imports.mjs, a baseline-allowlist checker (same pattern as check-host-boundaries.mjs): only Box, Flex, and Text from @radix-ui/themes may be added in new code; every other Radix component and every other @radix-ui/* package is denied. The 546 existing imports across 276 files are baselined in scripts/radix-allowlist.json, which only shrinks (--prune after migrating a file to quill). - Wire the check into CI (code-quality workflow) and `pnpm radix` - Document the freeze in root AGENTS.md (Forbidden Patterns, new "Radix Freeze" section, Commands, Key Libraries) and in the browser-tabs, canvas, and inbox feature docs - Add missing CLAUDE.md -> AGENTS.md symlinks in browser-tabs and canvas, matching the root convention Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MDJ4nfqaXqzioZtx5Ct3q4
1 parent 49b9e67 commit b52d131

10 files changed

Lines changed: 1314 additions & 5 deletions

File tree

‎.github/workflows/code-quality.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,6 @@ 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: node scripts/check-radix-imports.mjs

‎AGENTS.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,20 @@ 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+
Existing Radix imports are baselined in `scripts/radix-allowlist.json` and checked by `scripts/check-radix-imports.mjs` in CI. The allowlist only shrinks:
123+
124+
```bash
125+
node scripts/check-radix-imports.mjs # verify: fails on any Radix import not in the baseline
126+
node scripts/check-radix-imports.mjs --prune # shrink the baseline after migrating a file to quill
127+
```
128+
129+
Do not use `--init` to baseline new violations. When you touch a file that still uses frozen Radix components, prefer swapping them to quill and running `--prune`.
116130

117131
## Host Boundary
118132

@@ -199,6 +213,7 @@ await boot(container);
199213
- `pnpm --filter <pkg> typecheck|test|build`: run a scoped task.
200214
- `pnpm --filter code package|make`: package the Electron app.
201215
- `node scripts/check-host-boundaries.mjs`: verify host boundary allowlist.
216+
- `node scripts/check-radix-imports.mjs`: verify no new Radix imports beyond the baseline (`pnpm radix`).
202217

203218
## Merging PRs
204219

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

233248
## Key Libraries
234249

235-
- React 19, Radix UI Themes, Tailwind CSS, `@posthog/quill`
250+
- React 19, Tailwind CSS, `@posthog/quill` (Radix UI Themes is legacy — frozen to `Box`/`Flex`/`Text` for new code; see "Radix Freeze")
236251
- TanStack Query, TanStack Router
237252
- Zustand, InversifyJS (with `@inversifyjs/strongly-typed`), Zod
238253
- 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",

‎packages/ui/src/features/browser-tabs/AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ A browser-style tab strip in the Channels title bar (`/website/*`), each tab
44
fronting an open **canvas, task, or channel sub-section** (a `TabIdentity`:
55
`dashboardId | taskId | channel(+section) | blank`).
66
This file documents the UX and the model; edit it when the behaviour changes.
7+
The root `AGENTS.md` rules apply, including the Radix freeze: new UI here uses
8+
`@posthog/quill` — only `Box`/`Flex`/`Text` may come from `@radix-ui/themes`.
79

810
Canvases and tasks are equal citizens: navigating to either
911
(`/website/$channelId/dashboards/$dashboardId` or
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

‎packages/ui/src/features/canvas/AGENTS.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ The root `AGENTS.md` architecture rules still apply.
1010
- **Use `@posthog/quill`, not Radix.** New UI in this space pulls components from
1111
`@posthog/quill` (`Button`, `Dialog*`, `AlertDialog*`, `DropdownMenu*`,
1212
`ContextMenu*`, `Tooltip*`, `Collapsible*`, …). Do **not** reach for
13-
`@radix-ui/themes` or `@radix-ui/react-*`. Some older code here still imports
14-
`@radix-ui/themes` (`Box`, `Flex`, `Text`, `AlertDialog`) — that's legacy to be
15-
migrated, not a pattern to copy. When you touch such code, prefer swapping to
16-
the Quill equivalent.
13+
`@radix-ui/themes` or `@radix-ui/react-*`. This is now enforced repo-wide: the
14+
Radix freeze (root `AGENTS.md`, `scripts/check-radix-imports.mjs`) fails CI on
15+
any new Radix import beyond `Box`/`Flex`/`Text` from `@radix-ui/themes`. Some
16+
older code here still imports frozen Radix components (`AlertDialog`) — that's
17+
baselined legacy to be migrated, not a pattern to copy. When you touch such
18+
code, prefer swapping to the Quill equivalent and run
19+
`node scripts/check-radix-imports.mjs --prune`.
1720
- **Don't restyle Quill internals.** Quill components are already themed —
1821
spacing, typography, and especially **color** are baked in. Do not add
1922
`text-gray-*` / `text-muted-foreground` / `font-*` or other color/typography
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

‎packages/ui/src/features/inbox/CLAUDE.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ Shared primitives exist to keep the surfaces consistent:
130130

131131
When adding or changing UI, reuse those primitives first. Avoid encoding one-off layout systems inside a tab component.
132132

133+
The repo-wide Radix freeze (root `AGENTS.md`) applies: new components come from `@posthog/quill`, and only `Box`/`Flex`/`Text` may be imported from `@radix-ui/themes`. Existing frozen Radix imports are baselined in `scripts/radix-allowlist.json` — migrate them to quill when touched, don't extend them.
134+
133135
## Things to Avoid
134136

135137
- Do not reuse the deleted legacy `ReportListRow`, `ReportDetailPane`, or old list/detail stores.

‎scripts/check-radix-imports.mjs‎

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#!/usr/bin/env node
2+
import { execSync } from "node:child_process";
3+
import { existsSync, readFileSync, writeFileSync } 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 ALLOWLIST = join(ROOT, "scripts", "radix-allowlist.json");
9+
const SCAN_ROOTS = ["packages", "apps"];
10+
11+
const ALLOWED_THEMES_IMPORTS = new Set(["Box", "Flex", "Text"]);
12+
13+
const USAGE = `check-radix-imports — freeze Radix UI usage; new UI comes from @posthog/quill.
14+
15+
node scripts/check-radix-imports.mjs verify: fail on any Radix import not in the allowlist
16+
node scripts/check-radix-imports.mjs --init (re)generate the baseline allowlist from current imports
17+
node scripts/check-radix-imports.mjs --prune drop allowlist entries no longer imported (after migrating)
18+
19+
Only Box, Flex, and Text from @radix-ui/themes are permitted in new code. Every
20+
other Radix component and every other @radix-ui/* package is frozen: existing
21+
imports are baselined in scripts/radix-allowlist.json and must not grow. The
22+
allowlist size is the migration debt. Goal: 0.`;
23+
24+
// Matches static imports, re-exports, and dynamic imports of Radix packages.
25+
const IMPORT_RE =
26+
/(?:import|export)\s+(?:type\s+)?([\w$]+|\*\s+as\s+[\w$]+|\{[^}]*\}|[\w$]+\s*,\s*\{[^}]*\}|\*)?\s*(?:from\s*)?["'](@radix-ui\/[^"']+|radix-ui(?:\/[^"']*)?)["']/g;
27+
const DYNAMIC_RE =
28+
/import\s*\(\s*["'](@radix-ui\/[^"']+|radix-ui(?:\/[^"']*)?)["']\s*\)/g;
29+
30+
function listFiles() {
31+
const globs = SCAN_ROOTS.flatMap((r) => [
32+
`"${r}/**/*.ts"`,
33+
`"${r}/**/*.tsx"`,
34+
]);
35+
const out = execSync(`git -C "${ROOT}" ls-files ${globs.join(" ")}`, {
36+
encoding: "utf8",
37+
});
38+
return out
39+
.split("\n")
40+
.map((f) => f.trim())
41+
.filter(Boolean)
42+
.filter((f) => !f.endsWith(".d.ts") && !f.includes("/generated"));
43+
}
44+
45+
function importedNames(clause) {
46+
if (!clause) return ["*"]; // bare `import "pkg"` — side-effect import
47+
const names = [];
48+
const braces = clause.match(/\{([^}]*)\}/);
49+
if (braces) {
50+
for (let n of braces[1].split(",")) {
51+
n = n.trim().replace(/^type\s+/, "");
52+
if (!n) continue;
53+
names.push(n.split(/\s+as\s+/)[0].trim());
54+
}
55+
}
56+
const outsideBraces = clause
57+
.replace(/\{[^}]*\}/, "")
58+
.trim()
59+
.replace(/,$/, "")
60+
.trim();
61+
if (outsideBraces) names.push("*"); // default, namespace, or star import — all names reachable
62+
return names.length ? names : ["*"];
63+
}
64+
65+
function violationsInSource(src) {
66+
const hits = new Set();
67+
IMPORT_RE.lastIndex = 0;
68+
for (let m = IMPORT_RE.exec(src); m; m = IMPORT_RE.exec(src)) {
69+
const [, clause, spec] = m;
70+
if (spec === "@radix-ui/themes") {
71+
for (const name of importedNames(clause)) {
72+
if (!ALLOWED_THEMES_IMPORTS.has(name)) hits.add(`${spec}#${name}`);
73+
}
74+
} else {
75+
hits.add(spec);
76+
}
77+
}
78+
DYNAMIC_RE.lastIndex = 0;
79+
for (let m = DYNAMIC_RE.exec(src); m; m = DYNAMIC_RE.exec(src)) {
80+
hits.add(m[1] === "@radix-ui/themes" ? `${m[1]}#*` : m[1]);
81+
}
82+
return [...hits].sort();
83+
}
84+
85+
function findViolations() {
86+
const violations = {};
87+
for (const path of listFiles()) {
88+
let src;
89+
try {
90+
src = readFileSync(join(ROOT, path), "utf8");
91+
} catch {
92+
continue;
93+
}
94+
if (!src.includes("radix-ui")) continue;
95+
const hits = violationsInSource(src);
96+
if (hits.length) violations[path] = hits;
97+
}
98+
return violations;
99+
}
100+
101+
function loadAllowlist() {
102+
if (!existsSync(ALLOWLIST)) return {};
103+
return JSON.parse(readFileSync(ALLOWLIST, "utf8")).files ?? {};
104+
}
105+
106+
function saveAllowlist(files) {
107+
const sorted = Object.fromEntries(
108+
Object.keys(files)
109+
.sort()
110+
.map((k) => [k, files[k]]),
111+
);
112+
writeFileSync(
113+
ALLOWLIST,
114+
`${JSON.stringify({ note: "Radix imports frozen at baseline. Only Box/Flex/Text from @radix-ui/themes are allowed in new code; use @posthog/quill instead. Remove entries as you migrate. Goal: empty.", files: sorted }, null, 2)}\n`,
115+
);
116+
try {
117+
execSync(`pnpm exec biome format --write "${ALLOWLIST}"`, {
118+
cwd: ROOT,
119+
stdio: "ignore",
120+
});
121+
} catch {
122+
// biome unavailable (e.g. before pnpm install) — commit hooks/CI will format
123+
}
124+
}
125+
126+
const mode = process.argv[2];
127+
if (mode === "--help" || mode === "-h") {
128+
console.log(USAGE);
129+
process.exit(0);
130+
}
131+
132+
const current = findViolations();
133+
const allow = loadAllowlist();
134+
135+
if (mode === "--init") {
136+
saveAllowlist(current);
137+
console.log(
138+
`Baseline written: ${Object.keys(current).length} file(s) with frozen Radix imports.`,
139+
);
140+
process.exit(0);
141+
}
142+
143+
if (mode === "--prune") {
144+
const kept = {};
145+
for (const f of Object.keys(allow)) {
146+
if (!current[f]) continue;
147+
kept[f] = allow[f].filter((e) => current[f].includes(e));
148+
if (!kept[f].length) delete kept[f];
149+
}
150+
const before = Object.values(allow).flat().length;
151+
const after = Object.values(kept).flat().length;
152+
saveAllowlist(kept);
153+
console.log(
154+
`Pruned. ${before - after} import(s) migrated, ${after} remaining.`,
155+
);
156+
process.exit(0);
157+
}
158+
159+
const fresh = [];
160+
for (const [file, entries] of Object.entries(current)) {
161+
const allowed = new Set(allow[file] ?? []);
162+
for (const e of entries) if (!allowed.has(e)) fresh.push({ file, entry: e });
163+
}
164+
165+
const migrated = [];
166+
for (const [file, entries] of Object.entries(allow)) {
167+
const now = new Set(current[file] ?? []);
168+
for (const e of entries) if (!now.has(e)) migrated.push({ file, entry: e });
169+
}
170+
171+
if (migrated.length) {
172+
console.log(
173+
`\n✓ ${migrated.length} Radix import(s) migrated since baseline — run --prune to shrink the allowlist.`,
174+
);
175+
}
176+
177+
if (fresh.length) {
178+
console.error(
179+
`\n✗ ${fresh.length} NEW Radix import(s) — Radix is frozen; new UI comes from @posthog/quill:\n`,
180+
);
181+
for (const { file, entry } of fresh) console.error(` ${file}\n ${entry}`);
182+
console.error(
183+
`\nOnly Box, Flex, and Text from @radix-ui/themes are allowed in new code. Use the
184+
@posthog/quill equivalent (Button, Dialog*, Tooltip*, DropdownMenu*, ...) instead.
185+
If you are only moving already-baselined code between files, update
186+
scripts/radix-allowlist.json to match and justify it in review.`,
187+
);
188+
process.exit(1);
189+
}
190+
191+
console.log(
192+
`\n✓ No new Radix imports. ${Object.values(allow).flat().length} frozen import(s) across ${Object.keys(allow).length} file(s) (baseline). Goal: 0.`,
193+
);
194+
process.exit(0);

0 commit comments

Comments
 (0)