feat(web): migrate dashboard checkboxes to @nous-research/ui + DS polish (#28814)

* feat(web): migrate dashboard checkboxes to @nous-research/ui + DS polish

Replaces the hand-rolled shadcn-style `Checkbox` in `web/src/components/ui/`
with the Nous DS `Checkbox` (Radix-backed) from `@nous-research/ui`, bumps
the DS to 0.14.2, and picks up two regressions surfaced by the bump.

Checkbox migration
- bump `@nous-research/ui` 0.14.0 → ^0.14.2 and remove
  `web/src/components/ui/checkbox.tsx`
- migrate `ProfilesPage` and `ModelPickerDialog` to the DS Checkbox API
  (`onCheckedChange`, paired `<Label htmlFor>`)
- expose `Checkbox` on the dashboard plugin SDK
  (`web/src/plugins/registry.ts`) so plugin bundles can use the same
  DS component
- migrate the kanban dashboard plugin's 7 native `<input type="checkbox">`
  call sites to the SDK `Checkbox`, with a native-input fallback shim so
  the bundle still renders against older hosts that predate the SDK export

Fix: missing font registrations after the 0.14.x split
- import `@nous-research/ui/styles/fonts.css` before `globals.css` in
  `web/src/index.css`. As of 0.14.x, `globals.css` only declares the
  `--font-*` variables (Collapse, Mondwest, Rules Compressed/Expanded);
  the `@font-face` registrations now live in a separate `fonts.css`, so
  without this import the DS components silently fall back to a system
  font stack and look unstyled.

Fix: right-align page header toolbars on sm+ viewports
- The mobile dashboard polish in #28127 flipped four pages'
  `setEnd(...)` wrappers from `justify-end` to `w-full ... justify-start`
  so toolbars stack below the title and align left on small screens.
  But the outer `end` slot in `PageHeaderProvider` already has
  `sm:justify-end`, and that has no effect when its only child is
  `w-full` — once a flex child fills the row, the parent's `justify-*`
  can't move it. The toolbar pinned to the *left* of the right-side
  `sm:max-w-md` (~448px) slot, making the buttons appear to float a
  couple-hundred pixels off the right edge on Analytics, Models, Logs,
  and Plugins.
- Re-add `sm:justify-end` on the inner wrapper of each affected page,
  preserving the mobile stacked layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(nix): update web npmDeps hash for package-lock bump

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(nix): refresh npm lockfile hashes

* chore(ci): re-trigger checks after nix lockfile hash fix

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Austin Pickett 2026-05-20 08:00:17 -04:00 committed by GitHub
parent 42c4288411
commit edb2d91057
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 282 additions and 207 deletions

View file

@ -1,6 +1,8 @@
import { Button } from "@nous-research/ui/ui/components/button";
import { Checkbox } from "@nous-research/ui/ui/components/checkbox";
import { ListItem } from "@nous-research/ui/ui/components/list-item";
import { Spinner } from "@nous-research/ui/ui/components/spinner";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import type { GatewayClient } from "@/lib/gatewayClient";
import { Check, Search, X } from "lucide-react";
@ -283,15 +285,22 @@ export function ModelPickerDialog(props: Props) {
Saves to config.yaml applies to new sessions.
</span>
) : (
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer select-none">
<input
type="checkbox"
<div className="flex items-center gap-2">
<Checkbox
checked={persistGlobal}
onChange={(e) => setPersistGlobal(e.target.checked)}
className="cursor-pointer"
id="model-picker-persist-global"
onCheckedChange={(checked) =>
setPersistGlobal(checked === true)
}
/>
Persist globally (otherwise this session only)
</label>
<Label
className="font-sans normal-case tracking-normal text-xs text-muted-foreground cursor-pointer"
htmlFor="model-picker-persist-global"
>
Persist globally (otherwise this session only)
</Label>
</div>
)}
<div className="flex items-center gap-2 ml-auto">

