style(mcp): align widget UI with Nova theme

This commit is contained in:
ishaanxgupta 2026-07-01 20:26:57 +05:30 committed by ved015
parent 99a8adaab5
commit dd5451e361
16 changed files with 308 additions and 36 deletions

View file

@ -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,56 @@ export function App() {
}
}, [state, log])
if (state.kind === "loading") return <Loading />
if (state.kind === "error") return <ErrorView message={state.message} />
if (state.kind === "loading") {
return (
<WidgetShell>
<Loading />
</WidgetShell>
)
}
if (state.kind === "error") {
return (
<WidgetShell>
<ErrorView message={state.message} />
</WidgetShell>
)
}
if (state.kind === "raw") {
return (
<ErrorView message="Received unrecognized response from server. Try again." />
<WidgetShell>
<ErrorView message="Received unrecognized response from server. Try again." />
</WidgetShell>
)
}
return renderView(state.message, setView, setError)
return <WidgetShell>{renderView(state.message, setView, setError)}</WidgetShell>
}
function WidgetShell({ children }: { children: ReactNode }) {
return (
<div className="mcp-widget-shell">
<div aria-hidden className="mcp-widget-glow" />
<header aria-label="supermemory" className="mcp-widget-brand">
<span aria-hidden className="mcp-widget-brand-mark">
<svg role="img" viewBox="0 0 314 256">
<path
d="M313.728 100.982H197.297V0H159.68V109.567C159.68 121.205 164.284 132.381 172.466 140.615L267.535 236.283L294.134 209.517L223.917 138.858H313.75V101.004L313.728 100.982Z"
fill="currentColor"
/>
<path
d="M19.616 46.5043L89.8323 117.163H0V155.017H116.431V255.999H154.048V146.432C154.048 134.795 149.444 123.618 141.262 115.384L46.2144 19.7383L19.616 46.5043Z"
fill="currentColor"
/>
</svg>
</span>
<span className="mcp-widget-brand-copy">
<span className="mcp-widget-brand-name">supermemory</span>
<span className="mcp-widget-brand-mode">MCP</span>
</span>
</header>
<main className="mcp-widget-content">{children}</main>
</div>
)
}
function renderView(

View file

@ -85,9 +85,17 @@
}
body {
background: var(--bg-primary);
background:
radial-gradient(
ellipse at 50% 112%,
rgba(75, 160, 250, 0.34) 0%,
rgba(20, 16, 255, 0.22) 28%,
rgba(11, 17, 25, 0) 62%
),
linear-gradient(180deg, #0b1119 0%, #070b11 100%);
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,14 +115,114 @@
position: relative;
}
.mcp-widget-shell {
position: relative;
isolation: isolate;
min-height: 280px;
overflow: hidden;
background:
linear-gradient(rgba(75, 160, 250, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(75, 160, 250, 0.035) 1px, transparent 1px),
radial-gradient(
circle at 50% 108%,
rgba(75, 160, 250, 0.22),
transparent 42%
),
linear-gradient(180deg, #0b1119 0%, #070b11 100%);
background-size:
36px 36px,
36px 36px,
100% 100%,
100% 100%;
color: var(--text-primary);
}
.mcp-widget-glow {
position: absolute;
right: -18%;
bottom: -42%;
left: -18%;
z-index: -1;
height: 70%;
pointer-events: none;
background: radial-gradient(
ellipse at 50% 48%,
rgba(255, 255, 255, 0.34) 0%,
rgba(71, 168, 253, 0.42) 11%,
rgba(0, 153, 255, 0.32) 28%,
rgba(20, 16, 255, 0.28) 48%,
transparent 74%
);
filter: blur(18px);
opacity: 0.9;
}
.mcp-widget-brand {
display: inline-flex;
align-items: center;
gap: var(--space-2);
margin: var(--space-4) var(--page-header-px) 0;
padding: 7px 10px 7px 8px;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: rgba(13, 18, 26, 0.78);
backdrop-filter: blur(18px);
box-shadow: var(--shadow-sm);
}
.mcp-widget-brand-mark {
display: grid;
width: 26px;
height: 26px;
place-items: center;
color: var(--accent);
border-radius: var(--radius-md);
background: #080b0f;
box-shadow:
inset 1.5px 1.5px 4.5px rgba(0, 0, 0, 0.7),
inset 0 1px 1px rgba(255, 255, 255, 0.03);
}
.mcp-widget-brand-mark svg {
width: 17px;
height: 14px;
}
.mcp-widget-brand-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 1px;
}
.mcp-widget-brand-name {
color: var(--text-primary);
font-size: 12px;
font-weight: 700;
line-height: 1;
}
.mcp-widget-brand-mode {
color: var(--text-muted);
font-size: 10px;
font-weight: 500;
line-height: 1.15;
}
.mcp-widget-content {
position: relative;
z-index: 1;
}
/* Spinner — kept as global because primitives can't easily express a CSS animation */
.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border);
border: 2px solid rgba(255, 255, 255, 0.12);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
box-shadow: 0 0 20px rgba(75, 160, 250, 0.42);
}
@keyframes spin {
@ -141,7 +249,20 @@
* reads as a re-layout). The floor keeps the size continuous. */
min-height: 280px;
position: relative;
background: var(--bg-primary);
background:
linear-gradient(rgba(75, 160, 250, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(75, 160, 250, 0.035) 1px, transparent 1px),
radial-gradient(
circle at 50% 108%,
rgba(75, 160, 250, 0.22),
transparent 42%
),
var(--bg-primary);
background-size:
36px 36px,
36px 36px,
100% 100%,
100% 100%;
}
.graph-view.fullscreen {

View file

@ -277,3 +277,103 @@
--graph-control-border: #1a2333;
}
}
/* Nova widget theme
*
* PR-1120's widget screens run inside agent cells, so the light/dark host
* theme can vary independently from the Supermemory product theme. Force the
* MCP app surfaces onto Nova's dark product palette so all widget tools match
* apps/web: DM Sans, dark cards, soft borders, and the #4ba0fa accent.
*/
:root,
[data-theme="light"],
[data-theme="dark"] {
color-scheme: dark;
--accent-start: #369bfd;
--accent-mid: #4ba0fa;
--accent-end: #36fdfd;
--accent-gradient: linear-gradient(
94deg,
var(--accent-start),
var(--accent-end)
);
--accent: #4ba0fa;
--accent-foreground: #ffffff;
--accent-hover: #369bfd;
--accent-muted: rgba(75, 160, 250, 0.12);
--bg-primary: #0b1119;
--bg-secondary: #101822;
--bg-muted: #131b28;
--bg-elevated: #1b1f24;
--bg-overlay: rgba(13, 18, 26, 0.78);
--border: rgba(255, 255, 255, 0.12);
--border-muted: rgba(255, 255, 255, 0.07);
--border-accent: rgba(34, 97, 202, 0.45);
--text-primary: #fafafa;
--text-secondary: #d0dae7;
--text-muted: #a0aec4;
--text-inverse: #0b1119;
--success: #4ade80;
--success-muted: rgba(74, 222, 128, 0.14);
--error: #fb7185;
--error-muted: rgba(251, 113, 133, 0.14);
--warning: #fbbf24;
--warning-muted: rgba(251, 191, 36, 0.14);
--info: #60a5fa;
--info-muted: rgba(96, 165, 250, 0.14);
--danger: #ef4444;
--font-sans:
"DM Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-display:
"DM Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-brand:
"DM Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--shadow-sm:
0 2px 10px rgba(0, 0, 0, 0.18),
inset 0 1px 1px rgba(255, 255, 255, 0.06);
--shadow-md:
0 10px 30px rgba(0, 0, 0, 0.28),
inset 0 1px 1px rgba(255, 255, 255, 0.08);
--shadow-lg:
0 20px 60px rgba(0, 0, 0, 0.42),
inset 0 1px 1px rgba(255, 255, 255, 0.08);
--graph-bg: #0b1119;
--graph-doc-fill: #1b1f24;
--graph-doc-stroke: rgba(255, 255, 255, 0.12);
--graph-doc-inner: #0d121a;
--graph-mem-fill: #0d2034;
--graph-mem-fill-hover: #112840;
--graph-mem-stroke: #4ba0fa;
--graph-accent: #4ba0fa;
--graph-text-primary: #fafafa;
--graph-text-secondary: #d0dae7;
--graph-text-muted: #a0aec4;
--graph-edge-derives: #fbbf24;
--graph-edge-updates: #4ba0fa;
--graph-edge-extends: #36fdfd;
--graph-mem-border-forgotten: #ef4444;
--graph-mem-border-expiring: #f59e0b;
--graph-mem-border-recent: #10b981;
--graph-glow: #4ba0fa;
--graph-icon: #4ba0fa;
--graph-popover-bg: rgba(27, 31, 36, 0.94);
--graph-popover-border: rgba(255, 255, 255, 0.12);
--graph-popover-text-primary: #fafafa;
--graph-popover-text-secondary: #d0dae7;
--graph-popover-text-muted: #a0aec4;
--graph-control-bg: rgba(13, 18, 26, 0.78);
--graph-control-border: rgba(255, 255, 255, 0.12);
}

View file

@ -9,9 +9,9 @@ import { cn } from "../lib/cn"
const buttonVariants = cva(
[
"inline-flex items-center justify-center gap-2",
"uppercase tracking-[0.075em]",
"font-semibold",
"rounded-[var(--radius-md)]",
"transition-colors cursor-pointer",
"transition-all 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)]",
"[&_svg:not([class*='size-'])]:size-4 shrink-0",
@ -21,12 +21,13 @@ const buttonVariants = cva(
variant: {
primary: [
"bg-[var(--accent)] text-[var(--accent-foreground)]",
"hover:bg-[var(--accent)]/90",
"shadow-[0_10px_28px_rgba(75,160,250,0.24)]",
"hover:bg-[var(--accent-hover)] hover:shadow-[0_12px_32px_rgba(75,160,250,0.32)]",
].join(" "),
secondary: [
"bg-transparent text-[var(--text-primary)]",
"bg-[var(--bg-overlay)] text-[var(--text-primary)]",
"border border-[var(--border)]",
"hover:bg-[var(--bg-muted)]",
"hover:bg-[var(--bg-muted)] hover:border-[var(--border-accent)]",
].join(" "),
ghost: [
"text-[var(--text-primary)]",
@ -38,7 +39,7 @@ const buttonVariants = cva(
].join(" "),
},
size: {
sm: "h-[var(--height-sm)] px-[var(--space-4)] text-[length:var(--text-xs)]",
sm: "h-[var(--height-md)] px-[var(--space-4)] text-[length:var(--text-sm)]",
icon: "size-[var(--height-sm)] p-0",
},
},

View file

@ -7,14 +7,18 @@ import {
import { cn } from "../lib/cn"
const cardStyles = cva(
"text-left bg-bg-elevated border border-border rounded-lg p-4 transition-colors duration-150",
[
"text-left bg-bg-elevated/90 border border-border rounded-lg p-4",
"shadow-md backdrop-blur transition-colors duration-150",
].join(" "),
{
variants: {
variant: {
default: "",
interactive:
"cursor-pointer hover:bg-bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/20 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary",
active: "cursor-pointer border-accent bg-accent-muted",
active:
"cursor-pointer border-border-accent bg-accent-muted shadow-[0_0_30px_rgba(75,160,250,0.16)]",
},
},
defaultVariants: { variant: "default" },

View file

@ -58,10 +58,10 @@ export function FileUpload({
"flex min-h-[180px] flex-col items-center justify-center gap-(--space-3)",
"py-(--space-12) px-(--space-6)",
"border-2 border-dashed rounded-(--radius-lg)",
"cursor-pointer transition-colors",
"bg-[var(--bg-overlay)] cursor-pointer shadow-md backdrop-blur transition-colors",
dragOver
? "border-accent bg-accent-muted/30"
: "border-border hover:bg-bg-muted/40",
? "border-accent bg-accent-muted"
: "border-border hover:bg-bg-muted/80",
disabled && "pointer-events-none opacity-50",
className,
)}

View file

@ -7,13 +7,13 @@ import { cn } from "../lib/cn"
const inputVariants = cva(
[
"flex w-full",
"bg-transparent text-[var(--text-primary)]",
"bg-[var(--bg-overlay)] text-[var(--text-primary)]",
"border border-[var(--border)]",
"rounded-[var(--radius-lg)]",
"placeholder:text-[var(--text-muted)]",
"transition-colors",
"hover:bg-[var(--text-muted)]/10",
"focus-visible:outline-none focus-visible:shadow-[0_0_0_1px_var(--border)]",
"hover:bg-[var(--bg-muted)]",
"focus-visible:outline-none focus-visible:border-[var(--border-accent)] focus-visible:shadow-[0_0_0_1px_var(--border-accent)]",
"disabled:cursor-not-allowed disabled:opacity-50",
"aria-invalid:border-[var(--error)] aria-invalid:ring-[var(--error)]",
"file:border-0 file:bg-transparent file:text-[length:var(--text-sm)] file:font-medium",

View file

@ -16,9 +16,9 @@ const PopoverContent = forwardRef<
align={align}
className={cn(
"z-50 w-72 p-(--space-4)",
"bg-bg-elevated border border-border",
"rounded-(--radius-md)",
"shadow-md",
"bg-bg-elevated/95 border border-border",
"rounded-(--radius-lg)",
"shadow-lg backdrop-blur",
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className,

View file

@ -6,15 +6,15 @@ import { cn } from "../lib/cn"
// controls min-height via className.
const textAreaClass = [
"flex w-full",
"bg-transparent text-[var(--text-primary)]",
"bg-[var(--bg-overlay)] text-[var(--text-primary)]",
"border border-[var(--border)]",
"rounded-[var(--radius-lg)]",
"px-[var(--space-3)] py-[var(--space-2)]",
"text-[length:var(--text-sm)] leading-normal font-sans",
"placeholder:text-[var(--text-muted)]",
"transition-colors resize-y",
"hover:bg-[var(--text-muted)]/10",
"focus-visible:outline-none focus-visible:shadow-[0_0_0_1px_var(--border)]",
"hover:bg-[var(--bg-muted)]",
"focus-visible:outline-none focus-visible:border-[var(--border-accent)] focus-visible:shadow-[0_0_0_1px_var(--border-accent)]",
"disabled:cursor-not-allowed disabled:opacity-50",
"aria-invalid:border-[var(--error)] aria-invalid:ring-[var(--error)]",
].join(" ")

View file

@ -6,7 +6,7 @@
<title>Supermemory MCP</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="app"></div>

View file

@ -6,7 +6,7 @@
<title>Supermemory MCP · Studio</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="studio"></div>

View file

@ -9,7 +9,7 @@ export function Confirmation({ containerTag }: Props) {
return (
<Stack
align="center"
className="px-(--page-header-px) py-(--space-10) text-center"
className="mx-(--page-header-px) my-(--space-6) rounded-lg border border-border bg-bg-elevated/80 px-(--page-header-px) py-(--space-10) text-center shadow-lg backdrop-blur"
gap="md"
>
<CheckCircle className="size-12 text-success" />

View file

@ -9,7 +9,7 @@ export function ErrorView({ message }: Props) {
return (
<Stack
align="center"
className="px-(--page-header-px) py-(--space-10) text-center"
className="mx-(--page-header-px) my-(--space-6) rounded-lg border border-border bg-bg-elevated/80 px-(--page-header-px) py-(--space-10) text-center shadow-lg backdrop-blur"
gap="md"
>
<WarningCircle className="size-12 text-error" />

View file

@ -185,8 +185,13 @@ export function Graph({ documents, totalCount }: Props) {
}, [mode, toggleFullscreen])
return (
<div className="relative">
<div className={cn("graph-view", mode === "fullscreen" && "fullscreen")}>
<div className="relative px-(--page-header-px) pb-(--space-6) pt-(--space-4)">
<div
className={cn(
"graph-view overflow-hidden rounded-lg border border-border shadow-lg",
mode === "fullscreen" && "fullscreen rounded-none border-0",
)}
>
{/* 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.

View file

@ -82,7 +82,7 @@ export function Picker({
{/* Bounded, scrollable list keeps a stable height so filtering or a
long workspace list doesn't resize (jump) the whole widget. */}
<div className="min-h-[220px] max-h-[60vh] overflow-y-auto pr-1">
<div className="min-h-[220px] max-h-[60vh] overflow-y-auto pr-1 scrollbar-thin">
{filtered.length === 0 ? (
<p className="py-(--space-6) text-center text-(length:--text-sm) text-text-muted">
No workspaces match {query}.

View file

@ -21,7 +21,7 @@ export function Success(props: Props) {
return (
<Stack
align="center"
className="px-(--page-header-px) py-(--space-10) text-center"
className="mx-(--page-header-px) my-(--space-6) rounded-lg border border-border bg-bg-elevated/80 px-(--page-header-px) py-(--space-10) text-center shadow-lg backdrop-blur"
gap="md"
>
<CheckCircle className="size-12 text-success" />