Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
2657e49
chore(ui-core): a script installs a shadcn primitive and keeps its pr…
YaelAnaya Aug 26, 2026
32cb0da
test(ui-core): every primitive is its shadcn snapshot plus added line…
YaelAnaya Aug 26, 2026
719aa7b
test(ui-core): the canonical gate counts a deleted blank line as a de…
YaelAnaya Aug 26, 2026
f7dcfa6
refactor(ui-core): badge and alert are shadcn's canonical radix-nova …
YaelAnaya Aug 26, 2026
ef8a24a
fix(ui-core): the session-class chips stay list items
YaelAnaya Aug 26, 2026
76d4230
refactor(ui-core): the button is shadcn's canonical file, and the col…
YaelAnaya Aug 26, 2026
113d133
test(frontend): the colour gate admits only a mix of tokens
YaelAnaya Aug 26, 2026
d1d7019
refactor(ui-core): the card is shadcn's canonical file
YaelAnaya Aug 26, 2026
32aefa3
refactor(ui-core): the table is shadcn's canonical file
YaelAnaya Aug 26, 2026
9f5a387
refactor(ui-core): tabs are shadcn's canonical file
YaelAnaya Aug 26, 2026
4a3225d
fix(ui-core): a gate card and a release card keep a heading role
YaelAnaya Aug 26, 2026
23805e3
refactor(ui-core): input, textarea, label and field are shadcn's cano…
YaelAnaya Aug 26, 2026
d850ce7
refactor(ui-core): the dialog is shadcn's canonical file
YaelAnaya Aug 26, 2026
7e2de0e
fix(ui-core): a dialog with several descriptions names them at the ca…
YaelAnaya Aug 26, 2026
89f4d63
refactor(ui-core): dropdown menu and tooltip are shadcn's canonical f…
YaelAnaya Aug 26, 2026
dc379bb
fix(ui-core): every menu surface leaves on the frame it is dismissed
YaelAnaya Aug 26, 2026
5f8411f
refactor(ui-core): the select is shadcn's canonical file, and a two-l…
YaelAnaya Aug 26, 2026
f7f5339
fix(ui-core): a two-line select option is neither clamped nor squashe…
YaelAnaya Aug 26, 2026
80f5290
refactor(ui-core): progress, skeleton and sonner are shadcn's canonic…
YaelAnaya Aug 26, 2026
741c4b8
test(ui-core): the add-class provenance test stubs the observer canon…
YaelAnaya Aug 26, 2026
213c601
docs(design): the primitives are the shadcn CLI's files for b2iH, hel…
YaelAnaya Aug 26, 2026
a9e3fb5
docs(design): the primitives are installed by the CLI, not carried over
YaelAnaya Aug 26, 2026
71400be
test(ui-core): jsdom gets the observer canonical tooltips need, defin…
YaelAnaya Aug 26, 2026
4e8d35d
fix(ui-core): a menu surface sizes to its items and leaves on the fra…
YaelAnaya Aug 26, 2026
309feb2
fix(ui-core): a link button inside prose or a table cell is inline again
YaelAnaya Aug 26, 2026
4df2353
test(ui-core): the link button test asserts the shape its name claims
YaelAnaya Aug 26, 2026
0a2b28f
test(frontend): every button inside a form says what kind it is
YaelAnaya Aug 26, 2026
1c6068f
chore(ui-core): the shadcn installer runs through pnpm, like everythi…
YaelAnaya Aug 26, 2026
e3728cc
chore(ui-core): stale icon-set and path mentions catch up with the ca…
YaelAnaya Aug 26, 2026
5472604
merge: the canonical shadcn primitives branch onto main a4c3630
YaelAnaya Aug 27, 2026
9c279f4
fix(ui): the recipe count chip is a secondary badge like its neighbours
YaelAnaya Aug 27, 2026
98ea843
refactor(ui): the button keeps shadcn's hierarchy, and a saved frame …
YaelAnaya Aug 27, 2026
2e765fd
refactor(ui): the save-and-stay rationale describes an outline button
YaelAnaya Aug 27, 2026
9a70418
refactor(ui-core): badge status variants sit on emerald, amber, sky a…
YaelAnaya Aug 27, 2026
cdcab2d
refactor(ui): every status colour outside the badge comes from one pa…
YaelAnaya Aug 27, 2026
f16c925
chore(ui-core): the success and warning tokens retire with the forked…
YaelAnaya Aug 27, 2026
98a80ed
test(e2e): the batch progress bar is asserted against the primary token
YaelAnaya Aug 27, 2026
d55e294
refactor(ui-core): the sheet is shadcn's canonical file
YaelAnaya Aug 27, 2026
574bed3
chore(ui-core): the shadcn installer relativises every primitive the …
YaelAnaya Aug 27, 2026
26ea0e1
refactor(ui-core): the combobox is shadcn's canonical file on Base UI…
YaelAnaya Aug 27, 2026
ee89537
refactor(ui-core): the toaster follows the app's one theme source, wi…
YaelAnaya Aug 27, 2026
3e924fe
test(ui): the shadcn extension contract is machine-checked
YaelAnaya Aug 27, 2026
fae6abd
test(ui): the vocabulary gate finds a tag's real closing bracket
YaelAnaya Aug 27, 2026
9b0ce9c
docs(styleguide): the styleguide shows shadcn's variants and the four…
YaelAnaya Aug 27, 2026
02880d9
fix(ui): content under a line tab bar keeps its 16px rhythm at the pa…
YaelAnaya Aug 27, 2026
de065b0
docs(design): shadcn is the authoritative primitive contract, and Vis…
YaelAnaya Aug 27, 2026
13baa35
docs(design): the update workflow names the real dependency and what …
YaelAnaya Aug 27, 2026
ba2231d
test(e2e): reviewed visual baselines after the shadcn realignment
YaelAnaya Aug 27, 2026
c4d1ae0
test(e2e): the gallery asserts shadcn's variant names
YaelAnaya Aug 27, 2026
ec712ea
fix(ui): the recipe target chip is a quiet badge, and the gate forbid…
YaelAnaya Aug 27, 2026
ea24052
fix(ui): the mobile class picker cannot widen the schema at 320px
YaelAnaya Aug 27, 2026
dd3df49
docs: code and docs cite the design document's current headings and f…
YaelAnaya Aug 27, 2026
6465b7b
test(ui): every menu surface carries menuSurface, and the toaster fol…
YaelAnaya Aug 27, 2026
d632c52
docs(design): a status badge on a muted panel is re-measured first
YaelAnaya Aug 27, 2026
f6dbcbc
test(cycle): the cycle asserts shadcn's variant names
YaelAnaya Aug 27, 2026
4df02c3
Merge branch 'main' into refactor/shadcn-foundation-realignment
YaelAnaya Aug 27, 2026
7715075
test(ui): the vocabulary gate reads a spec's assertions, not just its…
YaelAnaya Aug 27, 2026
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
822 changes: 408 additions & 414 deletions DESIGN.md

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions docs/content/architecture/frontend/ui-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ whenever its asserted type is and `tsc` cannot see the mismatch.
colour `neutral`, chart palette `neutral`, icons `lucide`, Geist throughout with
the heading face inheriting the body's, radius `medium`, menu
`inverted`/`subtle`, pointer cursor on pressable controls) - the CLI's own
generated output, transcribed verbatim, plus five VisionSet extension roles
(`stage`, `brand`, `success`, `warning`, `origin-*`) added through shadcn's own
generated output, transcribed verbatim, plus three VisionSet extension roles
(`stage`, `brand`, `origin-*`) added through shadcn's own
extension convention. `components.json` (`style: "radix-nova"`,
`iconLibrary: "lucide"`, `menuColor: "inverted"`, `menuAccent: "subtle"`) holds
the preset properties shadcn's own tools read - the fields its config schema
Expand All @@ -91,12 +91,13 @@ 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
repository and there must not be one - the tokens would acquire a second home.

