mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-06 08:16:03 +00:00
Redesign MCP widget to match the console design language
Rebuild the widget's visual system around the console's real language: cool-neutral surfaces in light and dark, the inset-shadow signature on form fields, the file dropzone, and the workspace select, one restrained blue accent, DM Sans. Remove the blue glow blobs, card/graph gradients, grid backgrounds, and the v1-v4 variant system. - Picker is a framed, scrollable list; the active row is marked by color (no tick or rail), so the trailing badge stays aligned across rows. - Save/Upload use Nova's insideOut near-black pill for the primary action. - Success/confirmation drop the memory/document id and show the workspace as a calm accent-dot chip instead of a blue-on-blue badge. - Graph re-reads its palette whenever the document theme flips, so light mode no longer renders dark node fills and control chrome. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d4e978e268
commit
c0900939c0
22 changed files with 659 additions and 1176 deletions
|
|
@ -68,7 +68,6 @@ export function WidgetShell({
|
|||
|
||||
return (
|
||||
<div className={shellClassName}>
|
||||
<div aria-hidden className="mcp-widget-glow" />
|
||||
<header className="mcp-widget-brand">
|
||||
<span aria-hidden className="mcp-widget-brand-mark">
|
||||
<svg aria-hidden="true" viewBox="0 0 314 256">
|
||||
|
|
@ -138,13 +137,12 @@ function renderView(
|
|||
case "confirmation":
|
||||
return <Confirmation containerTag={msg.containerTag} />
|
||||
case "save-success":
|
||||
return <Success containerTag={msg.containerTag} id={msg.id} kind="save" />
|
||||
return <Success containerTag={msg.containerTag} kind="save" />
|
||||
case "upload-success":
|
||||
return (
|
||||
<Success
|
||||
containerTag={msg.containerTag}
|
||||
fileName={msg.fileName}
|
||||
id={msg.id}
|
||||
kind="upload"
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@ interface Props {
|
|||
export function PermissionBadge({ permission }: Props) {
|
||||
const isWrite = permission === "write"
|
||||
return (
|
||||
<span
|
||||
className="text-text-muted"
|
||||
style={{ fontFamily: "var(--font-brand)" }}
|
||||
>
|
||||
{isWrite ? "Read/write" : "Read only"}
|
||||
<span className="inline-flex items-center rounded-full bg-bg-muted px-2 py-0.5 text-[10px] font-medium text-text-muted">
|
||||
{isWrite ? "Read / write" : "Read only"}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Loader2 } from "../lib/icons"
|
||||
|
||||
export function Spinner() {
|
||||
return <div className="spinner" />
|
||||
return <Loader2 className="size-5 animate-spin text-accent" />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { ContainerTag, ContainerTagAccess } from "../../shared/types"
|
||||
import { Card } from "../design/ui"
|
||||
import { Check } from "../lib/icons"
|
||||
import { cn } from "../design/lib/cn"
|
||||
import { formatTagLabel } from "../lib/formatTag"
|
||||
import { PermissionBadge } from "./PermissionBadge"
|
||||
|
||||
|
|
@ -18,51 +17,38 @@ export function WorkspaceCard({
|
|||
onClick,
|
||||
}: Props) {
|
||||
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 (
|
||||
<Card
|
||||
as="button"
|
||||
className="workspace-card w-full p-3"
|
||||
<button
|
||||
className="workspace-card"
|
||||
data-active={active}
|
||||
onClick={() => onClick(containerTag.containerTag)}
|
||||
variant={active ? "active" : "interactive"}
|
||||
type="button"
|
||||
>
|
||||
<div className="workspace-card-inner">
|
||||
<div className="workspace-card-main">
|
||||
<div className="workspace-card-heading">
|
||||
<span
|
||||
className="workspace-card-title truncate text-sm font-medium text-text-primary"
|
||||
style={{ fontFamily: "var(--font-brand)" }}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{active ? (
|
||||
<Check
|
||||
aria-hidden
|
||||
className="workspace-card-check size-3 shrink-0 text-accent"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{(containerTag.documentCount > 0 || containerTag.memoryCount > 0) && (
|
||||
<div className="workspace-card-meta flex items-center gap-(--space-2) text-[11px] leading-normal text-text-muted">
|
||||
<span>
|
||||
{containerTag.documentCount} doc
|
||||
{containerTag.documentCount === 1 ? "" : "s"}
|
||||
</span>
|
||||
<span aria-hidden className="opacity-40">
|
||||
·
|
||||
</span>
|
||||
<span>
|
||||
{containerTag.memoryCount}{" "}
|
||||
{containerTag.memoryCount === 1 ? "memory" : "memories"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{access ? (
|
||||
<div className="workspace-card-trailing">
|
||||
<PermissionBadge permission={access.permission} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
<span className="workspace-card-inner">
|
||||
<span className="workspace-card-main">
|
||||
<span
|
||||
className={cn(
|
||||
"workspace-card-title truncate text-sm font-medium",
|
||||
active ? "text-accent" : "text-text-primary",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
<span className="workspace-card-meta text-[11px] leading-normal text-text-muted">
|
||||
{meta}
|
||||
</span>
|
||||
</span>
|
||||
<span className="workspace-card-trailing">
|
||||
{access ? <PermissionBadge permission={access.permission} /> : null}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
12
apps/mcp/src/widget/components/WorkspaceChip.tsx
Normal file
12
apps/mcp/src/widget/components/WorkspaceChip.tsx
Normal file
|
|
@ -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 (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-bg-muted px-2.5 py-1 text-(length:--text-xs) font-medium text-text-primary">
|
||||
<span aria-hidden className="size-1.5 shrink-0 rounded-full bg-accent" />
|
||||
{formatTagLabel(containerTag)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,11 +3,9 @@
|
|||
|
||||
@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);
|
||||
|
|
@ -20,6 +18,7 @@
|
|||
|
||||
--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);
|
||||
|
|
@ -63,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);
|
||||
|
|
@ -70,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;
|
||||
|
|
@ -111,10 +109,13 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
/* ── Shell ────────────────────────────────────────────────────────────────── */
|
||||
.mcp-widget-shell {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
min-height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 220px;
|
||||
overflow: hidden;
|
||||
background: var(--widget-shell-bg);
|
||||
color: var(--text-primary);
|
||||
|
|
@ -125,510 +126,153 @@
|
|||
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 {
|
||||
flex: 1;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
/* Spinner — kept as global because primitives can't easily express a CSS animation */
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
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);
|
||||
/* ── 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);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
.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 {
|
||||
|
|
@ -636,7 +280,7 @@
|
|||
min-width: calc(var(--super-loader-size, 42px) + 10px);
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--brand-accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.super-loader-mark {
|
||||
|
|
@ -689,22 +333,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* The graph canvas needs a definite height in both modes.
|
||||
/* ── 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: 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. */
|
||||
/* 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: transparent;
|
||||
|
|
|
|||
|
|
@ -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 <body> 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;
|
||||
--bg-elevated: #f8f7f3;
|
||||
--bg-overlay: rgba(250, 249, 244, 0.7);
|
||||
/* 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;
|
||||
|
||||
/* Borders */
|
||||
--border: #e3e0db;
|
||||
--border-muted: #f0ede8;
|
||||
--border-accent: var(--accent);
|
||||
--bg-primary: #f5f7fa;
|
||||
--bg-secondary: #eef1f6;
|
||||
--bg-muted: #e7ebf1;
|
||||
--bg-elevated: #ffffff;
|
||||
--bg-overlay: rgba(245, 247, 250, 0.8);
|
||||
--bg-panel: #ffffff;
|
||||
--bg-control: #ffffff;
|
||||
--bg-control-hover: #f4f6fa;
|
||||
|
||||
/* Text */
|
||||
--text-primary: #0a0a0a;
|
||||
--text-secondary: #525252;
|
||||
--text-muted: #a3a3a3;
|
||||
--border: #e2e7ef;
|
||||
--border-muted: #edf0f5;
|
||||
--border-control: #dce2ea;
|
||||
--border-accent: rgba(46, 111, 230, 0.45);
|
||||
|
||||
--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.35;
|
||||
--leading-relaxed: 1.35;
|
||||
--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,415 +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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,35 +3,42 @@ import { type ButtonHTMLAttributes, forwardRef, type ReactNode } from "react"
|
|||
import { Loader2 } from "../../lib/icons"
|
||||
import { cn } from "../lib/cn"
|
||||
|
||||
// Mirrors apps/web's pill-shaped inside-out controls while keeping the widget
|
||||
// variants small and dependency-free for MCP iframes.
|
||||
// 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",
|
||||
"font-semibold",
|
||||
"font-semibold whitespace-nowrap",
|
||||
"rounded-full",
|
||||
"transition-all cursor-pointer",
|
||||
"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)]/35 focus-visible:ring-offset-0",
|
||||
"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-[#0D121A] text-[#FAFAFA] shadow-inside-out",
|
||||
"hover:bg-[#121820] active:bg-[#080B0F]",
|
||||
"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-[#0D121A] text-[#FAFAFA] shadow-inside-out",
|
||||
"hover:bg-[#121820]",
|
||||
"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-secondary)]",
|
||||
"hover:bg-[var(--bg-muted)] hover:text-[var(--text-primary)]",
|
||||
].join(" "),
|
||||
ghost: ["text-[#737373]", "hover:bg-white/5 hover:text-[#FAFAFA]"].join(
|
||||
" ",
|
||||
),
|
||||
danger: [
|
||||
"bg-[#EF4444]/15 text-[#EF4444]",
|
||||
"hover:bg-[#EF4444]/25",
|
||||
"bg-[var(--error-muted)] text-[var(--error)]",
|
||||
"hover:bg-[var(--error-muted)] hover:brightness-95",
|
||||
].join(" "),
|
||||
},
|
||||
size: {
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@ import { cn } from "../lib/cn"
|
|||
|
||||
const cardStyles = cva(
|
||||
[
|
||||
"group relative overflow-hidden text-left",
|
||||
"rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-4",
|
||||
"transition-all duration-200",
|
||||
"group relative text-left",
|
||||
"rounded-xl border border-[var(--card-border)] bg-[var(--card-bg)] p-4",
|
||||
"transition-colors duration-150",
|
||||
].join(" "),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "",
|
||||
interactive:
|
||||
"cursor-pointer hover:border-[var(--card-border-hover)] hover:bg-[var(--card-bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/20 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary",
|
||||
"cursor-pointer hover:border-[var(--card-border-hover)] hover:bg-[var(--card-bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/30 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary",
|
||||
active:
|
||||
"cursor-pointer border-[var(--card-active-border)] bg-[var(--card-active-bg)] hover:border-[var(--card-border-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/20 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary",
|
||||
"cursor-pointer border-[var(--card-active-border)] bg-[var(--card-active-bg)] hover:border-[var(--card-active-border)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/30 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "default" },
|
||||
|
|
@ -39,12 +39,6 @@ export type CardProps = DivProps | ButtonElementProps
|
|||
export const Card = forwardRef<HTMLElement, CardProps>(
|
||||
({ className, variant, as = "div", children, ...props }, ref) => {
|
||||
const cls = cn(cardStyles({ variant }), className)
|
||||
const inner = (
|
||||
<>
|
||||
<div aria-hidden className="card-glow" />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
if (as === "button") {
|
||||
return (
|
||||
<button
|
||||
|
|
@ -53,7 +47,7 @@ export const Card = forwardRef<HTMLElement, CardProps>(
|
|||
type="button"
|
||||
{...(props as ButtonHTMLAttributes<HTMLButtonElement>)}
|
||||
>
|
||||
{inner}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
@ -63,7 +57,7 @@ export const Card = forwardRef<HTMLElement, CardProps>(
|
|||
ref={ref as React.Ref<HTMLDivElement>}
|
||||
{...(props as HTMLAttributes<HTMLDivElement>)}
|
||||
>
|
||||
{inner}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export function Field({ label, hint, error, className, children }: Props) {
|
|||
<div className={cn("flex flex-col gap-1.5", className)}>
|
||||
{label ? (
|
||||
// biome-ignore lint/a11y/noLabelWithoutControl: generic field wrapper where children provide the input
|
||||
<label className="text-xs font-medium uppercase text-text-secondary">
|
||||
<label className="text-[11px] font-semibold uppercase tracking-[0.06em] text-text-muted">
|
||||
{label}
|
||||
</label>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@ export function FileUpload({
|
|||
className={cn(
|
||||
"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)",
|
||||
"bg-[var(--bg-control)] cursor-pointer shadow-sm transition-all",
|
||||
"border border-dashed rounded-(--radius-lg)",
|
||||
"bg-[var(--bg-control)] cursor-pointer shadow-[var(--shadow-inset)] transition-colors",
|
||||
dragOver
|
||||
? "border-accent bg-accent-muted"
|
||||
? "border-accent bg-[var(--accent-muted)]"
|
||||
: "border-[var(--border-control)] hover:border-[var(--card-border-hover)] hover:bg-[var(--bg-control-hover)]",
|
||||
disabled && "pointer-events-none opacity-50",
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -2,20 +2,21 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { forwardRef, type InputHTMLAttributes } from "react"
|
||||
import { cn } from "../lib/cn"
|
||||
|
||||
// Mirrors console-v2's Input with a solid widget surface so the Nova grid stays
|
||||
// behind the form instead of bleeding through controls.
|
||||
// A recessed field: solid control surface, hairline border, and the console's
|
||||
// signature inset shadow. Focus is a clean accent ring — no glow.
|
||||
const inputVariants = cva(
|
||||
[
|
||||
"flex w-full",
|
||||
"bg-[var(--bg-control)] text-[var(--text-primary)]",
|
||||
"border border-[var(--border-control)]",
|
||||
"rounded-[var(--radius-lg)]",
|
||||
"shadow-[var(--shadow-inset)]",
|
||||
"placeholder:text-[var(--text-muted)]",
|
||||
"transition-all",
|
||||
"hover:bg-[var(--bg-control-hover)]",
|
||||
"focus-visible:outline-none focus-visible:border-[var(--border-accent)] focus-visible:shadow-[0_0_0_1px_var(--border-accent),0_0_24px_rgba(75,160,250,0.12)]",
|
||||
"transition-colors",
|
||||
"hover:border-[var(--card-border-hover)]",
|
||||
"focus-visible:outline-none focus-visible:border-[var(--border-accent)] focus-visible:shadow-[var(--shadow-inset),0_0_0_2px_var(--accent-ring)]",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"aria-invalid:border-[var(--error)] aria-invalid:ring-[var(--error)]",
|
||||
"aria-invalid:border-[var(--error)]",
|
||||
"file:border-0 file:bg-transparent file:text-[length:var(--text-sm)] file:font-medium",
|
||||
].join(" "),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import { forwardRef, type TextareaHTMLAttributes } from "react"
|
||||
import { cn } from "../lib/cn"
|
||||
|
||||
// Multi-line counterpart to Input. Solid surface keeps host/product background
|
||||
// treatments from showing through text entry areas.
|
||||
// Multi-line counterpart to Input — same recessed field, inset shadow, and clean
|
||||
// accent focus ring.
|
||||
const textAreaClass = [
|
||||
"flex w-full",
|
||||
"bg-[var(--bg-control)] text-[var(--text-primary)]",
|
||||
"border border-[var(--border-control)]",
|
||||
"rounded-[var(--radius-lg)]",
|
||||
"px-[var(--space-3)] py-[var(--space-2)]",
|
||||
"shadow-[var(--shadow-inset)]",
|
||||
"px-[var(--space-3)] py-[var(--space-3)]",
|
||||
"text-[length:var(--text-sm)] leading-normal font-sans",
|
||||
"placeholder:text-[var(--text-muted)]",
|
||||
"transition-all resize-y",
|
||||
"hover:bg-[var(--bg-control-hover)]",
|
||||
"focus-visible:outline-none focus-visible:border-[var(--border-accent)] focus-visible:shadow-[0_0_0_1px_var(--border-accent),0_0_24px_rgba(75,160,250,0.12)]",
|
||||
"transition-colors resize-y",
|
||||
"hover:border-[var(--card-border-hover)]",
|
||||
"focus-visible:outline-none focus-visible:border-[var(--border-accent)] focus-visible:shadow-[var(--shadow-inset),0_0_0_2px_var(--accent-ring)]",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"aria-invalid:border-[var(--error)] aria-invalid:ring-[var(--error)]",
|
||||
"aria-invalid:border-[var(--error)]",
|
||||
].join(" ")
|
||||
|
||||
export interface TextAreaProps
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { type ReactNode, useEffect, useMemo, useState } from "react"
|
||||
import { Check, ChevronDown, Search } from "../../lib/icons"
|
||||
import { ChevronDown, Search } from "../../lib/icons"
|
||||
import { cn } from "../lib/cn"
|
||||
import { Button } from "./Button"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./Popover"
|
||||
|
|
@ -63,9 +63,9 @@ export function WorkspaceSelect({
|
|||
<Button
|
||||
brandFont={false}
|
||||
className={cn(
|
||||
"workspace-select-trigger mcp-soft-button w-full justify-between normal-case tracking-normal",
|
||||
"focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
"data-[state=open]:bg-bg-muted",
|
||||
"workspace-select-trigger w-full justify-between normal-case tracking-normal font-medium",
|
||||
"shadow-[var(--shadow-inset)]",
|
||||
"data-[state=open]:border-[var(--border-accent)]",
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
|
|
@ -110,12 +110,13 @@ export function WorkspaceSelect({
|
|||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-start justify-between gap-(--space-2)",
|
||||
"flex items-start gap-(--space-2)",
|
||||
"px-(--space-3) py-(--space-2)",
|
||||
"rounded-(--radius-md)",
|
||||
"text-left cursor-pointer",
|
||||
"hover:bg-bg-muted transition-colors",
|
||||
"text-left cursor-pointer transition-colors",
|
||||
"hover:bg-bg-muted",
|
||||
"focus-visible:outline-none focus-visible:bg-bg-muted",
|
||||
isSelected && "bg-accent-muted",
|
||||
)}
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
|
|
@ -127,7 +128,14 @@ export function WorkspaceSelect({
|
|||
<span className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="flex items-center gap-(--space-2)">
|
||||
{option.icon ?? null}
|
||||
<span className="text-(length:--text-sm) text-text-primary truncate">
|
||||
<span
|
||||
className={cn(
|
||||
"text-(length:--text-sm) truncate",
|
||||
isSelected
|
||||
? "font-medium text-accent"
|
||||
: "text-text-primary",
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
</span>
|
||||
|
|
@ -137,9 +145,6 @@ export function WorkspaceSelect({
|
|||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{isSelected ? (
|
||||
<Check className="size-3.5 text-accent shrink-0 mt-0.5" />
|
||||
) : null}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type ReactNode, useState } from "react"
|
||||
import { type ReactNode, useEffect, useState } from "react"
|
||||
import { WidgetShell } from "../App"
|
||||
import { WorkspaceCard } from "../components/WorkspaceCard"
|
||||
import {
|
||||
|
|
@ -32,7 +32,6 @@ import {
|
|||
|
||||
type Theme = "light" | "dark"
|
||||
type Width = "narrow" | "wide" | "cursor"
|
||||
type Version = "v1" | "v2" | "v3" | "v4"
|
||||
|
||||
const NARROW = 420
|
||||
const WIDE = 720
|
||||
|
|
@ -121,9 +120,8 @@ function Swatch({ name, varName }: { name: string; varName: string }) {
|
|||
}
|
||||
|
||||
export function Studio() {
|
||||
const [theme, setTheme] = useState<Theme>("light")
|
||||
const [width, setWidth] = useState<Width>("cursor")
|
||||
const [version, setVersion] = useState<Version>("v1")
|
||||
const [theme, setTheme] = useState<Theme>("dark")
|
||||
const [width, setWidth] = useState<Width>("narrow")
|
||||
const frameWidth =
|
||||
width === "narrow" ? NARROW : width === "wide" ? WIDE : CURSOR
|
||||
|
||||
|
|
@ -132,6 +130,12 @@ export function Studio() {
|
|||
document.documentElement.setAttribute("data-theme", t)
|
||||
}
|
||||
|
||||
// Keep the document theme in sync so the gallery matches the toggle,
|
||||
// including on first paint.
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", theme)
|
||||
}, [theme])
|
||||
|
||||
const saveStub = useStubHandlers("Save")
|
||||
const uploadStub = useStubHandlers("Upload")
|
||||
const pickerStub = useStubHandlers("Picker")
|
||||
|
|
@ -167,23 +171,6 @@ export function Studio() {
|
|||
Dark
|
||||
</Chip>
|
||||
</div>
|
||||
<div className="flex items-center gap-(--space-2)">
|
||||
<span className="text-(length:--text-xs) text-text-muted">
|
||||
Version
|
||||
</span>
|
||||
<Chip onClick={() => setVersion("v1")} selected={version === "v1"}>
|
||||
v1
|
||||
</Chip>
|
||||
<Chip onClick={() => setVersion("v2")} selected={version === "v2"}>
|
||||
v2
|
||||
</Chip>
|
||||
<Chip onClick={() => setVersion("v3")} selected={version === "v3"}>
|
||||
v3
|
||||
</Chip>
|
||||
<Chip onClick={() => setVersion("v4")} selected={version === "v4"}>
|
||||
v4
|
||||
</Chip>
|
||||
</div>
|
||||
<div className="flex items-center gap-(--space-2)">
|
||||
<span className="text-(length:--text-xs) text-text-muted">
|
||||
Width
|
||||
|
|
@ -365,10 +352,7 @@ export function Studio() {
|
|||
description="The same widget shell Cursor renders, with mock MCP tool data."
|
||||
title="Cursor widgets"
|
||||
>
|
||||
<div
|
||||
data-widget-version={version}
|
||||
className="flex flex-wrap gap-(--space-8)"
|
||||
>
|
||||
<div className="flex flex-wrap gap-(--space-8)">
|
||||
<Frame label="Picker" width={frameWidth}>
|
||||
<WidgetShell>
|
||||
<Picker
|
||||
|
|
@ -412,11 +396,7 @@ export function Studio() {
|
|||
|
||||
<Frame label="Success (save)" width={frameWidth}>
|
||||
<WidgetShell>
|
||||
<Success
|
||||
containerTag="sm_project_marketing"
|
||||
id="mem_8fa92c"
|
||||
kind="save"
|
||||
/>
|
||||
<Success containerTag="sm_project_marketing" kind="save" />
|
||||
</WidgetShell>
|
||||
</Frame>
|
||||
|
||||
|
|
@ -425,7 +405,6 @@ export function Studio() {
|
|||
<Success
|
||||
containerTag="sm_project_marketing"
|
||||
fileName="q3-brief.pdf"
|
||||
id="doc_4b71a0"
|
||||
kind="upload"
|
||||
/>
|
||||
</WidgetShell>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Badge, Stack } from "../design/ui"
|
||||
import { CheckCircle } from "../lib/icons"
|
||||
import { formatTagLabel } from "../lib/formatTag"
|
||||
import { WorkspaceChip } from "../components/WorkspaceChip"
|
||||
import { Stack } from "../design/ui"
|
||||
import { Check } from "../lib/icons"
|
||||
|
||||
interface Props {
|
||||
containerTag: string
|
||||
|
|
@ -10,20 +10,20 @@ export function Confirmation({ containerTag }: Props) {
|
|||
return (
|
||||
<Stack
|
||||
align="center"
|
||||
className="mcp-panel mx-(--page-header-px) my-(--space-6) rounded-[20px] border border-[var(--panel-border)] bg-[var(--panel-bg)] px-(--page-header-px) py-(--space-10) text-center shadow-[var(--panel-shadow)]"
|
||||
className="mx-(--page-header-px) my-(--space-6) rounded-xl border border-[var(--panel-border)] bg-[var(--panel-bg)] px-(--page-header-px) py-(--space-10) text-center shadow-[var(--panel-shadow)]"
|
||||
gap="md"
|
||||
>
|
||||
<div aria-hidden className="panel-glow" />
|
||||
<CheckCircle className="size-12 text-success" />
|
||||
<span className="flex size-11 items-center justify-center rounded-full bg-[var(--success-muted)] text-success">
|
||||
<Check className="size-6" />
|
||||
</span>
|
||||
<Stack align="center" gap="xs">
|
||||
<div className="text-(length:--text-sm) font-medium text-text-primary">
|
||||
<div className="text-(length:--text-sm) font-semibold text-text-primary">
|
||||
Active workspace set
|
||||
</div>
|
||||
<Badge variant="accent">{formatTagLabel(containerTag)}</Badge>
|
||||
<WorkspaceChip containerTag={containerTag} />
|
||||
</Stack>
|
||||
<p className="max-w-xs text-(length:--text-xs) text-text-muted">
|
||||
All subsequent saves and recalls will use this workspace until you
|
||||
change it.
|
||||
<p className="max-w-xs text-(length:--text-xs) leading-relaxed text-text-muted">
|
||||
Saves and recalls will use this workspace until you change it.
|
||||
</p>
|
||||
</Stack>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ export function ErrorView({ message }: Props) {
|
|||
return (
|
||||
<Stack
|
||||
align="center"
|
||||
className="mx-(--page-header-px) my-(--space-6) rounded-[20px] border border-[var(--panel-border)] bg-[var(--panel-bg)] px-(--page-header-px) py-(--space-10) text-center shadow-[var(--panel-shadow)]"
|
||||
className="mx-(--page-header-px) my-(--space-6) rounded-xl border border-[var(--panel-border)] bg-[var(--panel-bg)] px-(--page-header-px) py-(--space-10) text-center shadow-[var(--panel-shadow)]"
|
||||
gap="md"
|
||||
>
|
||||
<WarningCircle className="size-12 text-error" />
|
||||
<div className="text-(length:--text-sm) font-medium text-text-primary">
|
||||
<span className="flex size-11 items-center justify-center rounded-full bg-[var(--error-muted)] text-error">
|
||||
<WarningCircle className="size-6" />
|
||||
</span>
|
||||
<div className="text-(length:--text-sm) font-semibold text-text-primary">
|
||||
Something went wrong
|
||||
</div>
|
||||
<p className="max-w-sm text-(length:--text-xs) leading-relaxed text-text-muted">
|
||||
|
|
|
|||
|
|
@ -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 <html> 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<GraphThemeColors>(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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ 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"
|
||||
import { Package, Search } from "../lib/icons"
|
||||
|
||||
interface Props {
|
||||
containerTags: ContainerTag[]
|
||||
|
|
@ -62,50 +62,64 @@ export function Picker({
|
|||
const count = containerTags.length
|
||||
const description =
|
||||
count === 0
|
||||
? "No workspaces available."
|
||||
: `${count} workspace${count === 1 ? "" : "s"} available`
|
||||
? "Create a workspace in Supermemory to get started."
|
||||
: "Pick the workspace to save and recall from."
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<PageHeader description={description} title="Workspaces" />
|
||||
<div className="flex flex-col gap-(--space-4) px-(--page-header-px) pb-(--space-6)">
|
||||
{count >= SEARCH_THRESHOLD ? (
|
||||
<div className="relative max-w-sm">
|
||||
<Search className="pointer-events-none absolute left-(--space-3) top-1/2 size-4 -translate-y-1/2 text-text-muted" />
|
||||
<Input
|
||||
className="pl-(--space-8)"
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search workspaces…"
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Bounded, scrollable list — keeps a stable height without showing scrollbar chrome. */}
|
||||
<div className="min-h-[220px] max-h-[60vh] overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{filtered.length === 0 ? (
|
||||
<p className="py-(--space-6) text-center text-(length:--text-sm) text-text-muted">
|
||||
No workspaces match “{query}”.
|
||||
<div className="flex flex-col gap-(--space-3) px-(--page-header-px) pb-(--space-6)">
|
||||
{count === 0 ? (
|
||||
<div className="flex flex-col items-center gap-(--space-2) rounded-xl border border-border bg-[var(--card-bg)] px-(--space-6) py-(--space-10) text-center">
|
||||
<Package className="size-7 text-text-muted" />
|
||||
<p className="text-(length:--text-sm) font-medium text-text-primary">
|
||||
No workspaces yet
|
||||
</p>
|
||||
) : (
|
||||
<div className="workspace-picker-grid grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-(--space-2)">
|
||||
{filtered.map((tag) => {
|
||||
const access = assignedTags?.find(
|
||||
(t) => t.containerTag === tag.containerTag,
|
||||
)
|
||||
return (
|
||||
<WorkspaceCard
|
||||
access={access}
|
||||
active={activeTag === tag.containerTag}
|
||||
containerTag={tag}
|
||||
key={tag.id || tag.containerTag}
|
||||
onClick={handleSelect}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="max-w-xs text-(length:--text-xs) leading-relaxed text-text-muted">
|
||||
Workspaces you create in Supermemory show up here, ready to save
|
||||
and recall from.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{count >= SEARCH_THRESHOLD ? (
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-(--space-3) top-1/2 size-4 -translate-y-1/2 text-text-muted" />
|
||||
<Input
|
||||
className="pl-(--space-8)"
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search workspaces…"
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="workspace-picker-grid items-center justify-center py-(--space-8)">
|
||||
<p className="px-(--space-4) text-center text-(length:--text-sm) text-text-muted">
|
||||
No workspaces match “{query}”.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="workspace-picker-grid">
|
||||
{filtered.map((tag) => {
|
||||
const access = assignedTags?.find(
|
||||
(t) => t.containerTag === tag.containerTag,
|
||||
)
|
||||
return (
|
||||
<WorkspaceCard
|
||||
access={access}
|
||||
active={activeTag === tag.containerTag}
|
||||
containerTag={tag}
|
||||
key={tag.id || tag.containerTag}
|
||||
onClick={handleSelect}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{pending ? (
|
||||
<p className="text-(length:--text-xs) text-text-muted">
|
||||
|
|
|
|||
|
|
@ -79,12 +79,11 @@ export function Save({
|
|||
title="Add Memory"
|
||||
/>
|
||||
<div className="px-(--page-header-px) pb-(--space-6)">
|
||||
<div className="mcp-panel rounded-[20px] border border-[var(--panel-border)] bg-[var(--panel-bg)] p-(--space-4) shadow-[var(--panel-shadow)]">
|
||||
<div aria-hidden className="panel-glow" />
|
||||
<div className="rounded-xl border border-[var(--panel-border)] bg-[var(--panel-bg)] p-(--space-4) shadow-[var(--panel-shadow)]">
|
||||
<Stack gap="lg">
|
||||
<Field label="Memory">
|
||||
<TextArea
|
||||
className="min-h-40 shadow-sm"
|
||||
className="min-h-40"
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Enter content to save as a memory…"
|
||||
value={content}
|
||||
|
|
@ -101,10 +100,9 @@ export function Save({
|
|||
</Field>
|
||||
) : null}
|
||||
|
||||
<div className="flex justify-end pt-(--space-2)">
|
||||
<div className="flex justify-end pt-(--space-1)">
|
||||
<ActionGroup>
|
||||
<Button
|
||||
className="mcp-soft-button"
|
||||
disabled={!canSave}
|
||||
loading={saving}
|
||||
onClick={handleSave}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import { Badge, Stack } from "../design/ui"
|
||||
import { CheckCircle } from "../lib/icons"
|
||||
import { formatTagLabel } from "../lib/formatTag"
|
||||
import { WorkspaceChip } from "../components/WorkspaceChip"
|
||||
import { Stack } from "../design/ui"
|
||||
import { Check } from "../lib/icons"
|
||||
|
||||
interface SaveProps {
|
||||
kind: "save"
|
||||
id: string
|
||||
containerTag: string
|
||||
}
|
||||
|
||||
interface UploadProps {
|
||||
kind: "upload"
|
||||
id: string
|
||||
fileName: string
|
||||
containerTag: string
|
||||
}
|
||||
|
|
@ -22,20 +20,18 @@ export function Success(props: Props) {
|
|||
return (
|
||||
<Stack
|
||||
align="center"
|
||||
className="mcp-panel mx-(--page-header-px) my-(--space-6) rounded-[20px] border border-[var(--panel-border)] bg-[var(--panel-bg)] px-(--page-header-px) py-(--space-10) text-center shadow-[var(--panel-shadow)]"
|
||||
className="mx-(--page-header-px) my-(--space-6) rounded-xl border border-[var(--panel-border)] bg-[var(--panel-bg)] px-(--page-header-px) py-(--space-10) text-center shadow-[var(--panel-shadow)]"
|
||||
gap="md"
|
||||
>
|
||||
<div aria-hidden className="panel-glow" />
|
||||
<CheckCircle className="size-12 text-success" />
|
||||
<span className="flex size-11 items-center justify-center rounded-full bg-[var(--success-muted)] text-success">
|
||||
<Check className="size-6" />
|
||||
</span>
|
||||
<Stack align="center" gap="xs">
|
||||
<div className="text-(length:--text-sm) font-medium text-text-primary">
|
||||
<div className="text-(length:--text-sm) font-semibold text-text-primary">
|
||||
{isUpload ? `Uploaded ${props.fileName}` : "Memory saved"}
|
||||
</div>
|
||||
<Badge variant="accent">{formatTagLabel(props.containerTag)}</Badge>
|
||||
<WorkspaceChip containerTag={props.containerTag} />
|
||||
</Stack>
|
||||
<p className="break-all font-mono text-(length:--text-xs) text-text-muted">
|
||||
{props.id}
|
||||
</p>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,11 +84,10 @@ export function Upload({ activeTag, writableTags, onAdvance, onError }: Props) {
|
|||
title="Upload File"
|
||||
/>
|
||||
<div className="px-(--page-header-px) pb-(--space-6)">
|
||||
<div className="mcp-panel rounded-[20px] border border-[var(--panel-border)] bg-[var(--panel-bg)] p-(--space-4) shadow-[var(--panel-shadow)]">
|
||||
<div aria-hidden className="panel-glow" />
|
||||
<div className="rounded-xl border border-[var(--panel-border)] bg-[var(--panel-bg)] p-(--space-4) shadow-[var(--panel-shadow)]">
|
||||
<Stack gap="lg">
|
||||
{file ? (
|
||||
<div className="flex items-center justify-between gap-(--space-3) rounded-(--radius-lg) border border-[var(--border-control)] bg-[var(--bg-control)] p-(--space-3) shadow-sm">
|
||||
<div className="flex items-center justify-between gap-(--space-3) rounded-(--radius-lg) border border-[var(--border-control)] bg-[var(--bg-control)] p-(--space-3) shadow-[var(--shadow-inset)]">
|
||||
<div className="flex min-w-0 items-center gap-(--space-3)">
|
||||
<FileText className="size-5 shrink-0 text-text-secondary" />
|
||||
<div className="flex min-w-0 flex-col">
|
||||
|
|
@ -126,10 +125,9 @@ export function Upload({ activeTag, writableTags, onAdvance, onError }: Props) {
|
|||
</Field>
|
||||
) : null}
|
||||
|
||||
<div className="flex justify-end pt-(--space-2)">
|
||||
<div className="flex justify-end pt-(--space-1)">
|
||||
<ActionGroup>
|
||||
<Button
|
||||
className="mcp-soft-button"
|
||||
disabled={!canUpload}
|
||||
loading={uploading}
|
||||
onClick={handleUpload}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue