From 60eb6176fdec081fd10484cbd243622680406737 Mon Sep 17 00:00:00 2001 From: Blake Bauman Date: Mon, 7 Sep 2026 08:25:39 -0700 Subject: [PATCH 01/13] Migrate UI from Adobe Spectrum to shadcn/ui (Maia) + Tailwind v4. Replaces the Spectrum component kit and Spectrum Charts with shadcn/ui primitives under app/src/components/ui, generated with the Maia preset (base-maia, bbVJxYW). Spectrum providers, style() macros, and the per-page style conversion script are gone. - SpectrumProvider -> AppProvider (theme + TooltipProvider) - components/spectrum/{Field,RowMenu,StatusBadge,Tip} -> components/* - charts/SpectrumChartsPanel -> charts/ChartsPanel - Adds MoreActionsMenu and SegmentedControl app helpers - Icons standardize on lucide-react via Icon / lucideProps - Refreshes brand marks, favicons, and generated app icons - Swaps .cursor/rules/spectrum-ui.mdc for shadcn-ui.mdc Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015dD6k7ZHtNg7d44xnKLMjH --- .cursor/rules/shadcn-ui.mdc | 33 + .cursor/rules/spectrum-ui.mdc | 33 - README.md | 4 +- app/components.json | 25 + app/package.json | 18 +- app/public/favicon.png | Bin 1287 -> 1460 bytes app/public/favicon.svg | 6 +- app/scripts/convert-pages-styles.mjs | 341 -------- app/src/App.tsx | 8 +- app/src/assets/brand/favicon.svg | 6 +- app/src/assets/brand/logo-dark.svg | 8 +- app/src/assets/brand/logo.svg | 8 +- app/src/assets/brand/mark.svg | 10 +- app/src/components/AnimatedOutlet.tsx | 11 +- app/src/components/AppFrame/AppFrame.tsx | 52 +- app/src/components/AppProvider.tsx | 9 + app/src/components/BootSplash.tsx | 54 +- app/src/components/CodeBlock.tsx | 44 +- app/src/components/CommandPalette.tsx | 221 ++---- app/src/components/ConfirmDialog.tsx | 59 +- app/src/components/ConsolePanel.tsx | 99 +-- app/src/components/ContainerMonitor.tsx | 120 +-- app/src/components/CopyButton.tsx | 30 +- app/src/components/DetailPane.tsx | 127 +-- app/src/components/DiskUsagePanel.tsx | 220 ++---- app/src/components/ExecTerminal.tsx | 78 +- app/src/components/{spectrum => }/Field.tsx | 24 +- app/src/components/GlassSheet.tsx | 70 +- app/src/components/GlobalSheets.tsx | 4 +- app/src/components/GpuPanel.tsx | 168 +--- app/src/components/HelpHint.tsx | 36 +- app/src/components/Icon.tsx | 2 +- app/src/components/InspectFields.tsx | 88 +-- app/src/components/ListPane.tsx | 274 +------ app/src/components/Logo.tsx | 52 +- app/src/components/MoreActionsMenu.tsx | 32 + app/src/components/PageShell.tsx | 171 +--- app/src/components/RowMenu.tsx | 117 +++ app/src/components/RunContainerSheet.tsx | 294 ++++--- app/src/components/SegmentedControl.tsx | 49 ++ app/src/components/SettingRow.tsx | 73 +- app/src/components/Sidebar.tsx | 252 ++---- app/src/components/SpectrumProvider.tsx | 28 - app/src/components/StatusBadge.tsx | 37 + app/src/components/StatusDock.tsx | 215 ++--- app/src/components/StatusHalo.tsx | 145 +--- app/src/components/TerminalChrome.tsx | 47 +- app/src/components/Tip.tsx | 33 + app/src/components/TitleBarDragRegion.tsx | 9 +- app/src/components/Toaster.tsx | 9 +- app/src/components/charts/ChartsPanel.tsx | 191 +++++ app/src/components/charts/LiveSparkline.tsx | 38 +- app/src/components/charts/MetricChart.tsx | 91 +-- .../components/charts/SpectrumChartsPanel.tsx | 146 ---- app/src/components/spectrum/RowMenu.tsx | 163 ---- app/src/components/spectrum/StatusBadge.tsx | 42 - app/src/components/spectrum/Tip.tsx | 25 - app/src/components/ui/alert-dialog.tsx | 185 +++++ app/src/components/ui/badge.tsx | 51 ++ app/src/components/ui/button.tsx | 55 ++ app/src/components/ui/card.tsx | 99 +++ app/src/components/ui/checkbox.tsx | 28 + app/src/components/ui/command.tsx | 196 +++++ app/src/components/ui/context-menu.tsx | 268 +++++++ app/src/components/ui/dialog.tsx | 160 ++++ app/src/components/ui/dropdown-menu.tsx | 265 +++++++ app/src/components/ui/input-group.tsx | 155 ++++ app/src/components/ui/input.tsx | 19 + app/src/components/ui/label.tsx | 17 + app/src/components/ui/popover.tsx | 87 ++ app/src/components/ui/progress.tsx | 80 ++ app/src/components/ui/radio-group.tsx | 37 + app/src/components/ui/scroll-area.tsx | 52 ++ app/src/components/ui/select.tsx | 203 +++++ app/src/components/ui/separator.tsx | 24 + app/src/components/ui/sheet.tsx | 138 ++++ app/src/components/ui/skeleton.tsx | 13 + app/src/components/ui/slider.tsx | 51 ++ app/src/components/ui/sonner.tsx | 39 + app/src/components/ui/switch.tsx | 29 + app/src/components/ui/table.tsx | 115 +++ app/src/components/ui/tabs.tsx | 79 ++ app/src/components/ui/textarea.tsx | 17 + app/src/components/ui/tooltip.tsx | 65 ++ app/src/hooks/useThemeSync.ts | 42 +- app/src/index.css | 199 +++-- app/src/lib/toast.ts | 17 +- app/src/lib/utils.ts | 2 + app/src/lib/version.ts | 2 +- app/src/router.tsx | 36 +- app/src/routes/BuildsPage.tsx | 205 ++--- app/src/routes/ContainersPage.tsx | 379 ++++----- app/src/routes/DashboardPage.tsx | 86 +- app/src/routes/ImagesPage.tsx | 217 ++--- app/src/routes/NetworksPage.tsx | 115 ++- app/src/routes/ProjectsPage.tsx | 307 +++----- app/src/routes/SettingsPage.tsx | 370 ++++----- app/src/routes/VolumesPage.tsx | 205 ++--- app/src/routes/k8s/DeploymentsPage.tsx | 98 +-- app/src/routes/k8s/HelmPage.tsx | 58 +- app/src/routes/k8s/K8sChrome.tsx | 80 +- app/src/routes/k8s/K8sOverviewPage.tsx | 28 +- app/src/routes/k8s/K8sResourcesPage.tsx | 87 +- app/src/routes/k8s/PodsPage.tsx | 58 +- .../routes/microvms/MicroVMsOverviewPage.tsx | 29 +- app/src/routes/microvms/MicroVMsPage.tsx | 219 +++--- app/vite.config.ts | 21 +- brand/README.md | 11 +- brand/favicon.svg | 6 +- brand/logo-dark.svg | 8 +- brand/logo.svg | 8 +- brand/mark.svg | 10 +- bun.lock | 740 +++++++++++------- docs/development.md | 10 +- scripts/tauri-frontend-dev.sh | 4 +- tauri/package.json | 4 +- tauri/src-tauri/icons/128x128.png | Bin 7411 -> 11260 bytes tauri/src-tauri/icons/128x128@2x.png | Bin 18357 -> 30696 bytes tauri/src-tauri/icons/32x32.png | Bin 1333 -> 1526 bytes tauri/src-tauri/icons/icon-1024.png | Bin 119907 -> 198855 bytes tauri/src-tauri/icons/icon.icns | Bin 415269 -> 759358 bytes tauri/src-tauri/icons/icon.ico | Bin 119649 -> 131413 bytes tauri/src-tauri/icons/icon.png | Bin 46690 -> 79200 bytes tauri/src-tauri/icons/mark.svg | 10 +- tauri/vite.config.ts | 21 +- 125 files changed, 5627 insertions(+), 5141 deletions(-) create mode 100644 .cursor/rules/shadcn-ui.mdc delete mode 100644 .cursor/rules/spectrum-ui.mdc create mode 100644 app/components.json delete mode 100644 app/scripts/convert-pages-styles.mjs create mode 100644 app/src/components/AppProvider.tsx rename app/src/components/{spectrum => }/Field.tsx (61%) create mode 100644 app/src/components/MoreActionsMenu.tsx create mode 100644 app/src/components/RowMenu.tsx create mode 100644 app/src/components/SegmentedControl.tsx delete mode 100644 app/src/components/SpectrumProvider.tsx create mode 100644 app/src/components/StatusBadge.tsx create mode 100644 app/src/components/Tip.tsx create mode 100644 app/src/components/charts/ChartsPanel.tsx delete mode 100644 app/src/components/charts/SpectrumChartsPanel.tsx delete mode 100644 app/src/components/spectrum/RowMenu.tsx delete mode 100644 app/src/components/spectrum/StatusBadge.tsx delete mode 100644 app/src/components/spectrum/Tip.tsx create mode 100644 app/src/components/ui/alert-dialog.tsx create mode 100644 app/src/components/ui/badge.tsx create mode 100644 app/src/components/ui/button.tsx create mode 100644 app/src/components/ui/card.tsx create mode 100644 app/src/components/ui/checkbox.tsx create mode 100644 app/src/components/ui/command.tsx create mode 100644 app/src/components/ui/context-menu.tsx create mode 100644 app/src/components/ui/dialog.tsx create mode 100644 app/src/components/ui/dropdown-menu.tsx create mode 100644 app/src/components/ui/input-group.tsx create mode 100644 app/src/components/ui/input.tsx create mode 100644 app/src/components/ui/label.tsx create mode 100644 app/src/components/ui/popover.tsx create mode 100644 app/src/components/ui/progress.tsx create mode 100644 app/src/components/ui/radio-group.tsx create mode 100644 app/src/components/ui/scroll-area.tsx create mode 100644 app/src/components/ui/select.tsx create mode 100644 app/src/components/ui/separator.tsx create mode 100644 app/src/components/ui/sheet.tsx create mode 100644 app/src/components/ui/skeleton.tsx create mode 100644 app/src/components/ui/slider.tsx create mode 100644 app/src/components/ui/sonner.tsx create mode 100644 app/src/components/ui/switch.tsx create mode 100644 app/src/components/ui/table.tsx create mode 100644 app/src/components/ui/tabs.tsx create mode 100644 app/src/components/ui/textarea.tsx create mode 100644 app/src/components/ui/tooltip.tsx diff --git a/.cursor/rules/shadcn-ui.mdc b/.cursor/rules/shadcn-ui.mdc new file mode 100644 index 0000000..30f541c --- /dev/null +++ b/.cursor/rules/shadcn-ui.mdc @@ -0,0 +1,33 @@ +--- +description: Deckhand UI must use shadcn/ui Maia + Tailwind only +globs: app/src/**/*.{tsx,ts,css} +alwaysApply: false +--- + +# shadcn Maia UI only + +Deckhand’s React UI uses **shadcn/ui** with the **Maia** preset (`base-maia`, preset `bbVJxYW`) and **Tailwind CSS v4**. + +Icons use **[Lucide React](https://lucide.dev/guide/react/)** (`lucide-react`) via `lucideProps()` / `Icon` in `app/src/components/Icon.tsx`. + +## Required + +- Primitives from `@/components/ui/*` (generated by shadcn CLI) +- Layout/tokens via Tailwind utility classes and CSS variables from `src/index.css` +- Root: `AppProvider` (theme + `TooltipProvider`), Sonner toasts via `@/lib/toast` +- Button events: standard `onClick` on shadcn/Radix components +- `cn()` from `@/lib/utils` for conditional classes + +## Do not add + +- Adobe React Spectrum (`@react-spectrum/s2`, `@adobe/react-spectrum`), Spectrum Charts, or `style()` macros +- Parallel component kits (MUI, Chakra, Ant Design) alongside shadcn +- Hand-rolled Radix wrappers that duplicate `@/components/ui` + +## App helpers + +Prefer shadcn primitives and small app helpers (`Tip`, `StatusBadge`, `RowMenu`, `Field`, `MoreActionsMenu`, `SegmentedControl`) under `app/src/components/` — do not reintroduce Spectrum providers or macros. + +## Custom widgets + +When shadcn has no component (e.g. xterm, custom charts), style chrome with Tailwind tokens. Do not reintroduce a second design system. diff --git a/.cursor/rules/spectrum-ui.mdc b/.cursor/rules/spectrum-ui.mdc deleted file mode 100644 index 50ec875..0000000 --- a/.cursor/rules/spectrum-ui.mdc +++ /dev/null @@ -1,33 +0,0 @@ ---- -description: Deckhand UI must use Adobe Spectrum 2 only -globs: app/src/**/*.{tsx,ts,css} -alwaysApply: false ---- - -# Spectrum 2 UI only - -Deckhand’s React UI uses **Adobe React Spectrum 2** (`@react-spectrum/s2`) for components and layout. -Icons use **[Lucide React](https://lucide.dev/guide/react/)** (`lucide-react`) — not official Spectrum, but the chosen set for this app. - -## Required - -- Components from `@react-spectrum/s2` -- Icons from `lucide-react` via `lucideProps()` / `Icon` in `app/src/components/Icon.tsx` (size tokens S/M/L, stroke 1.75) -- Layout/tokens via `import { style } from '@react-spectrum/s2/style' with { type: 'macro' }` -- Root: `Provider`, `@react-spectrum/s2/page.css`, `ToastContainer` / `ToastQueue` -- Button events: `onPress` (not `onClick`) on Spectrum components -- `style()` arguments must be **static** object literals (build-time macro) - -## Do not add - -- Radix UI, shadcn/ui, Tailwind utility styling, MUI, Chakra, Ant Design, or parallel component kits -- `@react-spectrum/s2/icons/*` (use Lucide instead) -- Sonner or non-Spectrum toast libraries - -## App helpers - -Small Spectrum helpers live under `app/src/components/spectrum/` (`Tip`, `StatusBadge`, `RowMenu`, `Field`/`Area`). Prefer these or raw `@react-spectrum/s2` — do not recreate `components/ui` or shadcn-style wrappers. - -## Custom widgets - -When S2 has no component (e.g. xterm, custom charts), style chrome with Spectrum tokens / `style` macros (and React Aria if needed). Do not reintroduce a second design system. diff --git a/README.md b/README.md index 6778b28..70cc622 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Local-first Docker and Kubernetes desktop app for **macOS** and **Linux**. -Tauri 2 shell · Adobe Spectrum UI · Go sidecar for Docker, Compose, Kubernetes, Helm, and Linux Firecracker microVMs. +Tauri 2 shell · shadcn/ui (Maia) · Go sidecar for Docker, Compose, Kubernetes, Helm, and Linux Firecracker microVMs. ## Features @@ -47,7 +47,7 @@ xattr -cr /Applications/Deckhand.app ## Architecture (sketch) ``` -UI (Spectrum / React) → HTTP localhost → Go sidecar → Docker / kubeconfig / Helm / Firecracker +UI (shadcn / React) → HTTP localhost → Go sidecar → Docker / kubeconfig / Helm / Firecracker ↑ Tauri 2 (spawn + tray) ``` diff --git a/app/components.json b/app/components.json new file mode 100644 index 0000000..b34e571 --- /dev/null +++ b/app/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-maia", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/app/package.json b/app/package.json index d076008..46efe0c 100644 --- a/app/package.json +++ b/app/package.json @@ -8,9 +8,9 @@ "preview": "vite preview" }, "dependencies": { - "@adobe/react-spectrum": "^3.47.2", - "@react-spectrum/s2": "^1.5.1", - "@spectrum-charts/react-spectrum-charts-s2": "^0.3.2", + "@base-ui/react": "^1.8.0", + "@fontsource-variable/inter": "^5.3.0", + "@tailwindcss/vite": "^4.3.3", "@tanstack/react-query": "^5.66.9", "@tanstack/react-router": "^1.105.0", "@tauri-apps/api": "^2.3.0", @@ -18,23 +18,29 @@ "@tauri-apps/plugin-shell": "^2", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", + "class-variance-authority": "^0.7.1", + "cmdk": "^1.1.1", + "cn": "^0.2.6", "framer-motion": "^12.4.7", "lucide-react": "^1.25.0", + "next-themes": "^0.4.6", "react": "^19", - "react-aria-components": "^1.19.0", "react-dom": "^19", + "shadcn": "^4.21.0", + "sonner": "^2.0.8", + "tailwindcss": "^4.3.3", + "tw-animate-css": "^1.4.0", "vega": "^6.2.0", "vega-lite": "^6.4.3", "zustand": "^5.0.3" }, "devDependencies": { - "@react-aria/optimize-locales-plugin": "^2.0.0", + "@types/node": "^26.4.1", "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^4.3.4", "lightningcss": "^1.32.0", "typescript": "~5.7.3", - "unplugin-parcel-macros": "^0.2.0", "vite": "^6.1.0" } } diff --git a/app/public/favicon.png b/app/public/favicon.png index 3fa85701664de0b68d0ad19487e30c358f159c81..6f0e0cc8bc6896b9a4dfbe84a1fac8da326a3bec 100644 GIT binary patch delta 1425 zcmV;C1#bF>3bYH5HGc(ZNklO6tN(R zXceU(czN^ESM^DX2>u0%_@E#tRx5%J-f!R)FRfO^+K9zgbJ3z{Yhz856LUGU_gX&8 zZO@!@Y~8TV%^&ju0ZcRFG*MdiP|K0YbWnM}SW~fj?@hbf_nQ50>H08ozhs_kZiSXg zt6^^%l?sT4HGk0sW8>i{Fm|_!=`Ja)=Gq6Au7x3V_kyZnVOFuONyM6fRsj{oIbG#k z+k+9p4mFh*ntAU5*l>|y(?~!N6#I`I5hlk3ktXkFAOzGYB8oUggc~C#Y%GPU;(h4~ zY#B8SZVvE=F)*|>APnKBucfI2s%7Kq6el_qsG9RcKz|*GB%+>(ckSt;!MHy*YzgS; zGo^P#NW8U*wTg`uXBBI~#o8T-Xeq?4NTUjC*0wgDjlOk;zyH)cHHo(rVRwp(A}(2r zM9N|I(@^C^d*@m0)6tbiFEyU#IthQ40$YHJ)B~Uu0C}0EK;}FF1uGRk8tu=VP&bc@#RIBo)m1HW7%$kGfJdQ~)H(J38U=9Z}xpve7$W_~JF&qBW=aPL*!o z*}>Wi$_j=@Bc6Yx#^Rh5QE*CxcuZFaBhz3!i-c7AeKy2_jlDY8#%c*82m)`PP12D! zqJK>BVy*o0i78>>3``tKkY_zz#E(G2^i1(5FRgI#_BML^4MQVQ?!f$v!_~`t2&znv z>tzr#)5_Rqj@p^DD$=r2DfXy%Y`4mpQ^LMimpMM^@;$Ssgq7uj{Weab5oBG_Ep`uL;>QGt3%cenG$mKIUr@l8@GmB zb!%~O9Qmfs^mvR*qvyOT2ufw;ff)Nts4pi9OaRy1-o`~&gn7wTzmS|$Fv?{&w12bn z{_ebz7Hi7Kd;ckU?@QG5;{mA6D*N8BTzhBI6HJiwhvCaYe*QM5qpJYW*$tyNv~%(H zc6tYj8g>?5d#uLnN#}J)FI`qKuRKxL1_0ja)jw#s=8iTxyAA(mEY*}(AFnYrR&U;l zy%f~S#tWu9r=0lJ@%Gaz{C3!#-G3^HBS#L@d3N_~6XRYJvWi%lk5rfhwiQgS9v;fv zjPk(?%k&Q#HeVVsG!oIm2@kh3WA)b)v;VR6I{cpzo@hZ7nz1V(#RW zaD3eI>C0e@(szDRo-=<~;^ng#vyF2o@0b@K)3uSUPdKMNEl@TIBmb%RTz~YaGFK16 z&B0x}%_*_$5~tdrxT#Qn_LTZ{p5NO>QALem*E0v#|CLjF&m)Vsspxa!SY1V3{m3Xh zKUeRIIbhBhc0Kw1hS$OScMP!S;o87T%mY|%c20-IvFd-N!=EcfhXoP&#bDlD3Bo6z fIk^6>%;vwo8x-KvR=;il015yANkvXXu0mjf9+cGk delta 1250 zcmV<81ReXd3x^7jHGc%WNklZU1$_n6vuxvnca1b?SmzKXa%cj5`B^42k|8o zg&M^u5o8r&Dg`i+i@FT=Dq7)QSqYr)|s6>sVeo)jzFuQTx zow@h+VfN0>?97@?@xWp3ow?`y|L5ag?ilz)ZP@TFO6|w8et!aHBV`f*DP7fLr3&hw zN3C(k8H5ELL9+U zQ%=i*1<=?SB%2qdl$-9n)5MV@R4iQ@Fpsyk@_(_q+AUEe0qr=M41>EU1hS=fOrC)7 z5%81KVk8o%WYQz5cZsF?ZT|7A-;OG z7eMaSs{ql$HuN9CsT4eaULbJo8tmN*!^7Y>?$**IeAy9FdTI|3Gv3}#c5u*J;tUTX zGJhGQlmRupbo#R}Fi>#5bt{;r7rCHtnrVsb@OYeLTbs9LqQBp>^=puT))OZR-fL@N z_wGR5^cM1q|ELu}vb`Ovy4uT7s;4KCs2OM~tv7E%Uthudfdi;jt3rF&53YeF5N~SY z%dRf(d;0WgGPiFB)`c@rOr)a&-o0}@Sbr83=gwhtbRc4}Pzf{%6^~Q7e?MRK_JU;r zAVx=d+u0c)=vPA}lPc=OQ%a$hFUP!aA+RmwlPBc5yD{tQiRbSIAA5Vr-MYo}ZQHQs z&h^%+T#lD3RxokpN@V&4V5meQL_ptOBtSz0)|oRHRaHzy#Ml`BwzQDGbSX0OLVpiP znewOK-{}IduU;inUyptEY - - - + + + diff --git a/app/scripts/convert-pages-styles.mjs b/app/scripts/convert-pages-styles.mjs deleted file mode 100644 index 981cdf2..0000000 --- a/app/scripts/convert-pages-styles.mjs +++ /dev/null @@ -1,341 +0,0 @@ -import fs from "node:fs"; - -const path = new URL("../src/routes/pages.tsx", import.meta.url); -let src = fs.readFileSync(path, "utf8"); - -// Icons -src = src.replace( - `import { FolderPlus, ListFilter, Plus, RefreshCw } from "lucide-react";`, - `import Filter from "@react-spectrum/s2/icons/Filter"; -import FolderAdd from "@react-spectrum/s2/icons/FolderAdd"; -import Add from "@react-spectrum/s2/icons/Add"; -import DataRefresh from "@react-spectrum/s2/icons/DataRefresh";`, -); -src = src.replaceAll("ListFilter", "Filter"); -src = src.replaceAll("FolderPlus", "FolderAdd"); -src = src.replaceAll("RefreshCw", "DataRefresh"); -src = src.replaceAll(//g, ""); -src = src.replaceAll(//g, ""); -src = src.replace( - ``, - ``, -); - -// Import style macro if missing -if (!src.includes("@react-spectrum/s2/style")) { - src = src.replace( - `import type { ComposeProject } from "@/lib/api";`, - `import type { ComposeProject } from "@/lib/api"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" };`, - ); -} - -const replacements = [ - [ - `className="glass mb-5 flex items-center justify-between gap-4 rounded-[1.75rem] px-5 py-4"`, - `className={style({ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, marginBottom: 20, paddingX: 20, paddingY: 16, backgroundColor: "layer-1", borderRadius: "xl" })}`, - ], - [ - `className="hidden max-w-sm text-right text-xs text-muted-foreground md:block"`, - `className={style({ font: "body-xs", color: "neutral-subdued", textAlign: "end", maxWidth: 384 })}`, - ], - [ - `className="space-y-6"`, - `className={style({ display: "flex", flexDirection: "column", gap: 24 })}`, - ], - [ - `className="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-5"`, - `className={style({ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 12 })}`, - ], - [ - `className="glass-soft rounded-[1.75rem] px-5 py-4"`, - `className={style({ backgroundColor: "layer-1", borderRadius: "xl", paddingX: 20, paddingY: 16 })}`, - ], - [ - `className="mb-2 flex items-center justify-between"`, - `className={style({ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 8 })}`, - ], - [ - `className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground"`, - `className={style({ display: "flex", alignItems: "center", gap: 6, font: "detail-xs", color: "neutral-subdued" })}`, - ], - [ - `className="text-xs text-muted-foreground"`, - `className={style({ font: "body-xs", color: "neutral-subdued" })}`, - ], - [ - `className="grid gap-6 lg:grid-cols-2"`, - `className={style({ display: "grid", gridTemplateColumns: "1fr", gap: 24 })}`, - ], - [ - `className="mb-3 flex items-center justify-between gap-2"`, - `className={style({ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, marginBottom: 12 })}`, - ], - [ - `className="flex items-center gap-1.5"`, - `className={style({ display: "flex", alignItems: "center", gap: 6 })}`, - ], - [ - `className="text-lg font-semibold tracking-tight"`, - `className={style({ font: "title" })}`, - ], - [ - `className="flex h-full min-h-0 gap-6"`, - `className={style({ display: "flex", height: "full", minHeight: 0, gap: 24 })}`, - ], - [ - `className="flex items-center gap-2"`, - `className={style({ display: "flex", alignItems: "center", gap: 8 })}`, - ], - [ - `className="flex items-center justify-between gap-2"`, - `className={style({ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 })}`, - ], - [ - `className="flex min-w-0 items-center gap-2"`, - `className={style({ display: "flex", alignItems: "center", gap: 8, minWidth: 0 })}`, - ], - [ - `className="flex shrink-0 items-center gap-1"`, - `className={style({ display: "flex", alignItems: "center", gap: 4, flexShrink: 0 })}`, - ], - [ - `className="truncate font-medium"`, - `className={style({ font: "body", fontWeight: "medium", truncate: true })}`, - ], - [ - `className="truncate text-xs text-muted-foreground"`, - `className={style({ font: "body-xs", color: "neutral-subdued", truncate: true })}`, - ], - [ - `className="space-y-4 pb-6"`, - `className={style({ display: "flex", flexDirection: "column", gap: 16, paddingBottom: 24 })}`, - ], - [ - `className="space-y-3 pb-6"`, - `className={style({ display: "flex", flexDirection: "column", gap: 12, paddingBottom: 24 })}`, - ], - [ - `className="space-y-5 pb-2"`, - `className={style({ display: "flex", flexDirection: "column", gap: 20, paddingBottom: 8 })}`, - ], - [ - `className="flex flex-wrap items-center gap-2"`, - `className={style({ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8 })}`, - ], - [ - `className="flex flex-wrap items-start justify-between gap-2"`, - `className={style({ display: "flex", flexWrap: "wrap", alignItems: "start", justifyContent: "space-between", gap: 8 })}`, - ], - [ - `className="flex flex-wrap items-center justify-between gap-2"`, - `className={style({ display: "flex", flexWrap: "wrap", alignItems: "center", justifyContent: "space-between", gap: 8 })}`, - ], - [ - `className="flex flex-wrap gap-2"`, - `className={style({ display: "flex", flexWrap: "wrap", gap: 8 })}`, - ], - [ - `className="flex flex-wrap gap-1.5"`, - `className={style({ display: "flex", flexWrap: "wrap", gap: 6 })}`, - ], - [ - `className="text-xl font-semibold"`, - `className={style({ font: "heading" })}`, - ], - [ - `className="truncate text-xl font-semibold"`, - `className={style({ font: "heading", truncate: true })}`, - ], - [ - `className="relative"`, - `className={style({ position: "relative" })}`, - ], - [ - `className="absolute right-3 top-3 z-10"`, - `className={style({ position: "absolute", top: 12, end: 12, zIndex: 10 })}`, - ], - [ - `className="glass-soft max-h-[60vh] overflow-auto rounded-[1.25rem] p-4 font-mono text-xs leading-relaxed"`, - `className={style({ backgroundColor: "layer-1", borderRadius: "xl", padding: 16, font: "code-xs", maxHeight: "[60vh]", overflow: "auto" })}`, - ], - [ - `className="glass-soft max-h-[40vh] overflow-auto rounded-[1.25rem] p-4 font-mono text-xs leading-relaxed"`, - `className={style({ backgroundColor: "layer-1", borderRadius: "xl", padding: 16, font: "code-xs", maxHeight: "[40vh]", overflow: "auto" })}`, - ], - [ - `className="glass-soft space-y-3 rounded-[1.5rem] p-4"`, - `className={style({ display: "flex", flexDirection: "column", gap: 12, backgroundColor: "layer-1", borderRadius: "xl", padding: 16 })}`, - ], - [ - `className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground"`, - `className={style({ font: "detail-xs", color: "neutral-subdued" })}`, - ], - [ - `className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground"`, - `className={style({ font: "detail-xs", color: "neutral-subdued" })}`, - ], - [ - `className="flex gap-2"`, - `className={style({ display: "flex", gap: 8 })}`, - ], - [ - `className="mt-1 font-mono text-xs text-muted-foreground"`, - `className={style({ font: "code-xs", color: "neutral-subdued", marginTop: 4 })}`, - ], - [ - `className="mt-1 text-sm text-muted-foreground"`, - `className={style({ font: "body-sm", color: "neutral-subdued", marginTop: 4 })}`, - ], - [ - `className="space-y-2"`, - `className={style({ display: "flex", flexDirection: "column", gap: 8 })}`, - ], - [ - `className="space-y-1.5"`, - `className={style({ display: "flex", flexDirection: "column", gap: 6 })}`, - ], - [ - `className="space-y-3"`, - `className={style({ display: "flex", flexDirection: "column", gap: 12 })}`, - ], - [ - `className="flex items-center justify-between gap-2 rounded-[1rem] bg-muted/35 px-3 py-2 text-sm"`, - `className={style({ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, backgroundColor: "layer-2", borderRadius: "lg", paddingX: 12, paddingY: 8, font: "body-sm" })}`, - ], - [ - `className="shrink-0 font-mono text-[11px] text-muted-foreground"`, - `className={style({ font: "code-xs", color: "neutral-subdued", flexShrink: 0 })}`, - ], - [ - `className="flex h-full min-h-0"`, - `className={style({ display: "flex", height: "full", minHeight: 0 })}`, - ], - [ - `className="flex min-h-0 min-w-0 flex-1 flex-col"`, - `className={style({ display: "flex", flexDirection: "column", flexGrow: 1, minWidth: 0, minHeight: 0 })}`, - ], - [ - `className="shrink-0 border-t border-border/60 px-1 py-4"`, - `className={style({ flexShrink: 0, paddingX: 4, paddingY: 16, marginTop: 8 })}`, - ], - [ - `className="h-full"`, - `className={style({ height: "full" })}`, - ], - [ - `className="mb-4 flex flex-wrap items-center gap-3"`, - `className={style({ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 12, marginBottom: 16 })}`, - ], - [ - `className="flex h-[calc(100%-3rem)] min-h-0 gap-6"`, - `className={style({ display: "flex", height: "full", minHeight: 0, gap: 24 })}`, - ], - [ - `className="grid grid-cols-2 gap-3 md:grid-cols-4"`, - `className={style({ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 12 })}`, - ], - [ - `className="glass-soft rounded-[1.35rem]"`, - `className={style({ backgroundColor: "layer-1", borderRadius: "xl" })}`, - ], - [ - `className="mb-4 grid gap-2 md:grid-cols-3"`, - `className={style({ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 8, marginBottom: 16 })}`, - ], - [ - `className="mb-4 h-32 font-mono text-xs"`, - `className={style({ marginBottom: 16, font: "code-xs" })}`, - ], - [ - `className="mb-4 space-y-2"`, - `className={style({ display: "flex", flexDirection: "column", gap: 8, marginBottom: 16 })}`, - ], - [ - `className="flex items-center justify-between glass-soft rounded-[1.25rem] px-4 py-3.5"`, - `className={style({ display: "flex", alignItems: "center", justifyContent: "space-between", backgroundColor: "layer-1", borderRadius: "xl", paddingX: 16, paddingY: 14 })}`, - ], - [ - `className="font-medium"`, - `className={style({ fontWeight: "medium" })}`, - ], - [ - `className="mb-4 grid gap-2 md:grid-cols-2"`, - `className={style({ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 8, marginBottom: 16 })}`, - ], - [ - `className="glass-soft rounded-[1.25rem] px-4 py-3.5"`, - `className={style({ backgroundColor: "layer-1", borderRadius: "xl", paddingX: 16, paddingY: 14 })}`, - ], - [ - `className="flex items-center justify-between gap-3"`, - `className={style({ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 })}`, - ], - [ - `className="mt-4 max-h-64 overflow-auto rounded-2xl bg-black/80 p-4 text-xs text-emerald-200"`, - `className={style({ marginTop: 16, maxHeight: 256, overflow: "auto", borderRadius: "xl", padding: 16, font: "code-xs", backgroundColor: "layer-2" })}`, - ], - [ - `className="font-mono text-xs text-muted-foreground"`, - `className={style({ font: "code-xs", color: "neutral-subdued" })}`, - ], - [ - `className="px-5 py-2"`, - `className={style({ paddingX: 20, paddingY: 8 })}`, - ], - [ - `className="px-5 py-4"`, - `className={style({ paddingX: 20, paddingY: 16 })}`, - ], - [ - `className="mt-3 text-xs text-muted-foreground"`, - `className={style({ font: "body-xs", color: "neutral-subdued", marginTop: 12 })}`, - ], - [ - `className="text-sm text-muted-foreground"`, - `className={style({ font: "body-sm", color: "neutral-subdued" })}`, - ], - [ - `className="rounded-[1.15rem] border border-border/70 bg-muted/30 px-3.5 py-3"`, - `className={style({ backgroundColor: "layer-2", borderRadius: "xl", paddingX: 14, paddingY: 12 })}`, - ], - [ - `className="mb-1 text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground"`, - `className={style({ font: "detail-xs", color: "neutral-subdued", marginBottom: 4 })}`, - ], - [ - `className="flex items-start gap-2"`, - `className={style({ display: "flex", alignItems: "start", gap: 8 })}`, - ], - [ - `className="min-w-0 flex-1 break-all font-mono text-xs leading-relaxed"`, - `className={style({ font: "code-xs", flexGrow: 1, minWidth: 0 })}`, - ], - [ - `className="flex items-center gap-1.5"`, - `className={style({ display: "flex", alignItems: "center", gap: 6 })}`, - ], - [ - `className="max-w-[14rem] truncate font-mono text-[11px]"`, - `className={style({ font: "code-xs", truncate: true, maxWidth: 224 })}`, - ], - [ - `className="h-48 font-mono text-xs leading-relaxed"`, - `className={style({ font: "code-xs" })}`, - ], - [ - `className="h-16"`, - `className={style({ height: 64 })}`, - ], -]; - -for (const [from, to] of replacements) { - src = src.replaceAll(from, to); -} - -// Remaining glass / muted leftovers → layer panels -src = src.replaceAll(/className="glass[^"]*"/g, `className={style({ backgroundColor: "layer-1", borderRadius: "xl", padding: 16 })}`); -src = src.replaceAll(/className="[^"]*text-muted-foreground[^"]*"/g, `className={style({ color: "neutral-subdued", font: "body-xs" })}`); - -fs.writeFileSync(path, src); -console.log("Converted pages.tsx styles/icons"); diff --git a/app/src/App.tsx b/app/src/App.tsx index f0d0e43..847c4cc 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,6 +1,4 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Provider } from "@react-spectrum/s2"; -import "@react-spectrum/s2/page.css"; import { useEffect, useState } from "react"; import { AppRouter } from "@/router"; import { BootSplash } from "@/components/BootSplash"; @@ -117,11 +115,7 @@ export default function App() { }, [ready]); if (!ready) { - return ( - - - - ); + return ; } return ( diff --git a/app/src/assets/brand/favicon.svg b/app/src/assets/brand/favicon.svg index 48f8dd4..d20953c 100644 --- a/app/src/assets/brand/favicon.svg +++ b/app/src/assets/brand/favicon.svg @@ -1,9 +1,9 @@ - - - + + + diff --git a/app/src/assets/brand/logo-dark.svg b/app/src/assets/brand/logo-dark.svg index 674691e..ab6f398 100644 --- a/app/src/assets/brand/logo-dark.svg +++ b/app/src/assets/brand/logo-dark.svg @@ -2,9 +2,9 @@ - - - + + + @@ -28,6 +28,6 @@ Deckhand - LOCAL CONTAINER OPS + LOCAL CONTAINER OPS diff --git a/app/src/assets/brand/logo.svg b/app/src/assets/brand/logo.svg index 41cb3ce..dfbb817 100644 --- a/app/src/assets/brand/logo.svg +++ b/app/src/assets/brand/logo.svg @@ -3,9 +3,9 @@ - - - + + + @@ -29,6 +29,6 @@ Deckhand - LOCAL CONTAINER OPS + LOCAL CONTAINER OPS diff --git a/app/src/assets/brand/mark.svg b/app/src/assets/brand/mark.svg index 8ccce43..2ff08f2 100644 --- a/app/src/assets/brand/mark.svg +++ b/app/src/assets/brand/mark.svg @@ -1,18 +1,18 @@ Deckhand mark - Bold white anchor on Adobe red with subtle depth. Mark inspired by Lucide Icons (ISC). + Bold white anchor on Obsidian violet with subtle depth. Mark inspired by Lucide Icons (ISC). - - - + + + - + diff --git a/app/src/components/AnimatedOutlet.tsx b/app/src/components/AnimatedOutlet.tsx index 3a2a40e..41fd0ab 100644 --- a/app/src/components/AnimatedOutlet.tsx +++ b/app/src/components/AnimatedOutlet.tsx @@ -1,20 +1,11 @@ import { Outlet, useRouterState } from "@tanstack/react-router"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" }; /** Route outlet keyed by pathname for remount on navigation. */ export function AnimatedOutlet() { const pathname = useRouterState({ select: (s) => s.location.pathname }); return ( -
+
); diff --git a/app/src/components/AppFrame/AppFrame.tsx b/app/src/components/AppFrame/AppFrame.tsx index 0f8008d..84b7003 100644 --- a/app/src/components/AppFrame/AppFrame.tsx +++ b/app/src/components/AppFrame/AppFrame.tsx @@ -1,52 +1,14 @@ import type { CSSProperties, ReactNode } from "react"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" }; import { TitleBarDragRegion } from "@/components/TitleBarDragRegion"; +import { cn } from "@/lib/utils"; import { isTauriShell } from "@/lib/platform"; /** * Reserved space under live content so scrollports end above the fixed dock * with a visible breathing gap (dock itself stays content-height). - * Applied as inline padding-bottom — Spectrum style() can drop arbitrary clearance. */ export const STATUS_DOCK_CLEARANCE = 96; -const frameBase = style({ - display: "flex", - flexDirection: "column", - height: "screen", - overflow: "hidden", - backgroundColor: "base", -}); - -const frameDesktop = style({ - display: "flex", - flexDirection: "column", - height: "screen", - overflow: "hidden", - backgroundColor: "base", - paddingTop: 56, - boxSizing: "border-box", -}); - -const content = style({ - position: "relative", - zIndex: 0, - minHeight: 0, - flexGrow: 1, - overflow: "hidden", - boxSizing: "border-box", -}); - -/** Content-sized dock — top hairline only (avoid borderStyle painting all sides). */ -const dock = style({ - position: "fixed", - insetX: 0, - bottom: 0, - zIndex: 40, - backgroundColor: "layer-1", - paddingY: 12, -}); - export function AppFrame({ children, dock: dockSlot, @@ -57,17 +19,23 @@ export function AppFrame({ const desktop = isTauriShell(); return ( -
+
{desktop ? : null}
{children}
{dockSlot ? (
diff --git a/app/src/components/AppProvider.tsx b/app/src/components/AppProvider.tsx new file mode 100644 index 0000000..4fed987 --- /dev/null +++ b/app/src/components/AppProvider.tsx @@ -0,0 +1,9 @@ +import { TooltipProvider } from "@/components/ui/tooltip"; +import { useThemeSync } from "@/hooks/useThemeSync"; +import type { ReactNode } from "react"; + +/** App chrome: theme sync + tooltip context. */ +export function AppProvider({ children }: { children: ReactNode }) { + useThemeSync(); + return {children}; +} diff --git a/app/src/components/BootSplash.tsx b/app/src/components/BootSplash.tsx index 85716c4..3cc8d2d 100644 --- a/app/src/components/BootSplash.tsx +++ b/app/src/components/BootSplash.tsx @@ -1,55 +1,15 @@ -import { Heading, ProgressCircle, Text } from "@react-spectrum/s2"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" }; +import { Loader2 } from "lucide-react"; import { LogoMark } from "@/components/Logo"; export function BootSplash({ message = "Starting sidecar…" }: { message?: string }) { return ( -
+
-
- - Deckhand - -
- - - {message} - +
+

Deckhand

+
+ + {message}
diff --git a/app/src/components/CodeBlock.tsx b/app/src/components/CodeBlock.tsx index d86e1f2..be9db03 100644 --- a/app/src/components/CodeBlock.tsx +++ b/app/src/components/CodeBlock.tsx @@ -1,6 +1,4 @@ import type { ReactNode } from "react"; -import { Text } from "@react-spectrum/s2"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" }; import { CopyButton } from "@/components/CopyButton"; /** Structured code/JSON panel with a quiet toolbar (inspect, VM logs, etc.). */ @@ -23,38 +21,16 @@ export function CodeBlock({
-
- {title} +
+ {title} {meta ? ( - {meta} + {meta} ) : null}
@@ -63,15 +39,7 @@ export function CodeBlock({ className={[ "dh-code-block__body", hasContent ? "" : "is-muted", - style({ - margin: 0, - paddingX: 16, - paddingY: 16, - font: "code-xs", - overflow: "auto", - whiteSpace: "pre-wrap", - overflowWrap: "anywhere", - }), + "px-4 py-4 m-0 overflow-auto font-mono text-xs", ] .filter(Boolean) .join(" ")} diff --git a/app/src/components/CommandPalette.tsx b/app/src/components/CommandPalette.tsx index 26ab3d8..3c59cf6 100644 --- a/app/src/components/CommandPalette.tsx +++ b/app/src/components/CommandPalette.tsx @@ -1,14 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { useQuery } from "@tanstack/react-query"; -import { - CustomDialog, - DialogContainer, - Heading, - SearchField, - Text, -} from "@react-spectrum/s2"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" }; import type { LucideIcon } from "lucide-react"; import { Archive, @@ -31,6 +23,9 @@ import { lucideProps } from "@/components/Icon"; import { isTauriShell } from "@/lib/platform"; import { useUIStore } from "@/stores/uiStore"; import { containerName, shortId } from "@/lib/utils"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; + type Action = { id: string; @@ -269,145 +264,77 @@ export function CommandPalette({ }, [q, filtered.length]); return ( - onOpenChange(false)}> - {open ? ( - - - Command palette - -
- { - if (e.key === "ArrowDown") { - e.preventDefault(); - setActive((i) => Math.min(filtered.length - 1, i + 1)); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setActive((i) => Math.max(0, i - 1)); - } else if (e.key === "Enter") { - e.preventDefault(); - filtered[active]?.run(); - } - }} - placeholder="Search containers, Compose, images, volumes, Hub…" - /> -
-
- {filtered.length === 0 ? ( -
- No matches -
- ) : ( - filtered.map((a, i) => { - const Icon = a.icon; - const selected = i === active; - return ( - - ); - }) - )} -
-
- - ↑↓ navigate · ↵ select · esc close - {hub.isFetching ? " · searching Hub…" : ""} - -
-
- ) : null} -
+ + + + Command palette + Search containers, images, volumes, and Hub + +
+ setQ(e.target.value)} + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setActive((i) => Math.min(filtered.length - 1, i + 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setActive((i) => Math.max(0, i - 1)); + } else if (e.key === "Enter") { + e.preventDefault(); + filtered[active]?.run(); + } + }} + placeholder="Search containers, Compose, images, volumes, Hub…" + /> +
+
+ {filtered.length === 0 ? ( +
+ No matches +
+ ) : ( + filtered.map((a, i) => { + const Icon = a.icon; + const selected = i === active; + return ( + + ); + }) + )} +
+
+ + ↑↓ navigate · ↵ select · esc close + {hub.isFetching ? " · searching Hub…" : ""} + +
+
+
); } diff --git a/app/src/components/ConfirmDialog.tsx b/app/src/components/ConfirmDialog.tsx index 85058aa..e7b99a8 100644 --- a/app/src/components/ConfirmDialog.tsx +++ b/app/src/components/ConfirmDialog.tsx @@ -1,6 +1,15 @@ -import { AlertDialog, DialogContainer } from "@react-spectrum/s2"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; -/** Confirm dialog — Spectrum AlertDialog, no dismiss X. */ +/** Confirm dialog — no dismiss X; cancel / confirm only. */ export function ConfirmDialog({ open, onOpenChange, @@ -23,26 +32,30 @@ export function ConfirmDialog({ onConfirm: () => void | Promise; }) { return ( - onOpenChange(false)}> - {open ? ( - onOpenChange(false)} - onPrimaryAction={() => { - void Promise.resolve(onConfirm()) - .then(() => onOpenChange(false)) - .catch(() => { - /* keep open — callers toast errors */ - }); - }} - > - {description} - - ) : null} - + + + + {title} + {description} + + + {cancelLabel} + { + e.preventDefault(); + void Promise.resolve(onConfirm()) + .then(() => onOpenChange(false)) + .catch(() => { + /* keep open — callers toast errors */ + }); + }} + > + {confirmLabel} + + + + ); } diff --git a/app/src/components/ConsolePanel.tsx b/app/src/components/ConsolePanel.tsx index 51c1ccd..d9b2788 100644 --- a/app/src/components/ConsolePanel.tsx +++ b/app/src/components/ConsolePanel.tsx @@ -1,20 +1,16 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { - ActionButton, - SearchField, - StatusLight, - Tooltip, - TooltipTrigger, -} from "@react-spectrum/s2"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" }; import { Download, Pause, Play, Trash2 } from "lucide-react"; import { CopyButton } from "@/components/CopyButton"; import { lucideProps } from "@/components/Icon"; +import { Tip } from "@/components/Tip"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { TerminalFrame, TerminalToolbarEnd, TerminalToolbarStart, } from "@/components/TerminalChrome"; +import { cn } from "@/lib/utils"; /** Streaming console for container/pod logs. */ export function ConsolePanel({ @@ -117,76 +113,71 @@ export function ConsolePanel({ toolbar={ <> - -
- +
+ {title} - + {error ? "Disconnected" : live ? "Live stream" : "Paused"} {matchCount != null ? ` · ${matchCount} match${matchCount === 1 ? "" : "es"}` : ""}
- setFilter(e.target.value)} placeholder="Filter lines…" - size="S" - styles={style({ width: 180 })} + className="w-[180px]" /> - - + + } > {error ? (
- + {error}
@@ -200,17 +191,7 @@ export function ConsolePanel({ className={[ "dh-terminal__body", isPlaceholder ? "is-muted" : "", - style({ - minHeight: 0, - flexGrow: 1, - overflow: "auto", - paddingX: 16, - paddingY: 16, - margin: 0, - font: "code-xs", - whiteSpace: "pre-wrap", - overflowWrap: "anywhere", - }), + "flex-1 px-4 py-4 m-0 min-h-0 overflow-auto font-mono text-xs min-h-0", ] .filter(Boolean) .join(" ")} diff --git a/app/src/components/ContainerMonitor.tsx b/app/src/components/ContainerMonitor.tsx index c5e37a5..12683f7 100644 --- a/app/src/components/ContainerMonitor.tsx +++ b/app/src/components/ContainerMonitor.tsx @@ -1,7 +1,5 @@ import { useEffect, useState } from "react"; -import { Badge, Heading, InlineAlert, Content, Text } from "@react-spectrum/s2"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" }; -import { Pause } from "lucide-react"; +import { Loader2, Pause } from "lucide-react"; import { lucideProps } from "@/components/Icon"; import { api, type ContainerStats } from "@/lib/api"; import { LiveSparkline } from "@/components/charts/LiveSparkline"; @@ -9,13 +7,13 @@ import { MetricCard, WaveBars } from "@/components/charts/MetricChart"; import { useEasedNumber } from "@/hooks/useEasedSeries"; import { useLiveStats } from "@/hooks/useLiveStats"; import { formatBytes } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; -/** Live runtime metrics (docker stats / cgroup) — SVG sparklines, not Vega. */ +/** Live runtime metrics (docker stats / cgroup) — SVG sparklines. */ export function ContainerMonitor({ containerId, running }: { containerId: string; running?: boolean }) { const [error, setError] = useState(null); const [connected, setConnected] = useState(false); const { sampleLabel, hist, pushSample } = useLiveStats(!!running); - // Match sparkline tip duration so headline spikes land with the chart. const cpuEased = useEasedNumber(sampleLabel?.cpu ?? 0, 280); const memEased = useEasedNumber(sampleLabel?.mem ?? 0, 280); const netEased = useEasedNumber(sampleLabel?.netRate ?? 0, 280); @@ -84,99 +82,49 @@ export function ContainerMonitor({ containerId, running }: { containerId: string if (!running) { return ( -
-
- +
+
+
- Container stopped - + Container stopped + Start it to stream CPU, memory, network, and block I/O from cgroups. - +
); } if (error) { return ( - - Stats unavailable - {error} - +
+

Stats unavailable

+

{error}

+
); } if (!connected || !sampleLabel) { return ( -
+
- +
+ Connecting to stats stream… - +
); } return ( -
-
- Live - - Docker stats stream · updates ~1s - +
+
+ + Live + + Docker stats stream · ~1s
-
+
@@ -189,16 +137,7 @@ export function ContainerMonitor({ containerId, running }: { containerId: string
-
+
-
+
void onCopy()} + variant="ghost" + size={iconOnly ? "icon-sm" : "sm"} + disabled={!value} + onClick={() => void onCopy()} data-no-drag + className={cn(dark && "text-white hover:bg-white/10 hover:text-white")} > {copied ? ( - + ) : ( )} - {!iconOnly ? {copied ? copiedLabel : label} : null} - + {!iconOnly ? {copied ? copiedLabel : label} : null} + ); - // Disabled controls cannot host tooltips — omit tip when there is nothing to copy. if (!value) return button; return ( - - {button} - {copied ? copiedLabel : label} - + + + {copied ? copiedLabel : label} + ); } diff --git a/app/src/components/DetailPane.tsx b/app/src/components/DetailPane.tsx index 8a0fa4b..689dd70 100644 --- a/app/src/components/DetailPane.tsx +++ b/app/src/components/DetailPane.tsx @@ -1,23 +1,11 @@ import type { ReactNode } from "react"; -import { Heading, Text } from "@react-spectrum/s2"; import { MousePointerClick } from "lucide-react"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" }; import { lucideProps } from "@/components/Icon"; +import { cn } from "@/lib/utils"; -/** Detail title — matches ListPaneTitle (heading-lg) for list/detail alignment. */ +/** Detail title — matches ListPaneTitle for list/detail alignment. */ export function DetailHeading({ children }: { children: ReactNode }) { - return ( - - {children} - - ); + return

{children}

; } export function DetailEmpty({ @@ -33,59 +21,15 @@ export function DetailEmpty({ icon?: ReactNode; }) { return ( -
-
+
+
{icon ?? }
- - {title} - +

{title}

{description ? ( - - {description} - - ) : null} - {action ? ( -
{action}
+

{description}

) : null} + {action ?
{action}
: null}
); } @@ -113,60 +57,15 @@ export function DetailPane({ ); return ( -
+
{!selectionKey ? ( -
+
{emptyNode}
) : ( -
- {header ? ( -
{header}
- ) : null} - {/* Native overflow — Console/Exec need nested scroll. */} -
- {children} -
+
+ {header ?
{header}
: null} +
{children}
)}
diff --git a/app/src/components/DiskUsagePanel.tsx b/app/src/components/DiskUsagePanel.tsx index aed4ff1..f5b96bb 100644 --- a/app/src/components/DiskUsagePanel.tsx +++ b/app/src/components/DiskUsagePanel.tsx @@ -1,11 +1,14 @@ import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Button, Checkbox, ProgressBar, ProgressCircle, Text } from "@react-spectrum/s2"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" }; +import { Loader2 } from "lucide-react"; import { api, type SystemPruneBody } from "@/lib/api"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { HelpHint } from "@/components/HelpHint"; import { toast } from "@/components/Toaster"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { Progress } from "@/components/ui/progress"; import { formatBytes } from "@/lib/utils"; import { useUIStore } from "@/stores/uiStore"; @@ -66,10 +69,10 @@ const defaultSelected: SystemPruneBody = { export function DiskUsagePanel({ compact, - /** When true, omit the built-in title (parent card already labels it). */ hideTitle, }: { compact?: boolean; + /** When true, omit the built-in title (parent card already labels it). */ hideTitle?: boolean; }) { const qc = useQueryClient(); @@ -132,19 +135,15 @@ export function DiskUsagePanel({ if (df.isLoading && !data) { return (
- - Measuring disk usage… + + Measuring disk usage… + docker system df can take a while on large engines
); } @@ -152,192 +151,109 @@ export function DiskUsagePanel({ if (df.isError) { return (
- Disk usage unavailable - - Is the Docker engine running? - + Disk usage unavailable + Is the Docker engine running?
); } return ( -
-
-
+
+
+
{!hideTitle ? ( -
- - Engine disk - +
+ Engine disk
) : null} -
- +
+ {formatBytes(reclaimable)} - - reclaimable + + reclaimable
{!compact ? ( ) : null}
-
+
{rows.map((row) => { const size = data?.[row.key] || 0; const active = data?.[row.active] ?? 0; const total = data?.[row.total] ?? 0; + const pct = Math.round((size / max) * 100); + const id = `prune-${row.pruneKey}`; return ( -
-
-
+
+
+
setSelected((s) => ({ ...s, [row.pruneKey]: next }))} - > + id={id} + checked={!!selected[row.pruneKey]} + onCheckedChange={(next) => + setSelected((s) => ({ ...s, [row.pruneKey]: !!next })) + } + /> + +
-
- {formatBytes(size)} - +
+ {formatBytes(size)} + {active}/{total} in use - +
- +
); })}
-
- setSelected((s) => ({ ...s, networks }))} - > - Unused networks - +
+
+ setSelected((s) => ({ ...s, networks: !!networks }))} + /> + +
{compact ? ( ) : ( - - {selectedCount} selected - + {selectedCount} selected )}
- {result ? ( - {result} - ) : null} + {result ? {result} : null} - -
- + +
+ {title} - + {statusLabel} · {shell}
- { - const next = [...keys][0] as Shell | undefined; - if (next) setShell(next); - }} - density="compact" - size="S" - staticColor="white" - isQuiet - > - - sh - - - bash - - - ash - - - - + + + } >
termRef.current?.focus()} /> diff --git a/app/src/components/spectrum/Field.tsx b/app/src/components/Field.tsx similarity index 61% rename from app/src/components/spectrum/Field.tsx rename to app/src/components/Field.tsx index e6e4d42..f9118d2 100644 --- a/app/src/components/spectrum/Field.tsx +++ b/app/src/components/Field.tsx @@ -1,8 +1,8 @@ import type { KeyboardEvent } from "react"; -import { TextArea, TextField } from "@react-spectrum/s2"; -import { style } from "@react-spectrum/s2/style" with { type: "macro" }; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; -/** Controlled string TextField with a familiar onChange(string) API. */ +/** Controlled string input with a familiar onChange(string) API. */ export function Field({ value, onChange, @@ -19,14 +19,14 @@ export function Field({ "aria-label"?: string; }) { return ( - onChange(e.target.value)} placeholder={placeholder} - isDisabled={isDisabled} - onKeyDown={onKeyDown as never} + disabled={isDisabled} + onKeyDown={onKeyDown} aria-label={ariaLabel || placeholder} - styles={style({ width: "full" })} + className="w-full" /> ); } @@ -43,13 +43,13 @@ export function Area({ isDisabled?: boolean; }) { return ( -