From c9e5a9bb087ca7704f65d4dfffaa71fff4eaee71 Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Fri, 22 May 2026 21:57:59 -0400 Subject: [PATCH] refactor(web): consume DS primitives, remove local component copies Replace locally-forked UI components and hooks with their newly promoted counterparts from @nous-research/ui: Deleted local components (now in DS): - components/ui/input.tsx, label.tsx, separator.tsx, card.tsx, confirm-dialog.tsx - components/Toast.tsx, BottomPickSheet.tsx, NouiTypography.tsx - hooks/useToast.ts, useModalBehavior.ts, useBelowBreakpoint.ts, useConfirmDelete.ts Import updates across 25 files to use DS deep imports: - @nous-research/ui/ui/components/{input,label,separator,card, confirm-dialog,toast,bottom-sheet} - @nous-research/ui/ui/components/typography (replaces NouiTypography) - @nous-research/ui/hooks/{use-toast,use-modal-behavior, use-below-breakpoint,use-confirm-delete} Requires design-language >= feat/promote-hermes-web-primitives. Co-authored-by: Cursor --- web/src/App.tsx | 2 +- web/src/components/AutoField.tsx | 4 +- web/src/components/BottomPickSheet.tsx | 224 --------------------- web/src/components/ChatSidebar.tsx | 2 +- web/src/components/DeleteConfirmDialog.tsx | 2 +- web/src/components/LanguageSwitcher.tsx | 10 +- web/src/components/ModelPickerDialog.tsx | 4 +- web/src/components/NouiTypography.tsx | 63 ------ web/src/components/OAuthLoginModal.tsx | 4 +- web/src/components/OAuthProvidersCard.tsx | 4 +- web/src/components/PlatformsCard.tsx | 2 +- web/src/components/SidebarFooter.tsx | 2 +- web/src/components/ThemeSwitcher.tsx | 10 +- web/src/components/Toast.tsx | 40 ---- web/src/components/ui/card.tsx | 52 ----- web/src/components/ui/confirm-dialog.tsx | 136 ------------- web/src/components/ui/input.tsx | 16 -- web/src/components/ui/label.tsx | 13 -- web/src/components/ui/separator.tsx | 19 -- web/src/contexts/SystemActions.tsx | 2 +- web/src/hooks/useBelowBreakpoint.ts | 19 -- web/src/hooks/useConfirmDelete.ts | 41 ---- web/src/hooks/useModalBehavior.ts | 44 ---- web/src/hooks/useToast.ts | 15 -- web/src/pages/AnalyticsPage.tsx | 2 +- web/src/pages/ChatPage.tsx | 2 +- web/src/pages/ConfigPage.tsx | 10 +- web/src/pages/CronPage.tsx | 16 +- web/src/pages/EnvPage.tsx | 12 +- web/src/pages/LogsPage.tsx | 4 +- web/src/pages/ModelsPage.tsx | 6 +- web/src/pages/PluginsPage.tsx | 12 +- web/src/pages/ProfilesPage.tsx | 16 +- web/src/pages/SessionsPage.tsx | 10 +- web/src/pages/SkillsPage.tsx | 8 +- web/src/plugins/registry.ts | 8 +- 36 files changed, 77 insertions(+), 759 deletions(-) delete mode 100644 web/src/components/BottomPickSheet.tsx delete mode 100644 web/src/components/NouiTypography.tsx delete mode 100644 web/src/components/Toast.tsx delete mode 100644 web/src/components/ui/card.tsx delete mode 100644 web/src/components/ui/confirm-dialog.tsx delete mode 100644 web/src/components/ui/input.tsx delete mode 100644 web/src/components/ui/label.tsx delete mode 100644 web/src/components/ui/separator.tsx delete mode 100644 web/src/hooks/useBelowBreakpoint.ts delete mode 100644 web/src/hooks/useConfirmDelete.ts delete mode 100644 web/src/hooks/useModalBehavior.ts delete mode 100644 web/src/hooks/useToast.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index 987252ce0bb..ab8ffcacaa6 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -47,7 +47,7 @@ import { Button } from "@nous-research/ui/ui/components/button"; import { ListItem } from "@nous-research/ui/ui/components/list-item"; import { SelectionSwitcher } from "@nous-research/ui/ui/components/selection-switcher"; import { Spinner } from "@nous-research/ui/ui/components/spinner"; -import { Typography } from "@/components/NouiTypography"; +import { Typography } from "@nous-research/ui/ui/components/typography"; import { cn } from "@/lib/utils"; import { Backdrop } from "@/components/Backdrop"; import { SidebarFooter } from "@/components/SidebarFooter"; diff --git a/web/src/components/AutoField.tsx b/web/src/components/AutoField.tsx index 0f96d420425..83509e1ed6c 100644 --- a/web/src/components/AutoField.tsx +++ b/web/src/components/AutoField.tsx @@ -1,7 +1,7 @@ import { Select, SelectOption } from "@nous-research/ui/ui/components/select"; import { Switch } from "@nous-research/ui/ui/components/switch"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +import { Input } from "@nous-research/ui/ui/components/input"; +import { Label } from "@nous-research/ui/ui/components/label"; function FieldHint({ schema, schemaKey }: { schema: Record; schemaKey: string }) { const keyPath = schemaKey.includes(".") ? schemaKey : ""; diff --git a/web/src/components/BottomPickSheet.tsx b/web/src/components/BottomPickSheet.tsx deleted file mode 100644 index 1490f4090c8..00000000000 --- a/web/src/components/BottomPickSheet.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { - type PointerEvent as ReactPointerEvent, - type ReactNode, - useEffect, - useRef, - useState, -} from "react"; -import { createPortal } from "react-dom"; -import { Typography } from "@/components/NouiTypography"; -import { cn } from "@/lib/utils"; - -const CLOSE_DRAG_MIN_PX = 72; -const CLOSE_DRAG_RATIO = 0.18; -const SHEET_TRANSITION_MS = 280; - -/** - * Mobile-first picker shell: fixed backdrop + bottom sheet, portaled to `body` - * so nested overflow/transform in the sidebar cannot clip menus (theme / - * language switchers). Open/close uses slide + fade; teardown is delayed until - * the exit animation finishes so animations can complete. - * - * Drag the header/handle downward to dismiss (skipped when reduced motion is on). - */ -export function BottomPickSheet({ - backdropDismissLabel = "Dismiss", - children, - onClose, - open, - title, -}: BottomPickSheetProps) { - const [renderPortal, setRenderPortal] = useState(open); - const [entered, setEntered] = useState(false); - const [dragOffsetPx, setDragOffsetPx] = useState(0); - const [dragActive, setDragActive] = useState(false); - - const closeTimerRef = useRef | null>(null); - const sheetRef = useRef(null); - const dragTrackingRef = useRef(false); - const dragStartYRef = useRef(0); - const dragOffsetRef = useRef(0); - - const reducedMotion = - typeof window !== "undefined" && - window.matchMedia("(prefers-reduced-motion: reduce)").matches; - - const syncDragPx = (next: number) => { - dragOffsetRef.current = next; - setDragOffsetPx(next); - }; - - useEffect(() => { - if (closeTimerRef.current) { - clearTimeout(closeTimerRef.current); - closeTimerRef.current = null; - } - - const ms = reducedMotion ? 0 : SHEET_TRANSITION_MS; - - let openRafId = 0; - let exitRafId = 0; - - if (open) { - openRafId = requestAnimationFrame(() => { - dragTrackingRef.current = false; - dragOffsetRef.current = 0; - setDragActive(false); - setDragOffsetPx(0); - setRenderPortal(true); - requestAnimationFrame(() => { - requestAnimationFrame(() => setEntered(true)); - }); - }); - } else { - exitRafId = requestAnimationFrame(() => { - dragTrackingRef.current = false; - setDragActive(false); - setEntered(false); - closeTimerRef.current = window.setTimeout(() => { - dragOffsetRef.current = 0; - setDragOffsetPx(0); - setRenderPortal(false); - closeTimerRef.current = null; - }, ms); - }); - } - - return () => { - cancelAnimationFrame(openRafId); - cancelAnimationFrame(exitRafId); - if (closeTimerRef.current) { - clearTimeout(closeTimerRef.current); - closeTimerRef.current = null; - } - }; - }, [open, reducedMotion]); - - useEffect(() => { - if (!renderPortal) return; - const prev = document.body.style.overflow; - document.body.style.overflow = "hidden"; - return () => { - document.body.style.overflow = prev; - }; - }, [renderPortal]); - - if (!renderPortal || typeof document === "undefined") return null; - - const durationClass = reducedMotion ? "duration-0" : "duration-[280ms]"; - - const draggingVisual = dragActive || dragOffsetPx > 0; - - const onDragPointerDown = (e: ReactPointerEvent) => { - if (reducedMotion || !entered) return; - if (e.pointerType === "mouse" && e.button !== 0) return; - - dragTrackingRef.current = true; - setDragActive(true); - dragStartYRef.current = e.clientY; - syncDragPx(0); - e.currentTarget.setPointerCapture(e.pointerId); - }; - - const onDragPointerMove = (e: ReactPointerEvent) => { - if (!dragTrackingRef.current) return; - const dy = e.clientY - dragStartYRef.current; - const next = Math.max(0, dy); - const sheetH = sheetRef.current?.offsetHeight ?? 560; - syncDragPx(Math.min(next, sheetH)); - }; - - const endDrag = (e: ReactPointerEvent) => { - if (!dragTrackingRef.current) return; - dragTrackingRef.current = false; - setDragActive(false); - try { - e.currentTarget.releasePointerCapture(e.pointerId); - } catch { - /* already released */ - } - - const sheetH = sheetRef.current?.offsetHeight ?? 560; - const threshold = Math.max(CLOSE_DRAG_MIN_PX, sheetH * CLOSE_DRAG_RATIO); - const d = dragOffsetRef.current; - - if (d >= threshold) { - onClose(); - return; - } - syncDragPx(0); - }; - - return createPortal( -
- {useMobileSheet && ( - setOpen(false)} open={open} @@ -96,7 +96,7 @@ export function LanguageSwitcher({ dropUp = false }: LanguageSwitcherProps) { setOpen={setOpen} />
- + )} {open && !useMobileSheet && ( diff --git a/web/src/components/ModelPickerDialog.tsx b/web/src/components/ModelPickerDialog.tsx index d01a46b01a0..a17a6edfdff 100644 --- a/web/src/components/ModelPickerDialog.tsx +++ b/web/src/components/ModelPickerDialog.tsx @@ -2,8 +2,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 { Input } from "@nous-research/ui/ui/components/input"; +import { Label } from "@nous-research/ui/ui/components/label"; import type { GatewayClient } from "@/lib/gatewayClient"; import { Check, Search, X } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; diff --git a/web/src/components/NouiTypography.tsx b/web/src/components/NouiTypography.tsx deleted file mode 100644 index eb26d75cc1c..00000000000 --- a/web/src/components/NouiTypography.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { forwardRef, type ElementType, type HTMLAttributes, type ReactNode } from "react"; -import { cn } from "@/lib/utils"; - -type TypographyProps = HTMLAttributes & { - as?: ElementType; - children?: ReactNode; - compressed?: boolean; - courier?: boolean; - expanded?: boolean; - mondwest?: boolean; - mono?: boolean; - sans?: boolean; - variant?: "sm" | "md" | "lg" | "xl"; -}; - -const variantClasses: Record, string> = { - sm: "leading-[1.4] text-[.9375rem] tracking-[0.1875rem]", - md: "text-[2.625rem] leading-[1] tracking-[0.0525rem]", - lg: "text-[2.625rem] leading-[1] tracking-[0.0525rem]", - xl: "text-[4.5rem] leading-[1] tracking-[0.135rem]", -}; - -export const Typography = forwardRef(function Typography( - { - as: Component = "span", - className, - compressed, - courier, - expanded, - mondwest, - mono, - sans, - variant, - ...props - }, - ref, -) { - const hasFontVariant = compressed || courier || expanded || mondwest || mono || sans; - - return ( - - ); -}); - -export const H2 = forwardRef>(function H2( - { className, variant = "lg", ...props }, - ref, -) { - return ; -}); diff --git a/web/src/components/OAuthLoginModal.tsx b/web/src/components/OAuthLoginModal.tsx index f4eb610c16c..2c99ae31ce4 100644 --- a/web/src/components/OAuthLoginModal.tsx +++ b/web/src/components/OAuthLoginModal.tsx @@ -3,9 +3,9 @@ import { ExternalLink, X, Check } from "lucide-react"; import { Button } from "@nous-research/ui/ui/components/button"; import { CopyButton } from "@nous-research/ui/ui/components/command-block"; import { Spinner } from "@nous-research/ui/ui/components/spinner"; -import { H2 } from "@/components/NouiTypography"; +import { H2 } from "@nous-research/ui/ui/components/typography/h2"; import { api, type OAuthProvider, type OAuthStartResponse } from "@/lib/api"; -import { Input } from "@/components/ui/input"; +import { Input } from "@nous-research/ui/ui/components/input"; import { useI18n } from "@/i18n"; interface Props { diff --git a/web/src/components/OAuthProvidersCard.tsx b/web/src/components/OAuthProvidersCard.tsx index 987f4c0eeef..45901f58c9a 100644 --- a/web/src/components/OAuthProvidersCard.tsx +++ b/web/src/components/OAuthProvidersCard.tsx @@ -18,9 +18,9 @@ import { CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; +} from "@nous-research/ui/ui/components/card"; import { Badge } from "@nous-research/ui/ui/components/badge"; -import { ConfirmDialog } from "@/components/ui/confirm-dialog"; +import { ConfirmDialog } from "@nous-research/ui/ui/components/confirm-dialog"; import { OAuthLoginModal } from "@/components/OAuthLoginModal"; import { useI18n } from "@/i18n"; diff --git a/web/src/components/PlatformsCard.tsx b/web/src/components/PlatformsCard.tsx index 24cc668c65b..c4f46c756d9 100644 --- a/web/src/components/PlatformsCard.tsx +++ b/web/src/components/PlatformsCard.tsx @@ -2,7 +2,7 @@ import { AlertTriangle, Radio, Wifi, WifiOff } from "lucide-react"; import type { PlatformStatus } from "@/lib/api"; import { isoTimeAgo } from "@/lib/utils"; import { Badge } from "@nous-research/ui/ui/components/badge"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@nous-research/ui/ui/components/card"; import { useI18n } from "@/i18n"; export function PlatformsCard({ platforms }: PlatformsCardProps) { diff --git a/web/src/components/SidebarFooter.tsx b/web/src/components/SidebarFooter.tsx index c1810f10e0e..889cec9a530 100644 --- a/web/src/components/SidebarFooter.tsx +++ b/web/src/components/SidebarFooter.tsx @@ -1,4 +1,4 @@ -import { Typography } from "@/components/NouiTypography"; +import { Typography } from "@nous-research/ui/ui/components/typography"; import { useSidebarStatus } from "@/hooks/useSidebarStatus"; import { cn } from "@/lib/utils"; import { useI18n } from "@/i18n"; diff --git a/web/src/components/ThemeSwitcher.tsx b/web/src/components/ThemeSwitcher.tsx index 17e0ae3d6da..23d2347ef3d 100644 --- a/web/src/components/ThemeSwitcher.tsx +++ b/web/src/components/ThemeSwitcher.tsx @@ -2,9 +2,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Palette, Check } from "lucide-react"; import { Button } from "@nous-research/ui/ui/components/button"; import { ListItem } from "@nous-research/ui/ui/components/list-item"; -import { BottomPickSheet } from "@/components/BottomPickSheet"; -import { Typography } from "@/components/NouiTypography"; -import { useBelowBreakpoint } from "@/hooks/useBelowBreakpoint"; +import { BottomSheet } from "@nous-research/ui/ui/components/bottom-sheet"; +import { Typography } from "@nous-research/ui/ui/components/typography"; +import { useBelowBreakpoint } from "@nous-research/ui/hooks/use-below-breakpoint"; import { BUILTIN_THEMES, useTheme } from "@/themes"; import type { DashboardTheme, ThemeListEntry } from "@/themes"; import { useI18n } from "@/i18n"; @@ -84,7 +84,7 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) { {useMobileSheet && ( - - + )} {open && !useMobileSheet && ( diff --git a/web/src/components/Toast.tsx b/web/src/components/Toast.tsx deleted file mode 100644 index e6bb349e896..00000000000 --- a/web/src/components/Toast.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect, useState } from "react"; -import { createPortal } from "react-dom"; - -export function Toast({ toast }: { toast: { message: string; type: "success" | "error" } | null }) { - const [visible, setVisible] = useState(false); - const [current, setCurrent] = useState(toast); - - useEffect(() => { - if (toast) { - setCurrent(toast); - setVisible(true); - } else { - setVisible(false); - const timer = setTimeout(() => setCurrent(null), 200); - return () => clearTimeout(timer); - } - }, [toast]); - - if (!current) return null; - - // Portal to document.body so the toast escapes any ancestor stacking context - // (e.g.
has `relative z-2`, which would trap z-50 below the header's z-40). - return createPortal( -
- {current.message} -
, - document.body, - ); -} diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx deleted file mode 100644 index e4046adab22..00000000000 --- a/web/src/components/ui/card.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { cn } from "@/lib/utils"; - -/** - * Themed card primitive. Themes can restyle every card without touching - * call sites by setting CSS vars under the `card` component-style bucket: - * - * componentStyles: - * card: - * clipPath: "polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px)" - * border: "1px solid var(--color-ring)" - * background: "linear-gradient(180deg, var(--color-card) 0%, transparent 100%)" - * boxShadow: "0 0 0 1px var(--color-ring) inset, 0 0 24px -8px var(--warm-glow)" - * - * All properties are optional — vars that aren't set compute to their - * CSS initial value, so the default shadcn-y card keeps looking normal - * for themes that don't override anything. - */ -const CARD_STYLE: React.CSSProperties = { - clipPath: "var(--component-card-clip-path)", - borderImage: "var(--component-card-border-image)", - background: "var(--component-card-background)", - boxShadow: "var(--component-card-box-shadow)", -}; - -export function Card({ className, style, ...props }: React.HTMLAttributes) { - return ( -
- ); -} - -export function CardHeader({ className, ...props }: React.HTMLAttributes) { - return
; -} - -export function CardTitle({ className, ...props }: React.HTMLAttributes) { - return

; -} - -export function CardDescription({ className, ...props }: React.HTMLAttributes) { - return

; -} - -export function CardContent({ className, ...props }: React.HTMLAttributes) { - return

; -} diff --git a/web/src/components/ui/confirm-dialog.tsx b/web/src/components/ui/confirm-dialog.tsx deleted file mode 100644 index e8529e2b58b..00000000000 --- a/web/src/components/ui/confirm-dialog.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useEffect, useRef } from "react"; -import { createPortal } from "react-dom"; -import { AlertTriangle } from "lucide-react"; -import { Button } from "@nous-research/ui/ui/components/button"; -import { cn } from "@/lib/utils"; - -export function ConfirmDialog({ - cancelLabel = "Cancel", - confirmLabel = "Confirm", - description, - destructive = false, - loading = false, - onCancel, - onConfirm, - open, - title, -}: ConfirmDialogProps) { - const dialogRef = useRef(null); - - // Focus the confirm button when opened; trap ESC to cancel. - useEffect(() => { - if (!open) return; - - const prevActive = document.activeElement as HTMLElement | null; - dialogRef.current - ?.querySelector("[data-confirm]") - ?.focus(); - - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") { - e.preventDefault(); - onCancel(); - } - }; - - document.addEventListener("keydown", onKey); - const prevOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - - return () => { - document.removeEventListener("keydown", onKey); - document.body.style.overflow = prevOverflow; - prevActive?.focus?.(); - }; - }, [open, onCancel]); - - if (!open) return null; - - return createPortal( -
{ - if (e.target === e.currentTarget) onCancel(); - }} - className={cn( - "fixed inset-0 z-50 flex items-center justify-center", - "bg-black/60 backdrop-blur-sm", - "animate-[fade-in_150ms_ease-out]", - )} - > -
-
- {destructive && ( -
- -
- )} - -
-

- {title} -

- - {description && ( -

- {description} -

- )} -
-
- -
- - -
-
-
, - document.body, - ); -} - -interface ConfirmDialogProps { - cancelLabel?: string; - confirmLabel?: string; - description?: string; - destructive?: boolean; - loading?: boolean; - onCancel: () => void; - onConfirm: () => void; - open: boolean; - title: string; -} diff --git a/web/src/components/ui/input.tsx b/web/src/components/ui/input.tsx deleted file mode 100644 index 1e1199e6478..00000000000 --- a/web/src/components/ui/input.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { cn } from "@/lib/utils"; - -export function Input({ className, ...props }: React.InputHTMLAttributes) { - return ( - - ); -} diff --git a/web/src/components/ui/label.tsx b/web/src/components/ui/label.tsx deleted file mode 100644 index a5807e4bd4f..00000000000 --- a/web/src/components/ui/label.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { cn } from "@/lib/utils"; - -export function Label({ className, ...props }: React.LabelHTMLAttributes) { - return ( -