From e82df588a7256265ea8af21f39a4d19ff879b38c Mon Sep 17 00:00:00 2001
From: YaelAnaya
Date: Mon, 24 Aug 2026 19:33:21 -0700
Subject: [PATCH 1/5] chore(frontend): lucide-react is the icon dependency
again
Swaps the declared icon library back: `lucide-react` in, `@tabler/icons-react`
out, in both manifests and the lockfile. The lockfile is resolved from the
manifests and a frozen install runs from an empty tree, so the two agree rather
than being asserted to.
Tabler goes out in the same change rather than being left declared. Nothing
imports it once the sources move, and an icon library nobody draws from is the
dependency the previous cleanup removed for exactly this reason.
---
frontend/app/package.json | 2 +-
frontend/ui-core/package.json | 2 +-
pnpm-lock.yaml | 36 +++++++++++++++--------------------
3 files changed, 17 insertions(+), 23 deletions(-)
diff --git a/frontend/app/package.json b/frontend/app/package.json
index cdfa9a2b..273dd79f 100644
--- a/frontend/app/package.json
+++ b/frontend/app/package.json
@@ -17,9 +17,9 @@
"cycle": "playwright test -c playwright.cycle.config.ts"
},
"dependencies": {
- "@tabler/icons-react": "^3.46.0",
"@visionset/annotator": "workspace:*",
"@visionset/ui-core": "workspace:*",
+ "lucide-react": "^1.32.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^8.3.0"
diff --git a/frontend/ui-core/package.json b/frontend/ui-core/package.json
index 53e67df0..1a2320fc 100644
--- a/frontend/ui-core/package.json
+++ b/frontend/ui-core/package.json
@@ -53,12 +53,12 @@
"@radix-ui/react-slot": "^1.3.3",
"@radix-ui/react-tabs": "^1.1.21",
"@radix-ui/react-tooltip": "^1.2.16",
- "@tabler/icons-react": "^3.46.0",
"@tanstack/react-query": "^5.101.4",
"@tanstack/react-virtual": "^3.14.9",
"@visionset/annotator": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "lucide-react": "^1.32.0",
"openapi-fetch": "^0.17.0",
"shadcn": "^4.18.0",
"sonner": "^2.0.8",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0190b8da..ec3962b4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -50,15 +50,15 @@ importers:
frontend/app:
dependencies:
- '@tabler/icons-react':
- specifier: ^3.46.0
- version: 3.46.0(react@19.2.8)
'@visionset/annotator':
specifier: workspace:*
version: link:../annotator
'@visionset/ui-core':
specifier: workspace:*
version: link:../ui-core
+ lucide-react:
+ specifier: ^1.32.0
+ version: 1.33.0(react@19.2.8)
react:
specifier: ^19.0.0
version: 19.2.8
@@ -153,9 +153,6 @@ importers:
'@radix-ui/react-tooltip':
specifier: ^1.2.16
version: 1.2.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
- '@tabler/icons-react':
- specifier: ^3.46.0
- version: 3.46.0(react@19.2.8)
'@tanstack/react-query':
specifier: ^5.101.4
version: 5.101.4(react@19.2.8)
@@ -171,6 +168,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ lucide-react:
+ specifier: ^1.32.0
+ version: 1.33.0(react@19.2.8)
openapi-fetch:
specifier: ^0.17.0
version: 0.17.0
@@ -1208,14 +1208,6 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
- '@tabler/icons-react@3.46.0':
- resolution: {integrity: sha512-CCm7xJWhDT2PH4ZIFkP6AgYKtVhq0gpYkjUN+GVh1AzmIQaa77OW0bQPBPQiTE0PsXMR9oSxFqA3qBglzPyrVQ==}
- peerDependencies:
- react: '>= 16'
-
- '@tabler/icons@3.46.0':
- resolution: {integrity: sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A==}
-
'@tailwindcss/node@4.3.3':
resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==}
@@ -2505,6 +2497,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ lucide-react@1.33.0:
+ resolution: {integrity: sha512-MTRwMy0ZlL8Ur/vOAiJ9XGHE+kFPC7brq6MxAm0GiGXEBj0qy0jA/pG4N675oSzciO/UCdX8T+5yUQdmDeTLxg==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -4296,13 +4293,6 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
- '@tabler/icons-react@3.46.0(react@19.2.8)':
- dependencies:
- '@tabler/icons': 3.46.0
- react: 19.2.8
-
- '@tabler/icons@3.46.0': {}
-
'@tailwindcss/node@4.3.3':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -5522,6 +5512,10 @@ snapshots:
dependencies:
yallist: 3.1.1
+ lucide-react@1.33.0(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+
lz-string@1.5.0: {}
magic-string@0.30.21:
From 73df31581d50c5696e709f4bdfa260fc29acd9fe Mon Sep 17 00:00:00 2001
From: YaelAnaya
Date: Mon, 24 Aug 2026 19:33:22 -0700
Subject: [PATCH 2/5] refactor(ui-core): the frontend draws lucide across all
39 modules
One hundred and ninety-six references over thirty-nine files - the primitives, the
screens, the patterns, the annotation workspace, the shell and the styleguide -
move from `@tabler/icons-react` back to `lucide-react`. All seventy-two distinct
glyphs were resolved against the installed package before anything was rewritten,
and every one has an equivalent.
The rewrite is keyed on the mapped names rather than on an `Icon*` prefix, which
matters in one place: the demo's tool strip has a local component called
`IconButton`, and a prefix rewrite would have renamed a component that belongs to
neither library.
`ProjectNav` needed a second edit the mapping could not carry. It imports the icon
*component type*, and the two libraries spell it differently - Tabler's `Icon`
against lucide's `LucideIcon` - so the name moved with the import while the local
`icon: Icon` destructuring, which is a value and not the type, stayed as it was.
The geometry glyphs return to the spelling they had before: `Spline` for the
polygon and `Waypoints` for the lane. `GeometryIcon`'s own note argues that pairing
and is coherent again as written.
---
frontend/app/src/shell/AppShell.tsx | 21 +++----
frontend/app/src/shell/NotFound.tsx | 4 +-
frontend/app/src/styleguide/Styleguide.tsx | 12 ++--
.../ui-core/src/annotator/AddClassDialog.tsx | 6 +-
.../ui-core/src/annotator/AnnotationPage.tsx | 62 +++++++------------
.../ui-core/src/annotator/AnnotatorPanel.tsx | 14 ++---
frontend/ui-core/src/annotator/AssetImage.tsx | 4 +-
.../ui-core/src/annotator/CanvasReassign.tsx | 4 +-
.../ui-core/src/annotator/ClassRegion.tsx | 6 +-
.../ui-core/src/annotator/GeometryIcon.tsx | 12 ++--
.../ui-core/src/annotator/ReassignMenu.tsx | 4 +-
.../ui-core/src/annotator/SuggestPanel.tsx | 30 ++++-----
.../ui-core/src/annotator/ToolPalette.tsx | 21 +++----
frontend/ui-core/src/annotator/ZoomWidget.tsx | 10 +--
frontend/ui-core/src/data/TokenGate.tsx | 4 +-
frontend/ui-core/src/patterns/AsyncStates.tsx | 6 +-
frontend/ui-core/src/patterns/BackLink.tsx | 4 +-
frontend/ui-core/src/patterns/ClassFields.tsx | 6 +-
frontend/ui-core/src/patterns/ProjectNav.tsx | 37 ++++-------
.../ui-core/src/patterns/projectNav.test.tsx | 8 ++-
frontend/ui-core/src/primitives/Dialog.tsx | 4 +-
frontend/ui-core/src/primitives/Select.tsx | 10 +--
.../ui-core/src/screens/AssetThumbnail.tsx | 6 +-
.../ui-core/src/screens/BatchLifecycle.tsx | 6 +-
.../ui-core/src/screens/BatchesScreen.tsx | 8 +--
.../ui-core/src/screens/CorrectionBatch.tsx | 6 +-
.../src/screens/DatasetAssetDialog.tsx | 16 ++---
.../ui-core/src/screens/DatasetScreen.tsx | 28 +++------
frontend/ui-core/src/screens/DeleteBatch.tsx | 6 +-
.../ui-core/src/screens/GalleryScreen.tsx | 33 ++++------
frontend/ui-core/src/screens/HomeScreen.tsx | 46 +++++---------
frontend/ui-core/src/screens/IngestScreen.tsx | 43 +++++--------
frontend/ui-core/src/screens/ModelsScreen.tsx | 39 ++++--------
.../ui-core/src/screens/OverviewPanel.tsx | 12 ++--
.../ui-core/src/screens/PreLabelDialog.tsx | 4 +-
.../src/screens/ProjectPreLabelDialog.tsx | 4 +-
.../ui-core/src/screens/ProjectScreen.tsx | 8 +--
.../ui-core/src/screens/ProjectsScreen.tsx | 8 +--
.../ui-core/src/screens/PromoteButton.tsx | 6 +-
frontend/ui-core/src/screens/SchemaEditor.tsx | 6 +-
40 files changed, 237 insertions(+), 337 deletions(-)
diff --git a/frontend/app/src/shell/AppShell.tsx b/frontend/app/src/shell/AppShell.tsx
index e0734198..17712eb5 100644
--- a/frontend/app/src/shell/AppShell.tsx
+++ b/frontend/app/src/shell/AppShell.tsx
@@ -82,14 +82,7 @@
* right level for it.
*/
-import {
- IconCpu,
- IconFolders,
- IconHome,
- IconLayoutSidebarLeftCollapse,
- IconLayoutSidebarLeftExpand,
- IconLogout,
-} from "@tabler/icons-react";
+import { Cpu, Folders, House, LogOut, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import {
Button,
buttonVariants,
@@ -145,21 +138,21 @@ export function AppShell(): JSX.Element {
onClick={toggle}
>
{collapsed ? (
-
+
) : (
-
+
)}
-
+
-
+
-
+
@@ -243,7 +236,7 @@ function SignOut({ collapsed }: { readonly collapsed: boolean }): JSX.Element {
const label = access === "session" ? "Use a token" : "Sign out";
return (
-
+
{!collapsed && {label}}
);
diff --git a/frontend/app/src/shell/NotFound.tsx b/frontend/app/src/shell/NotFound.tsx
index 4c6a133a..a57d8462 100644
--- a/frontend/app/src/shell/NotFound.tsx
+++ b/frontend/app/src/shell/NotFound.tsx
@@ -9,7 +9,7 @@
*/
import { Button, EmptyState } from "@visionset/ui-core";
-import { IconCompass } from "@tabler/icons-react";
+import { Compass } from "lucide-react";
import type { JSX } from "react";
import { Link, useLocation } from "react-router";
@@ -18,7 +18,7 @@ export function NotFound(): JSX.Element {
return (
}
+ icon={}
title="No such page"
description={pathname}
action={
diff --git a/frontend/app/src/styleguide/Styleguide.tsx b/frontend/app/src/styleguide/Styleguide.tsx
index 9a6a6c95..81ad4045 100644
--- a/frontend/app/src/styleguide/Styleguide.tsx
+++ b/frontend/app/src/styleguide/Styleguide.tsx
@@ -80,7 +80,7 @@ import {
formatGeometries,
toast,
} from "@visionset/ui-core";
-import { IconPlus, IconPointer, IconSquare, IconTrash } from "@tabler/icons-react";
+import { MousePointer2, Plus, Square, Trash2 } from "lucide-react";
import type { JSX, ReactNode } from "react";
/** The demo schema, borrowed so the swatches show the real palette rule. */
@@ -204,13 +204,13 @@ export function Styleguide(): JSX.Element {
@@ -220,7 +220,7 @@ export function Styleguide(): JSX.Element {
Select (V)
@@ -228,7 +228,7 @@ export function Styleguide(): JSX.Element {
Box (B)
@@ -458,7 +458,7 @@ export function Styleguide(): JSX.Element {
meta="11 images · ingested Aug 7, 2026"
actions={
}
diff --git a/frontend/ui-core/src/annotator/AddClassDialog.tsx b/frontend/ui-core/src/annotator/AddClassDialog.tsx
index e67e356e..934be458 100644
--- a/frontend/ui-core/src/annotator/AddClassDialog.tsx
+++ b/frontend/ui-core/src/annotator/AddClassDialog.tsx
@@ -85,7 +85,7 @@
* where every other refusal here renders.
*/
-import { IconPlus, IconX } from "@tabler/icons-react";
+import { Plus, X } from "lucide-react";
import { useEffect, useRef, useState, type JSX } from "react";
import { asApiError } from "../data/errors";
@@ -634,7 +634,7 @@ export function AddClassDialog({
onBank?.(banked);
}}
>
-
+
))}
@@ -830,7 +830,7 @@ export function AddClassDialog({
disabled={pending || !readyForm}
onClick={addAnother}
>
-
+
Create and add another
⌘↵
diff --git a/frontend/ui-core/src/annotator/AnnotationPage.tsx b/frontend/ui-core/src/annotator/AnnotationPage.tsx
index 7741847e..c0808e6e 100644
--- a/frontend/ui-core/src/annotator/AnnotationPage.tsx
+++ b/frontend/ui-core/src/annotator/AnnotationPage.tsx
@@ -131,21 +131,7 @@ import {
type Viewport,
} from "@visionset/annotator";
import { AnnotatorStore as Store } from "@visionset/annotator";
-import {
- IconAlertTriangle,
- IconArrowBackUp,
- IconArrowLeft,
- IconCheck,
- IconChecks,
- IconChevronLeft,
- IconChevronRight,
- IconDevices,
- IconDots,
- IconEye,
- IconHelpCircle,
- IconLayoutGrid,
- IconPlayerSkipForward,
-} from "@tabler/icons-react";
+import { ArrowLeft, Check, CheckCheck, ChevronLeft, ChevronRight, CircleHelp, Eye, Grid3x3, MonitorSmartphone, MoreHorizontal, SkipForward, TriangleAlert, Undo2 } from "lucide-react";
import {
useCallback,
useEffect,
@@ -491,7 +477,7 @@ function TooNarrow({
return (
}
+ icon={}
title="This screen is too narrow to annotate on"
description={`Annotating is precision work on a large surface: the editor needs at least ${ANNOTATOR_MIN_VIEWPORT_PX}px of width for the canvas, the tools and the object list to coexist. Rotate to landscape, widen the window, or open this job on a larger screen.`}
{...(onOpenGallery === undefined || destination === undefined
@@ -503,7 +489,7 @@ function TooNarrow({
data-testid="too-narrow-gallery"
onClick={() => onOpenGallery(destination.project_id, destination.id)}
>
-
+
Back to the batch
),
@@ -1926,7 +1912,7 @@ function Workspace({
onClick={() => attempt(onOpenGallery)}
disabled={onOpenGallery === undefined}
>
-
+
{/* The batch's pin, not the project's active version. Named here because
@@ -2038,7 +2024,7 @@ function Workspace({
data-testid="open-gallery"
onClick={() => setGalleryOpen(true)}
>
-
+
{/*
`tabular-nums` is the whole reason this is a separate span rather
@@ -2072,7 +2058,7 @@ function Workspace({
disabled={assetIndex >= assetCount - 1}
onClick={() => go(1)}
>
-
+
@@ -2117,7 +2103,7 @@ function Workspace({
disabled={!declares(asset, ASSET_ACTION.restore) || setProgress.isPending}
onClick={unskip}
>
-
+
Un-skip
) : (
@@ -2129,7 +2115,7 @@ function Workspace({
disabled={!declares(asset, ASSET_ACTION.skip) || setProgress.isPending}
onClick={() => settle("skipped")}
>
-
+
Skip
X
@@ -2222,7 +2208,7 @@ function Workspace({
});
}}
>
-
+
{jobState === "completed" ? "Finished" : "Finish job"}
@@ -2290,7 +2276,7 @@ function Workspace({
onClick={() => go(1)}
>
{flowLabel}
-
+
)}
@@ -2337,7 +2323,7 @@ function Workspace({
disabled={readOnly || !dirty || save.isPending}
onClick={() => attempt()}
>
-
+
Save and stay
@@ -2394,7 +2380,7 @@ function Workspace({
disabled={setProgress.isPending}
onClick={() => settle(reviewAction.progress)}
>
-
+
{reviewAction.label}
@@ -2427,7 +2413,7 @@ function Workspace({
aria-label="More actions"
data-testid="more-actions"
>
-
+
@@ -2444,7 +2430,7 @@ function Workspace({
disabled={readOnly || !dirty || save.isPending}
onSelect={() => attempt()}
>
-
+
Save and stay
)}
@@ -2456,7 +2442,7 @@ function Workspace({
disabled={setProgress.isPending}
onSelect={() => settle(reviewAction.progress)}
>
-
+
{reviewAction.label}
)}
@@ -2472,7 +2458,7 @@ function Workspace({
data-testid="return-to-annotator"
onSelect={() => settle("annotated")}
>
-
+
Return to annotator
{/* Inside the conditional, not above the row below it: the two
@@ -2483,7 +2469,7 @@ function Workspace({
>
)}
setHelpOpen(true)}>
-
+
Keyboard shortcuts
@@ -2510,7 +2496,7 @@ function Workspace({
className="flex shrink-0 items-center gap-2 border-b border-border bg-muted px-3 py-1.5 text-xs text-muted-foreground"
data-testid="readonly-banner"
>
-
+ Viewing only.
{workflowBecause ?? settledBecause}
{/*
@@ -2553,7 +2539,7 @@ function Workspace({
className="flex shrink-0 items-center gap-2 border-b border-destructive/30 bg-destructive/5 px-3 py-1.5 text-xs text-destructive"
data-testid="skipped-notice"
>
-
+
This asset is skipped, so it will not reach the dataset and its annotations will not
count towards the job. Un-skip it to put it back in play.
@@ -2717,7 +2703,7 @@ function Workspace({
}
+ icon={}
title={asApiError(openingRefusal).code}
>
{refusalProse(openingRefusal)}
@@ -2736,7 +2722,7 @@ function Workspace({
}
+ icon={}
title={asApiError(save.error).code}
>
This frame could not be saved
@@ -2766,7 +2752,7 @@ function Workspace({
}
+ icon={}
title={asApiError(actionRefusal).code}
>
{refusalProse(actionRefusal)}
@@ -3201,7 +3187,7 @@ function SaveState({
// so the exception is retired rather than inherited. The tick still
// carries the meaning on its own — state is never colour alone.
-
+
Saved
);
diff --git a/frontend/ui-core/src/annotator/AnnotatorPanel.tsx b/frontend/ui-core/src/annotator/AnnotatorPanel.tsx
index ebb8e8e4..f562f194 100644
--- a/frontend/ui-core/src/annotator/AnnotatorPanel.tsx
+++ b/frontend/ui-core/src/annotator/AnnotatorPanel.tsx
@@ -82,7 +82,7 @@ import {
type LabelClass,
type Tool,
} from "@visionset/annotator";
-import { IconCheck, IconEye, IconEyeOff, IconSparkles, IconTag, IconTrash } from "@tabler/icons-react";
+import { Check, Eye, EyeOff, Sparkles, Tag, Trash2 } from "lucide-react";
import { useEffect, useRef, useState, type JSX, type RefObject } from "react";
import { geometryLabel } from "../data/geometryCategory";
@@ -284,7 +284,7 @@ export function AnnotatorPanel({
disabled={drawn.length === 0}
onClick={() => onHiddenChange(allHidden ? new Set() : new Set(drawn.map((o) => o.id)))}
>
- {allHidden ? : }
+ {allHidden ? : }
@@ -428,7 +428,7 @@ function TagRegion({
/>
{declared.name}
{on ? (
-
+
) : (
{hotkeyForClass(schema, declared.name) ?? "—"}
@@ -487,7 +487,7 @@ function ModelMark({
aria-label={`Model-produced by ${annotation.model_ref ?? "an unnamed model"}`}
className="flex shrink-0 items-center text-xs text-muted-foreground"
>
-
+
{/* The full reference, which is far too long for the row and is exactly
@@ -591,7 +591,7 @@ function ObjectRow({
data-testid={`object-visibility-${index}`}
onClick={onToggleVisible}
>
- {hidden ? : }
+ {hidden ? : }
);
@@ -639,7 +639,7 @@ function RowReassign({
aria-label={`Reassign object ${index + 1}`}
data-testid={`object-reclass-${index}`}
>
-
+
-
+ The asset’s content could not be loaded.
);
diff --git a/frontend/ui-core/src/annotator/CanvasReassign.tsx b/frontend/ui-core/src/annotator/CanvasReassign.tsx
index 26bc4a3a..9e7f8150 100644
--- a/frontend/ui-core/src/annotator/CanvasReassign.tsx
+++ b/frontend/ui-core/src/annotator/CanvasReassign.tsx
@@ -54,7 +54,7 @@ import {
useAnnotatorSnapshot,
} from "@visionset/annotator";
import type { AnnotatorStore, Geometry, Viewport } from "@visionset/annotator";
-import { IconTag } from "@tabler/icons-react";
+import { Tag } from "lucide-react";
import type { JSX } from "react";
import { Button } from "../primitives/Button";
@@ -130,7 +130,7 @@ export function CanvasReassign({
aria-label={`Reassign the selected ${subject.label_class}`}
data-testid="canvas-reclass"
>
-
+ onAddClass?.("")}
>
-
+
@@ -267,7 +267,7 @@ export function ClassRegion({
className="flex h-9 w-full shrink-0 items-center gap-2 border-t border-border px-3 text-left text-sm text-muted-foreground hover:bg-muted"
onClick={() => onAddClass?.(creatable)}
>
-
+ Create class “{creatable}”
)}
diff --git a/frontend/ui-core/src/annotator/GeometryIcon.tsx b/frontend/ui-core/src/annotator/GeometryIcon.tsx
index e1858876..4402549e 100644
--- a/frontend/ui-core/src/annotator/GeometryIcon.tsx
+++ b/frontend/ui-core/src/annotator/GeometryIcon.tsx
@@ -10,7 +10,7 @@
* size. The strip can afford them — it is five controls learned once.
*/
-import { IconPointer, IconPolygon, IconSquare, IconVectorSpline } from "@tabler/icons-react";
+import { MousePointer2, Spline, Square, Waypoints } from "lucide-react";
import type { JSX } from "react";
export interface GeometryIconProps {
@@ -27,10 +27,10 @@ export interface GeometryIconProps {
/** One glyph per geometry: a closed rectangle, a closed path, an open path, or the pointer. */
export function GeometryIcon({ tool, className = "size-4" }: GeometryIconProps): JSX.Element {
- if (tool === "bbox") return ;
- if (tool === "polygon") return ;
- // A lane is a path, and it must read as an OPEN one — `IconPolygon` is already
+ if (tool === "bbox") return ;
+ if (tool === "polygon") return ;
+ // A lane is a path, and it must read as an OPEN one — `Spline` is already
// the closed shape above it, and using it here would say "closed" for a lane.
- if (tool === "polyline") return ;
- return ;
+ if (tool === "polyline") return ;
+ return ;
}
diff --git a/frontend/ui-core/src/annotator/ReassignMenu.tsx b/frontend/ui-core/src/annotator/ReassignMenu.tsx
index 29e77783..bca1d259 100644
--- a/frontend/ui-core/src/annotator/ReassignMenu.tsx
+++ b/frontend/ui-core/src/annotator/ReassignMenu.tsx
@@ -47,7 +47,7 @@
import { digitFromCode, hotkeyForClass } from "@visionset/annotator";
import type { Annotation, AnnotationSchema, LabelClass } from "@visionset/annotator";
-import { IconCheck } from "@tabler/icons-react";
+import { Check } from "lucide-react";
import type { JSX, KeyboardEvent } from "react";
import { formatGeometries } from "../data/geometryCategory";
@@ -143,7 +143,7 @@ export function ReassignMenu({
style={{ background: classColor(declared, declared.name) }}
/>
{declared.name}
- {current && }
+ {current && }
{ok ? (
hotkey !== null && (
diff --git a/frontend/ui-core/src/annotator/SuggestPanel.tsx b/frontend/ui-core/src/annotator/SuggestPanel.tsx
index f47615a1..83e280ba 100644
--- a/frontend/ui-core/src/annotator/SuggestPanel.tsx
+++ b/frontend/ui-core/src/annotator/SuggestPanel.tsx
@@ -63,13 +63,7 @@ import {
type SuggestionState,
type Detail,
} from "@visionset/annotator";
-import {
- IconAlertTriangle,
- IconCheck,
- IconLoader2,
- IconSparkles,
- IconX,
-} from "@tabler/icons-react";
+import { Check, Loader2, Sparkles, TriangleAlert, X } from "lucide-react";
import type { JSX, ReactNode } from "react";
import { EditorNotice } from "./EditorNotice";
@@ -224,7 +218,7 @@ export function SuggestPanel({
*/
if (isParked(session)) {
return (
- }>
+ }>
{heldClass === null
? "Nothing selected to suggest for"
@@ -248,7 +242,7 @@ export function SuggestPanel({
data-testid="suggest-discard"
onClick={onDiscard}
>
-
+
Put the tool away
@@ -265,9 +259,9 @@ export function SuggestPanel({
tone={copy.tone}
icon={
copy.tone === "warn" ? (
-
+
) : (
-
+
)
}
>
@@ -294,7 +288,7 @@ export function SuggestPanel({
if (session.status === "refused") {
return (
- }>
+ }>
That suggestion could not be made
{/* The server's sentence, verbatim. It is the one that carries the
install command when the cause is a missing extra. */}
@@ -315,7 +309,7 @@ export function SuggestPanel({
// lives in `usePendingIndicator` rather than here.
if (session.status === "asking") {
return (
- }>
+ }>
Looking at that…
@@ -334,7 +328,7 @@ export function SuggestPanel({
if (session.status === "none") {
return (
- }>
+ }>
Nothing to suggest there
@@ -375,7 +369,7 @@ export function SuggestPanel({
*/
if (isAcceptable(session)) {
return (
- }>
+ }>
A shape for “{session.labelClass}”
@@ -384,7 +378,7 @@ export function SuggestPanel({
);
diff --git a/frontend/ui-core/src/annotator/ZoomWidget.tsx b/frontend/ui-core/src/annotator/ZoomWidget.tsx
index 94524044..410236fc 100644
--- a/frontend/ui-core/src/annotator/ZoomWidget.tsx
+++ b/frontend/ui-core/src/annotator/ZoomWidget.tsx
@@ -25,7 +25,7 @@
* a person could change to get it.
*/
-import { IconMaximize, IconMinimize, IconMinus, IconPlus, IconScan } from "@tabler/icons-react";
+import { Maximize2, Minimize2, Minus, Plus, Scan } from "lucide-react";
import { useEffect, useState, type JSX } from "react";
import { Button } from "../primitives/Button";
@@ -70,7 +70,7 @@ export function ZoomWidget({
reason={floorReason}
onClick={onZoomOut}
>
-
+
{/* The stage's own scale, capped by it — so the ceiling reads exactly
@@ -89,7 +89,7 @@ export function ZoomWidget({
reason={ceilingReason}
onClick={onZoomIn}
>
-
+
@@ -97,7 +97,7 @@ export function ZoomWidget({
{/* The same implementation `mod+0` reaches, which is why that chord stays
intercepted rather than forwarded to the host. */}
-
+
{fullscreen !== null && (
@@ -108,7 +108,7 @@ export function ZoomWidget({
reason=""
onClick={fullscreen.toggle}
>
- {fullscreen.active ? : }
+ {fullscreen.active ? : }
)}
diff --git a/frontend/ui-core/src/data/TokenGate.tsx b/frontend/ui-core/src/data/TokenGate.tsx
index f0c488de..fa9727b7 100644
--- a/frontend/ui-core/src/data/TokenGate.tsx
+++ b/frontend/ui-core/src/data/TokenGate.tsx
@@ -45,7 +45,7 @@
* for ten minutes.
*/
-import { IconKey } from "@tabler/icons-react";
+import { Key } from "lucide-react";
import { useEffect, useState, type FormEvent, type JSX, type ReactNode } from "react";
import { createApiClient } from "../client";
@@ -123,7 +123,7 @@ export function TokenForm(): JSX.Element {
-
+
Connect to a workspace
diff --git a/frontend/ui-core/src/patterns/AsyncStates.tsx b/frontend/ui-core/src/patterns/AsyncStates.tsx
index cecbf2f6..6c85bfac 100644
--- a/frontend/ui-core/src/patterns/AsyncStates.tsx
+++ b/frontend/ui-core/src/patterns/AsyncStates.tsx
@@ -17,7 +17,7 @@
* incident id where a person can quote it. What a retry *does* is the caller's.
*/
-import { IconAlertTriangle, IconInbox } from "@tabler/icons-react";
+import { Inbox, TriangleAlert } from "lucide-react";
import type { JSX, ReactNode } from "react";
import { cn } from "../lib/cn";
@@ -78,7 +78,7 @@ export function EmptyState({
)}
>
- {icon ?? }
+ {icon ?? }
{title}
{description !== undefined && (
@@ -126,7 +126,7 @@ export function ErrorState({
variant="destructive"
title={
-
+
{message}
}
diff --git a/frontend/ui-core/src/patterns/BackLink.tsx b/frontend/ui-core/src/patterns/BackLink.tsx
index a19f2454..1eafdbeb 100644
--- a/frontend/ui-core/src/patterns/BackLink.tsx
+++ b/frontend/ui-core/src/patterns/BackLink.tsx
@@ -17,7 +17,7 @@
* renders no control rather than a dead one — the caller omits it.
*/
-import { IconArrowLeft } from "@tabler/icons-react";
+import { ArrowLeft } from "lucide-react";
import type { JSX } from "react";
import { Button } from "../primitives/Button";
@@ -42,7 +42,7 @@ export function BackLink({ label, onNavigate }: BackLinkProps): JSX.Element {
className="-ml-2 w-fit max-w-64 text-muted-foreground hover:text-foreground"
onClick={onNavigate}
>
-
+ {label}
);
diff --git a/frontend/ui-core/src/patterns/ClassFields.tsx b/frontend/ui-core/src/patterns/ClassFields.tsx
index a7e5c376..9d857649 100644
--- a/frontend/ui-core/src/patterns/ClassFields.tsx
+++ b/frontend/ui-core/src/patterns/ClassFields.tsx
@@ -19,7 +19,7 @@
* mounted.
*/
-import { IconPlus, IconTrash } from "@tabler/icons-react";
+import { Plus, Trash2 } from "lucide-react";
import type { JSX } from "react";
import { geometryLabel, groupGeometries } from "../data/geometryCategory";
@@ -287,7 +287,7 @@ function Attributes({
onChange([...attributes, { name: "", kind: "string", required: false, default: null }])
}
>
-
+
Add
@@ -404,7 +404,7 @@ function Attributes({
disabled={disabled}
onClick={() => onChange(attributes.filter((_, i) => i !== index))}
>
-
+
diff --git a/frontend/ui-core/src/patterns/ProjectNav.tsx b/frontend/ui-core/src/patterns/ProjectNav.tsx
index 517b8c17..a5853501 100644
--- a/frontend/ui-core/src/patterns/ProjectNav.tsx
+++ b/frontend/ui-core/src/patterns/ProjectNav.tsx
@@ -45,18 +45,7 @@
* and a hairline, no `sidebar-*` token anywhere in it.
*/
-import {
- IconChevronDown,
- IconDatabase,
- IconDots,
- IconLayoutGrid,
- IconPencil,
- IconSitemap,
- IconStack2,
- IconTrash,
- IconUpload,
- type Icon,
-} from "@tabler/icons-react";
+import { ChevronDown, Database, Grid3x3, Layers, MoreHorizontal, Network, Pencil, Trash2, type LucideIcon, Upload } from "lucide-react";
import { cva } from "class-variance-authority";
import type { JSX, MouseEvent, ReactNode } from "react";
@@ -87,7 +76,7 @@ export const DEFAULT_PROJECT_SECTION: ProjectSection = "overview";
interface SectionLabel {
readonly label: string;
- readonly icon: Icon;
+ readonly icon: LucideIcon;
}
/**
@@ -109,10 +98,10 @@ const navItemVariants = cva(
);
const SECTION_LABELS: Record = {
- overview: { label: "Overview", icon: IconLayoutGrid },
- schema: { label: "Schema", icon: IconSitemap },
- batches: { label: "Batches", icon: IconStack2 },
- dataset: { label: "Dataset", icon: IconDatabase },
+ overview: { label: "Overview", icon: Grid3x3 },
+ schema: { label: "Schema", icon: Network },
+ batches: { label: "Batches", icon: Layers },
+ dataset: { label: "Dataset", icon: Database },
};
/** One batch the annotator can be opened on: what the `Annotate ▾` menu lists. */
@@ -251,7 +240,7 @@ function Cta({ annotate, onIngest, contentOwnsTheAction = false, layout }: Proje
className={wide}
onClick={onIngest}
>
-
+
Ingest
);
@@ -277,7 +266,7 @@ function AnnotateAction({
if (targets.length === 1 && only !== undefined) {
return (
);
@@ -288,9 +277,9 @@ function AnnotateAction({
{/* Same testid and variant as the jumping form: one control with two
shapes, and the chevron is what tells them apart. */}
@@ -319,20 +308,20 @@ function Overflow({ onRename, onDelete }: ProjectNavProps): JSX.Element | null {
{onRename !== undefined && (
-
+
Rename
)}
{onRename !== undefined && onDelete !== undefined && }
{onDelete !== undefined && (
-
+
Delete
)}
diff --git a/frontend/ui-core/src/patterns/projectNav.test.tsx b/frontend/ui-core/src/patterns/projectNav.test.tsx
index 566e2ed5..0aa9b18e 100644
--- a/frontend/ui-core/src/patterns/projectNav.test.tsx
+++ b/frontend/ui-core/src/patterns/projectNav.test.tsx
@@ -163,10 +163,14 @@ describe("ProjectNav", () => {
expect(screen.getByTestId("project-menu")).toBeTruthy();
});
- it("is drawn with Tabler only", () => {
+ it("draws from the one icon set and no other", () => {
// `import.meta.url` is an `http://localhost/` URL under jsdom, so the path is
// resolved from the package root vitest runs in rather than from this module.
const source = readFileSync(resolve(process.cwd(), "src/patterns/ProjectNav.tsx"), "utf8");
- expect(source).not.toMatch(/lucide-react/);
+ // Both directions: the set it draws, and the one it must not reach back for.
+ // Asserting only the absence would pass for a file that had stopped drawing
+ // icons at all.
+ expect(source).toMatch(/lucide-react/);
+ expect(source).not.toMatch(/@tabler\/icons-react/);
});
});
diff --git a/frontend/ui-core/src/primitives/Dialog.tsx b/frontend/ui-core/src/primitives/Dialog.tsx
index 1846a89f..9109ca68 100644
--- a/frontend/ui-core/src/primitives/Dialog.tsx
+++ b/frontend/ui-core/src/primitives/Dialog.tsx
@@ -14,7 +14,7 @@
*/
import * as DialogPrimitive from "@radix-ui/react-dialog";
-import { IconX } from "@tabler/icons-react";
+import { X } from "lucide-react";
import {
createContext,
forwardRef,
@@ -95,7 +95,7 @@ export const DialogContent = forwardRef<
"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:hover:bg-muted/50"
}
>
-
+
diff --git a/frontend/ui-core/src/primitives/Select.tsx b/frontend/ui-core/src/primitives/Select.tsx
index de28c64e..9f3aad0a 100644
--- a/frontend/ui-core/src/primitives/Select.tsx
+++ b/frontend/ui-core/src/primitives/Select.tsx
@@ -65,7 +65,7 @@
*/
import * as SelectPrimitive from "@radix-ui/react-select";
-import { IconCheck, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
+import { Check, ChevronDown, ChevronUp } from "lucide-react";
import {
forwardRef,
type ComponentPropsWithoutRef,
@@ -112,7 +112,7 @@ export const SelectTrigger = forwardRef<
>
{children}
-
+
);
@@ -156,7 +156,7 @@ export const SelectContent = forwardRef<
because it is the fix `models.spec.ts` asserts.
*/}
-
+
-
+
@@ -214,7 +214,7 @@ export const SelectItem = forwardRef<
-
+
diff --git a/frontend/ui-core/src/screens/AssetThumbnail.tsx b/frontend/ui-core/src/screens/AssetThumbnail.tsx
index 5922d8f7..c6f33a60 100644
--- a/frontend/ui-core/src/screens/AssetThumbnail.tsx
+++ b/frontend/ui-core/src/screens/AssetThumbnail.tsx
@@ -36,7 +36,7 @@
* gigabytes.
*/
-import { IconPhoto, IconPhotoOff } from "@tabler/icons-react";
+import { Image, ImageOff } from "lucide-react";
import { useEffect, useState, type JSX } from "react";
import { useApiClient } from "../data/ApiProvider";
@@ -71,9 +71,9 @@ export function ThumbnailPlaceholder({
className={`flex items-center justify-center bg-muted text-muted-foreground ${className ?? ""}`}
>
{broken ? (
-
+
) : (
-
+
)}
{alt}
diff --git a/frontend/ui-core/src/screens/BatchLifecycle.tsx b/frontend/ui-core/src/screens/BatchLifecycle.tsx
index ed6de80d..75acbf04 100644
--- a/frontend/ui-core/src/screens/BatchLifecycle.tsx
+++ b/frontend/ui-core/src/screens/BatchLifecycle.tsx
@@ -23,7 +23,7 @@
*/
import { useState, type JSX } from "react";
-import { IconPlayerPlay, IconSquareCheck } from "@tabler/icons-react";
+import { Play, SquareCheck } from "lucide-react";
import { asApiError } from "../data/errors";
import { refusalProse } from "../data/refusals";
@@ -152,7 +152,7 @@ export function CompleteBatchButton({
disabled={outstanding > 0 || finish.isPending}
onClick={() => finish.mutate()}
>
-
+
{finish.isPending ? "Completing…" : "Complete"}
@@ -356,7 +356,7 @@ export function StartAnnotatingButton({
disabled={start.isPending}
onClick={() => start.mutate()}
>
-
+
{start.isPending ? "Starting…" : "Start annotating"}
{refusal !== null && (
diff --git a/frontend/ui-core/src/screens/BatchesScreen.tsx b/frontend/ui-core/src/screens/BatchesScreen.tsx
index adbb0a64..8e1a67af 100644
--- a/frontend/ui-core/src/screens/BatchesScreen.tsx
+++ b/frontend/ui-core/src/screens/BatchesScreen.tsx
@@ -30,7 +30,7 @@
* UUIDs. A program has the SDK and the API.
*/
-import { IconPlayerPlay, IconStack2, IconUpload } from "@tabler/icons-react";
+import { Layers, Play, Upload } from "lucide-react";
import { useState, type JSX } from "react";
import { Async } from "../data/Async";
@@ -89,7 +89,7 @@ export function BatchesScreen({
<>
{onIngest !== undefined && (
)}
@@ -285,7 +285,7 @@ function Lifecycle({
// several filled buttons down the same column, under a page header whose
// "Annotate" is the actual forward action.
);
@@ -300,7 +300,7 @@ function Lifecycle({
disabled={start.isPending}
onClick={() => start.mutate()}
>
-
+
Start
{start.isError && (
diff --git a/frontend/ui-core/src/screens/CorrectionBatch.tsx b/frontend/ui-core/src/screens/CorrectionBatch.tsx
index 1605aee0..6fd8e5b7 100644
--- a/frontend/ui-core/src/screens/CorrectionBatch.tsx
+++ b/frontend/ui-core/src/screens/CorrectionBatch.tsx
@@ -31,7 +31,7 @@
* selection is a thing somebody chose.
*/
-import { IconGitBranch } from "@tabler/icons-react";
+import { GitBranch } from "lucide-react";
import { useState, type JSX } from "react";
import { BATCH_ACTION, declares } from "../data/capabilities";
@@ -119,7 +119,7 @@ export function CorrectionButton({
data-testid={`correct-${batch.name}`}
onClick={() => setShowing(true)}
>
-
+
Create correction batch
-
+
Correction of{" "}
{onOpenParent === undefined ? (
parentName
diff --git a/frontend/ui-core/src/screens/DatasetAssetDialog.tsx b/frontend/ui-core/src/screens/DatasetAssetDialog.tsx
index d315b98d..25b37ab4 100644
--- a/frontend/ui-core/src/screens/DatasetAssetDialog.tsx
+++ b/frontend/ui-core/src/screens/DatasetAssetDialog.tsx
@@ -22,13 +22,7 @@
* because coordinates without a frame cannot be placed.
*/
-import {
- IconChevronLeft,
- IconChevronRight,
- IconEye,
- IconEyeOff,
- IconTrash,
-} from "@tabler/icons-react";
+import { ChevronLeft, ChevronRight, Eye, EyeOff, Trash2 } from "lucide-react";
import { useState, type CSSProperties, type JSX, type KeyboardEvent } from "react";
import {
BboxShape,
@@ -164,7 +158,7 @@ export function DatasetAssetDialog({
disabled={!hasPrevious}
onClick={() => onIndex(index - 1)}
>
-
+
@@ -194,7 +188,7 @@ export function DatasetAssetDialog({
data-testid="preview-remove"
onClick={() => onRemove(asset)}
>
-
+
Remove from dataset
}
@@ -496,7 +488,7 @@ function TrunkTile({
onClick={onRemove}
className="text-muted-foreground hover:text-destructive"
>
-
+
@@ -595,7 +587,7 @@ function ReleaseCard({ release }: { readonly release: Release }): JSX.Element {
-
+
{release.tag}
v{release.schema_version}
{release.split !== null && release.split !== undefined && (
@@ -618,7 +610,7 @@ function ReleaseCard({ release }: { readonly release: Release }): JSX.Element {