Three gates hold this: `tokens.test.ts` asserts `styles.css` and `tokens.ts`
Four gates hold this: `tokens.test.ts` asserts `styles.css` and `tokens.ts`
agree, declaration for declaration, and that no retired token has returned;
`tests/scripts/design_tokens.test.mjs` scans every tracked frontend file for a
raw colour in a class string, refuses a second `tailwind.config.js`, confines
`brand` to its two identity sites, and holds `components.json` to the
schema-supported field set; `tests/scripts/docs_links.test.mjs`
schema-supported field set; `tests/scripts/shadcn_canonical.test.mjs` holds
every primitive to its CLI snapshot; `tests/scripts/docs_links.test.mjs`
keeps [`DESIGN.md`](../../../../DESIGN.md)'s own cross-references honest.

## Libraries
Expand All @@ -106,10 +107,10 @@ 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 |
| 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 — each file there is the shadcn CLI's own output (snapshot in `shadcn/`), edited only by adding lines. The dependency is the `radix-ui` umbrella package, not the scoped `@radix-ui/react-*` packages it replaces |
| 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 |
| Styling | Tailwind v4, CSS-first `@theme`, on the shadcn preset `b2iH` - no `tailwind.config.js`, ever |
| Toasts | sonner, themed by a framework adapter in `sonner.tsx` (marked `SHADCN FRAMEWORK ADAPTER`) that reads VisionSet's one theme source — `.dark` on `<html>` — instead of shadcn's canonical `next-themes` hook, which has no provider to read here |
| Component tests | vitest + jsdom + @testing-library/react |
| Server state | TanStack Query v5 |