View file

@ -1,61 +0,0 @@
import { cn } from "@/lib/utils";
import { Check } from "lucide-react";
interface CheckboxProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
label?: React.ReactNode;
}
export function Checkbox({
className,
label,
id,
checked,
defaultChecked,
...props
}: CheckboxProps) {
// Support both controlled (checked prop) and uncontrolled (defaultChecked) usage.
// For visual rendering, prefer `checked` if provided; otherwise fall back to defaultChecked.
const isChecked = checked ?? defaultChecked ?? false;
return (
<label
htmlFor={id}
className={cn(
"group flex items-center gap-2.5 cursor-pointer select-none",
props.disabled && "cursor-not-allowed opacity-50",
)}
>
<span
className={cn(
"flex h-4 w-4 shrink-0 items-center justify-center transition-all",
"border bg-background/40",
// Focus-visible ring for keyboard accessibility
"group-has-[:focus-visible]:ring-2 group-has-[:focus-visible]:ring-ring group-has-[:focus-visible]:ring-offset-1",
isChecked
? "border-foreground bg-foreground/20"
: "border-border group-hover:border-foreground/40",
className,
)}
>
<Check
className={cn(
"h-3 w-3 transition-opacity",
isChecked
? "text-foreground opacity-100"
: "text-foreground opacity-0",
)}
/>
</span>
<input
type="checkbox"
id={id}
checked={checked}
defaultChecked={checked === undefined ? defaultChecked : undefined}
className="sr-only"
{...props}
/>
{label && <span className="text-sm">{label}</span>}
</label>
);
}

View file

