From 76d8854a77ca6cf463ceee6af9e7dc3777c71119 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 29 Sep 2026 20:54:41 -0700 Subject: [PATCH] refactor(web): icon wrapper serves Lucide (CL-9551) Swap Phosphor for lucide-react behind @/lib/icons at a 2px stroke, keep export names, and replace glyph characters used as icons. --- .oxlintrc.json | 12 +-- DESIGN.md | 10 +- apps/web/package.json | 2 +- apps/web/src/lib/icons/index.test.tsx | 24 ----- apps/web/src/lib/icons/index.tsx | 101 ++++++++++---------- apps/web/src/pages/diff-view.tsx | 11 +-- apps/web/src/pages/insights-page.tsx | 7 +- apps/web/src/pages/new-workbench-picker.tsx | 6 +- apps/web/src/tailwind.css | 7 ++ apps/web/vite.config.ts | 2 +- bun.lock | 32 ++----- 11 files changed, 83 insertions(+), 131 deletions(-) delete mode 100644 apps/web/src/lib/icons/index.test.tsx diff --git a/.oxlintrc.json b/.oxlintrc.json index a2b1ca2e33..064637fe5e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -61,17 +61,7 @@ "paths": [ { "name": "lucide-react", - "message": "lucide-react is retired — import from @/lib/icons instead." - }, - { - "name": "@phosphor-icons/react", - "message": "Import icons from @/lib/icons, not @phosphor-icons/react directly." - } - ], - "patterns": [ - { - "group": ["@phosphor-icons/react/*"], - "message": "Import icons from @/lib/icons, not @phosphor-icons/react directly." + "message": "Import icons from @/lib/icons, not lucide-react directly." } ] } diff --git a/DESIGN.md b/DESIGN.md index 9e6a88623a..cf2996c6f9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -177,13 +177,13 @@ monospace (code, IDs, numeric/tabular contexts). Both are declared once in `apps/web/src/tailwind.css`'s `@theme` block; consumers use `font-sans` / `font-mono`, never a font-family override. -**Icons.** Phosphor, bold weight only, imported exclusively through +**Icons.** Lucide, 2px stroke only, imported exclusively through `@/lib/icons` (`apps/web/src/lib/icons/index.tsx`) — never straight from -`@phosphor-icons/react` or from any other icon package. That module is a +`lucide-react` or from any other icon package. That module is a curated re-export: only glyphs the product actually uses are named there, -so a stray import can't reach for an off-list icon or a different weight. -`BoldIconProvider` sets the bold default once at the app root; call sites -never repeat `weight="bold"`. **Sparkle and Sparkles are banned outright** +so a stray import can't reach for an off-list icon or a different stroke. +`BoldIconProvider` sets the `1em` size and 2px stroke once at the app root; +call sites never repeat `strokeWidth`. **Sparkle and Sparkles are banned outright** — they read as a generic "AI" cliché. Every spot that used to carry one now carries a glyph that means something specific to what it marks. diff --git a/apps/web/package.json b/apps/web/package.json index ebe8031a9e..8178130da8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -27,13 +27,13 @@ "@corbits/workflows": "workspace:*", "@intx/types": "workspace:*", "@isomorphic-git/lightning-fs": "4.7.0", - "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-slot": "^1.2.3", "@tanstack/react-query": "catalog:", "arktype": "catalog:", "buffer": "^6.0.3", "isomorphic-git": "catalog:", + "lucide-react": "^1.49.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-joyride": "^2.9.3" diff --git a/apps/web/src/lib/icons/index.test.tsx b/apps/web/src/lib/icons/index.test.tsx deleted file mode 100644 index ca1ed374c7..0000000000 --- a/apps/web/src/lib/icons/index.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import * as icons from "./index"; - -describe("@/lib/icons", () => { - test("never exports a Sparkle glyph", () => { - const names = Object.keys(icons); - const sparkleNames = names.filter((name) => /sparkle/i.test(name)); - expect(sparkleNames).toEqual([]); - }); - - test("re-exports the bold icon provider and icon type", () => { - expect(typeof icons.BoldIconProvider).toBe("function"); - }); - - // Regression: a bare `{ weight: "bold" }` silently drops Phosphor's own - // `size` default, since the context value is replaced, not merged. - test("BoldIconProvider preserves Phosphor's size default alongside bold weight", () => { - expect(icons.boldIconContextValue).toEqual({ - size: "1em", - weight: "bold", - }); - }); -}); diff --git a/apps/web/src/lib/icons/index.tsx b/apps/web/src/lib/icons/index.tsx index e1389589c2..bcbe7983ff 100644 --- a/apps/web/src/lib/icons/index.tsx +++ b/apps/web/src/lib/icons/index.tsx @@ -1,88 +1,85 @@ // Sparkle/Sparkles is banned outright — it read as a generic "AI" cliché. // A curated re-export, not a full pass-through, so a stray import can't -// reach for an off-list icon or tiptoe around the bold-weight rule. -import { IconContext, type Icon, type IconProps } from "@phosphor-icons/react"; +// reach for an off-list icon or tiptoe around the stroke-width rule. +import { LucideProvider, type LucideIcon, type LucideProps } from "lucide-react"; import type { ReactNode } from "react"; -export type { Icon, IconProps }; +export type Icon = LucideIcon; +export type IconProps = LucideProps; export { Archive, - ArrowBendUpLeft, - ArrowClockwise, ArrowDown, ArrowLeft, - ArrowsDownUp, - ArrowsIn, - ArrowsOut, - ArrowSquareOut, + ArrowRight, ArrowUp, + ArrowUpDown as ArrowsDownUp, Bell, - BookBookmark, - CaretDown, - CaretLeft, - CaretRight, - ChartBar, - ChatCircle, - ChatCircleDots, + BookMarked as BookBookmark, + Bot as Robot, Check, - CircleNotch, + ChartColumn as ChartBar, + ChevronDown as CaretDown, + ChevronLeft as CaretLeft, + ChevronRight as CaretRight, + CircleAlert as WarningCircle, + CircleUser as UserCircle, + CirclePlay as PlayCircle, Clock, Compass, + CornerUpLeft as ArrowBendUpLeft, Copy, Cpu, - DotsThree, - FileDashed, + Diff as GitDiff, + Ellipsis as DotsThree, FileText, - FlowArrow, + FileQuestionMark as FileDashed, FolderOpen, GitBranch, - GitDiff, GitPullRequest, Hash, Key, - Lightning, - LinkSimple, - ListBullets, + Layers as Stack, + LayoutGrid as SquaresFour, + Link as LinkSimple, + List as ListBullets, + LoaderCircle as CircleNotch, Lock, - MagnifyingGlass, - Microphone, - MoonStars, - PaperPlaneRight, + LogOut as SignOut, + Maximize2 as ArrowsOut, + MessageCircle as ChatCircle, + MessageCircleMore as ChatCircleDots, + Mic as Microphone, + Minimize2 as ArrowsIn, + MoonStar as MoonStars, Paperclip, - PencilSimple, - PlayCircle, + Pencil as PencilSimple, + Pin as PushPin, + PinOff as PushPinSlash, + Plug as Plugs, Plus, - Plugs, - PushPin, - PushPinSlash, Repeat, - Robot, + RotateCw as ArrowClockwise, + Search as MagnifyingGlass, + Send as PaperPlaneRight, Shield, - SignOut, SlidersHorizontal, - Smiley, - Stack, + Smile as Smiley, + Square as Stop, + SquareArrowOutUpRight as ArrowSquareOut, Star, - Stop, - SquaresFour, + TriangleAlert as Warning, User, - UserCircle, UserPlus, Users, - Warning, - WarningCircle, + Workflow as FlowArrow, X, -} from "@phosphor-icons/react"; + Zap as Lightning, +} from "lucide-react"; -// `IconContext.Provider` replaces Phosphor's whole context value rather -// than merging it, so every library default (`size`) must be restated -// alongside the override — dropping it silently un-sizes bare glyphs. -export const boldIconContextValue = { size: "1em", weight: "bold" } as const; - -/** Wraps a subtree so every Phosphor icon under it defaults to bold weight - * without repeating `weight="bold"` at each call site. Mounted once at each - * app's root (see `apps/web/src/app.tsx`). */ +/** Wraps a subtree so every icon under it defaults to a 2px stroke without + * repeating it at each call site (the `1em` default size is `tailwind.css`). + * Mounted once at each app's root (see `apps/web/src/app.tsx`). */ export function BoldIconProvider({ children }: { children: ReactNode }) { - return {children}; + return {children}; } diff --git a/apps/web/src/pages/diff-view.tsx b/apps/web/src/pages/diff-view.tsx index d407824ebf..9d27f1dfe8 100644 --- a/apps/web/src/pages/diff-view.tsx +++ b/apps/web/src/pages/diff-view.tsx @@ -4,13 +4,14 @@ import { Badge, Table, TableBody, TableCell, TableRow } from "@corbits/react-ui"; import { diffText } from "@/lib/text-diff"; import type { DiffLine } from "@/lib/text-diff"; -import { useMemo } from "react"; +import { ArrowRight, DotsThree } from "@/lib/icons"; +import { useMemo, type ReactNode } from "react"; -const MARKER: Record = { +const MARKER: Record = { context: " ", added: "+", removed: "-", - skipped: "⋯", + skipped: