From 291a124663d26f3d80a843a91b23d1e113277b4d Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 26 Apr 2026 13:16:24 +0700 Subject: [PATCH] refactor: split sidebar callout from generic Alert primitive Rename CalloutCard to SidebarCallout (it was always sidebar-specific) and add a shadcn-style Alert primitive with info/success/warning/error variants for inline use in screens. Replace the misused SidebarCallout in project settings with Alert and surface the underlying RPC error message instead of generic copy. Tune destructive color per theme to a calm tinted red. --- docs/DESIGN-SYSTEM.md | 51 +++--- packages/app/src/components/left-sidebar.tsx | 6 +- ...card.test.tsx => sidebar-callout.test.tsx} | 24 +-- .../{callout-card.tsx => sidebar-callout.tsx} | 32 ++-- packages/app/src/components/ui/alert.tsx | 121 +++++++++++++ .../src/contexts/sidebar-callout-context.tsx | 18 +- .../desktop/updates/update-callout-source.tsx | 15 +- .../screens/project-settings-screen.test.tsx | 21 +-- .../src/screens/project-settings-screen.tsx | 168 +++++++++--------- packages/app/src/styles/theme.ts | 10 +- 10 files changed, 296 insertions(+), 170 deletions(-) rename packages/app/src/components/{callout-card.test.tsx => sidebar-callout.test.tsx} (83%) rename packages/app/src/components/{callout-card.tsx => sidebar-callout.tsx} (88%) create mode 100644 packages/app/src/components/ui/alert.tsx diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md index dd6ec8554..11fe829ec 100644 --- a/docs/DESIGN-SYSTEM.md +++ b/docs/DESIGN-SYSTEM.md @@ -33,8 +33,8 @@ Hierarchy is conveyed through weight and color, not size. Most labels, titles, a Weight has three tiers, applied by role: - **Screen titles** — the title at the top of a screen — use `` (`packages/app/src/components/headers/screen-title.tsx:31-34`), which renders `fontSize.base` at weight `400` on compact and `300` on desktop. Top-of-screen titles are lighter on desktop, not heavier. The workspace screen header follows the same rule (`packages/app/src/screens/workspace/workspace-screen.tsx:3052-3057`). -- **Structural labels** use `fontWeight.medium`. This applies to section labels above a stack of rows (`packages/app/src/components/agent-list.tsx:519-523`, `packages/app/src/components/keyboard-shortcuts-dialog.tsx:63-67`), form field labels above an input inside a modal (`packages/app/src/components/add-host-modal.tsx:19-23`, `packages/app/src/components/pair-link-modal.tsx:24-28`), the title at the top of a modal/sheet/dialog (`packages/app/src/components/adaptive-modal-sheet.tsx:90-94`, `packages/app/src/components/ui/combobox.tsx:1607-1611`, `packages/app/src/components/welcome-screen.tsx:48-53`), action button labels in tight components such as the callout actions (`packages/app/src/components/callout-card.tsx:218-221`), and inline data emphasis on dense metadata rows (`packages/app/src/components/git-diff-pane.tsx:2322-2327`, `packages/app/src/components/file-explorer-pane.tsx:1115-1122`). -- **Content** uses `fontWeight.normal`. This applies to settings rows (`packages/app/src/styles/settings.ts`), sidebar primary list-item titles (`packages/app/src/components/sidebar-workspace-list.tsx:2680-2686`, `packages/app/src/components/agent-list.tsx:572-578`), ` + ); } +function resolveReadFailureCopy(input: { + kind: ReadFailureCalloutProps["kind"]; + error: unknown; + hasMultipleHosts: boolean; +}): { testID: string; title: string; description: string } { + if (input.kind === "invalid_project_config") { + return { + testID: "invalid-callout", + title: "paseo.json couldn't be parsed", + description: "Fix the file on disk, then reload.", + }; + } + if (input.kind === "project_not_found") { + return { + testID: "project-not-found-callout", + title: "This host doesn't have this project", + description: input.hasMultipleHosts + ? "Switch to another host above, or reload." + : "The selected host has no record of this project.", + }; + } + if (input.kind === "transport") { + const detail = errorToDetail(input.error); + return { + testID: "read-transport-callout", + title: "Couldn't load paseo.json", + description: detail ?? "The host didn't respond.", + }; + } + return { + testID: "read-failed-callout", + title: "Couldn't load paseo.json", + description: "Reload to try again.", + }; +} + +function errorToDetail(error: unknown): string | null { + if (error instanceof Error && error.message.length > 0) return error.message; + if (typeof error === "string" && error.length > 0) return error; + return null; +} + interface ProjectConfigFormProps { baseConfig: PaseoConfigRaw; revision: PaseoConfigRevision | null; @@ -508,18 +497,6 @@ function ProjectConfigForm({ [draft.scripts], ); - const staleActions = useMemo( - () => [{ label: "Reload", onPress: handleReload, variant: "primary" }], - [handleReload], - ); - const writeFailedActions = useMemo( - () => [ - { label: "Try again", onPress: handleSave, variant: "primary" }, - { label: "Reload", onPress: handleReload, variant: "secondary" }, - ], - [handleSave, handleReload], - ); - const scriptsTrailing = useMemo( () => ( - + > + + ) : null} {isWriteFailed ? ( - + > + + + ) : null} @@ -1033,8 +1034,7 @@ const styles = StyleSheet.create((theme) => ({ padding: theme.spacing[6], }, errorBlock: { - padding: theme.spacing[4], - gap: theme.spacing[3], + marginTop: theme.spacing[2], }, lifecycleInput: { color: theme.colors.foreground, diff --git a/packages/app/src/styles/theme.ts b/packages/app/src/styles/theme.ts index d597d3cd5..4b20000f5 100644 --- a/packages/app/src/styles/theme.ts +++ b/packages/app/src/styles/theme.ts @@ -164,7 +164,7 @@ const lightSemanticColors = { accentForeground: "#ffffff", // Semantic - destructive: "#dc2626", + destructive: "#b04138", // dark warm red on white — calm but unambiguously red destructiveForeground: "#ffffff", success: "#20744A", successForeground: "#ffffff", @@ -233,6 +233,7 @@ interface DarkThemeConfig { borderAccent: string; accent: string; accentBright: string; + destructive: string; } const darkTerminalAnsi = { @@ -276,7 +277,7 @@ function buildDarkSemanticColors(tint: DarkThemeConfig) { accentBright: tint.accentBright, accentForeground: "#ffffff", - destructive: "#ef4444", + destructive: tint.destructive, destructiveForeground: "#ffffff", success: tint.accent, successForeground: "#ffffff", @@ -332,6 +333,7 @@ const paseoDarkColors = buildDarkSemanticColors({ borderAccent: "#2F3534", accent: "#20744A", accentBright: "#7ccba0", + destructive: "#c64f43", // warm red, hue ~7 — reads as red (not pink) against the green tint }); // Zinc — neutral gray, no tint @@ -350,6 +352,7 @@ const zincDarkColors = buildDarkSemanticColors({ borderAccent: "#303036", accent: "#20744A", accentBright: "#7ccba0", + destructive: "#c44a4a", // neutral red, hue 0 — clearly red without screaming }); // Midnight — subtle blue tint @@ -368,6 +371,7 @@ const midnightDarkColors = buildDarkSemanticColors({ borderAccent: "#2e3040", accent: "#3b6fcf", accentBright: "#7eaaeb", + destructive: "#c44a52", // red with a hint of cool lean against the blue tint }); // Claude — warm neutral with subtle orange undertone @@ -386,6 +390,7 @@ const claudeDarkColors = buildDarkSemanticColors({ borderAccent: "#36332f", accent: "#d97757", accentBright: "#e89a7f", + destructive: "#cf513e", // warm orange-red, hue ~10 — sits with the Claude orange accent }); // Ghostty — blue-tinted dark based on Ghostty default background @@ -404,6 +409,7 @@ const ghosttyDarkColors = buildDarkSemanticColors({ borderAccent: "#3f4454", accent: "#89b4fa", accentBright: "#b4d0fc", + destructive: "#c44a55", // red with slight cool lean against the slate-blue surfaces }); const commonTheme = {