@ -1,4 +1,11 @@
@import 'tailwindcss';
/* `fonts.css` must come BEFORE `globals.css`: as of @nous-research/ui 0.14.x,
`globals.css` only declares the `--font-*` CSS variables (Collapse, Rules
Compressed/Expanded, Mondwest). The `@font-face` registrations live in
`fonts.css`, so without this import the DS variables resolve to font
families the browser never loads and components fall back to a system
stack (Tabs, Segmented, Typography, Buttons, etc. all look unstyled). */
@import '@nous-research/ui/styles/fonts.css';
@import '@nous-research/ui/styles/globals.css';
/* Scan the published design-system bundle so its utility classes survive

View file

@ -439,7 +439,7 @@ export default function AnalyticsPage() {
);
setEnd(
showTokens === false ? null : (
<div className="flex w-full min-w-0 flex-wrap items-center justify-start gap-2 sm:gap-2">
<div className="flex w-full min-w-0 flex-wrap items-center justify-start gap-2 sm:justify-end sm:gap-2">
<div className="flex flex-wrap items-center gap-1.5">
{PERIODS.map((p) => (
<Button

View file

@ -93,7 +93,7 @@ export default function LogsPage() {
</span>,
);
setEnd(
<div className="flex w-full min-w-0 flex-wrap items-center justify-start gap-2 sm:gap-3">
<div className="flex w-full min-w-0 flex-wrap items-center justify-start gap-2 sm:justify-end sm:gap-3">
<div className="flex items-center gap-2">
<Switch
checked={autoRefresh}

View file

@ -827,7 +827,7 @@ export default function ModelsPage() {
</span>,
);
setEnd(
<div className="flex w-full min-w-0 flex-wrap items-center justify-start gap-2 sm:gap-2">
<div className="flex w-full min-w-0 flex-wrap items-center justify-start gap-2 sm:justify-end sm:gap-2">
<div className="flex flex-wrap items-center gap-1.5">
{PERIODS.map((p) => (
<Button

View file

@ -60,7 +60,7 @@ export default function PluginsPage() {
useEffect(() => {
setEnd(
<div className="flex w-full min-w-0 justify-start">
<div className="flex w-full min-w-0 justify-start sm:justify-end">
<Button
ghost
size="sm"

View file

@ -1,5 +1,19 @@
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { ChevronDown, Pencil, Plus, Terminal, Trash2, Users, X } from "lucide-react";
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import {
ChevronDown,
Pencil,
Plus,
Terminal,
Trash2,
Users,
X,
} from "lucide-react";
import spinners from "unicode-animations";
import { H2 } from "@/components/NouiTypography";
import { api } from "@/lib/api";
@ -14,7 +28,7 @@ import { Badge } from "@nous-research/ui/ui/components/badge";
import { Button } from "@nous-research/ui/ui/components/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Checkbox } from "@nous-research/ui/ui/components/checkbox";
import { useI18n } from "@/i18n";
import { usePageHeader } from "@/contexts/usePageHeader";
@ -131,7 +145,10 @@ export default function ProfilesPage() {
}
try {
await api.renameProfile(renamingFrom, target);
showToast(`${t.profiles.renamed}: ${renamingFrom}${target}`, "success");
showToast(
`${t.profiles.renamed}: ${renamingFrom}${target}`,
"success",
);
setRenamingFrom(null);
setRenameTo("");
load();
@ -214,10 +231,7 @@ export default function ProfilesPage() {
// Put "Create" button in page header
useLayoutEffect(() => {
setEnd(
<Button
size="sm"
onClick={() => setCreateModalOpen(true)}
>
<Button size="sm" onClick={() => setCreateModalOpen(true)}>
<Plus className="h-3 w-3" />
{t.common.create}
</Button>,
@ -266,7 +280,9 @@ export default function ProfilesPage() {
<div
ref={createModalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
onClick={(e) => e.target === e.currentTarget && setCreateModalOpen(false)}
onClick={(e) =>
e.target === e.currentTarget && setCreateModalOpen(false)
}
role="dialog"
aria-modal="true"
aria-labelledby="create-profile-title"
@ -313,12 +329,22 @@ export default function ProfilesPage() {
</p>
</div>
<Checkbox
id="clone-from-default"
checked={cloneFromDefault}
onChange={(e) => setCloneFromDefault(e.target.checked)}
label={t.profiles.cloneFromDefault}
/>
<div className="flex items-center gap-2.5">
<Checkbox
checked={cloneFromDefault}
id="clone-from-default"
onCheckedChange={(checked) =>
setCloneFromDefault(checked === true)
}
/>
<Label
className="font-sans normal-case tracking-normal text-sm cursor-pointer"
htmlFor="clone-from-default"
>
{t.profiles.cloneFromDefault}
</Label>
</div>
<div className="flex justify-end">
<Button size="sm" onClick={handleCreate} disabled={creating}>
@ -426,10 +452,7 @@ export default function ProfilesPage() {
<div className="flex items-center gap-1 shrink-0">
{isRenaming ? (
<>
<Button
size="sm"
onClick={handleRenameSubmit}
>
<Button size="sm" onClick={handleRenameSubmit}>
{t.common.save}
</Button>
<Button

View file

@ -21,6 +21,7 @@ import { api, fetchJSON } from "@/lib/api";
import { cn, timeAgo, isoTimeAgo } from "@/lib/utils";
import { Badge } from "@nous-research/ui/ui/components/badge";
import { Button } from "@nous-research/ui/ui/components/button";
import { Checkbox } from "@nous-research/ui/ui/components/checkbox";
import { Select, SelectOption } from "@nous-research/ui/ui/components/select";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
@ -121,7 +122,7 @@ export function exposePluginSDK() {
// Raw fetchJSON for plugin-specific endpoints
fetchJSON,
// UI components (shadcn/ui primitives)
// UI components — Nous DS where available, shadcn/ui primitives elsewhere.
components: {
Card,
CardHeader,
@ -129,6 +130,7 @@ export function exposePluginSDK() {
CardContent,
Badge,
Button,
Checkbox,
Input,
Label,
Select,