mirror of
https://github.com/hansjone/mind-map.git
synced 2026-10-09 04:30:46 +08:00
fix: enlarge prop editor and support pinned property tips
Make the double-click editor wider with auto-growing textareas, clamp hover tips inside the viewport, and allow click-to-pin / right-click-to-unpin for presentation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4c9bd5b86f
commit
993fcaa2fd
6 changed files with 273 additions and 67 deletions
|
|
@ -1,19 +1,68 @@
|
||||||
/** Floating property detail tip (card view hover). */
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/** Floating property detail tip (card view hover / pinned for present). */
|
||||||
|
|
||||||
export function PropTooltip({
|
export function PropTooltip({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
pinned,
|
||||||
|
hostW,
|
||||||
|
hostH,
|
||||||
|
onUnpin,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
pinned?: boolean;
|
||||||
|
hostW: number;
|
||||||
|
hostH: number;
|
||||||
|
onUnpin?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [pos, setPos] = useState({ x, y });
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) {
|
||||||
|
setPos({ x, y });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pad = 10;
|
||||||
|
const tw = el.offsetWidth;
|
||||||
|
const th = el.offsetHeight;
|
||||||
|
const maxX = Math.max(pad, hostW - pad - tw);
|
||||||
|
const maxY = Math.max(pad, hostH - pad - th);
|
||||||
|
setPos({
|
||||||
|
x: Math.min(maxX, Math.max(pad, x)),
|
||||||
|
y: Math.min(maxY, Math.max(pad, y)),
|
||||||
|
});
|
||||||
|
}, [x, y, label, value, hostW, hostH, pinned]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="prop-tooltip" style={{ left: x, top: y }} role="tooltip">
|
<div
|
||||||
<div className="prop-tooltip__label">{label}</div>
|
ref={ref}
|
||||||
|
className={`prop-tooltip${pinned ? " prop-tooltip--pinned" : ""}`}
|
||||||
|
style={{ left: pos.x, top: pos.y }}
|
||||||
|
role="tooltip"
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
if (!pinned) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onUnpin?.();
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
if (pinned) e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="prop-tooltip__label">
|
||||||
|
{label}
|
||||||
|
{pinned ? (
|
||||||
|
<span className="prop-tooltip__pin">钉住 · 右键释放</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<div className="prop-tooltip__value">{value}</div>
|
<div className="prop-tooltip__value">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,12 @@ export function TopologyCanvas() {
|
||||||
value: string;
|
value: string;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
pinned: boolean;
|
||||||
|
nodeId: string;
|
||||||
|
fieldKey: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const propTipRef = useRef(propTip);
|
||||||
|
propTipRef.current = propTip;
|
||||||
const [linkPreview, setLinkPreview] = useState<{
|
const [linkPreview, setLinkPreview] = useState<{
|
||||||
fromId: string;
|
fromId: string;
|
||||||
x: number;
|
x: number;
|
||||||
|
|
@ -283,7 +288,60 @@ export function TopologyCanvas() {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[nodes, posOf, dens],
|
[nodes, posOf, boxOf],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Resolve card property under pointer (hover tip / pin for讲解). */
|
||||||
|
const hitCardField = useCallback(
|
||||||
|
(
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
world: { x: number; y: number },
|
||||||
|
): {
|
||||||
|
nodeId: string;
|
||||||
|
field: CardChipHit;
|
||||||
|
tipX: number;
|
||||||
|
tipY: number;
|
||||||
|
} | null => {
|
||||||
|
if (!cardView || viewport.zoom < 0.35) return null;
|
||||||
|
const hit = hitTest(world.x, world.y);
|
||||||
|
if (!hit || propEditId === hit) return null;
|
||||||
|
const n = nodes.find((x) => x.id === hit);
|
||||||
|
const p = n ? posOf(hit) : null;
|
||||||
|
if (!n || !p) return null;
|
||||||
|
const { w: bw, h: bh } = boxOf(n);
|
||||||
|
const localX = world.x - (p.x - bw / 2);
|
||||||
|
const localY = world.y - (p.y - bh / 2);
|
||||||
|
const fields = cardFieldHits(n, dens, expandedCardId === n.id);
|
||||||
|
const field = fields.find(
|
||||||
|
(f) =>
|
||||||
|
localX >= f.x &&
|
||||||
|
localX <= f.x + f.w &&
|
||||||
|
localY >= f.y &&
|
||||||
|
localY <= f.y + f.h,
|
||||||
|
);
|
||||||
|
if (!field) return null;
|
||||||
|
const host = hostRef.current?.getBoundingClientRect();
|
||||||
|
const tipX = host
|
||||||
|
? clientX - host.left + 14
|
||||||
|
: hostSize.w / 2 + viewport.x + world.x * viewport.zoom + 14;
|
||||||
|
const tipY = host
|
||||||
|
? clientY - host.top + 14
|
||||||
|
: hostSize.h / 2 + viewport.y + world.y * viewport.zoom + 14;
|
||||||
|
return { nodeId: hit, field, tipX, tipY };
|
||||||
|
},
|
||||||
|
[
|
||||||
|
cardView,
|
||||||
|
viewport,
|
||||||
|
hitTest,
|
||||||
|
propEditId,
|
||||||
|
nodes,
|
||||||
|
posOf,
|
||||||
|
boxOf,
|
||||||
|
dens,
|
||||||
|
expandedCardId,
|
||||||
|
hostSize,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const worldBounds = useMemo(() => {
|
const worldBounds = useMemo(() => {
|
||||||
|
|
@ -1246,49 +1304,22 @@ export function TopologyCanvas() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Idle hover: property tip in card view, node hover otherwise
|
// Idle hover: property tip in card view (skip while a tip is pinned)
|
||||||
if (dragRef.current.mode === "none" && !ghost) {
|
if (dragRef.current.mode === "none" && !ghost) {
|
||||||
const hit = hitTest(world.x, world.y);
|
const hit = hitTest(world.x, world.y);
|
||||||
setHoverId(hit);
|
setHoverId(hit);
|
||||||
if (cardView && hit && viewport.zoom >= 0.35 && propEditId !== hit) {
|
if (propTipRef.current?.pinned) return;
|
||||||
const n = nodes.find((x) => x.id === hit);
|
const found = hitCardField(e.clientX, e.clientY, world);
|
||||||
const p = n ? posOf(hit) : null;
|
if (found) {
|
||||||
if (n && p) {
|
setPropTip({
|
||||||
const { w: bw, h: bh } = boxOf(n);
|
label: found.field.label,
|
||||||
const localX = world.x - (p.x - bw / 2);
|
value: found.field.fullValue,
|
||||||
const localY = world.y - (p.y - bh / 2);
|
x: found.tipX,
|
||||||
const fields = cardFieldHits(
|
y: found.tipY,
|
||||||
n,
|
pinned: false,
|
||||||
dens,
|
nodeId: found.nodeId,
|
||||||
expandedCardId === n.id,
|
fieldKey: found.field.key,
|
||||||
);
|
});
|
||||||
const field = fields.find(
|
|
||||||
(f: CardChipHit) =>
|
|
||||||
localX >= f.x &&
|
|
||||||
localX <= f.x + f.w &&
|
|
||||||
localY >= f.y &&
|
|
||||||
localY <= f.y + f.h,
|
|
||||||
);
|
|
||||||
if (field) {
|
|
||||||
const host = hostRef.current?.getBoundingClientRect();
|
|
||||||
const tipX = host
|
|
||||||
? e.clientX - host.left + 14
|
|
||||||
: hostSize.w / 2 + viewport.x + world.x * viewport.zoom + 14;
|
|
||||||
const tipY = host
|
|
||||||
? e.clientY - host.top + 14
|
|
||||||
: hostSize.h / 2 + viewport.y + world.y * viewport.zoom + 14;
|
|
||||||
setPropTip({
|
|
||||||
label: field.label,
|
|
||||||
value: field.fullValue,
|
|
||||||
x: tipX,
|
|
||||||
y: tipY,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setPropTip(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setPropTip(null);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setPropTip(null);
|
setPropTip(null);
|
||||||
}
|
}
|
||||||
|
|
@ -1373,13 +1404,30 @@ export function TopologyCanvas() {
|
||||||
} else if (mode === "node" && fromId && !moved) {
|
} else if (mode === "node" && fromId && !moved) {
|
||||||
setSelection([fromId]);
|
setSelection([fromId]);
|
||||||
if (cardView) {
|
if (cardView) {
|
||||||
setExpandedCardId((prev) => (prev === fromId ? null : fromId));
|
const found = hitCardField(e.clientX, e.clientY, world);
|
||||||
if (propEditId && propEditId !== fromId) setPropEditId(null);
|
if (found) {
|
||||||
|
// Click property → pin tip for present /讲解
|
||||||
|
setExpandedCardId(fromId);
|
||||||
|
if (propEditId && propEditId !== fromId) setPropEditId(null);
|
||||||
|
setPropTip({
|
||||||
|
label: found.field.label,
|
||||||
|
value: found.field.fullValue,
|
||||||
|
x: found.tipX,
|
||||||
|
y: found.tipY,
|
||||||
|
pinned: true,
|
||||||
|
nodeId: found.nodeId,
|
||||||
|
fieldKey: found.field.key,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setExpandedCardId((prev) => (prev === fromId ? null : fromId));
|
||||||
|
if (propEditId && propEditId !== fromId) setPropEditId(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (mode === "pan" && !moved) {
|
} else if (mode === "pan" && !moved) {
|
||||||
setExpandedCardId(null);
|
setExpandedCardId(null);
|
||||||
setPropEditId(null);
|
setPropEditId(null);
|
||||||
setPropTip(null);
|
// Keep pinned tip for讲解; only hover tip clears on blank click
|
||||||
|
setPropTip((t) => (t?.pinned ? t : null));
|
||||||
}
|
}
|
||||||
|
|
||||||
dragRef.current.mode = "none";
|
dragRef.current.mode = "none";
|
||||||
|
|
@ -1392,7 +1440,14 @@ export function TopologyCanvas() {
|
||||||
setDropForbidden(false);
|
setDropForbidden(false);
|
||||||
setDragCursor(null);
|
setDragCursor(null);
|
||||||
}}
|
}}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Right-click releases a pinned property tip
|
||||||
|
if (propTipRef.current?.pinned) {
|
||||||
|
setPropTip(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}}
|
||||||
onDoubleClick={(e) => {
|
onDoubleClick={(e) => {
|
||||||
if (locked) return;
|
if (locked) return;
|
||||||
const world = toWorld(e.clientX, e.clientY);
|
const world = toWorld(e.clientX, e.clientY);
|
||||||
|
|
@ -1783,6 +1838,10 @@ export function TopologyCanvas() {
|
||||||
value={propTip.value}
|
value={propTip.value}
|
||||||
x={propTip.x}
|
x={propTip.x}
|
||||||
y={propTip.y}
|
y={propTip.y}
|
||||||
|
pinned={propTip.pinned}
|
||||||
|
hostW={hostSize.w}
|
||||||
|
hostH={hostSize.h}
|
||||||
|
onUnpin={() => setPropTip(null)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ const zh: CanvasUi = {
|
||||||
card: "卡片",
|
card: "卡片",
|
||||||
topology: "拓扑",
|
topology: "拓扑",
|
||||||
bubbleTitle: "气泡视图:精简标题",
|
bubbleTitle: "气泡视图:精简标题",
|
||||||
cardTitle: "卡片视图:单击展开全文 · 悬停属性看详情 · 双击编辑",
|
cardTitle: "卡片:单击属性钉住浮层 · 右键释放 · 双击编辑",
|
||||||
topologyTitle: "拓扑视图:路由器图标 + 链路",
|
topologyTitle: "拓扑视图:路由器图标 + 链路",
|
||||||
unpin: "解钉",
|
unpin: "解钉",
|
||||||
unpinTitle: "取消钉住:让选中节点重新参与自动布局",
|
unpinTitle: "取消钉住:让选中节点重新参与自动布局",
|
||||||
|
|
@ -127,7 +127,7 @@ const en: CanvasUi = {
|
||||||
card: "Card",
|
card: "Card",
|
||||||
topology: "Topology",
|
topology: "Topology",
|
||||||
bubbleTitle: "Bubble view: title only",
|
bubbleTitle: "Bubble view: title only",
|
||||||
cardTitle: "Card view: click expand · hover props · double-click edit",
|
cardTitle: "Card: click prop to pin tip · right-click unpin · double-click edit",
|
||||||
topologyTitle: "Topology view: router icons + links",
|
topologyTitle: "Topology view: router icons + links",
|
||||||
unpin: "Unpin",
|
unpin: "Unpin",
|
||||||
unpinTitle: "Unpin selected node for auto layout",
|
unpinTitle: "Unpin selected node for auto layout",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,15 @@ type ApplyOps = (ops: Op[], summary?: string) => void | Promise<void>;
|
||||||
|
|
||||||
type KvRow = { id: string; key: string; value: string };
|
type KvRow = { id: string; key: string; value: string };
|
||||||
|
|
||||||
|
const OVERLAY_EDITOR_W = 440;
|
||||||
|
|
||||||
|
function autoResizeTextarea(el: HTMLTextAreaElement | null) {
|
||||||
|
if (!el) return;
|
||||||
|
el.style.height = "auto";
|
||||||
|
const next = Math.min(320, Math.max(el.scrollHeight, 96));
|
||||||
|
el.style.height = `${next}px`;
|
||||||
|
}
|
||||||
|
|
||||||
function rowsFromDescription(
|
function rowsFromDescription(
|
||||||
description: Record<string, unknown> | null | undefined,
|
description: Record<string, unknown> | null | undefined,
|
||||||
): KvRow[] {
|
): KvRow[] {
|
||||||
|
|
@ -160,6 +169,12 @@ export function NodePropEditor({
|
||||||
? ({ "--ns-brand": accent } as CSSProperties)
|
? ({ "--ns-brand": accent } as CSSProperties)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
if (variant === "overlay") e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onWheel={(e) => {
|
||||||
|
if (variant === "overlay") e.stopPropagation();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ns-editor__header"
|
className="ns-editor__header"
|
||||||
|
|
@ -394,18 +409,24 @@ function DescriptionListEditor({
|
||||||
}}
|
}}
|
||||||
onBlur={() => onCommit(descriptionFromRows(rowsRef.current))}
|
onBlur={() => onCommit(descriptionFromRows(rowsRef.current))}
|
||||||
/>
|
/>
|
||||||
<input
|
<textarea
|
||||||
className="ns-kv__val"
|
className="ns-kv__val"
|
||||||
value={row.value}
|
value={row.value}
|
||||||
placeholder="内容"
|
placeholder="内容"
|
||||||
aria-label="属性值"
|
aria-label="属性值"
|
||||||
|
rows={Math.min(10, Math.max(3, Math.ceil(row.value.length / 36)))}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setRows((prev) =>
|
setRows((prev) =>
|
||||||
prev.map((r) => (r.id === row.id ? { ...r, value } : r)),
|
prev.map((r) => (r.id === row.id ? { ...r, value } : r)),
|
||||||
);
|
);
|
||||||
|
autoResizeTextarea(e.target);
|
||||||
}}
|
}}
|
||||||
|
onFocus={(e) => autoResizeTextarea(e.target)}
|
||||||
onBlur={() => onCommit(descriptionFromRows(rowsRef.current))}
|
onBlur={() => onCommit(descriptionFromRows(rowsRef.current))}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) autoResizeTextarea(el);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -525,10 +546,22 @@ function FieldRow({
|
||||||
{def.kind === "textarea" || def.kind === "links" ? (
|
{def.kind === "textarea" || def.kind === "links" ? (
|
||||||
<textarea
|
<textarea
|
||||||
key={`${def.id}-${node.id}-${node.version}`}
|
key={`${def.id}-${node.id}-${node.version}`}
|
||||||
|
className={
|
||||||
|
def.id === "note" || seed.length > 80 ? "ns-textarea--lg" : undefined
|
||||||
|
}
|
||||||
defaultValue={seed}
|
defaultValue={seed}
|
||||||
rows={def.kind === "links" ? 3 : 2}
|
rows={
|
||||||
|
def.kind === "links"
|
||||||
|
? Math.min(8, Math.max(3, seed.split("\n").length + 1))
|
||||||
|
: Math.min(12, Math.max(5, Math.ceil(seed.length / 40)))
|
||||||
|
}
|
||||||
placeholder={def.kind === "links" ? "标题|url 每行一条" : ""}
|
placeholder={def.kind === "links" ? "标题|url 每行一条" : ""}
|
||||||
|
onFocus={(e) => autoResizeTextarea(e.target)}
|
||||||
|
onInput={(e) => autoResizeTextarea(e.currentTarget)}
|
||||||
onBlur={(e) => onCommit(e.target.value)}
|
onBlur={(e) => onCommit(e.target.value)}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) autoResizeTextarea(el);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : def.kind === "datetime" ? (
|
) : def.kind === "datetime" ? (
|
||||||
<input
|
<input
|
||||||
|
|
@ -575,9 +608,9 @@ export function NodePropOverlay({
|
||||||
applyOps,
|
applyOps,
|
||||||
screenX,
|
screenX,
|
||||||
screenY,
|
screenY,
|
||||||
zoom,
|
zoom: _zoom,
|
||||||
accent,
|
accent,
|
||||||
width,
|
width: _width,
|
||||||
showStylePreset,
|
showStylePreset,
|
||||||
}: {
|
}: {
|
||||||
node: MindNode;
|
node: MindNode;
|
||||||
|
|
@ -589,14 +622,16 @@ export function NodePropOverlay({
|
||||||
width: number;
|
width: number;
|
||||||
showStylePreset?: boolean;
|
showStylePreset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
// Keep editor at screen size (ignore canvas zoom / card width) so long text is editable.
|
||||||
|
const panelW = OVERLAY_EDITOR_W;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="ns-overlay"
|
className="ns-overlay ns-overlay--roomy"
|
||||||
style={{
|
style={{
|
||||||
left: screenX,
|
left: screenX,
|
||||||
top: screenY,
|
top: screenY,
|
||||||
width,
|
width: panelW,
|
||||||
transform: `translate(-50%, -50%) scale(${zoom})`,
|
transform: "translate(-50%, -50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NodePropEditor
|
<NodePropEditor
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
background: var(--ns-bg);
|
background: var(--ns-bg);
|
||||||
border: 1px solid var(--ns-border);
|
border: 1px solid var(--ns-border);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
backdrop-filter: blur(18px) saturate(1.15);
|
backdrop-filter: blur(18px) saturate(1.15);
|
||||||
-webkit-backdrop-filter: blur(18px) saturate(1.15);
|
-webkit-backdrop-filter: blur(18px) saturate(1.15);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
|
|
@ -28,17 +28,21 @@
|
||||||
font-family: var(--ns-font);
|
font-family: var(--ns-font);
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
min-width: 248px;
|
min-width: 360px;
|
||||||
max-width: 320px;
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
max-height: min(72vh, 640px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ns-editor--panel {
|
.ns-editor--panel {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
overflow: visible;
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
-webkit-backdrop-filter: none;
|
-webkit-backdrop-filter: none;
|
||||||
}
|
}
|
||||||
|
|
@ -97,6 +101,10 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 8px 10px 10px;
|
padding: 8px 10px 10px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ns-meta-chips {
|
.ns-meta-chips {
|
||||||
|
|
@ -169,9 +177,9 @@
|
||||||
|
|
||||||
.ns-kv__row {
|
.ns-kv__row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(64px, 0.34fr) 1fr auto;
|
grid-template-columns: minmax(72px, 0.28fr) 1fr auto;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: start;
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
@ -189,12 +197,20 @@
|
||||||
background: var(--ns-input);
|
background: var(--ns-input);
|
||||||
color: var(--ns-text);
|
color: var(--ns-text);
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 12px;
|
font-size: 12.5px;
|
||||||
|
line-height: 1.45;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 6px 8px;
|
padding: 8px 10px;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ns-kv__val {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 72px;
|
||||||
|
max-height: 240px;
|
||||||
|
field-sizing: content;
|
||||||
|
}
|
||||||
|
|
||||||
.ns-kv__key {
|
.ns-kv__key {
|
||||||
color: var(--ns-label);
|
color: var(--ns-label);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
@ -214,6 +230,8 @@
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
margin-top: 6px;
|
||||||
|
align-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ns-kv__del:hover {
|
.ns-kv__del:hover {
|
||||||
|
|
@ -345,14 +363,25 @@
|
||||||
color: var(--ns-text);
|
color: var(--ns-text);
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
|
line-height: 1.45;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 7px 10px;
|
padding: 8px 10px;
|
||||||
outline: none;
|
outline: none;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
transition: border-color 0.12s ease, background 0.12s ease;
|
transition: border-color 0.12s ease, background 0.12s ease;
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ns-row__ctrl textarea {
|
||||||
|
min-height: 96px;
|
||||||
|
max-height: 320px;
|
||||||
|
field-sizing: content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ns-row__ctrl textarea.ns-textarea--lg {
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
.ns-row__ctrl select option {
|
.ns-row__ctrl select option {
|
||||||
color: #f4f5f7;
|
color: #f4f5f7;
|
||||||
background: #1b1e28;
|
background: #1b1e28;
|
||||||
|
|
@ -518,8 +547,13 @@
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ns-overlay--roomy {
|
||||||
|
/* Editor stays screen-readable; do not shrink with canvas zoom. */
|
||||||
|
width: min(480px, calc(100vw - 48px));
|
||||||
|
}
|
||||||
|
|
||||||
.ns-editor {
|
.ns-editor {
|
||||||
pointer-events: none;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ns-editor--panel {
|
.ns-editor--panel {
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ input {
|
||||||
.prop-tooltip {
|
.prop-tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 8;
|
z-index: 8;
|
||||||
max-width: 320px;
|
max-width: min(360px, calc(100% - 20px));
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
@ -528,12 +528,39 @@ input {
|
||||||
"Noto Sans SC", system-ui, sans-serif;
|
"Noto Sans SC", system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prop-tooltip--pinned {
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 12;
|
||||||
|
border-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--dsw-alias-brand-primary, #5e6ad2) 55%,
|
||||||
|
rgba(255, 255, 255, 0.2)
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.06) inset,
|
||||||
|
0 0 0 1px color-mix(in srgb, var(--dsw-alias-brand-primary, #5e6ad2) 35%, transparent),
|
||||||
|
0 16px 44px rgba(0, 0, 0, 0.52);
|
||||||
|
}
|
||||||
|
|
||||||
.prop-tooltip__label {
|
.prop-tooltip__label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
color: var(--dsw-alias-label-secondary, #a5adb8);
|
color: var(--dsw-alias-label-secondary, #a5adb8);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-tooltip__pin {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: var(--dsw-alias-brand-primary, #5e6ad2);
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0.92;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prop-tooltip__value {
|
.prop-tooltip__value {
|
||||||
|
|
@ -543,6 +570,8 @@ input {
|
||||||
color: var(--dsw-alias-label-primary, #f4f5f7);
|
color: var(--dsw-alias-label-primary, #f4f5f7);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
max-height: min(40vh, 280px);
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-overlay {
|
.edit-overlay {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue