diff --git a/apps/mcp/src/widget/App.tsx b/apps/mcp/src/widget/App.tsx index 52d16d691..0d9f552a1 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,71 @@ 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( diff --git a/apps/mcp/src/widget/components/PermissionBadge.tsx b/apps/mcp/src/widget/components/PermissionBadge.tsx index ba8fc1abc..3ce84b9a4 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,11 @@ 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/WorkspaceCard.tsx b/apps/mcp/src/widget/components/WorkspaceCard.tsx index cd4122ef4..8af1697a6 100644 --- a/apps/mcp/src/widget/components/WorkspaceCard.tsx +++ b/apps/mcp/src/widget/components/WorkspaceCard.tsx @@ -1,6 +1,7 @@ import type { ContainerTag, ContainerTagAccess } from "../../shared/types" -import { Card, Stack } from "../design/ui" -import { Check, Package } from "../lib/icons" +import { Card } from "../design/ui" +import { Check } from "../lib/icons" +import { formatTagLabel } from "../lib/formatTag" import { PermissionBadge } from "./PermissionBadge" interface Props { @@ -16,51 +17,52 @@ export function WorkspaceCard({ access, onClick, }: Props) { - const name = containerTag.name || containerTag.containerTag + const name = containerTag.name || formatTagLabel(containerTag.containerTag) return ( onClick(containerTag.containerTag)} variant={active ? "active" : "interactive"} > - - - - {containerTag.emoji ? ( - - {containerTag.emoji} - - ) : ( - - )} +
+
+
{name} - - {active ? : null} - -
- {containerTag.containerTag} + {active ? ( + + ) : null} +
+ {(containerTag.documentCount > 0 || containerTag.memoryCount > 0) && ( +
+ + {containerTag.documentCount} doc + {containerTag.documentCount === 1 ? "" : "s"} + + + · + + + {containerTag.memoryCount}{" "} + {containerTag.memoryCount === 1 ? "memory" : "memories"} + +
+ )}
- {(containerTag.documentCount > 0 || containerTag.memoryCount > 0) && ( -
- - {containerTag.documentCount} doc - {containerTag.documentCount === 1 ? "" : "s"} - - · - - {containerTag.memoryCount} mem - {containerTag.memoryCount === 1 ? "" : "s"} - + {access ? ( +
+
- )} - {access ? : null} - + ) : null} +
) } diff --git a/apps/mcp/src/widget/design/globals.css b/apps/mcp/src/widget/design/globals.css index f159f5e5b..fff43ab51 100644 --- a/apps/mcp/src/widget/design/globals.css +++ b/apps/mcp/src/widget/design/globals.css @@ -14,6 +14,9 @@ --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); @@ -85,9 +88,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,14 +111,518 @@ position: relative; } +.mcp-widget-shell { + position: relative; + isolation: isolate; + min-height: 280px; + overflow: hidden; + background: var(--widget-shell-bg); + color: var(--text-primary); +} + +.mcp-widget-shell-graph { + min-height: 420px; + background: var(--widget-shell-graph-bg); +} + +.mcp-widget-shell-graph::before, +.mcp-widget-shell-graph::after { + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + content: ""; +} + +.mcp-widget-shell-graph::before { + background-image: radial-gradient( + circle at center, + rgba(105, 167, 240, 0.25) 1px, + transparent 1px + ); + background-size: 32px 32px; + mask-image: radial-gradient(ellipse at center, black 60%, transparent 100%); +} + +.mcp-widget-shell-graph::after { + background: + radial-gradient( + ellipse at 50% 112%, + rgba(75, 160, 250, 0.42) 0%, + rgba(20, 16, 255, 0.3) 34%, + transparent 66% + ), + linear-gradient(180deg, rgba(5, 8, 13, 0) 0%, rgba(5, 8, 13, 0.2) 100%); +} + +.mcp-widget-glow { + position: absolute; + right: -16%; + bottom: -44%; + left: -16%; + z-index: -1; + height: 68%; + pointer-events: none; + background: radial-gradient( + ellipse at 50% 42%, + var(--widget-glow-inner) 0%, + var(--widget-glow-mid) 22%, + var(--widget-glow-outer) 46%, + transparent 74% + ); + filter: blur(18px); + opacity: 0.82; +} + +/* Studio-only visual variants. Production widgets do not set this attribute. */ +[data-widget-version="v1"] .mcp-widget-shell { + --brand-accent: var(--accent); + --brand-opacity: 0.86; + --widget-glow-inner: rgba(255, 255, 255, 0.22); + --widget-glow-mid: rgba(71, 168, 253, 0.3); + --widget-glow-outer: rgba(20, 16, 255, 0.18); + background: + linear-gradient(rgba(75, 160, 250, 0.045) 1px, transparent 1px), + linear-gradient(90deg, rgba(75, 160, 250, 0.045) 1px, transparent 1px), + radial-gradient( + circle at 50% 108%, + rgba(75, 160, 250, 0.16), + transparent 42% + ), + var(--widget-shell-bg); + background-size: + 36px 36px, + 36px 36px, + 100% 100%, + 100% 100%; +} + +[data-widget-version="v1"] .super-loader, +[data-widget-version="v1"] .spinner { + color: var(--accent); +} + +[data-widget-version="v1"] .spinner { + border-top-color: var(--accent); + box-shadow: 0 0 20px rgba(75, 160, 250, 0.42); +} +[data-widget-version="v2"] .mcp-widget-shell { + --brand-accent: var(--accent); + --brand-opacity: 1; + --widget-shell-bg: color-mix( + in srgb, + var(--widget-shell-bg) 86%, + var(--bg-muted) + ); + --card-bg: color-mix(in srgb, var(--card-bg) 88%, var(--bg-muted)); + --panel-bg: color-mix(in srgb, var(--panel-bg) 88%, var(--bg-muted)); + --widget-glow-mid: color-mix( + in srgb, + var(--widget-glow-mid) 70%, + transparent + ); + --widget-glow-outer: color-mix( + in srgb, + var(--widget-glow-outer) 70%, + transparent + ); +} + +[data-widget-version="v2"] .mcp-widget-shell-graph::before { + background-image: + linear-gradient(rgba(105, 167, 240, 0.12) 1px, transparent 1px), + linear-gradient(90deg, rgba(105, 167, 240, 0.12) 1px, transparent 1px); + background-size: 32px 32px; + mask-image: radial-gradient(ellipse at center, black 60%, transparent 100%); +} + +[data-theme="light"] [data-widget-version="v2"] .mcp-widget-shell-graph::before, +:root:not([data-theme="dark"]) + [data-widget-version="v2"] + .mcp-widget-shell-graph::before { + background-image: + linear-gradient(rgba(75, 160, 250, 0.045) 1px, transparent 1px), + linear-gradient(90deg, rgba(75, 160, 250, 0.045) 1px, transparent 1px); +} + +[data-theme="light"] [data-widget-version="v2"] .mcp-widget-shell-graph::after, +:root:not([data-theme="dark"]) + [data-widget-version="v2"] + .mcp-widget-shell-graph::after { + content: none; +} + +[data-theme="light"] [data-widget-version="v2"] .mcp-soft-button, +:root:not([data-theme="dark"]) [data-widget-version="v2"] .mcp-soft-button { + background: var(--button-secondary-bg); + color: var(--button-secondary-text); + border: 1px solid var(--button-secondary-border); + box-shadow: var(--shadow-sm); +} + +[data-theme="light"] [data-widget-version="v2"] .mcp-soft-button:hover, +:root:not([data-theme="dark"]) + [data-widget-version="v2"] + .mcp-soft-button:hover { + background: var(--button-secondary-hover); +} + +/* Card/panel glow — hidden by default, activated by v3/v4. */ +.card-glow, +.panel-glow { + display: none; +} + +.mcp-panel { + position: relative; + overflow: hidden; +} + +.mcp-panel > :not(.panel-glow) { + position: relative; + z-index: 1; +} + +/* Workspace picker cards — grid layout (default). */ +.workspace-card { + min-height: 104px; +} + +.workspace-card-inner { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + min-height: 56px; +} + +.workspace-card-main { + display: flex; + flex-direction: column; + gap: var(--space-1); + min-width: 0; + padding-right: 72px; +} + +.workspace-card-heading { + display: flex; + align-items: center; + gap: var(--space-2); + min-width: 0; +} + +.workspace-card-title { + flex: 1; + min-width: 0; +} + +.workspace-card-trailing { + position: absolute; + top: 0; + right: 0; + font-size: 11px; + font-weight: 500; + line-height: 1; +} + +/* v3 — Bold inner glow on cards/panels (web-app BgGrad-inspired), plain shell background. */ +[data-widget-version="v3"] .mcp-widget-shell { + --brand-accent: var(--accent); + --brand-opacity: 1; + background: var(--widget-shell-bg); +} + +[data-widget-version="v3"] .mcp-widget-glow { + display: none; +} + +[data-widget-version="v3"] .card-glow, +[data-widget-version="v3"] .panel-glow { + display: block; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + background: + radial-gradient( + ellipse 80% 50% at 50% 100%, + rgba(20, 16, 255, 0.12) 0%, + rgba(0, 144, 255, 0.08) 30%, + transparent 70% + ), + radial-gradient( + ellipse 60% 40% at 30% 90%, + rgba(71, 168, 253, 0.1) 0%, + transparent 60% + ), + radial-gradient( + ellipse 60% 40% at 70% 90%, + rgba(75, 160, 250, 0.1) 0%, + transparent 60% + ); +} + +/* v4 — Fully refined for white/light + proper dark mode support. */ +[data-widget-version="v4"] .mcp-widget-shell { + --brand-accent: var(--accent); + --brand-opacity: 1; + display: flex; + flex-direction: column; + background: var(--widget-shell-bg); +} + +[data-widget-version="v4"] .mcp-widget-glow { + display: none; +} + +[data-theme="light"] [data-widget-version="v4"] .mcp-widget-shell, +:root:not([data-theme="dark"]) [data-widget-version="v4"] .mcp-widget-shell { + --widget-shell-bg: #faf9f7; + --card-bg: #ffffff; + --card-border: rgba(0, 0, 0, 0.06); + --card-border-hover: rgba(0, 0, 0, 0.1); + --card-active-border: rgba(63, 126, 232, 0.32); + --card-active-bg: #fafcff; + --panel-bg: #ffffff; + --panel-border: rgba(0, 0, 0, 0.06); + --panel-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 6px 16px rgba(0, 0, 0, 0.03); + --bg-control: #fafafa; + --bg-control-hover: #f5f5f5; + --border-control: rgba(0, 0, 0, 0.08); + --text-secondary: #6b7280; + --text-muted: #9ca3af; +} + +[data-theme="dark"] [data-widget-version="v4"] .mcp-widget-shell { + --widget-shell-bg: #111518; + --card-bg: #181d22; + --card-border: rgba(255, 255, 255, 0.06); + --card-border-hover: rgba(255, 255, 255, 0.1); + --card-active-border: rgba(106, 166, 248, 0.36); + --card-active-bg: #161c24; + --panel-bg: #181d22; + --panel-border: rgba(255, 255, 255, 0.06); + --panel-shadow: 0 1px 3px rgba(0, 0, 0, 0.16), 0 6px 16px rgba(0, 0, 0, 0.14); + --bg-control: #14191f; + --bg-control-hover: #1b222a; + --border-control: rgba(255, 255, 255, 0.08); + --text-secondary: #a0a8b4; + --text-muted: #6b7580; +} + +[data-widget-version="v4"] .card-glow, +[data-widget-version="v4"] .panel-glow { + display: block; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + background: radial-gradient( + ellipse 80% 50% at 50% 100%, + rgba(20, 16, 255, 0.07) 0%, + rgba(0, 144, 255, 0.04) 30%, + transparent 70% + ); +} + +/* v4 — Compact inline wordmark: logo + supermemory, no MCP subtitle. */ +[data-widget-version="v4"] .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); + opacity: 1; +} + +[data-widget-version="v4"] .mcp-widget-brand-mark { + width: 18px; + height: 18px; +} + +[data-widget-version="v4"] .mcp-widget-brand-mark svg { + width: 15px; + height: 12px; +} + +[data-widget-version="v4"] .mcp-widget-brand-copy { + flex-direction: row; + align-items: center; +} + +[data-widget-version="v4"] .mcp-widget-brand-name { + font-family: var(--font-brand); + font-size: 13px; + font-weight: 600; + letter-spacing: -0.02em; + line-height: 1; +} + +[data-widget-version="v4"] .mcp-widget-brand-mode { + display: none; +} + +/* v4 — Vertically center loading in the shell below the header. */ +[data-widget-version="v4"] .mcp-widget-content:has(.mcp-widget-loading) { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +[data-widget-version="v4"] .mcp-widget-loading { + flex: 1; + min-height: 180px; + padding: 0; +} + +/* v4 — Workspace picker as compact list rows. */ +[data-widget-version="v4"] .workspace-picker-grid { + display: flex; + flex-direction: column; + gap: 0; + overflow: hidden; + border: 1px solid var(--card-border); + border-radius: var(--radius-lg); + background: var(--card-bg); +} + +[data-widget-version="v4"] .workspace-card { + min-height: 0; + padding: var(--space-2) var(--space-3); + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +[data-widget-version="v4"] .workspace-card:not(:last-child) { + border-bottom: 1px solid var(--card-border); +} + +[data-widget-version="v4"] .workspace-card:hover { + background: var(--card-bg-hover); +} + +[data-widget-version="v4"] .workspace-card .card-glow { + display: none; +} + +[data-widget-version="v4"] .workspace-card-inner { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + height: auto; + min-height: 0; +} + +[data-widget-version="v4"] .workspace-card-main { + flex: 1; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + gap: var(--space-3); + min-width: 0; + padding-right: 0; + overflow: hidden; +} + +[data-widget-version="v4"] .workspace-card-heading { + flex: 1; + min-width: 0; +} + +[data-widget-version="v4"] .workspace-card-title { + min-width: 0; +} + +[data-widget-version="v4"] .workspace-card-meta { + flex-shrink: 0; + white-space: nowrap; +} + +[data-widget-version="v4"] .workspace-card-trailing { + position: static; + flex-shrink: 0; +} +.mcp-widget-shell-graph .mcp-widget-glow { + display: none; +} + +.mcp-widget-brand { + display: inline-flex; + align-items: center; + gap: var(--space-2); + position: relative; + z-index: 1; + margin: var(--space-4) var(--page-header-px) 0; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; + opacity: var(--brand-opacity); +} + +.mcp-widget-brand-mark { + display: grid; + width: 20px; + height: 20px; + place-items: center; + color: var(--brand-accent); +} + +.mcp-widget-brand-mark svg { + width: 16px; + height: 13px; +} + +.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: 650; + line-height: 1; +} + +.mcp-widget-brand-mode { + color: var(--text-muted); + font-size: 10px; + font-weight: 500; + line-height: 1.15; +} + +.shadow-inside-out { + box-shadow: + inset 0 2px 4px rgba(0, 0, 0, 0.3), + inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.mcp-widget-content { + position: relative; + z-index: 1; +} + +.mcp-widget-shell-graph .mcp-widget-content { + min-height: inherit; +} + /* 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: 2px solid rgba(255, 255, 255, 0.12); + border-top-color: var(--brand-accent); border-radius: 50%; animation: spin 0.8s linear infinite; + box-shadow: 0 0 20px rgba(75, 160, 250, 0.42); } @keyframes spin { @@ -123,6 +631,64 @@ } } +.super-loader { + display: inline-flex; + min-width: calc(var(--super-loader-size, 42px) + 10px); + align-items: center; + gap: var(--space-2); + color: var(--brand-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 { + stroke-dashoffset: 0; + opacity: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .super-loader-path { + stroke-dashoffset: 0; + opacity: 0.7; + animation: none; + } +} + /* The graph canvas needs a definite height in both modes. * * Inline: derive height from the (stable) width via aspect-ratio — width is @@ -135,13 +701,13 @@ * minimize). Keeping it in flow with an explicit height avoids that entirely. */ .graph-view { width: 100%; - aspect-ratio: 4 / 3; + aspect-ratio: 16 / 9; /* 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; + 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 54436c362..1124010cf 100644 --- a/apps/mcp/src/widget/design/tokens.css +++ b/apps/mcp/src/widget/design/tokens.css @@ -8,7 +8,8 @@ * via `@custom-variant dark (&:is([data-theme="dark"] *))`. */ -:root { +:root, +[data-theme="light"] { color-scheme: light; /* Accent gradient (Supermemory brand) */ @@ -32,7 +33,7 @@ --bg-primary: #faf9f4; --bg-secondary: #f3f1ec; --bg-muted: #e8e5e0; - --bg-elevated: #ffffff; + --bg-elevated: #f8f7f3; --bg-overlay: rgba(250, 249, 244, 0.7); /* Borders */ @@ -93,8 +94,8 @@ --font-bold: 700; --leading-tight: 1.25; - --leading-normal: 1.5; - --leading-relaxed: 1.75; + --leading-normal: 1.35; + --leading-relaxed: 1.35; /* Unified component heights */ --height-xs: 28px; @@ -182,7 +183,7 @@ --text-primary: #fafafa; --text-secondary: #a3a3a3; - --text-muted: #525252; + --text-muted: #8a8a8a; --text-inverse: #0a0a0a; --success-muted: rgba(74, 222, 128, 0.2); @@ -220,3 +221,303 @@ --graph-control-bg: #0c1829; --graph-control-border: #1a2333; } + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + color-scheme: dark; + + --bg-primary: #0a0a0a; + --bg-secondary: #171717; + --bg-muted: #262626; + --bg-elevated: #1c1c1c; + --bg-overlay: rgba(10, 10, 10, 0.7); + + --border: #2e2e2e; + --border-muted: #1f1f1f; + + --text-primary: #fafafa; + --text-secondary: #a3a3a3; + --text-muted: #8a8a8a; + --text-inverse: #0a0a0a; + + --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); + + --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); + + --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; + } +} + +/* Cursor MCP widget theme + * + * The web app palette is a useful reference, but these widgets live inside + * Cursor's chat surface. Keep the brand typography, then soften both themes so + * the widget feels embedded instead of like a separate high-contrast app frame. + */ +:root, +[data-theme="light"] { + color-scheme: light; + + --accent-start: #4f8df7; + --accent-mid: #4f8df7; + --accent-end: #8ec8ff; + --accent-gradient: linear-gradient( + 94deg, + var(--accent-start), + var(--accent-end) + ); + --accent: #3f7ee8; + --accent-foreground: #ffffff; + --accent-hover: #2f6fd8; + --accent-muted: rgba(63, 126, 232, 0.1); + + --app-bg: #f3f1ec; + --widget-shell-bg: #f6f4ef; + --widget-shell-graph-bg: #f4f2ec; + --brand-accent: #78746c; + --brand-opacity: 0.66; + --widget-glow-inner: rgba(255, 255, 255, 0.62); + --widget-glow-mid: rgba(110, 154, 220, 0.2); + --widget-glow-outer: rgba(63, 126, 232, 0.08); + + --bg-primary: #f6f4ef; + --bg-secondary: #efede7; + --bg-muted: #e7e3da; + --bg-elevated: #f8f7f3; + --bg-overlay: rgba(246, 244, 239, 0.74); + --bg-panel: #f1efe8; + --bg-control: #fbfaf7; + --bg-control-hover: #f4f2ec; + + --border: #ddd8ce; + --border-muted: #e9e5dc; + --border-control: #dcd6ca; + --border-accent: rgba(63, 126, 232, 0.42); + + --card-bg: #f8f7f3; + --card-bg-hover: #f3f1eb; + --card-border: #ddd8ce; + --card-border-hover: #cfc7b8; + --card-active-bg: #f8f7f3; + --card-active-border: rgba(63, 126, 232, 0.42); + + --panel-bg: #f1efe8; + --panel-border: #ddd8ce; + --panel-shadow: 0 10px 24px rgba(74, 68, 58, 0.08); + + --button-primary-bg: #111827; + --button-primary-hover: #1f2937; + --button-primary-active: #0b1120; + --button-primary-text: #ffffff; + --button-secondary-bg: #f8f7f3; + --button-secondary-hover: #f1eee7; + --button-secondary-text: #111827; + --button-secondary-border: #dcd6ca; + --button-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.22), 0 1px 2px rgba(15, 23, 42, 0.1); + + --text-primary: #111827; + --text-secondary: #4b5563; + --text-muted: #8a94a3; + --text-inverse: #ffffff; + + --success: #16a34a; + --success-muted: rgba(22, 163, 74, 0.1); + --error: #dc2626; + --error-muted: rgba(220, 38, 38, 0.1); + --warning: #d97706; + --warning-muted: rgba(217, 119, 6, 0.12); + --info: #2563eb; + --info-muted: rgba(37, 99, 235, 0.1); + --danger: #dc2626; + + --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 1px 2px rgba(15, 23, 42, 0.06); + --shadow-md: 0 12px 28px rgba(15, 23, 42, 0.09); + --shadow-lg: 0 20px 44px rgba(15, 23, 42, 0.12); + + --graph-bg: #f3f1ec; + --graph-doc-fill: #f8f7f3; + --graph-doc-stroke: #ddd8ce; + --graph-doc-inner: #efede7; + --graph-mem-fill: #edf2f7; + --graph-mem-fill-hover: #e3ebf3; + --graph-mem-stroke: #6aa6f8; + --graph-accent: #3f7ee8; + --graph-text-primary: #111827; + --graph-text-secondary: #4b5563; + --graph-text-muted: #8a94a3; + --graph-edge-derives: #c08403; + --graph-edge-updates: #3f7ee8; + --graph-edge-extends: #0891b2; + --graph-mem-border-forgotten: #dc2626; + --graph-mem-border-expiring: #d97706; + --graph-mem-border-recent: #16a34a; + --graph-glow: #6aa6f8; + --graph-icon: #6b7280; + --graph-popover-bg: #f8f7f3; + --graph-popover-border: #ddd8ce; + --graph-popover-text-primary: #111827; + --graph-popover-text-secondary: #4b5563; + --graph-popover-text-muted: #8a94a3; + --graph-control-bg: #f8f7f3; + --graph-control-border: #ddd8ce; +} + +[data-theme="dark"] { + color-scheme: dark; + + --accent-start: #6aa6f8; + --accent-mid: #6aa6f8; + --accent-end: #8bd4ff; + --accent-gradient: linear-gradient( + 94deg, + var(--accent-start), + var(--accent-end) + ); + --accent: #6aa6f8; + --accent-foreground: #0f141a; + --accent-hover: #82b7ff; + --accent-muted: rgba(106, 166, 248, 0.12); + + --app-bg: #12161b; + --widget-shell-bg: #151a20; + --widget-shell-graph-bg: #11161c; + --brand-accent: #7f8996; + --brand-opacity: 0.56; + --widget-glow-inner: rgba(255, 255, 255, 0.12); + --widget-glow-mid: rgba(106, 166, 248, 0.16); + --widget-glow-outer: rgba(106, 166, 248, 0.08); + + --bg-primary: #151a20; + --bg-secondary: #181e25; + --bg-muted: #20262e; + --bg-elevated: #1a2027; + --bg-overlay: rgba(21, 26, 32, 0.72); + --bg-panel: #1d232b; + --bg-control: #14191f; + --bg-control-hover: #1b222a; + + --border: #2b333d; + --border-muted: #232a33; + --border-control: #303946; + --border-accent: rgba(106, 166, 248, 0.38); + + --card-bg: #1a2026; + --card-bg-hover: #202731; + --card-border: #2b333d; + --card-border-hover: #3a4654; + --card-active-bg: #1b222b; + --card-active-border: rgba(106, 166, 248, 0.44); + + --panel-bg: #1c222a; + --panel-border: #2b333d; + --panel-shadow: + 0 14px 34px rgba(0, 0, 0, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.035); + + --button-primary-bg: #eef4ff; + --button-primary-hover: #ffffff; + --button-primary-active: #dceaff; + --button-primary-text: #101820; + --button-secondary-bg: #202731; + --button-secondary-hover: #27303b; + --button-secondary-text: #eef2f7; + --button-secondary-border: #34404d; + --button-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 1px 2px rgba(0, 0, 0, 0.22); + + --text-primary: #f4f6f8; + --text-secondary: #c1c9d2; + --text-muted: #87909c; + --text-inverse: #111827; + + --success: #4ade80; + --success-muted: rgba(74, 222, 128, 0.12); + --error: #fb7185; + --error-muted: rgba(251, 113, 133, 0.12); + --warning: #fbbf24; + --warning-muted: rgba(251, 191, 36, 0.12); + --info: #8bbcff; + --info-muted: rgba(139, 188, 255, 0.12); + --danger: #fb7185; + + --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 1px 2px rgba(0, 0, 0, 0.18); + --shadow-md: 0 12px 30px rgba(0, 0, 0, 0.22); + --shadow-lg: 0 20px 48px rgba(0, 0, 0, 0.28); + + --graph-bg: #151a20; + --graph-doc-fill: #202731; + --graph-doc-stroke: #35404c; + --graph-doc-inner: #171d24; + --graph-mem-fill: #172536; + --graph-mem-fill-hover: #1b2d42; + --graph-mem-stroke: #6aa6f8; + --graph-accent: #6aa6f8; + --graph-text-primary: #f4f6f8; + --graph-text-secondary: #c1c9d2; + --graph-text-muted: #87909c; + --graph-edge-derives: #fbbf24; + --graph-edge-updates: #6aa6f8; + --graph-edge-extends: #67e8f9; + --graph-mem-border-forgotten: #fb7185; + --graph-mem-border-expiring: #f59e0b; + --graph-mem-border-recent: #4ade80; + --graph-glow: #6aa6f8; + --graph-icon: #7ab2ff; + --graph-popover-bg: #1c222a; + --graph-popover-border: #34404d; + --graph-popover-text-primary: #f4f6f8; + --graph-popover-text-secondary: #c1c9d2; + --graph-popover-text-muted: #87909c; + --graph-control-bg: #1c222a; + --graph-control-border: #34404d; +} diff --git a/apps/mcp/src/widget/design/ui/Button.tsx b/apps/mcp/src/widget/design/ui/Button.tsx index c641215c8..e65d3a33e 100644 --- a/apps/mcp/src/widget/design/ui/Button.tsx +++ b/apps/mcp/src/widget/design/ui/Button.tsx @@ -3,43 +3,40 @@ 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. +// Mirrors apps/web's pill-shaped inside-out controls while keeping the widget +// variants small and dependency-free for MCP iframes. 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", + "rounded-full", + "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)]", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)]/35 focus-visible:ring-offset-0", "[&_svg:not([class*='size-'])]:size-4 shrink-0", ].join(" "), { variants: { variant: { primary: [ - "bg-[var(--accent)] text-[var(--accent-foreground)]", - "hover:bg-[var(--accent)]/90", + "bg-[#0D121A] text-[#FAFAFA] shadow-inside-out", + "hover:bg-[#121820] active:bg-[#080B0F]", ].join(" "), secondary: [ - "bg-transparent text-[var(--text-primary)]", - "border border-[var(--border)]", - "hover:bg-[var(--bg-muted)]", - ].join(" "), - ghost: [ - "text-[var(--text-primary)]", - "hover:bg-[var(--bg-muted)]", + "bg-[#0D121A] text-[#FAFAFA] shadow-inside-out", + "hover:bg-[#121820]", ].join(" "), + ghost: ["text-[#737373]", "hover:bg-white/5 hover:text-[#FAFAFA]"].join( + " ", + ), danger: [ - "bg-[var(--danger)] text-[var(--text-inverse)]", - "hover:bg-[var(--danger)]/90", + "bg-[#EF4444]/15 text-[#EF4444]", + "hover:bg-[#EF4444]/25", ].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 +90,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) + const inner = ( + <> +
+ {children} + + ) if (as === "button") { return ( ) } return ( @@ -49,7 +62,9 @@ export const Card = forwardRef( className={cls} ref={ref as React.Ref} {...(props as HTMLAttributes)} - /> + > + {inner} +
) }, ) diff --git a/apps/mcp/src/widget/design/ui/Chip.tsx b/apps/mcp/src/widget/design/ui/Chip.tsx index d9ccc7a70..89e5d811e 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 7402d5304..89adc266d 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 -
+
+ + Version + + setVersion("v1")} selected={version === "v1"}> + v1 + + setVersion("v2")} selected={version === "v2"}> + v2 + + setVersion("v3")} selected={version === "v3"}> + v3 + + setVersion("v4")} selected={version === "v4"}> + v4 + +
Width @@ -175,11 +197,17 @@ export function Studio() { setWidth("wide")} selected={width === "wide"}> Wide {WIDE} + setWidth("cursor")} + selected={width === "cursor"} + > + Cursor {CURSOR} +
-
+
{/* ── Primitives ───────────────────────────────────────── */}
Selected Unselected - sm_project_marketing + Marketing
@@ -334,73 +362,94 @@ export function Studio() { {/* ── Views ────────────────────────────────────────────── */}
-
+
- + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + +
diff --git a/apps/mcp/src/widget/views/Confirmation.tsx b/apps/mcp/src/widget/views/Confirmation.tsx index 354fceba1..32660f066 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 { formatTagLabel } from "../lib/formatTag" interface Props { containerTag: string @@ -9,15 +10,16 @@ export function Confirmation({ containerTag }: Props) { return ( +
Active workspace set
- {containerTag} + {formatTagLabel(containerTag)}

All subsequent saves and recalls will use this workspace until you diff --git a/apps/mcp/src/widget/views/Error.tsx b/apps/mcp/src/widget/views/Error.tsx index 9f2cc82ef..5dfb5f409 100644 --- a/apps/mcp/src/widget/views/Error.tsx +++ b/apps/mcp/src/widget/views/Error.tsx @@ -9,7 +9,7 @@ export function ErrorView({ message }: Props) { return ( diff --git a/apps/mcp/src/widget/views/Graph.tsx b/apps/mcp/src/widget/views/Graph.tsx index 21ab8537a..5c621b2c1 100644 --- a/apps/mcp/src/widget/views/Graph.tsx +++ b/apps/mcp/src/widget/views/Graph.tsx @@ -151,6 +151,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 +194,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 +216,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 e5a03cd0b..513498abb 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 45c696058..40ccfa251 100644 --- a/apps/mcp/src/widget/views/Picker.tsx +++ b/apps/mcp/src/widget/views/Picker.tsx @@ -8,6 +8,7 @@ import { WorkspaceCard } from "../components/WorkspaceCard" import { Input, PageHeader } from "../design/ui" import { useApp } from "../hooks/useApp" import { useLog } from "../hooks/useLog" +import { formatTagLabel } from "../lib/formatTag" import { Search } from "../lib/icons" interface Props { @@ -62,11 +63,11 @@ export function Picker({ const description = count === 0 ? "No workspaces available." - : `${count} workspace${count === 1 ? "" : "s"} available — select one to set your active context.` + : `${count} workspace${count === 1 ? "" : "s"} available` return (
- +
{count >= SEARCH_THRESHOLD ? (
@@ -80,15 +81,14 @@ export function Picker({
) : null} - {/* Bounded, scrollable list — keeps a stable height so filtering or a - long workspace list doesn't resize (jump) the whole widget. */} -
+ {/* Bounded, scrollable list — keeps a stable height without showing scrollbar chrome. */} +
{filtered.length === 0 ? (

No workspaces match “{query}”.

) : ( -
+
{filtered.map((tag) => { const access = assignedTags?.find( (t) => t.containerTag === tag.containerTag, @@ -109,7 +109,7 @@ export function Picker({ {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 0dbd9b162..10e2683b1 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], ) @@ -73,39 +79,43 @@ export function Save({ title="Add Memory" />
- - -