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

View file

@ -14,14 +14,25 @@ export function canvasFont(
export function paintNodeShadow( export function paintNodeShadow(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
zoom: number, 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 ctx.shadowColor = opts?.selected
? "rgba(0, 0, 0, 0.45)" ? "rgba(0, 0, 0, 0.45)"
: "rgba(0, 0, 0, 0.28)"; : "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.shadowOffsetX = 0;
ctx.shadowOffsetY = 3 / Math.max(0.5, zoom); ctx.shadowOffsetY = 3 / z;
} }
export function clearShadow(ctx: CanvasRenderingContext2D) { export function clearShadow(ctx: CanvasRenderingContext2D) {
@ -122,7 +133,7 @@ export function paintGlassGlow(
ctx.shadowOffsetY = 0; 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( export function paintGlassHighlight(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
x: number, x: number,
@ -135,17 +146,19 @@ export function paintGlassHighlight(
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
const rr = Math.min(radius, h / 2, w / 2); const rr = Math.min(radius, h / 2, w / 2);
const band = Math.min(12, h * 0.2);
ctx.moveTo(x + rr, y); ctx.moveTo(x + rr, y);
ctx.lineTo(x + w - rr, y); ctx.lineTo(x + w - rr, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + rr); ctx.quadraticCurveTo(x + w, y, x + w, y + rr);
ctx.lineTo(x + w, y + Math.min(h * 0.45, 28)); ctx.lineTo(x + w, y + band);
ctx.lineTo(x, y + Math.min(h * 0.45, 28)); ctx.lineTo(x, y + band);
ctx.lineTo(x, y + rr); ctx.lineTo(x, y + rr);
ctx.quadraticCurveTo(x, y, x + rr, y); ctx.quadraticCurveTo(x, y, x + rr, y);
ctx.closePath(); 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(0, highlight);
g.addColorStop(1, "rgba(255,255,255,0)"); g.addColorStop(1, "rgba(255,255,255,0)");
ctx.globalAlpha = 0.55;
ctx.fillStyle = g; ctx.fillStyle = g;
ctx.fill(); ctx.fill();
ctx.restore(); ctx.restore();

View file

@ -45,19 +45,12 @@ export function drawPropSheet(
) { ) {
const { w, h } = estimateCardSize(node, dens); const { w, h } = estimateCardSize(node, dens);
const radius = dens ? 14 : 16; const radius = dens ? 14 : 16;
const z = Math.max(0.5, zoom);
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
roundRectPath(ctx, x, y, w, h, radius); roundRectPath(ctx, x, y, w, h, radius);
ctx.clip(); 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 // Low zoom: title only
if (zoom < 0.35) { if (zoom < 0.35) {
ctx.fillStyle = theme.labelPrimary; ctx.fillStyle = theme.labelPrimary;
@ -78,11 +71,7 @@ export function drawPropSheet(
return; return;
} }
// Glass header band // Title row (same surface as body — no striped header band)
ctx.fillStyle = theme.glassHeader;
ctx.fillRect(x, y, w, CARD_HEADER_H);
// Accent dot with soft halo
ctx.beginPath(); ctx.beginPath();
ctx.arc(x + 16, y + CARD_HEADER_H / 2, 7, 0, Math.PI * 2); ctx.arc(x + 16, y + CARD_HEADER_H / 2, 7, 0, Math.PI * 2);
ctx.fillStyle = accent; ctx.fillStyle = accent;
@ -114,15 +103,7 @@ export function drawPropSheet(
}); });
} }
// Hairline under header let cy = y + CARD_HEADER_H + Math.max(4, CARD_PAD_Y - 4);
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;
const contentW = w - CARD_PAD_X * 2; const contentW = w - CARD_PAD_X * 2;
// Hero image // Hero image
@ -179,21 +160,12 @@ export function drawPropSheet(
cy += lines.length * lh + CARD_GAP; 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); const chips = descriptionChips(node.description);
if (chips.length) { if (chips.length) {
const rowGap = dens ? 4 : 5;
for (let i = 0; i < chips.length; i++) { for (let i = 0; i < chips.length; i++) {
const chip = chips[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.fillStyle = theme.labelSecondary;
ctx.font = canvasFont(10, 500); ctx.font = canvasFont(10, 500);
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";
@ -207,7 +179,7 @@ export function drawPropSheet(
const vw = contentW - labelW - 10; const vw = contentW - labelW - 10;
const vLines = wrapTextLines(ctx, chip.value, vw, 1); const vLines = wrapTextLines(ctx, chip.value, vw, 1);
ctx.fillText(vLines[0] ?? chip.value, vx, cy + CARD_CHIP_H / 2); 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; cy += CARD_GAP - 4;
} }

View file

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

View file

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