Expand Down
6 changes: 3 additions & 3 deletions docs/content/ui/annotator.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ advancing — and neither ever collapses into the overflow.
first** as the third member of the resolve group — `Skip · [flow verb] · Save and stay`.
*Advance* and *persist in place* are one decision read two ways, so they are adjacent
rather than a bar apart. In the current implementation the flow verb is filled in
`primary` and Save and stay is filled in `success`; colour is what separates their
intent, since a second `primary` beside the first would read as a bar that could not
decide. It keeps the frame verbs' lifetime rather than the mode's: a closed batch or a
`primary` and Save and stay is an outline control beside it; adjacency is what says
they are alternatives, since a second filled `primary` beside the first would read as
a bar that could not decide. It keeps the frame verbs' lifetime rather than the mode's: a closed batch or a
finished job has nothing to save on any frame, so it leaves with Skip and the flow verb;
inside a working job it holds its slot, disabled, so the cluster does not change width as
somebody walks a mixed job.
Expand Down
4 changes: 2 additions & 2 deletions docs/content/ui/product-principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ emptied-result rule.

## Tabs

One tab shape, product-wide (`frontend/ui-core/src/primitives/Tabs.tsx`): a row on a
One tab shape, product-wide (`frontend/ui-core/src/primitives/tabs.tsx`): a row on a
full-width hairline; the active tab carries a 2px accent rule sitting on that hairline plus
foreground text; an inactive tab carries no border, no fill and no shadow, and gets a
subtle fill on hover or focus. The inactive tab keeps the same 2px border at `transparent`,
Expand Down Expand Up @@ -263,7 +263,7 @@ Presentational contracts, all in `ui-core`, all data-only — no fetching, no ro
| `ClassListRow` | Swatch + name + a `geometry · count` secondary line. Selected = tinted background + 2px left accent rule covering the row's full height. The whole row is the click target and a real `<button>` — except when it carries shape chips, which are press targets and cannot nest inside a button: that row is a `role="group"` with an inner name button, addressed by the same `-name` handle in both markups. A row carrying a refusal never takes the group form, because only the button can be disabled. |
| `EmptyState` | Icon + a headline naming the space + one line of body + a verb-first CTA. Never a bare "No items". |
| `ThumbnailGrid` | Square tiles, 6px gap, small radius. The last tile becomes a `+N` overflow linking onward. Missing thumbnails show a photo icon on a subtle fill — never a broken-image glyph. |
| Chip | `primitives/Badge.tsx`, which already is one. It gains variants; it is not reimplemented. |
| Chip | `primitives/badge.tsx`, which already is one. It gains variants; it is not reimplemented. |

**Class colour is data, not chrome.** It appears as a small swatch or a thin bar and never
floods a card or a row — the content-over-chrome rule applied to a colour the *kernel*
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/cycle/cycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,9 @@ test("the whole cycle, from opening the app to a downloaded export", async ({ pa
// road below, which is the one that exercises the partition dialog.
await expect(page.getByTestId("approve-start-cycle-batch")).toHaveAttribute(
"data-variant",
"primary",
"default",
);
await expect(page.getByTestId("open-batch")).toHaveAttribute("data-variant", "secondary");
await expect(page.getByTestId("open-batch")).toHaveAttribute("data-variant", "outline");
await expect(page.getByTestId("approve-needs-schema")).toHaveCount(0);
await page.getByTestId("open-batch").click();
await expect(page).toHaveURL(/\/projects\/[0-9a-f-]+\/batches\/[0-9a-f-]+$/);
Expand Down Expand Up @@ -481,10 +481,10 @@ test("the whole cycle, from opening the app to a downloaded export", async ({ pa
await openProject(page, PROJECT, "batches");
await expect(page.getByTestId("batches-table")).toBeVisible();
await expect(page.getByTestId("batch-cycle-batch")).toContainText("pending approval");
// A draft row offers the composed step too, secondary like every row action.
// A draft row offers the composed step too, outline like every row action.
await expect(page.getByTestId("approve-start-cycle-batch")).toHaveAttribute(
"data-variant",
"secondary",
"outline",
);

await page.getByTestId("approve-cycle-batch").click();
Expand Down
14 changes: 11 additions & 3 deletions frontend/app/e2e/gallery.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ test("Complete and promote finishes the job, closes the batch, promotes, and say
await openGallery(page, sent, { settled: true });

const composed = page.getByTestId("complete-promote-drive-01");
await expect(composed).toHaveAttribute("data-variant", "secondary");
await expect(composed).toHaveAttribute("data-variant", "outline");
await composed.click();

// In order, and the job first: the batch refuses while its job is open.
Expand Down Expand Up @@ -815,9 +815,17 @@ test("a one-job batch draws its job flat: no accordion, one bar, the job's contr
await expect(workspace.getByTestId("timeline")).toBeVisible();
// The door, Pre-label's slot and the assignee line are the job's, under the
// header — and none of them is the page's filled control.
await expect(workspace.getByTestId(`start-job-${JOB}`)).toHaveAttribute("data-variant", "secondary");
await expect(workspace.getByTestId(`start-job-${JOB}`)).toHaveAttribute("data-variant", "outline");
await expect(workspace.getByTestId(`assignee-${JOB}`)).toContainText("Unassigned");
await expect(page.getByTestId("gallery").locator('[data-variant="primary"]')).toHaveCount(0);
// Scoped to `Button`, not `[data-variant]` alone: the batch-state `Badge`
// legitimately carries `data-variant="default"` too (`in_annotation` reads
// as the near-black `default` tone, per `batchState.ts`), and a bare
// attribute selector would catch that badge instead of asking the question
// this line means to ask — whether the job's door duplicates the page's one
// filled button.
await expect(
page.getByTestId("gallery").locator('[data-slot="button"][data-variant="default"]'),
).toHaveCount(0);
await expect(workspace.getByTestId("segments")).toBeVisible();
await expect(workspace.getByTestId("tile-asset-0")).toBeVisible();
});
Expand Down
10 changes: 6 additions & 4 deletions frontend/app/e2e/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ async function channelsOf(page: Page, colour: string): Promise<readonly number[]
}, colour);
}

test("a batch row's progress fills with the success colour on a bordered muted track", async ({
test("a batch row's progress fills with the primary colour on a bordered muted track", async ({
page,
}) => {
await openCold(page, `/projects/${PROJECT}/batches`);
Expand All @@ -399,16 +399,18 @@ test("a batch row's progress fills with the success colour on a bordered muted t
const tokens = await page.evaluate(() => {
const style = getComputedStyle(document.documentElement);
return {
success: style.getPropertyValue("--success"),
primary: style.getPropertyValue("--primary"),
muted: style.getPropertyValue("--muted"),
border: style.getPropertyValue("--border"),
};
});
const painted = async (locator: typeof bar, prop: string): Promise<readonly number[]> =>
channelsOf(page, await locator.evaluate((node, p) => getComputedStyle(node).getPropertyValue(p), prop));

expect(await painted(bar.locator("> *").first(), "background-color")).toEqual(
await channelsOf(page, tokens.success),
const indicator = bar.locator('[data-slot="progress-indicator"]');
await expect(indicator).toHaveClass(/bg-primary/);
expect(await painted(indicator, "background-color")).toEqual(
await channelsOf(page, tokens.primary),
);
expect(await painted(bar, "background-color")).toEqual(await channelsOf(page, tokens.muted));
expect(await painted(bar, "border-top-color")).toEqual(await channelsOf(page, tokens.border));
Expand Down
66 changes: 65 additions & 1 deletion frontend/app/e2e/styleguide.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ test("the token utilities reach the browser as the contract's values", async ({
* nowhere near the primary button" — the palette's whole point was that a
* functional fill was never the identity colour. Task 5 of the shadcn preset
* rewrite changed the *premise*, not just the value: the progress indicator is
* now `bg-primary` (`ui-core/src/primitives/Feedback.tsx`), the same functional
* now `bg-primary` (`ui-core/src/primitives/progress.tsx`), the same functional
* colour as the primary button, and `brand` dropped to exactly two call sites —
* the wordmark and its styleguide swatch — neither of which is this page's
* progress bar. So the claim this test can still make, and the one worth
Expand Down Expand Up @@ -445,3 +445,67 @@ test("a two-line option grows its trigger, and a one-line one is Nova's 32px", a
const id = page.getByLabel("Model").locator("span", { hasText: "facebook/sam2.1-hiera-base-plus" }).first();
await expect(id).toHaveCSS("text-overflow", "clip");
});

/**
* Base UI's Combobox, read back from a real browser rather than jsdom.
*
* `combobox.test.tsx` proves the same shape — filter-as-you-type, Escape closes
* — against the fruit demo; this is the styleguide's own seven class names,
* and it adds the one thing jsdom cannot: keyboard selection actually closing
* the popup and committing a value, not just removing the highlighted option
* from the tree.
*/
test("the Combobox filters as you type, ArrowDown+Enter selects and closes, and Escape backs out", async ({
page,
}) => {
const input = page.getByLabel("Class");
await input.click();
await input.fill("an");

// Of the seven demo classes, "lane" is the only one containing "an".
await expect(page.getByRole("option")).toHaveCount(1);
await expect(page.getByRole("option", { name: "lane" })).toBeVisible();

await page.keyboard.press("ArrowDown");
await page.keyboard.press("Enter");
await expect(page.getByRole("option")).toHaveCount(0);
await expect(input).toHaveValue("lane");

await input.click();
await input.fill("an");
await expect(page.getByRole("option")).toHaveCount(1);
await page.keyboard.press("Escape");
await expect(page.getByRole("option")).toHaveCount(0);
});

/**
* `Sheet` shares Radix's `Dialog` primitive under the hood (`shadcn/sheet.tsx`),
* so this is the same claim as the dialog test above, made about the other
* shape: it opens as `role="dialog"`, Escape closes it, and focus comes back
* to the outline trigger that opened it.
*/
test("a sheet opens as a dialog, closes on Escape and returns focus to its trigger", async ({ page }) => {
const trigger = page.getByRole("button", { name: "Open sheet" });
await trigger.click();

const sheet = page.getByRole("dialog");
await expect(sheet).toBeVisible();

await page.keyboard.press("Escape");
await expect(sheet).toHaveCount(0);
await expect(trigger).toBeFocused();
});

/**
* The extension contract's own claim (`shadcn_extensions.test.mjs`, "a status
* Badge paints a soft surface and readable ink, never a coloured stroke"),
* read back as a computed style rather than a class string — and in both
* themes, since `border-transparent` is a base class every variant inherits
* rather than something `dark:` has to repeat.
*/
test("a status Badge is a soft fill with no visible border, in both themes", async ({ page }) => {
for (const variant of ["success", "warning", "info", "quiet"]) {
await expect(page.getByTestId(`badge-${variant}-light`)).toHaveCSS("border-color", "rgba(0, 0, 0, 0)");
await expect(page.getByTestId(`badge-${variant}-dark`)).toHaveCSS("border-color", "rgba(0, 0, 0, 0)");
}
});
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/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ createRoot(container).render(
<ApiProvider baseUrl={API_BASE_URL}>
<TooltipProvider>
<AppRoutes />
<Toaster />
<Toaster position="bottom-right" />
</TooltipProvider>
</ApiProvider>
</BrowserRouter>
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/src/shell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ function RailLink({
// one — so the geometry comes from `buttonVariants` directly.
className={({ isActive }) =>
cn(
buttonVariants({ variant: "ghost", size: "md" }),
buttonVariants({ variant: "ghost", size: "default" }),
"w-full justify-start gap-2 px-2 text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground dark:hover:bg-sidebar-accent",
collapsed && "justify-center",
isActive &&
Expand Down Expand Up @@ -303,7 +303,7 @@ function RailButton({
return (
<Button
variant="ghost"
size={wide === true ? "md" : "icon"}
size={wide === true ? "default" : "icon"}
data-testid={testId}
aria-label={label}
title={label}
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/src/shell/NotFound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function NotFound(): JSX.Element {
title="No such page"
description={pathname}
action={
<Button asChild variant="primary">
<Button asChild variant="default">
<Link to="/">Back to Home</Link>
</Button>
}
Expand Down
Loading
Loading