fix: make glass cards a solid elevated surface without strip dividers

Remove header bands and inner hairlines that read as stacked strips; restore a lighter opaque fill and heavier shadow so cards stay readable on the dark canvas.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-13 23:46:48 +08:00
parent 74f0d8e78a
commit 4bffec1d81
5 changed files with 59 additions and 104 deletions

View file

@ -5,8 +5,6 @@ import {
fillAccentWash,
paintAccentRail,
paintBadgeChip,
paintGlassGlow,
paintGlassHighlight,
paintNodeShadow,
} from "./canvas-chrome";
import {
@ -606,24 +604,11 @@ export function TopologyCanvas() {
ctx.shadowColor = flashNodeColor;
ctx.shadowBlur = 14 / viewport.zoom;
} else if (cardView) {
// Accent bloom pass (low alpha), then glass body with drop shadow
ctx.save();
paintGlassGlow(ctx, fill, viewport.zoom, isSel);
ctx.globalAlpha = (inGhost ? 0.7 : 1) * (isSel ? 0.28 : 0.16);
ctx.fillStyle = fill;
drawNodeShape(
ctx,
"card",
x,
y,
boxW,
boxH,
dens ? 14 : 16,
);
ctx.fill();
ctx.restore();
if (inGhost) ctx.globalAlpha = 0.7;
paintNodeShadow(ctx, viewport.zoom, { selected: isSel });
// Solid body + deep drop shadow (skip floaty accent bloom)
paintNodeShadow(ctx, viewport.zoom, {
selected: isSel,
weight: "heavy",
});
} else {
paintNodeShadow(ctx, viewport.zoom, { selected: isSel });
}
@ -640,22 +625,12 @@ export function TopologyCanvas() {
ctx.fill();
clearShadow(ctx);
ctx.globalAlpha = inGhost ? 0.7 : 1;
// Soft accent tint only — no header band / dividers
fillAccentWash(
ctx,
fill,
cardView ? 0.06 : n.stylePreset === "title" ? 0.22 : 0.14,
cardView ? 0.08 : n.stylePreset === "title" ? 0.22 : 0.14,
);
if (cardView) {
paintGlassHighlight(
ctx,
x,
y,
boxW,
boxH,
cornerR,
theme.glassHighlight,
);
}
if (
!cardView &&
n.stylePreset !== "decision" &&
@ -679,7 +654,7 @@ export function TopologyCanvas() {
? fill
: theme.glassBorder;
ctx.lineWidth =
(isDrop || isFlash || isSel || isHover ? 1.6 : 1) / viewport.zoom;
(isDrop || isFlash || isSel || isHover ? 1.8 : 1.25) / viewport.zoom;
} else {
ctx.strokeStyle = isForbidden
? theme.warn
@ -723,9 +698,7 @@ export function TopologyCanvas() {
() => setImageTick((t) => t + 1),
);
} else {
// Match glass header so overlay mount doesn't flash empty
ctx.fillStyle = theme.glassHeader;
ctx.fillRect(x, y, boxW, 40);
// Title-only placeholder under HTML overlay (same flat surface)
ctx.beginPath();
ctx.arc(x + 16, y + 20, 4, 0, Math.PI * 2);
ctx.fillStyle = fill;

View file

@ -14,14 +14,25 @@ export function canvasFont(
export function paintNodeShadow(
ctx: CanvasRenderingContext2D,
zoom: number,
opts?: { selected?: boolean },
opts?: { selected?: boolean; weight?: "default" | "heavy" },
) {
const heavy = opts?.weight === "heavy";
const z = Math.max(0.5, zoom);
if (heavy) {
ctx.shadowColor = opts?.selected
? "rgba(0, 0, 0, 0.62)"
: "rgba(0, 0, 0, 0.5)";
ctx.shadowBlur = (opts?.selected ? 28 : 22) / z;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 7 / z;
return;
}
ctx.shadowColor = opts?.selected
? "rgba(0, 0, 0, 0.45)"
: "rgba(0, 0, 0, 0.28)";
ctx.shadowBlur = (opts?.selected ? 18 : 12) / Math.max(0.5, zoom);
ctx.shadowBlur = (opts?.selected ? 18 : 12) / z;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 3 / Math.max(0.5, zoom);
ctx.shadowOffsetY = 3 / z;
}
export function clearShadow(ctx: CanvasRenderingContext2D) {
@ -122,7 +133,7 @@ export function paintGlassGlow(
ctx.shadowOffsetY = 0;
}
/** Inner top highlight strip for frosted-glass feel. */
/** Soft top sheen — keep short so it does not read as a header divider. */
export function paintGlassHighlight(
ctx: CanvasRenderingContext2D,
x: number,
@ -135,17 +146,19 @@ export function paintGlassHighlight(
ctx.save();
ctx.beginPath();
const rr = Math.min(radius, h / 2, w / 2);
const band = Math.min(12, h * 0.2);
ctx.moveTo(x + rr, y);
ctx.lineTo(x + w - rr, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + rr);
ctx.lineTo(x + w, y + Math.min(h * 0.45, 28));
ctx.lineTo(x, y + Math.min(h * 0.45, 28));
ctx.lineTo(x + w, y + band);
ctx.lineTo(x, y + band);
ctx.lineTo(x, y + rr);
ctx.quadraticCurveTo(x, y, x + rr, y);
ctx.closePath();
const g = ctx.createLinearGradient(x, y, x, y + 28);
const g = ctx.createLinearGradient(x, y, x, y + band);
g.addColorStop(0, highlight);
g.addColorStop(1, "rgba(255,255,255,0)");
ctx.globalAlpha = 0.55;
ctx.fillStyle = g;
ctx.fill();
ctx.restore();

View file

@ -45,19 +45,12 @@ export function drawPropSheet(
) {
const { w, h } = estimateCardSize(node, dens);
const radius = dens ? 14 : 16;
const z = Math.max(0.5, zoom);
ctx.save();
ctx.beginPath();
roundRectPath(ctx, x, y, w, h, radius);
ctx.clip();
// Soft top accent hairline (identity without thick colored frame)
ctx.fillStyle = accent;
ctx.globalAlpha = 0.85;
ctx.fillRect(x, y, w, 2.5);
ctx.globalAlpha = 1;
// Low zoom: title only
if (zoom < 0.35) {
ctx.fillStyle = theme.labelPrimary;
@ -78,11 +71,7 @@ export function drawPropSheet(
return;
}
// Glass header band
ctx.fillStyle = theme.glassHeader;
ctx.fillRect(x, y, w, CARD_HEADER_H);
// Accent dot with soft halo
// Title row (same surface as body — no striped header band)
ctx.beginPath();
ctx.arc(x + 16, y + CARD_HEADER_H / 2, 7, 0, Math.PI * 2);
ctx.fillStyle = accent;
@ -114,15 +103,7 @@ export function drawPropSheet(
});
}
// Hairline under header
ctx.strokeStyle = theme.glassBorder;
ctx.lineWidth = 1 / z;
ctx.beginPath();
ctx.moveTo(x + CARD_PAD_X, y + CARD_HEADER_H);
ctx.lineTo(x + w - CARD_PAD_X, y + CARD_HEADER_H);
ctx.stroke();
let cy = y + CARD_HEADER_H + CARD_PAD_Y;
let cy = y + CARD_HEADER_H + Math.max(4, CARD_PAD_Y - 4);
const contentW = w - CARD_PAD_X * 2;
// Hero image
@ -179,21 +160,12 @@ export function drawPropSheet(
cy += lines.length * lh + CARD_GAP;
}
// Description as readable key-value list (never raw JSON)
// Description as one compact key-value block (no row dividers)
const chips = descriptionChips(node.description);
if (chips.length) {
const rowGap = dens ? 4 : 5;
for (let i = 0; i < chips.length; i++) {
const chip = chips[i]!;
if (i > 0) {
ctx.strokeStyle = theme.glassBorder;
ctx.globalAlpha = 0.55;
ctx.lineWidth = 1 / z;
ctx.beginPath();
ctx.moveTo(x + CARD_PAD_X, cy - 2);
ctx.lineTo(x + w - CARD_PAD_X, cy - 2);
ctx.stroke();
ctx.globalAlpha = 1;
}
ctx.fillStyle = theme.labelSecondary;
ctx.font = canvasFont(10, 500);
ctx.textBaseline = "middle";
@ -207,7 +179,7 @@ export function drawPropSheet(
const vw = contentW - labelW - 10;
const vLines = wrapTextLines(ctx, chip.value, vw, 1);
ctx.fillText(vLines[0] ?? chip.value, vx, cy + CARD_CHIP_H / 2);
cy += CARD_CHIP_H + 6;
cy += CARD_CHIP_H + rowGap;
}
cy += CARD_GAP - 4;
}

View file

@ -2,10 +2,10 @@
.ns-editor {
--ns-bg: color-mix(
in srgb,
var(--dsw-alias-bg-layer-1, #1b1e28) 72%,
transparent
#2d3444 88%,
var(--dsw-alias-bg-layer-1, #1b1e28)
);
--ns-border: rgba(255, 255, 255, 0.14);
--ns-border: rgba(255, 255, 255, 0.2);
--ns-label: var(--dsw-alias-label-secondary, #a5adb8);
--ns-text: var(--dsw-alias-label-primary, #f4f5f7);
--ns-brand: var(--dsw-alias-brand-primary, #5e6ad2);
@ -19,11 +19,11 @@
border: 1px solid var(--ns-border);
border-radius: 16px;
overflow: visible;
backdrop-filter: blur(16px) saturate(1.2);
-webkit-backdrop-filter: blur(16px) saturate(1.2);
backdrop-filter: blur(18px) saturate(1.15);
-webkit-backdrop-filter: blur(18px) saturate(1.15);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.1) inset,
0 12px 40px rgba(0, 0, 0, 0.42);
0 10px 28px rgba(0, 0, 0, 0.45),
0 22px 56px rgba(0, 0, 0, 0.38);
color: var(--ns-text);
font-family: var(--ns-font);
font-size: 12.5px;
@ -47,13 +47,9 @@
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.08) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border-bottom: 1px solid var(--ns-border);
padding: 12px 14px 8px;
background: transparent;
border-bottom: none;
border-radius: 16px 16px 0 0;
cursor: grab;
user-select: none;
@ -140,10 +136,10 @@
.ns-kv {
margin: 4px 2px 6px;
padding: 8px 8px 10px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
padding: 4px 2px 6px;
border-radius: 0;
background: transparent;
border: none;
}
.ns-kv__title {
@ -160,7 +156,7 @@
padding: 0;
display: flex;
flex-direction: column;
gap: 0;
gap: 4px;
}
.ns-kv__row {
@ -168,8 +164,8 @@
grid-template-columns: minmax(64px, 0.34fr) 1fr auto;
gap: 6px;
align-items: center;
padding: 6px 2px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding: 2px 0;
border-top: none;
}
.ns-kv__row:first-child {

View file

@ -26,10 +26,11 @@ const FALLBACK_DARK: SystemTheme = {
bubble: "rgba(37,42,58,0.9)",
warn: "#f59e5b",
error: "#ef4444",
glassFill: "rgba(28, 32, 44, 0.72)",
glassBorder: "rgba(255, 255, 255, 0.14)",
glassHighlight: "rgba(255, 255, 255, 0.1)",
glassHeader: "rgba(255, 255, 255, 0.045)",
// Elevated card surface — clearly above canvas, not near-black
glassFill: "rgba(45, 52, 68, 0.96)",
glassBorder: "rgba(255, 255, 255, 0.22)",
glassHighlight: "rgba(255, 255, 255, 0.06)",
glassHeader: "rgba(255, 255, 255, 0.05)",
};
const FALLBACK_LIGHT: SystemTheme = {
@ -42,9 +43,9 @@ const FALLBACK_LIGHT: SystemTheme = {
bubble: "rgba(255,255,255,0.92)",
warn: "#d97706",
error: "#dc2626",
glassFill: "rgba(255, 255, 255, 0.78)",
glassBorder: "rgba(0, 0, 0, 0.1)",
glassHighlight: "rgba(255, 255, 255, 0.65)",
glassFill: "rgba(255, 255, 255, 0.96)",
glassBorder: "rgba(0, 0, 0, 0.12)",
glassHighlight: "rgba(255, 255, 255, 0.4)",
glassHeader: "rgba(0, 0, 0, 0.03)",
};