From 600537cd9e6b90b9cbabe20efb4e407e99a49cfc Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Tue, 21 Jul 2026 19:38:57 +0530 Subject: [PATCH] Fix MCP widgets according to consumer app (#1189) Co-authored-by: ved015 Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 Co-authored-by: Prasanna A P <106952318+Prasanna721@users.noreply.github.com> --- apps/mcp/src/widget/App.tsx | 68 ++- .../src/widget/components/PermissionBadge.tsx | 11 +- apps/mcp/src/widget/components/Spinner.tsx | 4 +- .../src/widget/components/WorkspaceCard.tsx | 74 ++- .../src/widget/components/WorkspaceChip.tsx | 12 + apps/mcp/src/widget/design/globals.css | 282 +++++++++-- apps/mcp/src/widget/design/tokens.css | 459 +++++++++++------- apps/mcp/src/widget/design/ui/Button.tsx | 42 +- apps/mcp/src/widget/design/ui/Card.tsx | 21 +- apps/mcp/src/widget/design/ui/Chip.tsx | 2 +- apps/mcp/src/widget/design/ui/Field.tsx | 2 +- apps/mcp/src/widget/design/ui/FileUpload.tsx | 8 +- apps/mcp/src/widget/design/ui/Input.tsx | 15 +- apps/mcp/src/widget/design/ui/Popover.tsx | 4 +- apps/mcp/src/widget/design/ui/TextArea.tsx | 18 +- .../src/widget/design/ui/WorkspaceSelect.tsx | 32 +- apps/mcp/src/widget/index.html | 2 +- apps/mcp/src/widget/lib/formatTag.ts | 15 + apps/mcp/src/widget/studio.html | 2 +- apps/mcp/src/widget/studio/Studio.tsx | 120 +++-- apps/mcp/src/widget/views/Confirmation.tsx | 20 +- apps/mcp/src/widget/views/Error.tsx | 8 +- apps/mcp/src/widget/views/Graph.tsx | 45 +- apps/mcp/src/widget/views/Loading.tsx | 26 +- apps/mcp/src/widget/views/Picker.tsx | 100 ++-- apps/mcp/src/widget/views/Save.tsx | 70 +-- apps/mcp/src/widget/views/Success.tsx | 20 +- apps/mcp/src/widget/views/Upload.tsx | 108 +++-- 28 files changed, 1036 insertions(+), 554 deletions(-) create mode 100644 apps/mcp/src/widget/components/WorkspaceChip.tsx create mode 100644 apps/mcp/src/widget/lib/formatTag.ts diff --git a/apps/mcp/src/widget/App.tsx b/apps/mcp/src/widget/App.tsx index 52d16d69..158fa364 100644 --- a/apps/mcp/src/widget/App.tsx +++ b/apps/mcp/src/widget/App.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react" +import { type ReactNode, useEffect } from "react" import type { ViewMessage } from "../shared/types" import { useApplyHostTheme } from "./hooks/useApplyHostTheme" import { useLog } from "./hooks/useLog" @@ -25,15 +25,70 @@ export function App() { } }, [state, log]) - if (state.kind === "loading") return - if (state.kind === "error") return + if (state.kind === "loading") { + return ( + + + + ) + } + if (state.kind === "error") { + return ( + + + + ) + } if (state.kind === "raw") { return ( - + + + ) } - return renderView(state.message, setView, setError) + const isGraphView = state.message.view === "graph" + return ( + + {renderView(state.message, setView, setError)} + + ) +} + +export function WidgetShell({ + children, + immersive = false, +}: { + children: ReactNode + immersive?: boolean +}) { + const shellClassName = immersive + ? "mcp-widget-shell mcp-widget-shell-graph" + : "mcp-widget-shell" + + return ( +
+
+ + + + + supermemory + MCP + +
+
{children}
+
+ ) } function renderView( @@ -82,13 +137,12 @@ function renderView( case "confirmation": return case "save-success": - return + return case "upload-success": return ( ) diff --git a/apps/mcp/src/widget/components/PermissionBadge.tsx b/apps/mcp/src/widget/components/PermissionBadge.tsx index ba8fc1ab..a248467d 100644 --- a/apps/mcp/src/widget/components/PermissionBadge.tsx +++ b/apps/mcp/src/widget/components/PermissionBadge.tsx @@ -1,5 +1,3 @@ -import { Badge } from "../design/ui" - interface Props { permission: string } @@ -7,11 +5,8 @@ interface Props { export function PermissionBadge({ permission }: Props) { const isWrite = permission === "write" return ( - - {isWrite ? "read/write" : "read only"} - + + {isWrite ? "Read / write" : "Read only"} + ) } diff --git a/apps/mcp/src/widget/components/Spinner.tsx b/apps/mcp/src/widget/components/Spinner.tsx index 39b8dc2d..0d3de414 100644 --- a/apps/mcp/src/widget/components/Spinner.tsx +++ b/apps/mcp/src/widget/components/Spinner.tsx @@ -1,3 +1,5 @@ +import { Loader2 } from "../lib/icons" + export function Spinner() { - return
+ return } diff --git a/apps/mcp/src/widget/components/WorkspaceCard.tsx b/apps/mcp/src/widget/components/WorkspaceCard.tsx index 24f39211..18b96f9d 100644 --- a/apps/mcp/src/widget/components/WorkspaceCard.tsx +++ b/apps/mcp/src/widget/components/WorkspaceCard.tsx @@ -1,6 +1,6 @@ import type { ContainerTag, ContainerTagAccess } from "../../shared/types" -import { Card, Stack } from "../design/ui" -import { Check, Package } from "../lib/icons" +import { cn } from "../design/lib/cn" +import { formatTagLabel } from "../lib/formatTag" import { PermissionBadge } from "./PermissionBadge" interface Props { @@ -16,51 +16,39 @@ export function WorkspaceCard({ access, onClick, }: Props) { - const name = containerTag.name || containerTag.containerTag + const name = containerTag.name || formatTagLabel(containerTag.containerTag) + const docs = containerTag.documentCount + const mems = containerTag.memoryCount + const meta = + docs > 0 || mems > 0 + ? `${docs} doc${docs === 1 ? "" : "s"} · ${mems} ${mems === 1 ? "memory" : "memories"}` + : "No memories yet" + return ( - onClick(containerTag.containerTag)} - variant={active ? "active" : "interactive"} + type="button" > - - - - {containerTag.emoji ? ( - - {containerTag.emoji} - - ) : ( - + + + - {name} - + > + {name} - {active ? : null} - -
- {containerTag.containerTag} -
- {(containerTag.documentCount > 0 || containerTag.memoryCount > 0) && ( -
- - {containerTag.documentCount} doc - {containerTag.documentCount === 1 ? "" : "s"} - - · - - {containerTag.memoryCount}{" "} - {containerTag.memoryCount === 1 ? "memory" : "memories"} - -
- )} - {access ? : null} -
-
+ + {meta} + + + + {access ? : null} + + + ) } diff --git a/apps/mcp/src/widget/components/WorkspaceChip.tsx b/apps/mcp/src/widget/components/WorkspaceChip.tsx new file mode 100644 index 00000000..e397f75d --- /dev/null +++ b/apps/mcp/src/widget/components/WorkspaceChip.tsx @@ -0,0 +1,12 @@ +import { formatTagLabel } from "../lib/formatTag" + +// A calm workspace pill: a small accent dot + the workspace name on a neutral +// surface. Reads like a "space" tag rather than a blue-on-blue system badge. +export function WorkspaceChip({ containerTag }: { containerTag: string }) { + return ( + + + {formatTagLabel(containerTag)} + + ) +} diff --git a/apps/mcp/src/widget/design/globals.css b/apps/mcp/src/widget/design/globals.css index f159f5e5..e63673d5 100644 --- a/apps/mcp/src/widget/design/globals.css +++ b/apps/mcp/src/widget/design/globals.css @@ -3,20 +3,22 @@ @custom-variant dark (&:is([data-theme="dark"] *)); -/* Map design tokens to Tailwind utilities. Mirrors console-v2's mapping - * so utility class names like `bg-bg-primary`, `text-text-primary`, - * `border-border-accent`, `font-brand`, `text-xs`, `shadow-md` etc. - * resolve from one source of truth. - */ +/* Map design tokens to Tailwind utilities so class names like `bg-bg-primary`, + * `text-text-primary`, `border-border-accent`, `text-xs`, `shadow-md` resolve + * from one source of truth. */ @theme inline { --color-bg-primary: var(--bg-primary); --color-bg-secondary: var(--bg-secondary); --color-bg-muted: var(--bg-muted); --color-bg-elevated: var(--bg-elevated); --color-bg-overlay: var(--bg-overlay); + --color-bg-panel: var(--bg-panel); + --color-bg-control: var(--bg-control); + --color-bg-control-hover: var(--bg-control-hover); --color-border: var(--border); --color-border-muted: var(--border-muted); + --color-border-control: var(--border-control); --color-border-accent: var(--border-accent); --color-text-primary: var(--text-primary); @@ -60,6 +62,7 @@ --radius-sm: var(--radius-sm); --radius-md: var(--radius-md); --radius-lg: var(--radius-lg); + --radius-xl: var(--radius-xl); --radius-full: var(--radius-full); --shadow-sm: var(--shadow-sm); @@ -67,15 +70,13 @@ --shadow-lg: var(--shadow-lg); } -/* Excalidraw pattern: NO heights on root containers. The MCP host owns - * the iframe height; ext-apps' App.autoResize observes documentElement - * and reports content size back so the host right-sizes the iframe. - * Setting any height (vh, %, etc.) here breaks that feedback loop. +/* Excalidraw pattern: NO heights on root containers. The MCP host owns the + * iframe height; ext-apps' App.autoResize observes documentElement and reports + * content size back so the host right-sizes the iframe. Any height here (vh, %, + * etc.) breaks that feedback loop. * - * The reset lives INSIDE @layer base so Tailwind's @layer utilities (which - * come after base) win — an unlayered universal padding:0 reset would override - * every padding utility in the widget. box-sizing/margin are belt-and-braces - * on top of Tailwind preflight. */ + * The reset lives INSIDE @layer base so Tailwind's @layer utilities win — an + * unlayered universal padding:0 would override every padding utility. */ @layer base { * { margin: 0; @@ -85,9 +86,10 @@ } body { - background: var(--bg-primary); + background: var(--app-bg); color: var(--text-primary); font-family: var(--font-sans); + letter-spacing: -0.01em; line-height: var(--leading-normal); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -107,41 +109,249 @@ position: relative; } -/* Spinner — kept as global because primitives can't easily express a CSS animation */ -.spinner { - width: 24px; - height: 24px; - border: 2px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 0.8s linear infinite; +/* ── Shell ────────────────────────────────────────────────────────────────── */ +.mcp-widget-shell { + position: relative; + isolation: isolate; + display: flex; + flex-direction: column; + min-height: 220px; + overflow: hidden; + background: var(--widget-shell-bg); + color: var(--text-primary); } -@keyframes spin { +.mcp-widget-shell-graph { + min-height: 420px; + background: var(--widget-shell-graph-bg); +} + +.mcp-widget-content { + position: relative; + z-index: 1; +} + +.mcp-widget-shell-graph .mcp-widget-content { + flex: 1; + min-height: inherit; +} + +/* ── Brand header — compact inline wordmark with a hairline divider ───────── */ +.mcp-widget-brand { + display: flex; + align-items: center; + gap: var(--space-2); + margin: var(--space-3) var(--page-header-px) 0; + padding-bottom: var(--space-3); + border-bottom: 1px solid var(--border-muted); +} + +.mcp-widget-brand-mark { + display: grid; + width: 18px; + height: 18px; + place-items: center; + color: var(--accent); +} + +.mcp-widget-brand-mark svg { + width: 15px; + height: 12px; +} + +.mcp-widget-brand-copy { + display: flex; + align-items: baseline; + gap: var(--space-2); + min-width: 0; +} + +.mcp-widget-brand-name { + color: var(--text-primary); + font-size: 13px; + font-weight: 600; + letter-spacing: -0.02em; + line-height: 1; +} + +.mcp-widget-brand-mode { + color: var(--text-muted); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + line-height: 1; +} + +/* ── Workspace picker — a framed, scrollable list of rows ─────────────────── */ +.workspace-picker-grid { + display: flex; + flex-direction: column; + max-height: 320px; + overflow-y: auto; + overflow-x: hidden; + border: 1px solid var(--card-border); + border-radius: var(--radius-lg); + background: var(--card-bg); + /* Hide scrollbar chrome — the framed list keeps a stable height. */ + scrollbar-width: none; +} + +.workspace-picker-grid::-webkit-scrollbar { + display: none; +} + +.workspace-card { + position: relative; + display: block; + width: 100%; + padding: var(--space-3) var(--space-3); + text-align: left; + background: transparent; + border: 0; + cursor: pointer; + transition: background-color 0.15s ease; +} + +.workspace-card:not(:last-child) { + border-bottom: 1px solid var(--border-muted); +} + +.workspace-card:hover { + background: var(--card-bg-hover); +} + +.workspace-card:focus-visible { + outline: none; + background: var(--card-bg-hover); + box-shadow: inset 0 0 0 2px var(--accent-ring); +} + +/* Active workspace is marked by an accent-tinted row + accent-colored name + * (see WorkspaceCard) — no rail, no check, so the trailing badge stays aligned. */ +.workspace-card[data-active="true"] { + background: var(--card-active-bg); +} + +.workspace-card-inner { + display: flex; + align-items: center; + gap: var(--space-3); + min-width: 0; +} + +.workspace-card-main { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + min-width: 0; +} + +.workspace-card-title { + min-width: 0; +} + +.workspace-card-trailing { + display: flex; + align-items: center; + gap: var(--space-3); + flex-shrink: 0; +} + +/* ── Loading — draw-in wordmark, no glow ──────────────────────────────────── */ +.mcp-widget-content:has(.mcp-widget-loading) { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.mcp-widget-loading { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + min-height: 180px; +} + +.super-loader { + display: inline-flex; + min-width: calc(var(--super-loader-size, 42px) + 10px); + align-items: center; + gap: var(--space-2); + color: var(--accent); +} + +.super-loader-mark { + width: calc(var(--super-loader-size, 42px) * 0.5); + height: calc(var(--super-loader-size, 42px) * 0.5); + flex-shrink: 0; + overflow: visible; +} + +.super-loader-path { + fill: none; + stroke: currentColor; + stroke-width: 1.4; + stroke-linecap: round; + stroke-linejoin: round; + stroke-dasharray: 1; + stroke-dashoffset: 1; + opacity: 0.2; + animation: super-loader-draw 0.9s ease-in-out infinite alternate; +} + +.super-loader-path-left { + animation-delay: 0.18s; +} + +.super-loader-label { + color: var(--text-muted); + font-size: calc(var(--super-loader-size, 42px) * 0.25); + font-weight: 500; + white-space: nowrap; +} + +@keyframes super-loader-draw { + from { + stroke-dashoffset: 1; + opacity: 0.2; + } + to { - transform: rotate(360deg); + stroke-dashoffset: 0; + opacity: 1; } } -/* The graph canvas needs a definite height in both modes. +@media (prefers-reduced-motion: reduce) { + .super-loader-path { + stroke-dashoffset: 0; + opacity: 0.7; + animation: none; + } +} + +/* ── Graph canvas ─────────────────────────────────────────────────────────── */ +/* The graph needs a definite height in both modes. * * Inline: derive height from the (stable) width via aspect-ratio — width is - * fixed by the chat column, so this breaks the autoResize feedback loop. + * fixed by the chat column, so this doesn't feed the autoResize loop. * * Fullscreen: the host expands the iframe, so 100dvh = the fullscreen height. - * We stay IN FLOW (no position:fixed) — taking the element out of flow - * collapses the document, autoResize shrinks the iframe, and a fixed inset:0 - * box in a collapsed iframe ends up with zero height (and never recovers on - * minimize). Keeping it in flow with an explicit height avoids that entirely. */ + * We stay IN FLOW (no position:fixed) — taking the element out of flow collapses + * the document, autoResize shrinks the iframe, and a fixed inset:0 box in a + * collapsed iframe ends up with zero height. Keeping it in flow with an explicit + * height avoids that. */ .graph-view { width: 100%; - aspect-ratio: 4 / 3; - /* Never collapse to 0 during a resize/transition — a 0-size frame makes the - * package drop its "already fitted" flag and re-fit (a camera animation that - * reads as a re-layout). The floor keeps the size continuous. */ - min-height: 280px; + aspect-ratio: 16 / 9; + /* Never collapse to 0 during a resize — a 0-size frame makes the package + * re-fit (a camera animation that reads as a re-layout). */ + min-height: 420px; position: relative; - background: var(--bg-primary); + background: transparent; } .graph-view.fullscreen { diff --git a/apps/mcp/src/widget/design/tokens.css b/apps/mcp/src/widget/design/tokens.css index 815f303f..221e3130 100644 --- a/apps/mcp/src/widget/design/tokens.css +++ b/apps/mcp/src/widget/design/tokens.css @@ -1,64 +1,80 @@ -/* Design tokens for Supermemory MCP widget. +/* Design tokens for the Supermemory MCP widget. * - * Mirrors console-v2's tokens 1:1 so brand identity is consistent across - * surfaces. Diverge only when the widget's iframed context demands it. + * The widget renders inside a host chat surface (Cursor, Claude, ChatGPT). It + * should read as a native extension of the Supermemory console, whose language + * is: flat cool-neutral surfaces, a signature recessed inset-shadow on inputs, + * one restrained blue accent, and DM Sans. No gradients, no glows, no grids. * - * Dark mode: MCP host applies `data-theme="dark"` on via - * `applyDocumentTheme`. Tailwind's `dark:` variant is wired in globals.css - * via `@custom-variant dark (&:is([data-theme="dark"] *))`. + * The host applies `data-theme="light" | "dark"` on the document via ext-apps' + * `applyDocumentTheme`; Tailwind's `dark:` variant is wired in globals.css via + * `@custom-variant dark (&:is([data-theme="dark"] *))`. When no host is present + * (standalone / Studio) we fall back to the OS preference. */ +/* ── Light (default) ──────────────────────────────────────────────────────── */ :root, [data-theme="light"] { color-scheme: light; - /* Accent gradient (Supermemory brand) */ - --accent-start: #1148f7; - --accent-mid: #117dff; - --accent-end: #cdf4ff; - --accent-gradient: linear-gradient( - 135deg, - var(--accent-start), - var(--accent-mid), - var(--accent-end) - ); - - /* Accent solid */ - --accent: #117dff; + /* Accent — one restrained blue. Deeper than the dark accent so it stays + * legible as text/border on white. */ + --accent: #2e6fe6; --accent-foreground: #ffffff; - --accent-hover: #1148f7; - --accent-muted: #cdf4ff; + --accent-hover: #2560d0; + --accent-muted: rgba(46, 111, 230, 0.1); + --accent-ring: rgba(46, 111, 230, 0.35); - /* Backgrounds (warm subtle) */ - --bg-primary: #faf9f4; - --bg-secondary: #f3f1ec; - --bg-muted: #e8e5e0; + /* Surfaces — cool light neutrals. The shell is the widget's own canvas; + * cards/panels sit a step brighter (white) so borders + shadow read. */ + --app-bg: #eef1f6; + --widget-shell-bg: #f5f7fa; + --widget-shell-graph-bg: #f5f7fa; + + --bg-primary: #f5f7fa; + --bg-secondary: #eef1f6; + --bg-muted: #e7ebf1; --bg-elevated: #ffffff; - --bg-overlay: rgba(250, 249, 244, 0.7); + --bg-overlay: rgba(245, 247, 250, 0.8); + --bg-panel: #ffffff; + --bg-control: #ffffff; + --bg-control-hover: #f4f6fa; - /* Borders */ - --border: #e3e0db; - --border-muted: #f0ede8; - --border-accent: var(--accent); + --border: #e2e7ef; + --border-muted: #edf0f5; + --border-control: #dce2ea; + --border-accent: rgba(46, 111, 230, 0.45); - /* Text */ - --text-primary: #0a0a0a; - --text-secondary: #525252; - --text-muted: #a3a3a3; + --card-bg: #ffffff; + --card-bg-hover: #f6f8fb; + --card-border: #e2e7ef; + --card-border-hover: #d2dae5; + --card-active-bg: rgba(46, 111, 230, 0.06); + --card-active-border: rgba(46, 111, 230, 0.4); + + --panel-bg: #ffffff; + --panel-border: #e2e7ef; + --panel-shadow: + 0 1px 2px rgba(15, 23, 42, 0.04), 0 10px 26px rgba(15, 23, 42, 0.05); + + /* Signature recessed shadow — subtle in light so fields read as inset + * without going muddy. */ + --shadow-inset: inset 0 1px 2px rgba(15, 23, 42, 0.06); + --shadow-inset-strong: inset 1px 1px 3px rgba(15, 23, 42, 0.08); + + --text-primary: #0f1729; + --text-secondary: #55617a; + --text-muted: #8b95a7; --text-inverse: #ffffff; - /* Status colors (soft pastel) */ - --success: #4ade80; - --success-muted: rgba(74, 222, 128, 0.18); - --error: #f87171; - --error-muted: rgba(248, 113, 113, 0.18); - --warning: #fbbf24; - --warning-muted: rgba(251, 191, 36, 0.18); - --info: #60a5fa; - --info-muted: rgba(96, 165, 250, 0.18); - - /* Action colors */ - --danger: #ef4444; + --success: #15935a; + --success-muted: rgba(21, 147, 90, 0.12); + --error: #d64545; + --error-muted: rgba(214, 69, 69, 0.12); + --warning: #b7791f; + --warning-muted: rgba(183, 121, 31, 0.12); + --info: #2e6fe6; + --info-muted: rgba(46, 111, 230, 0.12); + --danger: #d64545; /* Spacing scale (4px base) */ --space-1: 4px; @@ -72,11 +88,11 @@ --space-12: 48px; --space-16: 64px; - /* Typography */ - --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif; - --font-display: - "Space Grotesk", "Inter", ui-sans-serif, system-ui, sans-serif; - --font-brand: "Space Grotesk", "Inter", ui-sans-serif, system-ui, sans-serif; + /* Typography — DM Sans throughout, mirroring the console. */ + --font-sans: + "DM Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + --font-display: var(--font-sans); + --font-brand: var(--font-sans); --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; @@ -94,13 +110,13 @@ --font-bold: 700; --leading-tight: 1.25; - --leading-normal: 1.5; - --leading-relaxed: 1.75; + --leading-normal: 1.4; + --leading-relaxed: 1.55; /* Unified component heights */ --height-xs: 28px; - --height-sm: 32px; - --height-md: 36px; + --height-sm: 34px; + --height-md: 38px; --height-lg: 44px; /* Icon sizes */ @@ -109,169 +125,242 @@ --icon-md: 20px; --icon-lg: 24px; - /* Radius */ - --radius-sm: 3px; - --radius-md: 4px; - --radius-lg: 6px; + /* Radius — a touch rounder than the console for a consumer feel. */ + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; --radius-full: 9999px; - /* Border width */ --border-width: 1px; - /* Page layout (mirrors console-v2 — widget iframes are narrow so we - * use the mobile values throughout: 16px horizontal, 16-24px vertical). */ + /* Page layout — widget iframes are narrow, so use compact values. */ --page-header-px: var(--space-4); --page-header-py: var(--space-4); --page-title-size: var(--text-lg); --page-title-weight: var(--font-semibold); - --page-title-mt: var(--space-5); + --page-title-mt: var(--space-4); - /* Shadows */ - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --shadow-md: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); + /* Panel shadow family (also exposed to Tailwind as shadow-sm/md/lg). */ + --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06); + --shadow-md: 0 6px 18px rgba(15, 23, 42, 0.08); + --shadow-lg: 0 18px 40px rgba(15, 23, 42, 0.12); /* Host-provided iframe dimensions; fallback for standalone dev. */ --host-height: 100vh; --host-width: 100vw; - /* Memory graph canvas — read by @supermemory/memory-graph's useGraphTheme. - * Mirrors console-v2 so the widget graph matches the console exactly. */ - --graph-bg: var(--bg-secondary); - --graph-doc-fill: #e8e5e0; - --graph-doc-stroke: #d4d0cb; - --graph-doc-inner: #f3f1ec; - --graph-mem-fill: #dbeafe; - --graph-mem-fill-hover: #bfdbfe; - --graph-mem-stroke: #3b82f6; - --graph-accent: var(--accent); - --graph-text-primary: var(--text-primary); - --graph-text-secondary: var(--text-secondary); - --graph-text-muted: var(--text-muted); - /* Edge / legend colors — bright + "hot" so derives/updates/extends read - * clearly against the light canvas (dark mode keeps its own hot set below). */ - --graph-edge-derives: #f59e0b; - --graph-edge-updates: #8b5cf6; - --graph-edge-extends: #0ea5e9; - --graph-mem-border-forgotten: #dc2626; - --graph-mem-border-expiring: #d97706; - --graph-mem-border-recent: #059669; - --graph-glow: #3b82f6; - --graph-icon: #6b7280; - --graph-popover-bg: var(--bg-elevated); - --graph-popover-border: var(--border); - --graph-popover-text-primary: var(--text-primary); - --graph-popover-text-secondary: var(--text-secondary); - --graph-popover-text-muted: var(--text-muted); - --graph-control-bg: var(--bg-elevated); - --graph-control-border: var(--border); + /* Memory graph canvas — read by @supermemory/memory-graph's theme. + * Neutral fills, one accent, semantic (not decorative) relation edges. */ + --graph-bg: transparent; + --graph-doc-fill: #f2f5f9; + --graph-doc-stroke: #dce2ea; + --graph-doc-inner: #ffffff; + --graph-mem-fill: #eaf1fc; + --graph-mem-fill-hover: #dde9fb; + --graph-mem-stroke: #2e6fe6; + --graph-accent: #2e6fe6; + --graph-text-primary: #0f1729; + --graph-text-secondary: #55617a; + --graph-text-muted: #8b95a7; + --graph-edge-derives: #b0b7c3; + --graph-edge-updates: #2e6fe6; + --graph-edge-extends: #0e9488; + --graph-mem-border-forgotten: #d64545; + --graph-mem-border-expiring: #b7791f; + --graph-mem-border-recent: #15935a; + --graph-glow: rgba(46, 111, 230, 0.35); + --graph-icon: #8b95a7; + --graph-popover-bg: #ffffff; + --graph-popover-border: #e2e7ef; + --graph-popover-text-primary: #0f1729; + --graph-popover-text-secondary: #55617a; + --graph-popover-text-muted: #8b95a7; + --graph-control-bg: #ffffff; + --graph-control-border: #e2e7ef; } +/* ── Dark ─────────────────────────────────────────────────────────────────── */ [data-theme="dark"] { color-scheme: dark; - --bg-primary: #0a0a0a; - --bg-secondary: #171717; - --bg-muted: #262626; - --bg-elevated: #1c1c1c; - --bg-overlay: rgba(10, 10, 10, 0.7); + --accent: #4ba0fa; + --accent-foreground: #0a0f16; + --accent-hover: #66b0ff; + --accent-muted: rgba(75, 160, 250, 0.14); + --accent-ring: rgba(75, 160, 250, 0.4); - --border: #2e2e2e; - --border-muted: #1f1f1f; + --app-bg: #080c11; + --widget-shell-bg: #0b1119; + --widget-shell-graph-bg: #0b1119; - --text-primary: #fafafa; - --text-secondary: #a3a3a3; - --text-muted: #8a8a8a; - --text-inverse: #0a0a0a; + --bg-primary: #0b1119; + --bg-secondary: #0e1620; + --bg-muted: #17212f; + --bg-elevated: #101822; + --bg-overlay: rgba(11, 17, 25, 0.8); + --bg-panel: #101822; + --bg-control: #0d141d; + --bg-control-hover: #131d2a; - --success-muted: rgba(74, 222, 128, 0.2); - --error-muted: rgba(248, 113, 113, 0.2); - --warning-muted: rgba(251, 191, 36, 0.2); - --info-muted: rgba(96, 165, 250, 0.2); + --border: #1f2b3b; + --border-muted: #172230; + --border-control: #263141; + --border-accent: rgba(75, 160, 250, 0.4); - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); - --shadow-md: - 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.4); + --card-bg: #101822; + --card-bg-hover: #131d2a; + --card-border: #1f2b3b; + --card-border-hover: #2b3a4e; + --card-active-bg: rgba(75, 160, 250, 0.1); + --card-active-border: rgba(75, 160, 250, 0.45); - /* Memory graph canvas — dark (mirrors console-v2) */ - --graph-bg: #0f1419; - --graph-doc-fill: #1b1f24; - --graph-doc-stroke: #2a2f36; - --graph-doc-inner: #13161a; - --graph-mem-fill: #0d2034; - --graph-mem-fill-hover: #112840; - --graph-mem-stroke: #3b73b8; - --graph-edge-derives: #fbbf24; - --graph-edge-updates: #a78bfa; - --graph-edge-extends: #38bdf8; - --graph-mem-border-forgotten: #ef4444; - --graph-mem-border-expiring: #f59e0b; - --graph-mem-border-recent: #10b981; - --graph-glow: #3b73b8; - --graph-icon: #3b73b8; - --graph-popover-bg: #0c1829; - --graph-popover-border: #1a2333; - --graph-popover-text-primary: #ffffff; - --graph-popover-text-secondary: #e2e8f0; - --graph-popover-text-muted: #94a3b8; - --graph-control-bg: #0c1829; - --graph-control-border: #1a2333; + --panel-bg: #101822; + --panel-border: #1f2b3b; + --panel-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 1px 2px rgba(0, 0, 0, 0.4), + 0 14px 34px rgba(0, 0, 0, 0.35); + + /* Signature inset — the console's recessed emboss. */ + --shadow-inset: inset 2px 2px 5px rgba(4, 8, 13, 0.55); + --shadow-inset-strong: inset 2.4px 2.4px 4.5px rgba(6, 10, 16, 0.7); + + --text-primary: #eef2f7; + --text-secondary: #a9b4c2; + --text-muted: #6f7d8f; + --text-inverse: #0b1119; + + --success: #34d399; + --success-muted: rgba(52, 211, 153, 0.14); + --error: #f87171; + --error-muted: rgba(248, 113, 113, 0.14); + --warning: #fbbf24; + --warning-muted: rgba(251, 191, 36, 0.14); + --info: #4ba0fa; + --info-muted: rgba(75, 160, 250, 0.14); + --danger: #f87171; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35); + --shadow-md: 0 6px 18px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 18px 44px rgba(0, 0, 0, 0.5); + + --graph-bg: transparent; + --graph-doc-fill: #131d2a; + --graph-doc-stroke: #2b3a4e; + --graph-doc-inner: #0d141d; + --graph-mem-fill: #122437; + --graph-mem-fill-hover: #172d44; + --graph-mem-stroke: #4ba0fa; + --graph-accent: #4ba0fa; + --graph-text-primary: #eef2f7; + --graph-text-secondary: #a9b4c2; + --graph-text-muted: #6f7d8f; + --graph-edge-derives: #5b6675; + --graph-edge-updates: #4ba0fa; + --graph-edge-extends: #2dd4bf; + --graph-mem-border-forgotten: #f87171; + --graph-mem-border-expiring: #fbbf24; + --graph-mem-border-recent: #34d399; + --graph-glow: rgba(75, 160, 250, 0.45); + --graph-icon: #6f7d8f; + --graph-popover-bg: #101822; + --graph-popover-border: #1f2b3b; + --graph-popover-text-primary: #eef2f7; + --graph-popover-text-secondary: #a9b4c2; + --graph-popover-text-muted: #6f7d8f; + --graph-control-bg: #101822; + --graph-control-border: #1f2b3b; } +/* ── Standalone / no host: follow the OS preference ───────────────────────── */ @media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) { + :root:not([data-theme="light"]):not([data-theme="dark"]) { color-scheme: dark; - --bg-primary: #0a0a0a; - --bg-secondary: #171717; - --bg-muted: #262626; - --bg-elevated: #1c1c1c; - --bg-overlay: rgba(10, 10, 10, 0.7); + --accent: #4ba0fa; + --accent-foreground: #0a0f16; + --accent-hover: #66b0ff; + --accent-muted: rgba(75, 160, 250, 0.14); + --accent-ring: rgba(75, 160, 250, 0.4); - --border: #2e2e2e; - --border-muted: #1f1f1f; + --app-bg: #080c11; + --widget-shell-bg: #0b1119; + --widget-shell-graph-bg: #0b1119; - --text-primary: #fafafa; - --text-secondary: #a3a3a3; - --text-muted: #8a8a8a; - --text-inverse: #0a0a0a; + --bg-primary: #0b1119; + --bg-secondary: #0e1620; + --bg-muted: #17212f; + --bg-elevated: #101822; + --bg-overlay: rgba(11, 17, 25, 0.8); + --bg-panel: #101822; + --bg-control: #0d141d; + --bg-control-hover: #131d2a; - --success-muted: rgba(74, 222, 128, 0.2); - --error-muted: rgba(248, 113, 113, 0.2); - --warning-muted: rgba(251, 191, 36, 0.2); - --info-muted: rgba(96, 165, 250, 0.2); + --border: #1f2b3b; + --border-muted: #172230; + --border-control: #263141; + --border-accent: rgba(75, 160, 250, 0.4); - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); - --shadow-md: - 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.4); + --card-bg: #101822; + --card-bg-hover: #131d2a; + --card-border: #1f2b3b; + --card-border-hover: #2b3a4e; + --card-active-bg: rgba(75, 160, 250, 0.1); + --card-active-border: rgba(75, 160, 250, 0.45); - --graph-bg: #0f1419; - --graph-doc-fill: #1b1f24; - --graph-doc-stroke: #2a2f36; - --graph-doc-inner: #13161a; - --graph-mem-fill: #0d2034; - --graph-mem-fill-hover: #112840; - --graph-mem-stroke: #3b73b8; - --graph-edge-derives: #fbbf24; - --graph-edge-updates: #a78bfa; - --graph-edge-extends: #38bdf8; - --graph-mem-border-forgotten: #ef4444; - --graph-mem-border-expiring: #f59e0b; - --graph-mem-border-recent: #10b981; - --graph-glow: #3b73b8; - --graph-icon: #3b73b8; - --graph-popover-bg: #0c1829; - --graph-popover-border: #1a2333; - --graph-popover-text-primary: #ffffff; - --graph-popover-text-secondary: #e2e8f0; - --graph-popover-text-muted: #94a3b8; - --graph-control-bg: #0c1829; - --graph-control-border: #1a2333; + --panel-bg: #101822; + --panel-border: #1f2b3b; + --panel-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 1px 2px rgba(0, 0, 0, 0.4), + 0 14px 34px rgba(0, 0, 0, 0.35); + + --shadow-inset: inset 2px 2px 5px rgba(4, 8, 13, 0.55); + --shadow-inset-strong: inset 2.4px 2.4px 4.5px rgba(6, 10, 16, 0.7); + + --text-primary: #eef2f7; + --text-secondary: #a9b4c2; + --text-muted: #6f7d8f; + --text-inverse: #0b1119; + + --success: #34d399; + --success-muted: rgba(52, 211, 153, 0.14); + --error: #f87171; + --error-muted: rgba(248, 113, 113, 0.14); + --warning: #fbbf24; + --warning-muted: rgba(251, 191, 36, 0.14); + --info: #4ba0fa; + --info-muted: rgba(75, 160, 250, 0.14); + --danger: #f87171; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35); + --shadow-md: 0 6px 18px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 18px 44px rgba(0, 0, 0, 0.5); + + --graph-bg: transparent; + --graph-doc-fill: #131d2a; + --graph-doc-stroke: #2b3a4e; + --graph-doc-inner: #0d141d; + --graph-mem-fill: #122437; + --graph-mem-fill-hover: #172d44; + --graph-mem-stroke: #4ba0fa; + --graph-accent: #4ba0fa; + --graph-text-primary: #eef2f7; + --graph-text-secondary: #a9b4c2; + --graph-text-muted: #6f7d8f; + --graph-edge-derives: #5b6675; + --graph-edge-updates: #4ba0fa; + --graph-edge-extends: #2dd4bf; + --graph-mem-border-forgotten: #f87171; + --graph-mem-border-expiring: #fbbf24; + --graph-mem-border-recent: #34d399; + --graph-glow: rgba(75, 160, 250, 0.45); + --graph-icon: #6f7d8f; + --graph-popover-bg: #101822; + --graph-popover-border: #1f2b3b; + --graph-popover-text-primary: #eef2f7; + --graph-popover-text-secondary: #a9b4c2; + --graph-popover-text-muted: #6f7d8f; + --graph-control-bg: #101822; + --graph-control-border: #1f2b3b; } } diff --git a/apps/mcp/src/widget/design/ui/Button.tsx b/apps/mcp/src/widget/design/ui/Button.tsx index c641215c..d1a42fc7 100644 --- a/apps/mcp/src/widget/design/ui/Button.tsx +++ b/apps/mcp/src/widget/design/ui/Button.tsx @@ -3,43 +3,47 @@ import { type ButtonHTMLAttributes, forwardRef, type ReactNode } from "react" import { Loader2 } from "../../lib/icons" import { cn } from "../lib/cn" -// Mirrors console-v2's button: uppercase + tracked, brand-font by default, -// CVA primary/secondary/ghost/danger × sm/icon. We skip `asChild` because the -// widget has no router links and dropping it saves a @radix-ui/react-slot dep. +// Token-driven, theme-aware pill controls. Primary is a high-contrast slab that +// inverts per theme (dark-on-light / light-on-dark); secondary is a neutral, +// input-like surface so it never reads as a heavy block on light backgrounds. const buttonVariants = cva( [ "inline-flex items-center justify-center gap-2", - "uppercase tracking-[0.075em]", - "rounded-[var(--radius-md)]", - "transition-colors cursor-pointer", + "font-semibold whitespace-nowrap", + "rounded-full", + "transition-colors duration-150 cursor-pointer", "disabled:pointer-events-none disabled:opacity-50", - "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)]/20 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)]", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-ring)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-primary)]", "[&_svg:not([class*='size-'])]:size-4 shrink-0", ].join(" "), { variants: { variant: { + // Nova's "insideOut" primary button: a near-black pill with the + // shadow-inside-out recessed inset. Same in both themes, exactly as + // the console renders it. primary: [ - "bg-[var(--accent)] text-[var(--accent-foreground)]", - "hover:bg-[var(--accent)]/90", + "bg-[#0D121A] text-[#FAFAFA]", + "shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]", + "hover:bg-[#121820] active:bg-[#0A0E14]", ].join(" "), secondary: [ - "bg-transparent text-[var(--text-primary)]", - "border border-[var(--border)]", - "hover:bg-[var(--bg-muted)]", + "bg-[var(--bg-control)] text-[var(--text-primary)]", + "border border-[var(--border-control)]", + "hover:bg-[var(--bg-control-hover)] hover:border-[var(--card-border-hover)]", ].join(" "), ghost: [ - "text-[var(--text-primary)]", - "hover:bg-[var(--bg-muted)]", + "text-[var(--text-secondary)]", + "hover:bg-[var(--bg-muted)] hover:text-[var(--text-primary)]", ].join(" "), danger: [ - "bg-[var(--danger)] text-[var(--text-inverse)]", - "hover:bg-[var(--danger)]/90", + "bg-[var(--error-muted)] text-[var(--error)]", + "hover:bg-[var(--error-muted)] hover:brightness-95", ].join(" "), }, size: { - sm: "h-[var(--height-sm)] px-[var(--space-4)] text-[length:var(--text-xs)]", - icon: "size-[var(--height-sm)] p-0", + sm: "h-9 px-4 text-[13px]", + icon: "size-8 p-0", }, }, defaultVariants: { @@ -93,7 +97,7 @@ export const Button = forwardRef( {shortcut && !loading ? ( & export type CardProps = DivProps | ButtonElementProps export const Card = forwardRef( - ({ className, variant, as = "div", ...props }, ref) => { + ({ className, variant, as = "div", children, ...props }, ref) => { const cls = cn(cardStyles({ variant }), className) if (as === "button") { return ( @@ -41,7 +46,9 @@ export const Card = forwardRef( ref={ref as React.Ref} type="button" {...(props as ButtonHTMLAttributes)} - /> + > + {children} + ) } return ( @@ -49,7 +56,9 @@ export const Card = forwardRef( className={cls} ref={ref as React.Ref} {...(props as HTMLAttributes)} - /> + > + {children} +
) }, ) diff --git a/apps/mcp/src/widget/design/ui/Chip.tsx b/apps/mcp/src/widget/design/ui/Chip.tsx index d9ccc7a7..89e5d811 100644 --- a/apps/mcp/src/widget/design/ui/Chip.tsx +++ b/apps/mcp/src/widget/design/ui/Chip.tsx @@ -9,7 +9,7 @@ const chipStyles = cva( selected: { true: "bg-accent-muted border-accent text-accent", false: - "bg-bg-elevated border-border text-text-secondary hover:border-border-accent hover:bg-bg-muted", + "bg-bg-elevated border-[var(--card-border)] text-text-secondary hover:border-[var(--card-border-hover)] hover:bg-bg-control-hover", }, }, defaultVariants: { selected: false }, diff --git a/apps/mcp/src/widget/design/ui/Field.tsx b/apps/mcp/src/widget/design/ui/Field.tsx index 7402d530..9f970922 100644 --- a/apps/mcp/src/widget/design/ui/Field.tsx +++ b/apps/mcp/src/widget/design/ui/Field.tsx @@ -14,7 +14,7 @@ export function Field({ label, hint, error, className, children }: Props) {
{label ? ( // biome-ignore lint/a11y/noLabelWithoutControl: generic field wrapper where children provide the input -
-
+
{/* ── Primitives ───────────────────────────────────────── */}
Selected Unselected - sm_project_marketing + Marketing
@@ -334,73 +349,86 @@ export function Studio() { {/* ── Views ────────────────────────────────────────────── */}
- + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + +
diff --git a/apps/mcp/src/widget/views/Confirmation.tsx b/apps/mcp/src/widget/views/Confirmation.tsx index 354fceba..f6a8c7f3 100644 --- a/apps/mcp/src/widget/views/Confirmation.tsx +++ b/apps/mcp/src/widget/views/Confirmation.tsx @@ -1,5 +1,6 @@ -import { Badge, Stack } from "../design/ui" -import { CheckCircle } from "../lib/icons" +import { WorkspaceChip } from "../components/WorkspaceChip" +import { Stack } from "../design/ui" +import { Check } from "../lib/icons" interface Props { containerTag: string @@ -9,19 +10,20 @@ export function Confirmation({ containerTag }: Props) { return ( - + + + -
+
Active workspace set
- {containerTag} + -

- All subsequent saves and recalls will use this workspace until you - change it. +

+ Saves and recalls will use this workspace until you change it.

) diff --git a/apps/mcp/src/widget/views/Error.tsx b/apps/mcp/src/widget/views/Error.tsx index 9f2cc82e..ae518773 100644 --- a/apps/mcp/src/widget/views/Error.tsx +++ b/apps/mcp/src/widget/views/Error.tsx @@ -9,11 +9,13 @@ export function ErrorView({ message }: Props) { return ( - -
+ + + +
Something went wrong

diff --git a/apps/mcp/src/widget/views/Graph.tsx b/apps/mcp/src/widget/views/Graph.tsx index 21ab8537..33925ee4 100644 --- a/apps/mcp/src/widget/views/Graph.tsx +++ b/apps/mcp/src/widget/views/Graph.tsx @@ -58,19 +58,25 @@ function readGraphColors(): GraphThemeColors { return out } -// Read the graph palette from CSS, and re-read after a theme switch. We can't -// rely on the package's own useGraphTheme: it watches the `class` attribute, -// but the MCP host themes via `data-theme`, applied in a passive effect that -// runs AFTER children render. requestAnimationFrame fires after that effect, -// so by then `data-theme` (hence the --graph-* values) is current. +// Read the graph palette from CSS, and re-read whenever the document theme +// flips. We can't rely on the package's own useGraphTheme (it watches `class`), +// nor on the `theme` prop alone: the host themes via `data-theme`, and in +// standalone/Studio the attribute can change without the prop changing. So we +// observe `data-theme`/`class` on directly and re-read after the next +// paint (rAF), by which point the --graph-* values are current. function useGraphColors(theme: string): GraphThemeColors { const [colors, setColors] = useState(readGraphColors) useEffect(() => { - // `theme` change is the trigger; re-read after the host applies - // data-theme (rAF fires after that passive effect). if (!theme) return - const id = requestAnimationFrame(() => setColors(readGraphColors())) - return () => cancelAnimationFrame(id) + const reread = () => + requestAnimationFrame(() => setColors(readGraphColors())) + reread() + const observer = new MutationObserver(reread) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-theme", "class"], + }) + return () => observer.disconnect() }, [theme]) return colors } @@ -151,6 +157,14 @@ export function Graph({ documents, totalCount }: Props) { // mid-session theme switch re-themes the canvas. const theme = (ctx?.theme as string | undefined) ?? "light" const colors = useGraphColors(theme) + const graphColors = useMemo>( + () => ({ + ...colors, + bg: "transparent", + edgeDerives: "#9ca3af", + }), + [colors], + ) const fullscreenSupported = useMemo(() => { const modes = ( @@ -186,16 +200,21 @@ export function Graph({ documents, totalCount }: Props) { return (

-
+
{/* No remount on toggle. The CSS just resizes the container; the package's ResizeObserver re-lays-out and its node cache keeps positions stable, so expand/minimize is instant with no reload. Theme is fed reactively via the colors prop. */}
{fullscreenSupported ? ( @@ -203,7 +222,7 @@ export function Graph({ documents, totalCount }: Props) { aria-label={ mode === "fullscreen" ? "Exit fullscreen" : "Enter fullscreen" } - className="absolute top-4 right-4 z-40 inline-flex items-center justify-center w-9 h-9 rounded-md bg-bg-elevated/80 backdrop-blur border border-border text-text-secondary hover:text-text-primary hover:bg-bg-muted transition-colors" + className="absolute top-3 right-3 z-40 inline-flex items-center justify-center w-9 h-9 rounded-full bg-bg-elevated/90 backdrop-blur border border-border text-text-secondary hover:text-text-primary hover:bg-bg-muted transition-colors" onClick={toggleFullscreen} title={mode === "fullscreen" ? "Exit fullscreen" : "Fullscreen"} type="button" diff --git a/apps/mcp/src/widget/views/Loading.tsx b/apps/mcp/src/widget/views/Loading.tsx index e5a03cd0..513498ab 100644 --- a/apps/mcp/src/widget/views/Loading.tsx +++ b/apps/mcp/src/widget/views/Loading.tsx @@ -1,7 +1,29 @@ export function Loading() { return ( -
-
+
+ + + Loading... +
) } diff --git a/apps/mcp/src/widget/views/Picker.tsx b/apps/mcp/src/widget/views/Picker.tsx index a6a137d0..e48883cc 100644 --- a/apps/mcp/src/widget/views/Picker.tsx +++ b/apps/mcp/src/widget/views/Picker.tsx @@ -8,7 +8,8 @@ import { WorkspaceCard } from "../components/WorkspaceCard" import { Input, PageHeader } from "../design/ui" import { useApp } from "../hooks/useApp" import { useLog } from "../hooks/useLog" -import { Search } from "../lib/icons" +import { formatTagLabel } from "../lib/formatTag" +import { Package, Search } from "../lib/icons" interface Props { containerTags: ContainerTag[] @@ -70,55 +71,68 @@ export function Picker({ const count = containerTags.length const description = count === 0 - ? "No workspaces available." - : `${count} workspace${count === 1 ? "" : "s"} available — select one to set your active context.` + ? "Create a workspace in Supermemory to get started." + : "Pick the workspace to save and recall from." return (
- -
- {count >= SEARCH_THRESHOLD ? ( -
- - setQuery(e.target.value)} - placeholder="Search workspaces…" - value={query} - /> -
- ) : null} - - {/* Bounded, scrollable list — keeps a stable height so filtering or a - long workspace list doesn't resize (jump) the whole widget. */} -
- {filtered.length === 0 ? ( -

- No workspaces match “{query}”. + +

+ {count === 0 ? ( +
+ +

+ No workspaces yet

- ) : ( -
- {filtered.map((tag) => { - const access = assignedTags?.find( - (t) => t.containerTag === tag.containerTag, - ) - return ( - - ) - })} -
- )} -
+

+ Workspaces you create in Supermemory show up here, ready to save + and recall from. +

+
+ ) : ( + <> + {count >= SEARCH_THRESHOLD ? ( +
+ + setQuery(e.target.value)} + placeholder="Search workspaces…" + value={query} + /> +
+ ) : null} + + {filtered.length === 0 ? ( +
+

+ No workspaces match “{query}”. +

+
+ ) : ( +
+ {filtered.map((tag) => { + const access = assignedTags?.find( + (t) => t.containerTag === tag.containerTag, + ) + return ( + + ) + })} +
+ )} + + )} {pending ? (

- Setting workspace to {pending}… + Setting workspace to {formatTagLabel(pending)}…

) : null}
diff --git a/apps/mcp/src/widget/views/Save.tsx b/apps/mcp/src/widget/views/Save.tsx index 83dd7211..e558c05b 100644 --- a/apps/mcp/src/widget/views/Save.tsx +++ b/apps/mcp/src/widget/views/Save.tsx @@ -11,6 +11,7 @@ import { } from "../design/ui" import { useApp } from "../hooks/useApp" import { useLog } from "../hooks/useLog" +import { formatTagLabel } from "../lib/formatTag" interface Props { activeTag?: string | null @@ -42,7 +43,12 @@ export function Save({ }, [selectedTag, writableTags]) const options = useMemo( - () => writableTags.map((tag) => ({ value: tag, label: tag })), + () => + writableTags.map((tag) => ({ + value: tag, + label: formatTagLabel(tag), + description: tag, + })), [writableTags], ) @@ -84,39 +90,41 @@ export function Save({ title="Add Memory" />
- - -