Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/frontend/nodejs-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ pnpm add -w -D <pkg> # root tooling only
- `@visionset/annotator` keeps `react` as an **optional peer dependency** — never promote it to
a hard dependency; the core must stay usable without React.
- `@visionset/ui-core` UI primitives are **Radix behaviour + shadcn Nova styling, iconed with
Tabler**. Do not add another component library, and do not add a second icon set:
`@tabler/icons-react` is the only one the frontend declares, and a gate refuses another.
lucide**. Do not add another component library, and do not add a second icon set:
`lucide-react` is the only one the frontend declares, and a gate refuses another.
- `@visionset/app` is `private: true` — never published to npm; its bundle ships inside the
Python wheel.

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ client of the same SDK, and the release artifact is a plain `pip` package.
| Component | Location | Stack |
| --- | --- | --- |
| Python distribution | `src/visionset/` | Python 3.12+, pydantic v2, FastAPI, Typer, MCP, SQLAlchemy, uv |
| Frontend workspace | `frontend/` | Node 24, pnpm, TypeScript, React 19, Vite, vitest, Radix + Tabler |
| Frontend workspace | `frontend/` | Node 24, pnpm, TypeScript, React 19, Vite, vitest, Radix + lucide |
| Dev infra | `docker/` | Docker Compose (dev only) |

See `README.md` for the monorepo map and `CONTRIBUTING.md` for the full check list.
Expand Down
19 changes: 14 additions & 5 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ that CLI version is the reference for every value in this document. Nothing here
invented — every token, every derived radius, every chart colour traces back to the
CLI's own output.

**One property deliberately diverges: the icon set.** The preset decodes
`icons: tabler`, and that line above says so because it is what the code decodes to;
the product draws `lucide-react`, and `components.json` says `iconLibrary: lucide`.
The schema accepts the value, so this is a decision rather than drift — but it is the
one place where reading the preset and reading the configuration give different
answers, and it is written here so nobody has to discover that by diffing them.

`@visionset/ui-core` owns the implementation, in exactly three files:

| File | Role |
Expand Down Expand Up @@ -409,11 +416,13 @@ First-class, and part of every rule above rather than a section to satisfy after
- **Ad-hoc geometry that fights Nova.** A control's height, padding, or radius is not a
per-screen decision; reaching past the geometry table above for a bespoke size is a
design decision to make in this document, not in a component diff.
- **Mixing icon sets in new code.** Tabler is the set, and now the only one: every icon
the primitives, the screens and the annotation workspace draw is
`@tabler/icons-react`, no package declares a second icon library, and
`tests/scripts/design_tokens.test.mjs` refuses one that reappears in a manifest or an
import. A second set is a decision to make in this document, not a dependency to add.
- **Mixing icon sets in new code.** `lucide-react` is the set, and the only one: every
icon the primitives, the screens and the annotation workspace draw comes from it, no
package declares a second icon library, and `tests/scripts/design_tokens.test.mjs`
refuses one that reappears in a manifest or an import. The rule is *one* set rather
than one particular set — what costs a reader is two of them on a screen, where the
same idea arrives at two weights and two grids. Changing which one is a decision to
make in this document, not a dependency to add.
- **Brand in a functional control.** Robomous coral is identity — the wordmark and its
styleguide swatch, nothing else. A functional control reaching for `brand` is a
semantic-colour violation regardless of how many other sites already use it correctly.
9 changes: 6 additions & 3 deletions docs/content/architecture/frontend/ui-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,15 @@ Geist heading fonts, radius `medium`, menu `inverted`/`subtle`) - the CLI's own
generated output, transcribed verbatim, plus five VisionSet extension roles
(`stage`, `brand`, `success`, `warning`, `origin-*`) added through shadcn's own
extension convention. `components.json` (`style: "radix-nova"`,
`iconLibrary: "tabler"`, `menuColor: "inverted"`, `menuAccent: "subtle"`) holds
`iconLibrary: "lucide"`, `menuColor: "inverted"`, `menuAccent: "subtle"`) holds
the preset properties shadcn's own tools read - the fields its config schema
defines, and no others. The schema is strict, so the properties it has no field
for - the radius, the fonts, the chart palette, every colour - are values
carried by `styles.css` instead; see [`DESIGN.md`](../../../../DESIGN.md)'s
Source of Truth for the three layers. `tokens.ts` is the TypeScript mirror for a caller that
Source of Truth for the three layers. The icon set is the one place the
configuration and the preset disagree on purpose: the preset decodes `tabler`, the
product draws `lucide-react`, and `iconLibrary` records the product rather than the
preset. `tokens.ts` is the TypeScript mirror for a caller that
cannot read CSS. Both themes - light and dark - are declared in full from the
preset, so `bg-primary` in a component here and `bg-primary` in a screen mean
the same colour by construction. There is no `tailwind.config.js` in this
Expand All @@ -106,7 +109,7 @@ visual-design rule. The current choices:
| Concern | Choice |
| --- | --- |
| UI primitives | Radix (+ shadcn-style composition with `cva` and `cn`) - the open-code shadcn maintenance model is the direction: a primitive is VisionSet-owned source in `frontend/ui-core/src/primitives/`, edited directly, not a package dependency upgraded blindly |
| Icons | `@tabler/icons-react`, and nothing else: the primitives, the screens and the annotation workspace all draw from it, and no package declares a second icon library |
| Icons | `lucide-react`, and nothing else: the primitives, the screens and the annotation workspace all draw from it, and no package declares a second icon library |
| Styling | Tailwind v4, CSS-first `@theme`, on the shadcn preset `b3bXyyPdWj` - no `tailwind.config.js`, ever |
| Toasts | sonner |
| Component tests | vitest + jsdom + @testing-library/react |
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
21 changes: 7 additions & 14 deletions frontend/app/src/shell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -145,21 +138,21 @@ export function AppShell(): JSX.Element {
onClick={toggle}
>
{collapsed ? (
<IconLayoutSidebarLeftExpand aria-hidden="true" />
<PanelLeftOpen aria-hidden="true" />
) : (
<IconLayoutSidebarLeftCollapse aria-hidden="true" />
<PanelLeftClose aria-hidden="true" />
)}
</RailButton>
</div>

<RailLink to="/" end collapsed={collapsed} testId="rail-home" label="Home">
<IconHome aria-hidden="true" />
<House aria-hidden="true" />
</RailLink>
<RailLink to="/projects" collapsed={collapsed} testId="rail-projects" label="Projects">
<IconFolders aria-hidden="true" />
<Folders aria-hidden="true" />
</RailLink>
<RailLink to="/models" collapsed={collapsed} testId="rail-models" label="Models">
<IconCpu aria-hidden="true" />
<Cpu aria-hidden="true" />
</RailLink>

<div className="mt-auto">
Expand Down Expand Up @@ -243,7 +236,7 @@ function SignOut({ collapsed }: { readonly collapsed: boolean }): JSX.Element {
const label = access === "session" ? "Use a token" : "Sign out";
return (
<RailButton testId="rail-sign-out" label={label} onClick={signOut} wide={!collapsed}>
<IconLogout aria-hidden="true" />
<LogOut aria-hidden="true" />
{!collapsed && <span className="truncate">{label}</span>}
</RailButton>
);
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/src/shell/NotFound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -18,7 +18,7 @@ export function NotFound(): JSX.Element {
return (
<EmptyState
data-testid="not-found"
icon={<IconCompass className="size-8" aria-hidden="true" />}
icon={<Compass className="size-8" aria-hidden="true" />}
title="No such page"
description={pathname}
action={
Expand Down
12 changes: 6 additions & 6 deletions frontend/app/src/styleguide/Styleguide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -204,13 +204,13 @@ export function Styleguide(): JSX.Element {
<Section title="Buttons" description="Five variants, four sizes.">
<div className="flex flex-wrap items-center gap-2">
<Button variant="primary" data-testid="button-primary">
<IconPlus className="size-4" aria-hidden="true" />
<Plus className="size-4" aria-hidden="true" />
New project
</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">
<IconTrash className="size-4" aria-hidden="true" />
<Trash2 className="size-4" aria-hidden="true" />
Delete
</Button>
<Button variant="link">Learn more</Button>
Expand All @@ -220,15 +220,15 @@ export function Styleguide(): JSX.Element {
<Tooltip>
<TooltipTrigger asChild>
<Button variant="primary" size="icon" aria-label="Select (V)">
<IconPointer className="size-4" aria-hidden="true" />
<MousePointer2 className="size-4" aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Select (V)</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Box (B)">
<IconSquare className="size-4" aria-hidden="true" />
<Square className="size-4" aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">Box (B)</TooltipContent>
Expand Down Expand Up @@ -458,7 +458,7 @@ export function Styleguide(): JSX.Element {
meta="11 images · ingested Aug 7, 2026"
actions={
<Button variant="secondary">
<IconPlus className="size-4" aria-hidden="true" />
<Plus className="size-4" aria-hidden="true" />
Ingest
</Button>
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/ui-core/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"rsc": false,
"tsx": true,
"tailwind": { "config": "", "css": "src/styles.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" },
"iconLibrary": "tabler",
"iconLibrary": "lucide",
"rtl": false,
"aliases": { "components": "@/components", "utils": "@/lib/cn", "ui": "@/primitives", "lib": "@/lib", "hooks": "@/hooks" },
"menuColor": "inverted",
Expand Down
2 changes: 1 addition & 1 deletion frontend/ui-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions frontend/ui-core/src/annotator/AddClassDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -634,7 +634,7 @@ export function AddClassDialog({
onBank?.(banked);
}}
>
<IconX className="size-3" aria-hidden="true" />
<X className="size-3" aria-hidden="true" />
</button>
</li>
))}
Expand Down Expand Up @@ -830,7 +830,7 @@ export function AddClassDialog({
disabled={pending || !readyForm}
onClick={addAnother}
>
<IconPlus className="size-4" aria-hidden="true" />
<Plus className="size-4" aria-hidden="true" />
Create and add another
<kbd className="ml-1 rounded border border-border px-1 font-mono text-xs text-muted-foreground">
⌘↵
Expand Down
Loading
Loading