618 lines
27 KiB
Vue
618 lines
27 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* Variables tab: the context as it leaves this chain, one row per
|
||
* variable, with the module that set it. Multi-picks list their items the
|
||
* way `$name.K` indexes them, and a pick's `accepts` axes show as the
|
||
* `$name.AXIS` reads they answer.
|
||
*/
|
||
import Button from "@/kit/Button.vue";
|
||
import Chip from "@/kit/Chip.vue";
|
||
import CopyButton from "@/kit/CopyButton.vue";
|
||
import { computed, ref } from "vue";
|
||
import RichTextPreview from "../../manager/components/RichTextPreview.vue";
|
||
import { TEMPLATE_TYPE, valueTypeLook, type ValueTypeLook } from "../injector/value-types";
|
||
import {
|
||
KIND_ICONS,
|
||
NODE_KINDS,
|
||
NODE_KIND_TITLES,
|
||
kindColor,
|
||
type Kind,
|
||
type NodeInfo,
|
||
type RawDetail,
|
||
type TraceStep,
|
||
type VarRow,
|
||
type WarningRow,
|
||
} from "./debug-model";
|
||
|
||
const props = defineProps<{
|
||
rows: VarRow[];
|
||
pinned: Set<string>;
|
||
flashed: Set<string>;
|
||
uuidToName: Map<string, string>;
|
||
uuidToKind: Map<string, string>;
|
||
ctxActiveKey: string | null;
|
||
/** Title + codename of a graph node (both "" when unknown). */
|
||
nodeInfo?: (nodeId: string) => NodeInfo;
|
||
stepsByKey?: Map<string, TraceStep>;
|
||
warningsByStep?: Map<string, WarningRow[]>;
|
||
/** The host can select a node on the canvas. */
|
||
canFocus?: boolean;
|
||
}>();
|
||
|
||
/** The variables grouped by the node that wrote them last: each Loop,
|
||
* Model Info, Toggles or Injector node gets a card drawn its own way, each
|
||
* Context a block of module rows, and values from outside the trace go
|
||
* last. A plain list only when no node wrote anything (no trace). */
|
||
interface Section {
|
||
id: string;
|
||
kind: Kind | "context" | "upstream";
|
||
nodeId: string;
|
||
title: string;
|
||
/** A Context's codename, shown as a chip beside the title. */
|
||
codename: string;
|
||
rows: VarRow[];
|
||
firstKey: string;
|
||
}
|
||
const sections = computed((): Section[] => {
|
||
const byId = new Map<string, Section>();
|
||
const order: Section[] = [];
|
||
for (const v of props.rows) {
|
||
const nodeKind = v.writerKind && NODE_KINDS.has(v.writerKind) ? v.writerKind : null;
|
||
const id = !v.writerKey ? "upstream" : `${nodeKind ? "n" : "c"}:${v.writerNodeId}`;
|
||
let sec = byId.get(id);
|
||
if (!sec) {
|
||
const info = v.writerNodeId ? props.nodeInfo?.(v.writerNodeId) : undefined;
|
||
sec = {
|
||
id,
|
||
kind: !v.writerKey ? "upstream" : nodeKind ?? "context",
|
||
nodeId: v.writerNodeId,
|
||
title: !v.writerKey
|
||
? "From upstream"
|
||
: nodeKind
|
||
? info?.title || NODE_KIND_TITLES[nodeKind] || nodeKind
|
||
: info?.title || "WP Context",
|
||
codename: v.writerKey && !nodeKind ? info?.codename ?? "" : "",
|
||
rows: [],
|
||
firstKey: v.writerKey ?? "",
|
||
};
|
||
byId.set(id, sec);
|
||
order.push(sec);
|
||
}
|
||
sec.rows.push(v);
|
||
if (v.writerKey && Number(v.writerKey) < Number(sec.firstKey)) sec.firstKey = v.writerKey;
|
||
}
|
||
return order.sort((a, b) => {
|
||
if (a.kind === "upstream") return 1;
|
||
if (b.kind === "upstream") return -1;
|
||
return Number(a.firstKey) - Number(b.firstKey);
|
||
});
|
||
});
|
||
// Every node gets its card, even when it is the only one, so a lone Context
|
||
// looks the same as it does beside others; only a snapshot with no trace
|
||
// (every value "From upstream") stays a plain list.
|
||
const grouped = computed(() => sections.value.some((s) => s.kind !== "upstream"));
|
||
|
||
// ── Rows from other nodes ────────────────────────────────────────────────
|
||
// Every row keeps the same metrics and everything a Context row shows
|
||
// (multi-pick items, negatives, axes, tags, write count); a node's rows only
|
||
// add how that node works: a switch, a radio mark, a model value, a type.
|
||
/** Long values fold to a few lines (and long lists to their first items)
|
||
* until the row is opened, so one long story doesn't fill the panel. */
|
||
const LONG_VALUE = 160;
|
||
const LONG_NEGATIVE = 120;
|
||
const FOLDED_ITEMS = 3;
|
||
const openRows = ref(new Set<string>());
|
||
function isLong(v: VarRow): boolean {
|
||
if ((v.value ?? "").length > LONG_VALUE) return true;
|
||
if (v.items && (v.items.length > FOLDED_ITEMS || v.items.some((it) => it.length > LONG_VALUE))) return true;
|
||
return v.negatives.reduce((n, neg) => n + neg.text.length, 0) > LONG_NEGATIVE;
|
||
}
|
||
function isFolded(v: VarRow): boolean {
|
||
return isLong(v) && !openRows.value.has(v.name);
|
||
}
|
||
function shownItems(v: VarRow): string[] {
|
||
const items = v.items ?? [];
|
||
return isFolded(v) ? items.slice(0, FOLDED_ITEMS) : items;
|
||
}
|
||
function moreLabel(v: VarRow): string {
|
||
const hidden = (v.items?.length ?? 0) - FOLDED_ITEMS;
|
||
return isFolded(v) && hidden > 0 ? `Show all (${hidden} more)` : isFolded(v) ? "Show all" : "Show less";
|
||
}
|
||
function toggleOpen(name: string): void {
|
||
const next = new Set(openRows.value);
|
||
if (next.has(name)) next.delete(name);
|
||
else next.add(name);
|
||
openRows.value = next;
|
||
}
|
||
function stepOf(v: VarRow): TraceStep | null {
|
||
return v.writerKey ? props.stepsByKey?.get(v.writerKey) ?? null : null;
|
||
}
|
||
function nodeKindOf(v: VarRow): Kind | null {
|
||
return v.writerKind && NODE_KINDS.has(v.writerKind) ? v.writerKind : null;
|
||
}
|
||
function boolOf(v: string): boolean | null {
|
||
const t = v.trim().toLowerCase();
|
||
if (t === "true") return true;
|
||
if (t === "false") return false;
|
||
return null;
|
||
}
|
||
function isPick(v: VarRow): boolean {
|
||
return !!stepOf(v)?.source.startsWith("Pick one");
|
||
}
|
||
/** A toggle's on/off state (also each pick of a Pick one node); null for
|
||
* anything else. A toggle with a value text holds that text (empty while
|
||
* off) and its state stays readable as `$name.bool`. */
|
||
function switchOf(v: VarRow): boolean | null {
|
||
if (v.writerKind !== "toggle" || isPick(v) || v.items) return null;
|
||
const step = stepOf(v);
|
||
if (step) return step.toggleBool;
|
||
return boolOf(v.value);
|
||
}
|
||
function isSwitch(v: VarRow): boolean {
|
||
return switchOf(v) !== null;
|
||
}
|
||
function hasToggleValue(v: VarRow): boolean {
|
||
return !!stepOf(v)?.toggleHasValue;
|
||
}
|
||
/** Pick one mode: this node also wrote a Pick one variable. */
|
||
function inPickMode(sec: Section): boolean {
|
||
return sec.kind === "toggle" && sec.rows.some(isPick);
|
||
}
|
||
/** The source column for a node-written variable: how that node set it. */
|
||
function nodeNote(sec: Section, v: VarRow): string {
|
||
const step = stepOf(v);
|
||
switch (v.writerKind) {
|
||
case "toggle": {
|
||
const up = step?.source.endsWith("set upstream") ? ", set upstream" : "";
|
||
if (isPick(v)) return `pick one${up}`;
|
||
if (!inPickMode(sec)) return `switch${up}`;
|
||
return `${switchOf(v) ? "picked" : "not picked"}${up}`;
|
||
}
|
||
case "loop":
|
||
return v.name.endsWith("_value") ? "value line" : v.name.endsWith("_total") ? "frames" : "frame";
|
||
case "model":
|
||
return step?.source ?? "";
|
||
case "injector":
|
||
return step?.isTemplate ? "template" : "wired in";
|
||
default:
|
||
return "";
|
||
}
|
||
}
|
||
/** A Context module's one-line fact for the source column: how a wildcard
|
||
* picked (its odds, pinned, swept, matched) or which branch a derivation
|
||
* ran. Empty when there is nothing short to say. */
|
||
function moduleFact(v: VarRow): string {
|
||
const step = stepOf(v);
|
||
const d = step?.detail;
|
||
if (!step || !d) return "";
|
||
if (step.kind === "wildcard") {
|
||
if (d.swept) return "swept";
|
||
if (d.match_variable) return `matched $${d.match_variable}`;
|
||
if (d.mode === "pinned") return "pinned";
|
||
if (typeof d.chance === "number" && d.chance > 0 && d.chance < 1) return `${Math.round(d.chance * 100)}%`;
|
||
return "";
|
||
}
|
||
if (step.kind === "derivation") {
|
||
const fired = (d.rules ?? []).map((r) => r.fired).find((f) => f != null);
|
||
if (fired === "else") return "else";
|
||
if (typeof fired === "number") return `branch ${fired + 1}`;
|
||
}
|
||
return "";
|
||
}
|
||
/** The Injector's own icon + colour for this value's wire type. */
|
||
function injectorLook(v: VarRow): ValueTypeLook {
|
||
const step = stepOf(v);
|
||
if (step?.isTemplate) return TEMPLATE_TYPE;
|
||
return valueTypeLook(step?.valueType);
|
||
}
|
||
function injectorType(v: VarRow): string {
|
||
if (v.writerKind !== "injector") return "";
|
||
const step = stepOf(v);
|
||
if (!step) return "";
|
||
return step.isTemplate ? "TPL" : step.valueType;
|
||
}
|
||
/** The Loop's frame line: frame, seed, sweep combination, value line. */
|
||
function loopDetail(sec: Section): RawDetail | null {
|
||
if (sec.kind !== "loop") return null;
|
||
for (const v of sec.rows) {
|
||
const d = stepOf(v)?.detail;
|
||
if (d && typeof d.frame === "number") return d;
|
||
}
|
||
return null;
|
||
}
|
||
function frameParts(sec: Section): string[] {
|
||
const d = loopDetail(sec);
|
||
if (!d) return [];
|
||
const out = [d.of ? `frame ${d.frame} of ${d.of}` : `frame ${d.frame}`];
|
||
if (d.bypass) out.push("loop bypassed");
|
||
if (typeof d.combo === "number") out.push(`combination ${d.combo} / ${d.combos ?? "?"}`);
|
||
if (typeof d.value_line === "number") out.push(`line ${d.value_line} / ${d.values ?? "?"}`);
|
||
return out;
|
||
}
|
||
function loopSeed(sec: Section): string {
|
||
const s = loopDetail(sec)?.seed;
|
||
return typeof s === "number" ? String(s) : "";
|
||
}
|
||
function sectionWarnings(sec: Section): WarningRow[] {
|
||
if (sec.kind === "context" || sec.kind === "upstream") return [];
|
||
if (!props.warningsByStep || !props.stepsByKey) return [];
|
||
const out: WarningRow[] = [];
|
||
for (const [key, list] of props.warningsByStep) {
|
||
if (props.stepsByKey.get(key)?.nodeId === sec.nodeId) out.push(...list);
|
||
}
|
||
return out;
|
||
}
|
||
function sectionIcon(sec: Section): string {
|
||
if (sec.kind === "upstream") return "pi pi-arrow-down";
|
||
if (sec.kind === "context") return "pi pi-sitemap";
|
||
return KIND_ICONS[sec.kind];
|
||
}
|
||
function sectionColor(sec: Section): string {
|
||
if (sec.kind === "upstream") return "var(--wp-text-dim)";
|
||
if (sec.kind === "context") return "var(--wp-accent-text, var(--wp-accent))";
|
||
return kindColor(sec.kind);
|
||
}
|
||
|
||
/** What the source column calls a variable's writer: the module's name, or
|
||
* for a node-written variable (Toggles, Model Info, Loop, Injector) that
|
||
* node's title on the canvas. */
|
||
function writerLabel(v: VarRow): string {
|
||
if (v.writerName) return v.writerName;
|
||
const kind = v.writerKind;
|
||
if (kind && NODE_KINDS.has(kind)) {
|
||
const title = v.writerNodeId ? props.nodeInfo?.(v.writerNodeId).title ?? "" : "";
|
||
return title || NODE_KIND_TITLES[kind] || kind;
|
||
}
|
||
return kind ?? "";
|
||
}
|
||
const emit = defineEmits<{
|
||
(e: "goto-step", key: string): void;
|
||
(e: "row-menu", ev: MouseEvent, row: VarRow): void;
|
||
(e: "show-var", row: VarRow): void;
|
||
}>();
|
||
|
||
/** One name column for every row, sized to the longest name (capped;
|
||
* longer names wrap) so values line up down the list. */
|
||
const nameWidth = computed(() => {
|
||
const longest = props.rows.reduce((n, v) => Math.max(n, v.name.length), 0);
|
||
return `calc(${Math.min(longest + 1, 16)}ch + 26px)`;
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="wp-dbg-vars" :class="{ 'is-grouped': grouped }" data-test="dbg-vars" :style="{ '--wp-dbg-name-w': nameWidth }">
|
||
<section
|
||
v-for="sec in sections"
|
||
:key="sec.id"
|
||
class="wp-dbg-vsec"
|
||
:data-kind="sec.kind"
|
||
:style="{ '--wp-dbg-kind': sectionColor(sec) }"
|
||
data-test="dbg-var-section"
|
||
>
|
||
<header v-if="grouped" class="wp-dbg-vsec__head" data-test="dbg-var-section-head">
|
||
<i :class="['wp-dbg-vsec__icon', sectionIcon(sec)]" aria-hidden="true" />
|
||
<span class="wp-dbg-vsec__title">{{ sec.title }}</span>
|
||
<span v-if="sec.codename" class="wp-dbg-vsec__codename" data-test="dbg-var-section-codename">{{ sec.codename }}</span>
|
||
<span class="wp-dbg-vsec__count">{{ sec.rows.length }}</span>
|
||
<Button
|
||
v-if="sec.kind !== 'upstream' && sec.firstKey"
|
||
variant="link"
|
||
size="sm"
|
||
class="wp-dbg-vsec__goto"
|
||
icon="pi-bolt"
|
||
hint="Show this node's steps in the trace"
|
||
@click="emit('goto-step', sec.firstKey)"
|
||
>trace</Button>
|
||
</header>
|
||
<div v-if="frameParts(sec).length" class="wp-dbg-vsec__frame" data-test="dbg-loop-frame">
|
||
<span v-for="(p, i) in frameParts(sec)" :key="i" :class="{ 'is-lead': i === 0 }">{{ p }}</span>
|
||
<CopyButton
|
||
v-if="loopSeed(sec)"
|
||
:text="loopSeed(sec)"
|
||
label="Copy this frame's seed"
|
||
size="sm"
|
||
variant="ghost"
|
||
class="wp-dbg-vsec__seed"
|
||
><span class="wp-dbg-vsec__seed-label">seed</span>{{ loopSeed(sec) }}</CopyButton>
|
||
</div>
|
||
<div
|
||
v-for="v in sec.rows"
|
||
:key="v.name"
|
||
class="wp-dbg-var-row"
|
||
:style="{ '--wp-dbg-kind': v.writerKind ? kindColor(v.writerKind) : 'transparent' }"
|
||
:data-kind="v.writerKind ?? 'upstream'"
|
||
:class="{
|
||
'is-internal': v.internal,
|
||
'wp-row-flash': flashed.has(v.name),
|
||
'is-ctx': ctxActiveKey === `var:${v.name}`,
|
||
}"
|
||
data-test="dbg-var"
|
||
@contextmenu="(ev) => emit('row-menu', ev, v)"
|
||
>
|
||
<div class="wp-dbg-var-row__name">
|
||
<i v-if="pinned.has(`var:${v.name}`)" class="pi pi-star-fill wp-dbg-pin" aria-label="pinned" />
|
||
<span class="wp-dbg-var-row__mark" aria-hidden="true" />
|
||
<code>${{ v.name }}</code>
|
||
<i v-if="v.internal" class="pi pi-eye-slash wp-dbg-flag" title="Internal: downstream modules can read it, the prompt never shows it" />
|
||
<span v-if="v.writeCount > 1" class="wp-dbg-var-row__count" :title="`Written ${v.writeCount} times; the last write wins`">×{{ v.writeCount }}</span>
|
||
</div>
|
||
<div class="wp-dbg-var-row__value" :class="{ 'is-model': v.writerKind === 'model', 'is-folded': isFolded(v) }">
|
||
<Chip
|
||
v-if="isSwitch(v) && !hasToggleValue(v)"
|
||
class="wp-dbg-var-row__switch"
|
||
:class="switchOf(v) ? 'is-on' : 'is-off'"
|
||
:color="switchOf(v) ? kindColor('toggle') : null"
|
||
dot
|
||
:title="hasToggleValue(v) ? `$${v.name}.bool: the toggle is ${switchOf(v) ? 'on' : 'off'}` : `$${v.name}: the toggle is ${switchOf(v) ? 'on' : 'off'}`"
|
||
data-test="dbg-node-switch"
|
||
>{{ switchOf(v) ? "true" : "false" }}</Chip>
|
||
<template v-if="isSwitch(v) && !hasToggleValue(v)" />
|
||
<ol v-else-if="v.items" class="wp-dbg-var-row__items" data-test="dbg-var-items">
|
||
<li v-for="(it, i) in shownItems(v)" :key="i">
|
||
<span class="wp-dbg-var-row__idx" :title="`$${v.name}.${i}`">.{{ i }}</span>
|
||
<RichTextPreview :value="it" :uuid-to-name="uuidToName" :uuid-to-kind="uuidToKind" surface="wildcard" />
|
||
</li>
|
||
</ol>
|
||
<span v-else-if="v.value && v.writerKind === 'model'" class="wp-dbg-var-row__model">{{ v.value }}</span>
|
||
<span v-else-if="v.value && isPick(v)" class="wp-dbg-var-row__pick">
|
||
<i class="pi pi-circle-on wp-dbg-var-row__radio" title="The picked value" aria-hidden="true" />
|
||
<RichTextPreview :value="v.value" :uuid-to-name="uuidToName" :uuid-to-kind="uuidToKind" surface="wildcard" />
|
||
</span>
|
||
<RichTextPreview
|
||
v-else-if="v.value"
|
||
:value="v.value"
|
||
:uuid-to-name="uuidToName"
|
||
:uuid-to-kind="uuidToKind"
|
||
surface="wildcard"
|
||
/>
|
||
<span v-else class="wp-dbg-var-row__empty">{{ v.writerKind === "model" ? "not detected" : isPick(v) ? "nothing picked" : isSwitch(v) ? "empty while off" : "empty" }}</span>
|
||
<div
|
||
v-for="(n, i) in v.negatives"
|
||
:key="`neg-${i}`"
|
||
class="wp-dbg-var-row__neg"
|
||
data-test="dbg-var-neg"
|
||
title="Negative words: an Assembler that renders this variable adds them to its negative output"
|
||
>
|
||
<span class="wp-dbg-var-row__neg-mark" aria-hidden="true">−</span>
|
||
<span class="wp-dbg-var-row__neg-text">{{ n.text }}</span>
|
||
<span v-if="n.source" class="wp-dbg-var-row__neg-src" data-test="dbg-var-neg-src">· {{ n.source }}</span>
|
||
</div>
|
||
<div v-if="v.axes.length || v.tags.length || hasToggleValue(v)" class="wp-dbg-var-row__extra">
|
||
<Chip
|
||
v-if="hasToggleValue(v)"
|
||
class="wp-dbg-var-row__axis"
|
||
:class="{ 'is-on': switchOf(v) }"
|
||
:color="switchOf(v) ? kindColor('toggle') : null"
|
||
dot
|
||
data-test="dbg-var-bool"
|
||
:title="`$${v.name}.bool: the toggle is ${switchOf(v) ? 'on' : 'off'}`"
|
||
><code>.bool</code> {{ switchOf(v) ? "true" : "false" }}</Chip>
|
||
<Chip
|
||
v-for="a in v.axes"
|
||
:key="a.axis"
|
||
class="wp-dbg-var-row__axis"
|
||
:color="kindColor(v.writerKind ?? 'wildcard')"
|
||
data-test="dbg-var-axis"
|
||
:title="`$${v.name}.${a.axis}`"
|
||
><code>.{{ a.axis }}</code> {{ a.value || "—" }}</Chip>
|
||
<Chip v-for="t in v.tags" :key="t" class="wp-dbg-var-row__tag" icon="pi-tag">{{ t }}</Chip>
|
||
</div>
|
||
<Button
|
||
v-if="isLong(v)"
|
||
variant="link"
|
||
size="sm"
|
||
class="wp-dbg-var-row__more"
|
||
:icon="isFolded(v) ? 'pi-chevron-down' : 'pi-chevron-up'"
|
||
:aria-expanded="!isFolded(v)"
|
||
data-test="dbg-var-more"
|
||
@click="toggleOpen(v.name)"
|
||
>{{ moreLabel(v) }}</Button>
|
||
</div>
|
||
<Button
|
||
v-if="v.writerKey"
|
||
variant="link"
|
||
size="sm"
|
||
class="wp-dbg-var-row__src"
|
||
hint="Show in trace"
|
||
data-test="dbg-var-src"
|
||
@click="emit('goto-step', v.writerKey)"
|
||
>
|
||
<i :class="['wp-dbg-var-row__src-icon', KIND_ICONS[v.writerKind ?? 'unknown']]" aria-hidden="true" />
|
||
<Chip
|
||
v-if="injectorType(v)"
|
||
:color="injectorLook(v).color"
|
||
:icon="injectorLook(v).icon"
|
||
class="wp-dbg-var-row__type"
|
||
data-test="dbg-node-type"
|
||
>{{ injectorType(v) }}</Chip>
|
||
<span class="wp-dbg-var-row__src-name" :title="writerLabel(v)" :data-test="nodeKindOf(v) ? 'dbg-node-source' : undefined">{{ nodeKindOf(v) ? nodeNote(sec, v) || writerLabel(v) : writerLabel(v) }}</span>
|
||
<span v-if="!nodeKindOf(v) && moduleFact(v)" class="wp-dbg-var-row__fact" data-test="dbg-var-fact">{{ moduleFact(v) }}</span>
|
||
</Button>
|
||
<span v-else class="wp-dbg-var-row__src wp-dbg-var-row__src--upstream" title="Came in from upstream, no module in this trace wrote it">upstream</span>
|
||
</div>
|
||
<div
|
||
v-for="w in sectionWarnings(sec)"
|
||
:key="`w-${w.key}`"
|
||
class="wp-dbg-vsec__warn"
|
||
:class="`is-${w.severity}`"
|
||
data-test="dbg-node-warning"
|
||
>
|
||
<i class="pi pi-exclamation-triangle" aria-hidden="true" />
|
||
<span class="wp-dbg-vsec__warn-label">{{ w.label }}</span>
|
||
<span class="wp-dbg-vsec__warn-msg">{{ w.message }}</span>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.wp-dbg-vars { display: flex; flex-direction: column; }
|
||
.wp-dbg-vars.is-grouped { gap: 10px; }
|
||
.wp-dbg-vsec { display: flex; flex-direction: column; }
|
||
/* Grouped: each node is its own card, tinted by its kind. */
|
||
.wp-dbg-vars.is-grouped .wp-dbg-vsec {
|
||
border: 1px solid color-mix(in oklab, var(--wp-dbg-kind) 26%, var(--wp-border));
|
||
border-radius: min(7px, var(--wp-radius-cap, 999px));
|
||
background: color-mix(in oklab, var(--wp-dbg-kind) 3%, transparent);
|
||
overflow: hidden;
|
||
}
|
||
.wp-dbg-vsec__head {
|
||
display: flex; align-items: center; gap: 7px;
|
||
padding: 5px 8px;
|
||
font-size: 10px; color: var(--wp-text-dim);
|
||
background: color-mix(in oklab, var(--wp-dbg-kind) 10%, transparent);
|
||
border-bottom: 1px solid color-mix(in oklab, var(--wp-dbg-kind) 20%, var(--wp-border));
|
||
}
|
||
.wp-dbg-vsec__icon {
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
width: 18px; height: 18px; flex: none;
|
||
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
||
font-size: 10px; color: var(--wp-dbg-kind);
|
||
background: color-mix(in oklab, var(--wp-dbg-kind) 18%, transparent);
|
||
}
|
||
.wp-dbg-vsec__title { font: 600 11.5px/1.4 var(--wp-font-sans); color: var(--wp-text); }
|
||
.wp-dbg-vsec__codename {
|
||
display: inline-flex; align-items: center;
|
||
height: 18px; box-sizing: border-box;
|
||
font: 600 10px/1 var(--wp-font-mono);
|
||
letter-spacing: 0.03em;
|
||
color: var(--wp-text);
|
||
padding: 0 7px;
|
||
border-radius: min(5px, var(--wp-radius-cap, 999px));
|
||
border: 1px solid var(--wp-border-strong, var(--wp-border));
|
||
background: var(--wp-bg, transparent);
|
||
}
|
||
.wp-dbg-vsec__count { font: 500 9.5px/1 var(--wp-font-mono); }
|
||
.wp-dbg-vsec__goto.wp-btn { margin-left: auto; height: auto; min-height: 0; padding: 0; font-size: 10px; }
|
||
/* Only the word is underlined, not the icon (wp-kit fixes this for every
|
||
* link button from 0.20; drop this once the kit is bumped). */
|
||
.wp-dbg-vsec__goto.wp-btn:is(:hover, :focus-visible) { text-decoration: none; }
|
||
.wp-dbg-vsec__goto.wp-btn:is(:hover, :focus-visible) :deep(.wp-btn__label) { text-decoration: underline; }
|
||
.wp-dbg-var-row {
|
||
display: grid;
|
||
grid-template-columns: max(90px, var(--wp-dbg-name-w, 90px)) minmax(0, 1fr) auto;
|
||
column-gap: 12px;
|
||
align-items: start;
|
||
padding: 6px 8px;
|
||
border-top: 1px solid var(--wp-border-soft, var(--wp-border));
|
||
font-size: 11.5px;
|
||
}
|
||
.wp-dbg-vsec__head + .wp-dbg-var-row, .wp-dbg-vsec__frame + .wp-dbg-var-row, .wp-dbg-var-row:first-child { border-top: 0; }
|
||
/* The writer's kind as a small square before the name (none upstream). */
|
||
.wp-dbg-var-row__mark {
|
||
display: inline-block; vertical-align: 1px; margin-right: 6px;
|
||
width: 6px; height: 6px;
|
||
border-radius: min(2px, var(--wp-radius-cap, 999px));
|
||
background: var(--wp-dbg-kind, var(--wp-border-strong));
|
||
}
|
||
.wp-dbg-var-row[data-kind="upstream"] .wp-dbg-var-row__mark { background: transparent; box-shadow: inset 0 0 0 1px var(--wp-text-dim); }
|
||
.wp-dbg-var-row:hover { background: var(--wp-row-hover, var(--wp-bg2)); }
|
||
.wp-dbg-var-row.is-ctx { outline: 1px solid var(--wp-accent); outline-offset: -1px; }
|
||
/* Inline flow, not flex: a long name breaks inside itself and the square
|
||
stays on its first line. */
|
||
.wp-dbg-var-row__name { display: block; min-width: 0; line-height: 1.5; }
|
||
.wp-dbg-var-row__name > .wp-dbg-flag, .wp-dbg-var-row__name > .wp-dbg-var-row__count, .wp-dbg-var-row__name > .wp-dbg-pin { margin: 0 4px; }
|
||
.wp-dbg-var-row__name code { word-break: break-all; }
|
||
.wp-dbg-var-row__name code { font: 600 11.5px/1.5 var(--wp-font-mono); color: var(--wp-accent-text, var(--wp-accent)); }
|
||
.wp-dbg-var-row.is-internal .wp-dbg-var-row__name code { color: var(--wp-text-muted); }
|
||
.wp-dbg-flag { font-size: 9px; color: var(--wp-text-dim); }
|
||
.wp-dbg-pin { font-size: 9px; color: var(--wp-amber, var(--wp-warn)); }
|
||
.wp-dbg-var-row__count { font: 500 9px/1 var(--wp-font-mono); color: var(--wp-text-dim); }
|
||
.wp-dbg-var-row__value { min-width: 0; color: var(--wp-text); line-height: 1.5; word-break: break-word; }
|
||
.wp-dbg-var-row__empty { font-style: italic; color: var(--wp-text-dim); }
|
||
.wp-dbg-var-row__items { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 1px; }
|
||
.wp-dbg-var-row__items li { display: flex; gap: 6px; align-items: baseline; }
|
||
.wp-dbg-var-row__idx { font: 500 9.5px/1 var(--wp-font-mono); color: var(--wp-text-dim); min-width: 16px; }
|
||
.wp-dbg-var-row__neg { display: flex; gap: 5px; align-items: baseline; font: 500 10.5px/1.5 var(--wp-font-mono); }
|
||
.wp-dbg-var-row__neg-mark, .wp-dbg-var-row__neg-text { color: var(--wp-red); }
|
||
.wp-dbg-var-row__neg-text { min-width: 0; overflow-wrap: anywhere; }
|
||
.wp-dbg-var-row__neg-src { flex: none; color: var(--wp-text-dim); font-family: var(--wp-font-sans); }
|
||
/* Folded: the value shows three lines, each negative one, until opened. */
|
||
.wp-dbg-var-row__value.is-folded > .wp-rtp,
|
||
.wp-dbg-var-row__value.is-folded .wp-dbg-var-row__pick > .wp-rtp,
|
||
.wp-dbg-var-row__value.is-folded .wp-dbg-var-row__items li > .wp-rtp {
|
||
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden;
|
||
}
|
||
.wp-dbg-var-row__value.is-folded .wp-dbg-var-row__neg-text {
|
||
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; line-clamp: 1; overflow: hidden;
|
||
}
|
||
.wp-btn.wp-dbg-var-row__more { align-self: flex-start; height: auto; min-height: 0; padding: 0; font-size: 10.5px; }
|
||
.wp-dbg-var-row__extra { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 3px; }
|
||
/* Kit chips, sized like the switch chip; the accessor reads in mono.
|
||
18px, not 17: inside the 1px border that leaves an even 16px for a 10px
|
||
line, so the text and icon land on whole pixels. At 17 the half-pixel
|
||
gap rounded the icon up and the text down (Firefox, canvas zoom), and
|
||
the chip looked off-centre. Same for the switch/type chips and the
|
||
codename pill below. */
|
||
.wp-chip.wp-dbg-var-row__axis, .wp-chip.wp-dbg-var-row__tag {
|
||
height: 18px; min-height: 0; box-sizing: border-box;
|
||
padding: 0 6px; gap: 4px;
|
||
font: 500 10px/1 var(--wp-font-sans);
|
||
}
|
||
.wp-dbg-var-row__axis code { font: 600 10px/1 var(--wp-font-mono); }
|
||
.wp-dbg-var-row__fact {
|
||
flex: none;
|
||
font: 600 9.5px/1 var(--wp-font-mono);
|
||
padding: 2px 5px;
|
||
border-radius: min(3px, var(--wp-radius-cap, 999px));
|
||
color: var(--wp-dbg-kind);
|
||
background: color-mix(in oklab, var(--wp-dbg-kind) 14%, transparent);
|
||
}
|
||
/* The plain span (upstream) and the kit link Button share this look. */
|
||
.wp-dbg-var-row__src, .wp-dbg-var-row__src.wp-btn {
|
||
display: inline-flex; align-items: center; gap: 5px; height: auto; min-height: 0; padding: 1px 0;
|
||
color: var(--wp-text-dim); font: 500 10.5px/1.5 var(--wp-font-sans);
|
||
max-width: 150px; white-space: nowrap; text-decoration: none;
|
||
}
|
||
.wp-dbg-var-row__src.wp-btn:hover .wp-dbg-var-row__src-name { color: var(--wp-text); text-decoration: underline; }
|
||
.wp-dbg-var-row__src-name { overflow: hidden; text-overflow: ellipsis; }
|
||
|
||
/* Rows from other nodes: the same row, with how that node works. */
|
||
.wp-dbg-var-row__value.is-model { font: 600 11.5px/1.5 var(--wp-font-mono); color: var(--wp-dbg-kind); }
|
||
.wp-dbg-var-row__model { overflow-wrap: anywhere; }
|
||
.wp-dbg-var-row__pick { display: inline-flex; align-items: baseline; gap: 5px; min-width: 0; }
|
||
.wp-dbg-var-row__radio { font-size: 9px; color: var(--wp-dbg-kind); }
|
||
/* Kit chips sized to the row's text so they don't push it around. */
|
||
.wp-chip.wp-dbg-var-row__switch, .wp-chip.wp-dbg-var-row__type {
|
||
height: 18px; min-height: 0; box-sizing: border-box;
|
||
padding: 0 6px; gap: 4px;
|
||
font: 600 10px/1 var(--wp-font-mono);
|
||
vertical-align: middle;
|
||
}
|
||
.wp-dbg-vsec__frame {
|
||
display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px;
|
||
padding: 4px 8px;
|
||
font: 500 10.5px/1.6 var(--wp-font-sans);
|
||
color: var(--wp-text-dim);
|
||
border-bottom: 1px solid var(--wp-border-soft, var(--wp-border));
|
||
}
|
||
.wp-dbg-vsec__frame .is-lead { color: var(--wp-dbg-kind); font-weight: 600; }
|
||
.wp-copy-btn.wp-dbg-vsec__seed {
|
||
margin-left: auto;
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
height: auto; min-height: 0;
|
||
background: transparent; border: 0; padding: 0;
|
||
color: var(--wp-text-muted);
|
||
font: 500 10.5px/1.6 var(--wp-font-mono);
|
||
cursor: pointer;
|
||
}
|
||
.wp-copy-btn.wp-dbg-vsec__seed:hover { color: var(--wp-text); }
|
||
.wp-dbg-vsec__seed-label { color: var(--wp-text-dim); font-family: var(--wp-font-sans); }
|
||
.wp-dbg-vsec__warn {
|
||
display: flex; align-items: baseline; flex-wrap: wrap; gap: 5px;
|
||
padding: 4px 8px;
|
||
font-size: 10.5px; color: var(--wp-warn);
|
||
border-top: 1px solid var(--wp-border-soft, var(--wp-border));
|
||
background: color-mix(in oklab, var(--wp-warn) 8%, transparent);
|
||
}
|
||
.wp-dbg-vsec__warn.is-error { color: var(--wp-red); background: color-mix(in oklab, var(--wp-red) 8%, transparent); }
|
||
.wp-dbg-vsec__warn.is-info { color: var(--wp-text-muted); background: transparent; }
|
||
.wp-dbg-vsec__warn .pi { font-size: 9px; }
|
||
.wp-dbg-vsec__warn-label { font-weight: 600; }
|
||
.wp-dbg-vsec__warn-msg { color: var(--wp-text-muted); }
|
||
.wp-dbg-var-row__src--upstream { cursor: default; font-style: italic; }
|
||
.wp-dbg-var-row__src-icon { font-size: 9px; flex: none; color: var(--wp-dbg-kind); }
|
||
.wp-dbg-var-row__src.wp-btn :deep(.wp-btn__label) { display: inline-flex; align-items: center; gap: 5px; min-width: 0; }
|
||
.wp-dbg-var-row__src.wp-btn :deep(.wp-btn__label) { line-height: inherit; }
|
||
</style>
|