diff --git a/e2e/harness/tsLoader.js b/e2e/harness/tsLoader.js
index 9e82da6..66b8c35 100644
--- a/e2e/harness/tsLoader.js
+++ b/e2e/harness/tsLoader.js
@@ -66,6 +66,8 @@ function createTsLoader(options = {}) {
const target = resolveRelative(abs, request);
const targetRel = toRepoRelative(target);
if (Object.prototype.hasOwnProperty.call(mocks, targetRel)) return mocks[targetRel];
+ // Mirrors webpack's `asset/source` rule for stylesheets: the import is the file's text.
+ if (target.endsWith('.css')) return { __esModule: true, default: fs.readFileSync(target, 'utf8') };
return load(targetRel);
}
return repoRequire(request);
diff --git a/scripts/test-webview.js b/scripts/test-webview.js
index c532b38..5fcfa90 100644
--- a/scripts/test-webview.js
+++ b/scripts/test-webview.js
@@ -1047,6 +1047,8 @@ function testObjModSaveCommitsFocusedEditor() {
function testObjModTooltipFontWiring() {
const host = fs.readFileSync(path.join(root, 'src/features/objModPreview.ts'), 'utf8');
+ // Viewer CSS lives in its own stylesheet now; only the runtime @font-face rule stays in the host.
+ const css = fs.readFileSync(path.join(root, 'src/webview/objModEditor/objModEditor.css'), 'utf8');
const packageJson = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
const packageData = JSON.parse(packageJson);
@@ -1059,10 +1061,10 @@ function testObjModTooltipFontWiring() {
assert.ok(host.includes('@font-face'), 'objmod should declare the project font for tooltip previews');
assert.ok(!host.includes("Buffer.from(bytes).toString('base64')"), 'project fonts should not be embedded as large data URLs');
assert.ok(/\.tt-collapsed-box,\r?\n\.tt-preview \{/.test(host), 'the custom font should be assigned only to tooltip boxes');
- assert.ok(!host.includes('.tt-collapsed-box *'), 'the custom font should not use descendant-wide override selectors');
- assert.ok(host.includes('td.value { font-family: var(--font); }'), 'ordinary field values should use the VS Code UI font');
- assert.ok(host.includes('.tt-empty { color: var(--muted); font-style: normal; }'), 'ordinary empty values should not be italicized');
- assert.ok(host.includes('.tt-collapsed-box .tt-empty,'), 'only framed WC3 text may retain the italic empty placeholder');
+ assert.ok(!host.includes('.tt-collapsed-box *') && !css.includes('.tt-collapsed-box *'), 'the custom font should not use descendant-wide override selectors');
+ assert.ok(css.includes('td.value { font-family: var(--font); }'), 'ordinary field values should use the VS Code UI font');
+ assert.ok(css.includes('.tt-empty { color: var(--muted); font-style: normal; }'), 'ordinary empty values should not be italicized');
+ assert.ok(css.includes('.tt-collapsed-box .tt-empty,'), 'only framed WC3 text may retain the italic empty placeholder');
assert.ok(packageJson.includes('wurst.objModTooltipFont'), 'the tooltip font setting should be contributed by the extension');
assert.equal(packageData.contributes.configuration.properties['wurst.objModTooltipFont'].default, '', 'the tooltip font setting must default to disabled');
}
@@ -1164,11 +1166,14 @@ function testObjModSavedAndUsedTooltipColors() {
assert.ok(host.includes("context.globalState"), 'saved custom colors should follow the user across workspaces');
assert.ok(host.includes("msg.type === 'rememberCustomColor'"), 'the objmod host should persist custom colors sent by the picker');
assert.ok(host.includes('customColors,'), 'saved custom colors should be restored into new objmod webviews');
- assert.ok(host.includes('.tt-custom-colors[hidden] { display: none; }'), 'an empty saved palette should remain hidden despite its flex layout');
+ const css = fs.readFileSync(path.join(root, 'src/webview/objModEditor/objModEditor.css'), 'utf8');
+ assert.ok(css.includes('.tt-custom-colors[hidden] { display: none; }'), 'an empty saved palette should remain hidden despite its flex layout');
}
function testObjModDensityAndTreeStyling() {
- const host = fs.readFileSync(path.join(root, 'src/features/objModPreview.ts'), 'utf8');
+ // Host markup plus the extracted stylesheet: these guards cover both the DOM and its CSS.
+ const host = fs.readFileSync(path.join(root, 'src/features/objModPreview.ts'), 'utf8')
+ + '\n' + fs.readFileSync(path.join(root, 'src/webview/objModEditor/objModEditor.css'), 'utf8');
const webview = fs.readFileSync(path.join(root, 'src/webview/objModEditorWebview.ts'), 'utf8');
assert.ok(host.includes('id="density-toggle" class="density-toggle" role="switch"'), 'density must render as an obvious switch control');
diff --git a/src/features/mapDataPreview.ts b/src/features/mapDataPreview.ts
index 1398a66..d9e252f 100644
--- a/src/features/mapDataPreview.ts
+++ b/src/features/mapDataPreview.ts
@@ -7,6 +7,7 @@ import * as fs from 'fs';
import * as vscode from 'vscode';
import { BinReader, BinWriter, parseW3i as parseW3iFile, serializeW3i, W3iFile, W3iPlayer, W3iForce } from 'casc-ts/formats';
import { BinaryEdit, EditableBinaryDocument, EditableBinaryEditorProvider, ParsedPreviewContext, registerParsedPreviewer } from './preview/framework';
+import MAP_DATA_CSS from '../webview/mapData.css';
import {
loadTriggerStringsForUri, resolveTriggerString, ResolvedText, TriggerStringTable,
findWtsUri, applyWtsEdits,
@@ -387,185 +388,7 @@ function page(title: string, body: string, extraCss = '', scripts = false): stri
: "default-src 'none'; style-src 'unsafe-inline';",
title: escapeHtml(title),
extraCss: `
-.content { flex: 1; overflow: auto; }
-.md-header {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 16px 9px;
- border-bottom: 1px solid var(--border);
- background: var(--sidebar);
-}
-.md-title {
- color: var(--vscode-textLink-foreground, var(--fg));
- font-size: 15px;
- font-weight: 600;
- line-height: 1.3;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.md-meta { color: var(--muted); font-size: 12px; margin-top: 1px; }
-.readonly-badge {
- display: inline-block;
- margin-left: 8px;
- padding: 1px 5px;
- border: 1px solid var(--border);
- border-radius: 2px;
- color: var(--muted);
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
-}
-.dialog { max-width: 1060px; padding: 14px 16px 22px; }
-h2 {
- color: var(--fg);
- font-size: 12px;
- font-weight: 600;
- margin: 0 0 9px;
-}
-.section {
- border-top: 1px solid var(--border);
- padding-top: 12px;
- margin-top: 14px;
-}
-.section:first-child { border-top: 0; padding-top: 0; margin-top: 0; }
-.form-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
- gap: 9px 12px;
-}
-.form-grid.compact { grid-template-columns: repeat(auto-fit, minmax(145px, 1fr)); }
-.field { min-width: 0; }
-.field.wide { grid-column: 1 / -1; }
-.field-label {
- display: flex;
- align-items: center;
- gap: 6px;
- color: var(--muted);
- font-size: 11px;
- line-height: 1.25;
- margin-bottom: 3px;
-}
-.source-pill {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- border: 1px solid var(--border);
- border-radius: 2px;
- padding: 0 4px;
- color: var(--vscode-textLink-foreground, var(--muted));
- font-family: var(--mono);
- font-size: 10px;
-}
-.source-pill.missing {
- color: var(--vscode-errorForeground, #f14c4c);
-}
-.field-control {
- width: 100%;
- min-width: 0;
- height: 26px;
- padding: 3px 6px;
- color: var(--input-fg);
- background: var(--input-bg);
- border: 1px solid var(--input-border, var(--border));
- border-radius: 2px;
- font: inherit;
- line-height: 18px;
-}
-textarea.field-control {
- height: auto;
- min-height: 58px;
- resize: none;
- line-height: 1.35;
-}
-.field-control:disabled {
- opacity: 1;
- cursor: default;
-}
-.field-control.mono { font-family: var(--mono); }
-.checkbox-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
- gap: 7px 14px;
-}
-.check-row {
- display: flex;
- align-items: flex-start;
- gap: 7px;
- min-width: 0;
- color: var(--fg);
-}
-.check-row input {
- flex: 0 0 auto;
- margin: 1px 0 0;
- accent-color: var(--vscode-checkbox-selectBackground, var(--vscode-button-background));
-}
-.check-row input:disabled { opacity: 1; }
-.check-row span {
- min-width: 0;
- overflow-wrap: anywhere;
-}
-.empty { color: var(--muted); font-style: italic; }
-.error {
- color: var(--vscode-errorForeground, #f14c4c);
- border: 1px solid color-mix(in srgb, currentColor 65%, transparent);
- padding: 7px 9px;
- margin-bottom: 12px;
- border-radius: 2px;
-}
-.table-wrap {
- overflow: auto;
- border: 1px solid var(--border);
-}
-table { border-collapse: collapse; width: 100%; font-size: 12px; min-width: 620px; }
-th {
- text-align: left;
- background: var(--vscode-editorGroupHeader-tabsBackground, var(--sidebar));
- border-bottom: 1px solid var(--border);
- padding: 6px 8px;
- font-weight: 600;
-}
-td {
- border-bottom: 1px solid color-mix(in srgb, var(--border) 45%, transparent);
- padding: 5px 8px;
- vertical-align: top;
-}
-tbody tr:last-child td { border-bottom: 0; }
-td.num { text-align: right; font-family: var(--mono); }
-td.mono { font-family: var(--mono); }
-.metric-strip {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-bottom: 10px;
-}
-.metric {
- border: 1px solid var(--border);
- background: var(--input-bg);
- padding: 4px 7px;
- border-radius: 2px;
- color: var(--muted);
- font-size: 12px;
-}
-.metric strong { color: var(--fg); font-weight: 600; }
-.tag-list { display: flex; flex-wrap: wrap; gap: 5px; }
-.tag {
- border: 1px solid var(--border);
- background: var(--input-bg);
- border-radius: 2px;
- padding: 2px 6px;
- font-family: var(--mono);
- font-size: 12px;
-}
-canvas {
- image-rendering: pixelated;
- max-width: min(100%, 768px);
- border: 1px solid var(--border);
- background: var(--vscode-editor-background, #1e1e1e);
-}
-.swatch { display:inline-block; width:12px; height:12px; border:1px solid var(--border); vertical-align:-2px; margin-right:6px; }
+${MAP_DATA_CSS}
${extraCss}`,
body: `
${body}
`,
});
diff --git a/src/features/mpqViewer.ts b/src/features/mpqViewer.ts
index 4e795c9..38f200b 100644
--- a/src/features/mpqViewer.ts
+++ b/src/features/mpqViewer.ts
@@ -8,6 +8,7 @@ import * as fs from 'fs';
import { MpqReader, MpqFileEntry } from 'casc-ts/formats';
import { makeNonce, escapeHtml } from './webviewUtils';
import { buildPage, sep } from './webviewShared';
+import MPQ_CSS from '../webview/mpqViewer.css';
import { offerIssueReport } from './issueReporting';
const MPQ_VIEW_TYPE = 'wurst.mpqViewer';
@@ -290,163 +291,6 @@ function buildHtml(webview: vscode.Webview, archiveName: string, scriptUri: vsco
"style-src 'unsafe-inline'",
].join('; ');
- const MPQ_CSS = `
-/* ── search ──────────────────────────────────────────────────────────────── */
-.search-wrap {
- padding: 5px 10px;
- border-bottom: 1px solid var(--border);
- background: var(--sidebar);
- flex-shrink: 0;
- display: flex; align-items: center; gap: 6px;
-}
-.search-icon { flex-shrink: 0; opacity: 0.5; }
-.search-input {
- flex: 1;
- background: var(--input-bg); color: var(--input-fg);
- border: 1px solid var(--input-border); border-radius: 3px;
- padding: 3px 7px;
- font-family: var(--font); font-size: var(--font-size);
- outline: none;
-}
-.search-input::placeholder { color: var(--input-ph); }
-.search-input:focus { border-color: var(--vscode-focusBorder, #007fd4); }
-.match-count { color: var(--muted); font-size: 11px; white-space: nowrap; }
-
-/* ── tree ────────────────────────────────────────────────────────────────── */
-.tree-wrap {
- flex: 1; overflow-y: auto; overflow-x: hidden; padding: 2px 0;
-}
-.tree-wrap::-webkit-scrollbar { width: 8px; }
-.tree-wrap::-webkit-scrollbar-thumb { background: var(--vscode-scrollbarSlider-background, rgba(121,121,121,.4)); border-radius: 4px; }
-.tree-wrap::-webkit-scrollbar-thumb:hover { background: var(--vscode-scrollbarSlider-hoverBackground, rgba(100,100,100,.7)); }
-
-.row {
- display: flex; align-items: center; gap: 5px;
- padding: 2px 4px 2px 0;
- cursor: pointer; user-select: none; min-height: 22px;
- position: relative;
-}
-.row:hover { background: var(--hover); }
-.row:focus-visible { outline: 1px solid var(--vscode-focusBorder, #007fd4); outline-offset: -1px; }
-.row-select {
- flex: 1; min-width: 0; display: flex; align-items: center; gap: 5px;
- padding: 0; border: 0; background: transparent; color: inherit; font: inherit;
- text-align: left; cursor: pointer; user-select: none;
-}
-.row-select:focus-visible { outline: 1px solid var(--vscode-focusBorder, #007fd4); outline-offset: -1px; }
-.row.selected { background: var(--active); color: var(--active-fg); }
-.row.selected .size, .row.selected .folder-meta { color: var(--active-fg); opacity: 0.75; }
-.row.hidden,
-[data-type="folder"].hidden { display: none !important; }
-
-/* chevron */
-.chevron {
- flex-shrink: 0; width: 16px; height: 16px;
- display: flex; align-items: center; justify-content: center;
- transition: transform 0.12s ease;
- color: var(--icon-fg); opacity: 0.7;
-}
-.chevron svg { width: 10px; height: 10px; fill: currentColor; }
-.collapsed .chevron { transform: rotate(-90deg); }
-
-/* folder icon */
-.folder-icon {
- flex-shrink: 0; width: 16px; height: 16px;
- display: flex; align-items: center; justify-content: center;
- color: var(--vscode-symbolIcon-folderForeground, #dcb67a);
-}
-.folder-icon svg { width: 15px; height: 15px; fill: currentColor; }
-
-.folder-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.folder-meta { color: var(--muted); font-size: 11px; white-space: nowrap; flex-shrink: 0; margin-left: 4px; }
-
-.children { overflow: hidden; }
-.children.collapsed { display: none; }
-
-/* badge */
-.badge {
- flex-shrink: 0; font-size: 9px; font-weight: 700; letter-spacing: 0.04em;
- padding: 1px 4px; border-radius: 3px; color: #fff;
- min-width: 26px; text-align: center; font-family: var(--mono);
-}
-
-.file-main {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 1px;
-}
-.file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.file-desc {
- color: var(--muted);
- font-size: 11px;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.size {
- color: var(--muted); font-size: 11px; white-space: nowrap;
- flex-shrink: 0; font-family: var(--mono); text-align: right; min-width: 52px;
-}
-
-/* inline open button — only shows on hover / selection */
-.row-action {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- height: 22px;
- padding: 0 8px;
- margin-left: 6px;
- background: transparent;
- border: 1px solid transparent;
- border-radius: 3px;
- cursor: pointer;
- color: var(--icon-fg);
- opacity: 0;
- pointer-events: none;
- font: inherit;
- font-size: 11px;
- white-space: nowrap;
-}
-.row-action svg { width: 13px; height: 13px; fill: currentColor; }
-.row-action span { line-height: 1; }
-.row:hover .row-action,
-.row.selected .row-action,
-.row:focus-within .row-action {
- opacity: 0.92;
- pointer-events: auto;
-}
-.row-action:hover {
- background: var(--hover);
- border-color: var(--border);
- opacity: 1;
-}
-.row.selected .row-action:hover { background: rgba(255,255,255,0.15); }
-.row.selected .file-desc,
-.row.selected .size { color: var(--active-fg); opacity: 0.82; }
-
-/* ── busy / extraction in progress ───────────────────────────────────────── */
-.btn-spinner {
- display: none;
- width: 12px; height: 12px;
- border: 2px solid color-mix(in srgb, currentColor 35%, transparent);
- border-top-color: currentColor;
- border-radius: 50%;
- animation: mpq-spin 0.7s linear infinite;
- flex-shrink: 0;
-}
-.wv-btn.busy .btn-spinner { display: inline-block; }
-.wv-btn.busy svg { display: none; }
-.wv-btn.busy { opacity: 1; cursor: progress; } /* keep the active button readable while disabled */
-@keyframes mpq-spin { to { transform: rotate(360deg); } }
-body.extracting .row-action { opacity: 0.35; pointer-events: none; }
-body.extracting .tree-wrap { cursor: progress; }
-`;
return buildPage({
csp,
diff --git a/src/features/objModPreview.ts b/src/features/objModPreview.ts
index ca023ad..6d1ae9a 100644
--- a/src/features/objModPreview.ts
+++ b/src/features/objModPreview.ts
@@ -31,6 +31,7 @@ import {
ITEM_PROFILE_PATHS, DESTRUCTABLE_PROFILE_PATHS, DOODAD_PROFILE_PATHS,
} from './preview/wc3Data';
import { getGameAssetCacheDir, getModelThumbCacheDir, listGameAssetPaths } from './preview/cascStorage';
+import OBJMOD_EDITOR_CSS from '../webview/objModEditor/objModEditor.css';
import { showErrorWithLogs, showWarningWithLogs } from './diagnostics';
import { repairTooltipTrueTypeFont } from './preview/tooltipFont';
import {
@@ -1782,1385 +1783,9 @@ async function buildHtml(
return buildPage({
csp: `default-src 'none'; img-src ${context.webview.cspSource} data:; font-src ${context.webview.cspSource}; connect-src ${context.webview.cspSource}; style-src 'unsafe-inline'; script-src 'unsafe-inline' ${context.webview.cspSource}; worker-src blob:;`,
title: escapeHtml(fileName),
- extraCss: `
-:root {
- /* Single accent used for the editable badge, override markers and selection — was repeated as
- 'var(--vscode-textLink-foreground, #4ec9b0)' in half a dozen places. */
- --accent: var(--vscode-textLink-foreground, #4ec9b0);
- /* WC3 tooltips render light-coloured text (white/gold) on a dark in-game backdrop, so the colour
- preview is intentionally dark in every theme — using --input-bg would hide light text in light themes. */
- --wc3-tip-bg: #000;
- --wc3-tip-fg: #fff;
- /* The game's effective wrap width varies with UI scale and custom font metrics, so this maximum is
- configurable per workspace/folder through wurst.objModTooltipWidth. */
- --wc3-tip-width: ${tooltipWidthPx}px;
- /* Rendered size of each gold corner/edge border tile (see the .tt-collapsed-box, .tt-preview rule) —
- the box's own padding is set to clear this, so the frame sits flush at the edge instead of cutting
- across the text. */
- --wc3-tip-border-size: 9px;
- --model-bg: color-mix(in srgb, var(--bg) 72%, var(--fg) 28%);
- /* Default browse-list width. Mirrored by LIST_W_DEFAULT in objModEditorWebview.ts (the splitter's
- fallback when nothing is persisted yet) — keep the two in sync. */
- --list-w: 220px;
- /* Minimum width the field table needs before the details pane starts scrolling horizontally.
- Technical mode (5-7 columns instead of 2) raises it — see table.technical below. */
- --table-min-w: 300px;
-}
-
-/* ── Density ─────────────────────────────────────────────────────────────────────────────────────
- Two spacing scales, selected by the #density-toggle button in the header and persisted per document
- (see ui.density in state.ts). Compact is the default — this is a dense editor over files with
- hundreds of objects and thousands of fields, so vertical space is the scarce resource — and
- body.density-cozy restores the original roomier spacing for anyone who prefers it.
- Every density-sensitive number lives here as a variable so the two modes stay one block of
- overrides instead of two parallel stylesheets; only the handful of genuinely structural differences
- (two-line browse rows, wrapped header meta) need their own body.density-cozy rules further down. */
-:root {
- --pad-x: 10px; /* header / banner horizontal padding */
- --head-py: 4px;
- --title-fs: 13px; /* file name + object name */
- --meta-fs: 11px; /* meta strip, rawcode suffix */
- --chip-fs: 11px; /* toggle chips */
- --input-h: 22px; /* search boxes */
- --details-icon: 26px;
- --details-head-pad: 5px 9px 6px;
- --search-pad: 5px 6px;
- --tree-icon: 20px;
- --tree-heading-py: 2px;
- --tree-row-py: 1px;
- /* Browse-tree indent ladder: Group > Race [> Melee|Campaign > Units/Buildings/Heroes/Special], with
- object rows sitting one step past their own parent heading. */
- --ind-group: 6px;
- --ind-race: 16px;
- --ind-camp: 26px;
- --ind-kind: 36px;
- --ind-row: 26px;
- --ind-row-nested: 40px;
- --th-pad: 3px 6px;
- --td-pad: 1px 6px;
- --cat-pad: 3px 6px 2px;
- /* Sticky category rows sit directly under the sticky table header (th padding + 1px border). */
- --table-header-h: 23px;
- /* Row height for the field table's collapsed/edit cells, so toggling a cell into edit mode never
- resizes its row. */
- --cell-h: 20px;
- /* Saved-colour group inside the tooltip colour popover: the gap between its label and swatch grid,
- and the breathing room above the rule separating it from the presets. */
- --palette-group-gap: 4px;
- --palette-group-pad: 2px;
-}
-body.density-cozy {
- --pad-x: 16px;
- --head-py: 7px;
- --title-fs: 15px;
- --meta-fs: 12px;
- --chip-fs: 12px;
- --input-h: 26px;
- --details-icon: 32px;
- --details-head-pad: 11px 16px 10px;
- --search-pad: 8px;
- --tree-icon: 24px;
- --tree-heading-py: 3px;
- --tree-row-py: 2px;
- --ind-group: 10px;
- --ind-race: 22px;
- --ind-camp: 32px;
- --ind-kind: 42px;
- --ind-row: 34px;
- --ind-row-nested: 46px;
- --th-pad: 5px 8px;
- --td-pad: 3px 8px;
- --cat-pad: 5px 8px 4px;
- --table-header-h: 27px;
- --cell-h: 24px;
- --palette-group-gap: 6px;
- --palette-group-pad: 4px;
-}
-.content {
- flex: 1;
- height: 100%;
- min-height: 0;
- min-width: 0;
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-/* One line, not two: the file name, its meta summary and the save badge share a single strip so the
- editor itself starts ~20px higher. The meta line is the part that gives (it ellipsizes, full text
- in its title) — the name and the badge always stay legible. */
-.md-header {
- display: flex;
- align-items: baseline;
- flex-wrap: wrap;
- gap: 2px 8px;
- padding: var(--head-py) var(--pad-x);
- border-bottom: 1px solid var(--border);
- background: var(--sidebar);
- flex-shrink: 0;
-}
-.md-title {
- flex: 0 1 auto;
- color: var(--vscode-textLink-foreground, var(--fg));
- font-size: var(--title-fs);
- font-weight: 600;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.md-meta {
- flex: 1 1 auto;
- min-width: 0;
- color: var(--muted);
- font-size: var(--meta-fs);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-/* Cozy keeps the original two-line header: the meta strip is ordered last and forced to a full row,
- so it wraps below the (still single-line) name + save badge and can show its full text. */
-body.density-cozy .md-meta { order: 3; flex-basis: 100%; white-space: normal; }
-/* Without the meta strip to fill the first row, the badges need an explicit push to the right edge.
- On the first badge only — both carry .editable-badge, and giving each an auto margin would split
- the free space between them and strand one mid-header. */
-body.density-cozy #density-toggle { margin-left: auto; }
-.editable-badge {
- flex: 0 0 auto;
- padding: 1px 5px;
- border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent);
- border-radius: 2px;
- background: transparent;
- color: var(--accent);
- font: inherit;
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- cursor: pointer;
- transition: color .13s, border-color .13s, background-color .13s;
-}
-.editable-badge:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
-.editable-badge:focus-visible { outline: 1px solid var(--vscode-focusBorder, #007fd4); outline-offset: 1px; }
-.editable-badge.dirty {
- border-color: color-mix(in srgb, var(--vscode-editorWarning-foreground, #cca700) 60%, transparent);
- color: var(--vscode-editorWarning-foreground, #cca700);
-}
-.editable-badge.dirty:hover { background: color-mix(in srgb, var(--vscode-editorWarning-foreground, #cca700) 12%, transparent); }
-.refresh-button {
- flex: 0 0 auto;
- padding: 2px 7px;
- border: 1px solid var(--border);
- border-radius: 2px;
- background: transparent;
- color: var(--muted);
- font: inherit;
- font-size: 10px;
- cursor: pointer;
-}
-.refresh-button:hover { background: var(--hover); color: var(--fg); }
-.refresh-button:focus-visible { outline: 1px solid var(--vscode-focusBorder, #007fd4); outline-offset: 1px; }
-/* Same pill shape as the save badge, but a neutral (non-accent) affordance — it's a view preference,
- not document state. */
-.density-toggle {
- flex: 0 0 auto;
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 2px 6px;
- border: 1px solid var(--border);
- border-radius: 999px;
- background: var(--input-bg);
- color: var(--muted);
- font: inherit;
- font-size: 10px;
- cursor: pointer;
- transition: color .16s, border-color .16s, background-color .16s;
-}
-.density-toggle:hover { background: var(--hover); color: var(--fg); border-color: var(--vscode-focusBorder, var(--border)); }
-.density-toggle:focus-visible { outline: 2px solid var(--vscode-focusBorder, #007fd4); outline-offset: 1px; }
-.density-option { transition: color .16s, opacity .16s; }
-.density-option-compact { color: var(--fg); font-weight: 600; }
-.density-track {
- position: relative;
- width: 28px;
- height: 14px;
- box-sizing: border-box;
- border: 1px solid color-mix(in srgb, var(--muted) 65%, transparent);
- border-radius: 999px;
- background: color-mix(in srgb, var(--muted) 18%, transparent);
- transition: background-color .2s, border-color .2s;
-}
-.density-thumb {
- position: absolute;
- top: 1px;
- left: 1px;
- width: 10px;
- height: 10px;
- border-radius: 50%;
- background: var(--fg);
- box-shadow: 0 1px 3px rgba(0, 0, 0, .35);
- transform: translateX(0);
- transition: transform .22s cubic-bezier(.2, .8, .2, 1), background-color .2s;
-}
-body.density-cozy .density-option-compact { color: var(--muted); font-weight: 400; }
-body.density-cozy .density-option-cozy { color: var(--fg); font-weight: 600; }
-body.density-cozy .density-track { background: color-mix(in srgb, var(--accent) 42%, transparent); border-color: var(--accent); }
-body.density-cozy .density-thumb { transform: translateX(14px); background: var(--accent); }
-@media (prefers-reduced-motion: reduce) {
- .density-toggle,
- .density-option,
- .density-track,
- .density-thumb { transition: none; }
-}
-.error,
-.warning {
- padding: calc(var(--head-py) + 1px) var(--pad-x);
- border-bottom: 1px solid color-mix(in srgb, currentColor 55%, transparent);
- font-size: 12px;
- flex-shrink: 0;
-}
-.error { color: var(--vscode-errorForeground, #f14c4c); }
-.warning { color: var(--vscode-editorWarning-foreground, #cca700); }
-.value-editor { display: grid; grid-template-columns: minmax(0, 1fr); }
-.edit-raw {
- width: 100%;
- box-sizing: border-box;
- background: var(--input-bg);
- color: var(--input-fg);
- border: 1px solid var(--input-border);
- border-radius: 2px;
- padding: 2px 6px;
- font: inherit;
- font-family: var(--font);
-}
-/* Single-line inputs match the collapsed cell height exactly, so toggling edit never resizes the row. */
-input.edit-raw { height: var(--cell-h); }
-textarea.edit-raw { min-height: 48px; line-height: 1.4; padding: 4px 6px; resize: vertical; }
-.edit-raw:focus { outline: 1px solid var(--vscode-focusBorder, var(--vscode-textLink-foreground)); }
-/* The in-game tooltip's fill texture + gold corner/edge tiles (see requestTooltipBackdrop/
- requestTooltipBorder in imageAssetSupport.ts and applyTooltipBackdrop/applyTooltipBorder in
- messageHandler.ts), layered as 9 separate background-images: the 4 corners + 4 edges on top
- (each only set once their tile has actually loaded — var(..., none) otherwise), the tiled fill
- texture underneath, and --wc3-tip-bg as the plain-color fallback beneath everything. Shared between
- .tt-collapsed-box and .tt-preview since both are "the tooltip box", just editable vs. read-only —
- .tt-collapsed itself is just a plain row wrapper so the source-pill/edit-hint can sit beside the box
- instead of inside it (see the comment on .tt-collapsed below). */
-.tt-collapsed-box,
-.tt-preview {
- background-color: var(--wc3-tip-bg);
- background-image:
- var(--wc3-tip-corner-tl, none), var(--wc3-tip-corner-tr, none),
- var(--wc3-tip-corner-bl, none), var(--wc3-tip-corner-br, none),
- var(--wc3-tip-edge-left, none), var(--wc3-tip-edge-right, none),
- var(--wc3-tip-edge-top, none), var(--wc3-tip-edge-bottom, none),
- var(--wc3-tip-bg-image, none);
- background-position:
- top left, top right, bottom left, bottom right,
- left var(--wc3-tip-border-size), right var(--wc3-tip-border-size),
- var(--wc3-tip-border-size) top, var(--wc3-tip-border-size) bottom,
- 0 0;
- /* Edges are no-repeat, not repeat-y/repeat-x — see the background-size comment below for why. */
- background-repeat:
- no-repeat, no-repeat, no-repeat, no-repeat,
- no-repeat, no-repeat, no-repeat, no-repeat,
- repeat;
- /* Each border tile is a 16x16px source, but drawn at --wc3-tip-border-size (smaller) so it reads as a
- thin frame instead of a chunky inset block — at full 16px, two opposing corners alone (16+16=32px)
- were taller than a short single-line tooltip box, so they overlapped into a solid gold rectangle
- instead of a border. The box's own padding (below) is sized to clear this same value, so the frame
- sits at the true edge instead of cutting across the text.
- The 4 edges are sized to exactly the strip *between* the two corners on their side (100% minus both
- corners), rather than tiled the full box length — tiling from the same origin as the corners meant
- the corner's rounded/receding art didn't fully cover the straight edge tile underneath it, so the
- edge's straight line poked out past the curve at each corner like a little flag. Since none of
- these tiles vary along their own length (edgeLeft/edgeRight are solid at every row, edgeTop/
- edgeBottom at every column), stretching one copy to fill that exact strip looks identical to tiling
- it, but can no longer overlap the corners at all. */
- background-size:
- var(--wc3-tip-border-size) var(--wc3-tip-border-size), var(--wc3-tip-border-size) var(--wc3-tip-border-size),
- var(--wc3-tip-border-size) var(--wc3-tip-border-size), var(--wc3-tip-border-size) var(--wc3-tip-border-size),
- var(--wc3-tip-border-size) calc(100% - 2 * var(--wc3-tip-border-size)),
- var(--wc3-tip-border-size) calc(100% - 2 * var(--wc3-tip-border-size)),
- calc(100% - 2 * var(--wc3-tip-border-size)) var(--wc3-tip-border-size),
- calc(100% - 2 * var(--wc3-tip-border-size)) var(--wc3-tip-border-size),
- auto;
-}
-.tt-preview {
- min-width: 0;
- max-width: var(--wc3-tip-width);
- min-height: calc(var(--wc3-tip-border-size) * 2 + 1em);
- border: 1px dashed var(--border);
- border-radius: 2px;
- padding: calc(var(--wc3-tip-border-size) + 2px) calc(var(--wc3-tip-border-size) + 3px);
- color: var(--wc3-tip-fg);
- font-family: var(--vscode-font-family, sans-serif);
- white-space: pre-wrap;
- word-break: break-word;
- overflow: auto;
-}
-.tt-raw-toggle,
-.tt-copy-raw {
- margin-left: 4px;
- padding: 1px 6px;
- border: 1px solid var(--border);
- border-radius: 2px;
- background: var(--input-bg);
- color: var(--fg);
- font: inherit;
- font-size: 10px;
- cursor: pointer;
-}
-.tt-raw-toggle.active,
-.tt-raw-toggle:hover,
-.tt-copy-raw:hover { background: var(--hover); }
-/* In-place raw-text view: same box the rich editor occupies (.tt-collapsed-body), just swapped for a
- plain textarea showing the literal WC3 string (color codes, |n line breaks) — so toggling Raw never
- resizes the floating toolbar or shifts anything else on the page. */
-.tt-collapsed-raw {
- flex: 1 1 100%; /* same full-row basis as .tt-collapsed-body, for the same reason (see its comment) */
- min-width: 0;
- background: transparent;
- color: inherit;
- border: none;
- outline: none;
- resize: none;
- overflow: hidden;
- padding: 0;
- margin: 0;
- font: inherit;
- font-family: var(--mono);
- white-space: pre-wrap;
- word-break: break-word;
-}
-/* An explicit display on the base rule above would beat the UA [hidden] default regardless of the
- hidden DOM attribute (author styles win over UA styles even at equal specificity) — this is what
- made both the rich box and the raw textarea show at once after toggling. This rule keeps [hidden]
- authoritative. */
-.tt-collapsed-raw[hidden] { display: none; }
-.tt-preview.tt-readonly {
- display: inline-block;
- max-width: min(100%, var(--wc3-tip-width));
- vertical-align: top;
-}
-/* .tt-collapsed is just a plain row now — like the Hotkey/Name rows, the source-pill (e.g.
- "TRIGSTR_2431") and edit-hint pencil sit beside the tooltip box, not inside it. They used to be
- children of the dark box itself, where they either ate into its (now width-capped, see
- --wc3-tip-width) space or, once wrapped onto their own line, still looked like part of the in-game
- tooltip rather than an editor annotation. .tt-collapsed-box carries all the actual box styling
- (background/texture/border/font) so the outer row can stay a plain, unstyled flex container. */
-.tt-collapsed {
- display: flex;
- align-items: flex-start;
- gap: 6px;
- min-width: 0;
- min-height: var(--cell-h);
- cursor: text;
-}
-.tt-collapsed-box {
- min-width: 0;
- max-width: var(--wc3-tip-width);
- /* At least 2x the border tile size, so top+bottom (or left+right) corner tiles never overlap into a
- solid block on a short single-line tooltip — see the --wc3-tip-border-size comment above. */
- min-height: calc(var(--wc3-tip-border-size) * 2 + 1em);
- box-sizing: border-box;
- /* Padding clears the border tiles so text never sits under the frame art (this was the "inset
- overlapping" bug: at the old 1px/6px padding, both text and border occupied the same pixels). */
- padding: calc(var(--wc3-tip-border-size) + 2px) calc(var(--wc3-tip-border-size) + 3px);
- border: 1px solid transparent;
- border-radius: 3px;
- color: var(--wc3-tip-fg);
- cursor: text;
- font-family: var(--vscode-font-family, sans-serif);
- white-space: pre-wrap;
- word-break: break-word;
-}
-.tt-collapsed:hover .tt-collapsed-box,
-.tt-collapsed:focus-visible .tt-collapsed-box { border-color: var(--vscode-focusBorder, var(--vscode-textLink-foreground)); }
-.tt-collapsed:focus-visible { outline: none; }
-.tt-collapsed-body { min-width: 0; }
-.tt-collapsed-body[contenteditable="true"] { outline: none; cursor: text; }
-.tt-empty { color: var(--muted); font-style: normal; }
-.tt-collapsed-box .tt-empty,
-.tt-preview .tt-empty { font-style: italic; }
-.tt-edit-hint {
- flex-shrink: 0;
- color: var(--muted);
- font-size: 11px;
- opacity: 0.35; /* faintly visible at rest so cells read as editable; brightens on hover/focus */
- transition: opacity .1s;
-}
-.tt-collapsed:hover .tt-edit-hint,
-.tt-collapsed:focus-visible .tt-edit-hint { opacity: 0.9; }
-/* Editing indicator: outline only, on the box itself — the tooltip box's own background/text colors
- (--wc3-tip-bg/-fg) are left completely alone, so the preview never loses contrast while being edited. */
-.tt-collapsed.tt-editing .tt-collapsed-box {
- outline: 2px solid var(--vscode-focusBorder, var(--vscode-textLink-foreground));
- outline-offset: -1px;
-}
-/* Floating color-picker/raw-text toolbar for the in-place tooltip editor: JS-positioned (fixed) beside
- the row via positionFloatToolbar. It never touches the preview box itself, so entering/leaving edit
- mode causes zero layout shift and nothing to scroll into view. */
-.tt-float-toolbar {
- position: fixed;
- z-index: 60;
- box-sizing: border-box;
- background: var(--vscode-editorWidget-background, var(--sidebar));
- border: 1px solid var(--vscode-editorWidget-border, var(--border));
- border-radius: 4px;
- padding: 6px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
-}
-.tt-float-toolbar-row { display: flex; align-items: center; gap: 6px; }
-.tt-used-colors {
- display: inline-flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 3px;
- max-width: 123px;
- padding-left: 6px;
- margin-left: 2px;
- border-left: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
-}
-.tt-used-colors[hidden] { display: none; }
-.cell-edit {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 4px 6px;
- width: 100%;
- min-width: 0;
- min-height: var(--cell-h); /* same height as the single-line editor → no row resize on edit */
- box-sizing: border-box;
- padding: 0 4px;
- border: 1px solid transparent;
- border-radius: 3px;
- cursor: text;
- transition: background .08s, border-color .08s;
-}
-.cell-edit:hover,
-.cell-edit:focus-visible {
- background: var(--input-bg);
- border-color: color-mix(in srgb, var(--vscode-focusBorder, var(--vscode-textLink-foreground)) 60%, transparent);
- outline: none;
-}
-.cell-edit:hover .tt-edit-hint,
-.cell-edit:focus-visible .tt-edit-hint { opacity: 0.7; }
-/* Shrink-to-fit, not flex:1 — the "modified" badge, source pill and ✎ hint cluster right after the
- value instead of being flung to the far edge of a wide value column. The click target is still the
- full cell (.cell-edit stays width:100%). */
-.cell-edit-val { flex: 0 1 auto; min-width: 0; font-family: var(--font); word-break: break-word; white-space: pre-wrap; }
-.value-display {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr);
- column-gap: 7px;
- row-gap: 2px;
- align-items: center;
- min-width: 0;
-}
-.value-display.rawcodes { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
-.value-main {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-family: var(--font);
-}
-.value-raw {
- grid-column: 2;
- min-width: 0;
- color: var(--muted);
- font-family: var(--font);
- font-size: 10px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.resolved-chip {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- max-width: 180px;
- border: 1px solid var(--border);
- border-radius: 2px;
- padding: 1px 5px;
- background: color-mix(in srgb, var(--input-bg) 78%, transparent);
- font-family: var(--font);
- white-space: nowrap;
-}
-.resolved-chip.linked { cursor: pointer; color: var(--vscode-textLink-foreground, var(--fg)); }
-.resolved-chip.linked:hover { border-color: currentColor; background: var(--hover); }
-.resolved-chip .raw { color: var(--muted); font-family: var(--mono); font-size: 10px; }
-.asset-mini {
- width: 20px;
- height: 20px;
- display: grid;
- place-items: center;
- border: 1px solid var(--border);
- border-radius: 2px;
- background: var(--input-bg);
- color: var(--muted);
- font-family: var(--mono);
- font-size: 9px;
- font-weight: 600;
- overflow: hidden;
-}
-.asset-mini img { width: 100%; height: 100%; object-fit: cover; display: block; }
-.asset-mini.model-thumb {
- display: inline-block;
- vertical-align: middle;
-}
-.asset-open {
- width: auto;
- min-width: 20px;
- padding: 0 4px;
- height: 20px;
- cursor: pointer;
- white-space: nowrap;
-}
-.asset-open:hover {
- background: var(--btn-hover, var(--hover));
- color: var(--fg);
- border-color: var(--vscode-textLink-foreground, var(--border));
-}
-.mpv-box {
- position: fixed;
- right: 10px;
- bottom: 10px;
- /* Never wider than the pane it floats over — at ~300px the old fixed 220px box left almost nothing
- of the field table visible behind it. */
- width: min(220px, calc(100vw - 20px));
- z-index: 50;
- background: var(--sidebar, var(--bg));
- border: 1px solid var(--border);
- border-radius: 4px;
- box-shadow: 0 4px 16px rgba(0,0,0,0.4);
- overflow: hidden;
-}
-.mpv-head {
- display: flex;
- align-items: center;
- gap: 4px;
- padding: 4px 6px 4px 8px;
- border-bottom: 1px solid var(--border);
- background: var(--vscode-editorGroupHeader-tabsBackground, var(--sidebar));
- cursor: grab;
- user-select: none;
-}
-.mpv-head.dragging { cursor: grabbing; }
-.mpv-ctl,
-.mpv-close {
- flex: 0 0 auto;
- border: none;
- background: transparent;
- color: var(--muted);
- cursor: pointer;
- font-size: 12px;
- line-height: 1;
- padding: 2px 4px;
- border-radius: 2px;
-}
-.mpv-ctl:hover,
-.mpv-close:hover { background: var(--btn-hover, var(--hover)); color: var(--fg); }
-#mpv-help { cursor: help; }
-.mpv-name {
- flex: 1;
- min-width: 0;
- font-size: 11px;
- font-weight: 600;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.mpv-anim {
- flex: 0 1 auto;
- max-width: 110px;
- height: 18px;
- font-size: 10px;
- color: var(--input-fg);
- background: var(--input-bg);
- border: 1px solid var(--input-border, var(--border));
- border-radius: 2px;
- padding: 0 2px;
-}
-/* Square, but sized off the (now responsive) box width rather than a second hard-coded 220px. */
-.mpv-viewport { position: relative; width: 100%; aspect-ratio: 1; background: var(--model-bg); cursor: grab; }
-.mpv-viewport:active { cursor: grabbing; }
-.mpv-canvas { display: block; width: 100%; height: 100%; }
-.mpv-status {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 8px;
- text-align: center;
- font-size: 11px;
- color: var(--muted);
- pointer-events: none;
-}
-.mpv-status.hidden { display: none; }
-.picker-row { display: flex; gap: 4px; align-items: center; }
-.picker-row .edit-raw { flex: 1; min-width: 0; }
-.browse-btn {
- flex: 0 0 auto;
- font: inherit;
- font-size: 11px;
- color: var(--btn-fg, var(--fg));
- background: var(--btn-bg, var(--input-bg));
- border: 1px solid var(--border);
- border-radius: 2px;
- padding: 2px 8px;
- cursor: pointer;
-}
-.browse-btn:hover { background: var(--btn-hover, var(--hover)); }
-.ab-overlay {
- position: fixed;
- inset: 0;
- z-index: 100;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(0,0,0,0.5);
-}
-.ab-overlay[hidden] { display: none; }
-.ab-sr-only {
- position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
- overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
-}
-.ab-modal {
- display: flex;
- flex-direction: column;
- width: min(820px, 92vw);
- height: min(620px, 88vh);
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: 5px;
- box-shadow: 0 8px 32px rgba(0,0,0,0.5);
- overflow: hidden;
-}
-/* Wraps rather than squeezing the search box to nothing once the modal is only as wide as a narrow
- editor pane (min(820px, 92vw)). */
-.ab-head {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 5px 6px;
- padding: 5px 7px;
- border-bottom: 1px solid var(--border);
- background: var(--sidebar, var(--bg));
-}
-.ab-tabs { display: flex; gap: 2px; flex: 0 0 auto; }
-.ab-tab {
- font: inherit;
- font-size: 11px;
- color: var(--muted);
- background: transparent;
- border: 1px solid var(--border);
- border-radius: 3px;
- padding: 3px 9px;
- cursor: pointer;
-}
-.ab-tab:hover { background: var(--hover); color: var(--fg); }
-.ab-tab.active { background: var(--btn-bg, var(--active)); color: var(--active-fg, var(--fg)); border-color: var(--vscode-focusBorder, var(--border)); }
-.ab-search {
- flex: 1;
- min-width: 90px;
- height: 24px;
- padding: 2px 7px;
- color: var(--input-fg);
- background: var(--input-bg);
- border: 1px solid var(--input-border, var(--border));
- border-radius: 3px;
- font: inherit;
-}
-.ab-source {
- flex: 0 0 auto;
- height: 24px;
- color: var(--input-fg);
- background: var(--input-bg);
- border: 1px solid var(--input-border, var(--border));
- border-radius: 3px;
- font: inherit;
- font-size: 11px;
-}
-.ab-count { color: var(--muted); font-size: 11px; white-space: nowrap; }
-.ab-close {
- flex: 0 0 auto;
- border: none;
- background: transparent;
- color: var(--muted);
- cursor: pointer;
- font-size: 14px;
- padding: 2px 6px;
- border-radius: 2px;
-}
-.ab-close:hover { background: var(--btn-hover, var(--hover)); color: var(--fg); }
-.ab-grid {
- flex: 1;
- overflow-y: auto;
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(66px, 1fr));
- gap: 4px;
- padding: 6px;
- align-content: start;
-}
-.ab-card {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 2px;
- padding: 4px 2px;
- border: 1px solid transparent;
- border-radius: 4px;
- cursor: pointer;
- text-align: center;
- width: 100%;
- font: inherit;
- color: var(--fg);
- background: transparent;
-}
-.ab-card:hover { border-color: var(--vscode-focusBorder, #007fd4); background: var(--hover); }
-.ab-card:focus-visible { outline: 1px solid var(--vscode-focusBorder, #007fd4); outline-offset: -1px; }
-.ab-card .object-icon { width: 48px; height: 48px; }
-.model-thumb,
-.object-icon.model-thumb { background: var(--model-bg); }
-.sound-thumb {
- background: var(--input-bg);
- color: var(--muted);
- font-family: var(--mono);
- font-size: 11px;
- font-weight: 700;
-}
-.model-thumb img { object-fit: contain; }
-.model-thumb.pending {
- position: relative;
- background: var(--model-bg);
-}
-.model-thumb.pending::after {
- content: "";
- position: absolute;
- left: calc(50% - 8px);
- top: calc(50% - 8px);
- width: 16px;
- height: 16px;
- border: 2px solid color-mix(in srgb, var(--fg) 22%, transparent);
- border-top-color: var(--fg);
- border-radius: 50%;
- animation: wv-spin 0.8s linear infinite;
-}
-.thumb-render-canvas {
- position: fixed;
- left: -10000px;
- top: 0;
- width: 96px;
- height: 96px;
- pointer-events: none;
- opacity: 0;
-}
-.ab-card-label { font-size: 10px; line-height: 1.15; opacity: .75; max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
-.ab-empty { color: var(--muted); font-style: italic; padding: 18px; grid-column: 1 / -1; text-align: center; }
-.ab-empty.ab-error { font-style: normal; }
-.ab-empty.ab-error .details-error-reason { margin-inline: auto; }
-.picker-note {
- color: var(--muted);
- font-size: 10px;
- margin-top: 2px;
-}
-/* Number fields: text input (kept locale-independent, see webview NOTE) + integer/real steppers. */
-.num-editor { display: flex; align-items: stretch; max-width: 170px; }
-.num-editor .num-input { flex: 1; min-width: 0; border-right: none; border-radius: 2px 0 0 2px; }
-.num-steppers {
- display: flex;
- flex-direction: column;
- border: 1px solid var(--border);
- border-radius: 0 2px 2px 0;
- overflow: hidden;
-}
-.num-step {
- flex: 1;
- width: 18px;
- min-height: 0;
- padding: 0;
- line-height: 1;
- font-size: 8px;
- cursor: pointer;
- background: var(--input-bg);
- color: var(--muted);
- border: none;
- border-bottom: 1px solid var(--border);
-}
-.num-step:last-child { border-bottom: none; }
-.num-step:hover { background: var(--btn-hover, var(--hover)); color: var(--fg); }
-/* Scannable accent for customized (overridden) fields. */
-tr.overridden td.field { box-shadow: inset 2px 0 0 color-mix(in srgb, var(--accent) 70%, transparent); }
-.cell-edit .tt-edit-hint { flex-shrink: 0; }
-.tt-bar { position: relative; display: inline-flex; align-items: center; gap: 4px; }
-.tt-color-sq {
- width: 20px;
- height: 20px;
- padding: 0;
- border: 1px solid var(--input-border, var(--border));
- border-radius: 3px;
- cursor: pointer;
- background: #fff;
-}
-.tt-color-sq:hover { border-color: var(--fg); }
-.tt-color-sq:focus-visible,
-.tt-sw:focus-visible {
- outline: 2px solid var(--vscode-focusBorder, #007fd4);
- outline-offset: 1px;
-}
-.tt-pop {
- position: absolute;
- top: 24px;
- left: 0;
- z-index: 5;
- display: flex;
- flex-direction: column;
- gap: 6px;
- padding: 7px;
- background: var(--vscode-editorWidget-background, var(--sidebar));
- border: 1px solid var(--vscode-editorWidget-border, var(--border));
- border-radius: 4px;
- box-shadow: 0 2px 8px rgba(0,0,0,.35);
-}
-.tt-pop[hidden] { display: none; }
-.tt-palette-label {
- color: var(--muted);
- font-size: 10px;
- line-height: 1;
-}
-.tt-custom-colors {
- display: flex;
- flex-direction: column;
- gap: var(--palette-group-gap);
- padding-top: var(--palette-group-pad);
- border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
-}
-.tt-custom-colors[hidden] { display: none; }
-.tt-swatches { display: grid; grid-template-columns: repeat(6, 18px); gap: 4px; }
-.tt-sw {
- width: 18px;
- height: 18px;
- padding: 0;
- border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
- border-radius: 3px;
- cursor: pointer;
-}
-.tt-sw:hover { transform: scale(1.15); border-color: var(--fg); }
-.tt-pick {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 11px;
- color: var(--fg);
- cursor: pointer;
-}
-.tt-color { width: 22px; height: 18px; padding: 0; border: 0; background: none; cursor: pointer; }
-.tt-color::-webkit-color-swatch-wrapper { padding: 0; }
-.tt-color::-webkit-color-swatch { border: 1px solid var(--border); border-radius: 2px; }
-.readonly-trigstr { color: var(--muted); font-style: italic; }
-.field-search-wrap {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 3px 7px;
- margin-top: 5px;
-}
-.field-search {
- flex: 1;
- min-width: 80px;
- max-width: 300px;
- height: var(--input-h);
- background: var(--input-bg);
- color: var(--input-fg);
- border: 1px solid var(--input-border);
- border-radius: 2px;
- padding: 2px 7px;
- font: inherit;
-}
-.field-search:focus { outline: 1px solid var(--vscode-focusBorder, var(--vscode-textLink-foreground)); }
-.field-match { color: var(--muted); font-size: 11px; white-space: nowrap; }
-.cat-filter { position: relative; }
-.cat-filter-btn {
- background: none;
- border: 1px solid transparent;
- border-radius: 3px;
- padding: 2px 6px;
- font: inherit;
-}
-.cat-filter-btn:hover,
-.cat-filter-btn[aria-expanded="true"] { border-color: var(--border); background: var(--input-bg); }
-.cat-filter-badge {
- margin-left: 4px;
- color: var(--accent);
- font-size: 10px;
-}
-.cat-filter-pop {
- position: absolute;
- top: 26px;
- right: 0;
- z-index: 5;
- display: flex;
- flex-direction: column;
- gap: 3px;
- min-width: 150px;
- padding: 7px;
- background: var(--vscode-editorWidget-background, var(--sidebar));
- border: 1px solid var(--vscode-editorWidget-border, var(--border));
- border-radius: 4px;
- box-shadow: 0 2px 8px rgba(0,0,0,.35);
-}
-.cat-filter-pop[hidden] { display: none; }
-.cat-filter-opt {
- display: flex;
- align-items: center;
- gap: 6px;
- color: var(--fg);
- font-size: 12px;
- cursor: pointer;
- user-select: none;
-}
-.cat-filter-opt input { margin: 0; }
-.cat-filter-actions {
- display: flex;
- gap: 6px;
- margin-top: 4px;
- padding-top: 5px;
- border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
-}
-.cat-filter-actions button {
- flex: 1;
- padding: 2px 0;
- border: 1px solid var(--border);
- border-radius: 2px;
- background: var(--input-bg);
- color: var(--fg);
- font: inherit;
- font-size: 11px;
- cursor: pointer;
-}
-.cat-filter-actions button:hover { background: var(--hover); }
-tr.hidden { display: none; }
-/* Side-by-side is the layout that actually works, so it has to survive narrow panes: the browse list
- never takes more than 46% of the editor (whatever width the splitter last saved), and the details
- pane can shrink to 0 and scroll its own table. Stacking is a last resort below ~440px — see
- .object-editor.narrow. */
-.object-editor {
- flex: 1;
- height: 100%;
- min-height: 0;
- min-width: 0;
- display: grid;
- grid-template-columns: min(var(--list-w), 46%) 4px minmax(0, 1fr);
- overflow: hidden;
-}
-.splitter {
- cursor: col-resize;
- background: var(--border);
- opacity: 0.5;
- transition: opacity .12s, background .12s;
-}
-.splitter:hover,
-.splitter.dragging { opacity: 1; background: var(--vscode-textLink-foreground, var(--fg)); }
-.splitter:focus-visible { outline: 1px solid var(--vscode-focusBorder, #007fd4); outline-offset: -1px; opacity: 1; }
-.object-list {
- min-width: 0;
- min-height: 0;
- border-right: 1px solid var(--border);
- background: var(--sidebar);
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-.search-wrap {
- position: relative;
- display: flex;
- align-items: center;
- gap: 4px;
- padding: var(--search-pad);
- border-bottom: 1px solid var(--border);
- flex-shrink: 0;
-}
-.search-input {
- flex: 1;
- min-width: 0;
- height: var(--input-h);
- background: var(--input-bg);
- color: var(--input-fg);
- border: 1px solid var(--input-border);
- border-radius: 2px;
- padding: 2px 6px;
- font: inherit;
-}
-.search-input:focus { outline: 1px solid var(--vscode-focusBorder, var(--vscode-textLink-foreground)); }
-.search-match { flex-shrink: 0; color: var(--muted); font-size: 11px; white-space: nowrap; }
-.search-clear {
- flex-shrink: 0;
- width: 18px;
- height: 18px;
- display: none;
- place-items: center;
- padding: 0;
- background: transparent;
- border: 0;
- border-radius: 3px;
- color: var(--muted);
- font-size: 15px;
- line-height: 1;
- cursor: pointer;
-}
-.search-clear.show { display: grid; }
-.search-clear:hover { background: var(--hover); color: var(--fg); }
-.search-clear:focus-visible { outline: 1px solid var(--vscode-focusBorder, #007fd4); }
-.tree {
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- overflow-x: hidden;
- padding: 2px 0 6px;
-}
-/* Indent scale for the Group > Race [> Melee|Campaign > Units/Buildings/Heroes/Special] > object
- hierarchy: 6px base, ~10px per level. A row sits one step past its parent heading's own indent, so
- the nesting still reads at a glance while leaving room for actual names in a ~200px pane (the old
- 10/22/32/42/46px ladder spent a quarter of that width on whitespace alone). */
-.group-heading,
-.race-heading,
-.camp-heading,
-.kind-heading {
- width: 100%;
- display: flex;
- align-items: center;
- gap: 5px;
- background: transparent;
- border: 0;
- font-size: 11px;
- font-weight: 500;
- color: var(--muted);
- text-align: left;
- cursor: pointer;
-}
-.group-heading {
- position: sticky;
- top: 0;
- z-index: 1;
- padding: var(--tree-heading-py) var(--ind-group);
- color: var(--fg);
- font-weight: 600;
- background: var(--sidebar);
-}
-.race-heading {
- padding: var(--tree-heading-py) var(--ind-group) var(--tree-heading-py) var(--ind-race);
-}
-.camp-heading { padding: var(--tree-row-py) var(--ind-group) var(--tree-row-py) var(--ind-camp); }
-.kind-heading { padding: var(--tree-row-py) var(--ind-group) var(--tree-row-py) var(--ind-kind); }
-.group-heading:hover,
-.race-heading:hover,
-.camp-heading:hover,
-.kind-heading:hover {
- background: var(--hover);
-}
-.group-heading:focus-visible,
-.race-heading:focus-visible,
-.camp-heading:focus-visible,
-.kind-heading:focus-visible,
-.object-row:focus-visible {
- outline: 1px solid var(--vscode-focusBorder, #007fd4);
- outline-offset: -1px;
-}
-.twisty {
- width: 10px;
- display: inline-block;
- color: var(--muted);
- font-family: var(--mono);
- font-size: 10px;
-}
-.folder-count {
- margin-left: auto;
- color: var(--muted);
- font-size: 10px;
- font-weight: 400;
-}
-/* Compact puts each object on one line: icon, name (+ source pill), rawcode pinned right. The rawcode
- used to sit on a second line, which nearly doubled every row's height — on a 600-object .w3a that's
- the difference between seeing ~20 and ~35 objects at once. Long ids still win their space (the name
- ellipsizes first); the full "base -> new" pair stays in the row's tooltip/aria-label. Cozy restores
- the stacked two-line form below — same markup, different grid. */
-.object-row {
- width: 100%;
- display: grid;
- /* minmax(0, auto) rather than plain auto for the id: a 4-char rawcode is tiny, but the track must
- still be allowed to shrink (and ellipsize) instead of squeezing the name track to nothing. */
- grid-template-columns: var(--tree-icon) minmax(0, 1fr) minmax(0, auto);
- gap: 6px;
- align-items: center;
- padding: var(--tree-row-py) var(--ind-group) var(--tree-row-py) var(--ind-row);
- color: var(--fg);
- background: transparent;
- border: 0;
- border-left: 2px solid transparent;
- text-align: left;
- font: inherit;
- line-height: 1.3;
- cursor: pointer;
-}
-/* Nested one level deeper under a kind-heading (Units/Buildings/Heroes/Special) than a plain
- race-heading — matches kind-heading's own indent so rows still read as its children. */
-.object-row.nested { padding-left: var(--ind-row-nested); }
-body.density-cozy .object-row {
- grid-template-columns: var(--tree-icon) minmax(0, 1fr);
- gap: 0 7px;
-}
-body.density-cozy .object-row > .object-icon { grid-area: 1 / 1 / span 2 / 1; }
-body.density-cozy .object-name { grid-area: 1 / 2; }
-body.density-cozy .object-id { grid-area: 2 / 2; }
-.object-row:hover { background: var(--hover); }
-.object-row.active {
- background: var(--active);
- color: var(--active-fg);
- border-left-color: var(--vscode-textLink-foreground, var(--fg));
-}
-.object-name {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.object-id {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: var(--muted);
- font-family: var(--mono);
- font-size: 10px;
-}
-.object-row.active .object-id {
- color: var(--active-fg);
- opacity: .75;
-}
-.object-icon {
- width: 20px;
- height: 20px;
- display: grid;
- place-items: center;
- border: 1px solid var(--border);
- border-radius: 2px;
- background: var(--input-bg);
- overflow: hidden;
-}
-/* Only the browse-tree thumbnails follow the density scale — the details header, field cells and
- asset-browser cards each have their own deliberate size. */
-.object-row > .object-icon { width: var(--tree-icon); height: var(--tree-icon); }
-.object-icon img {
- width: 100%;
- height: 100%;
- display: block;
- object-fit: cover;
-}
-.icon-spinner {
- width: 11px;
- height: 11px;
- border: 2px solid color-mix(in srgb, var(--muted) 28%, transparent);
- border-top-color: var(--muted);
- border-radius: 50%;
- /* Reuses the shared wv-spin keyframes from webviewShared.ts (same 360° rotation the model-thumb
- spinner already used) instead of declaring a second identical @keyframes here. */
- animation: wv-spin .8s linear infinite;
-}
-.object-icon.missing .icon-spinner {
- display: none;
-}
-.object-icon.missing::before {
- content: '?';
- color: var(--muted);
- font-size: 11px;
-}
-.details {
- min-width: 0;
- min-height: 0;
- height: 100%;
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-.details-head {
- padding: var(--details-head-pad);
- border-bottom: 1px solid var(--border);
- flex-shrink: 0;
-}
-.details-title-row {
- display: grid;
- grid-template-columns: var(--details-icon) minmax(0, 1fr);
- gap: 8px;
- align-items: center;
-}
-.details-icon {
- width: var(--details-icon);
- height: var(--details-icon);
-}
-.details-title {
- font-size: var(--title-fs);
- font-weight: 600;
- color: var(--fg);
- overflow-wrap: anywhere;
-}
-.details-rawcode {
- margin-left: 7px;
- color: var(--muted);
- font-family: var(--mono);
- font-size: var(--meta-fs);
- font-weight: 400;
-}
-.toggle-chip {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- color: var(--muted);
- font-size: var(--chip-fs);
- cursor: pointer;
- user-select: none;
-}
-.toggle-chip input { margin: 0; }
-.toggle-chip-group {
- display: inline-flex;
- align-items: center;
- gap: 9px;
- margin-left: auto;
-}
-.table-wrap {
- flex: 1;
- min-height: 0;
- min-width: 0;
- overflow: auto;
-}
-table {
- border-collapse: collapse;
- width: 100%;
- min-width: var(--table-min-w);
- font-size: 12px;
- table-layout: fixed; /* stable column widths so expanding an editor never reflows the table */
-}
-/* Technical mode swaps the 2-column Field/Value layout for 5-7 narrow metadata columns, so it needs
- more room before scrolling — the compact default must not pay for that. */
-table.technical { --table-min-w: 560px; }
-/* Explicit per-column widths (table-layout: fixed ignores cell min-widths, so this is the only lever).
- Previously a blanket "thead th:first-child { width: 34% }" also hit technical mode, where the first
- column is a 4-character field id — a third of the table for "unam". */
-/* Flat percentages only: a fixed-layout table silently drops a column width that contains min()/clamp()
- with a percentage inside it (measured in Chromium — the column falls back to an equal share), so
- there's no way to cap this proportional width without giving up the proportional part. */
-.col-field { width: 32%; }
-.col-id { width: 58px; }
-.col-label { width: 24%; }
-.col-group { width: 74px; }
-.col-type { width: 66px; }
-.col-lvl { width: 44px; }
-th {
- position: sticky;
- top: 0;
- z-index: 1;
- background: var(--vscode-editorGroupHeader-tabsBackground, var(--sidebar));
- text-align: left;
- padding: var(--th-pad);
- font-weight: 600;
- border-bottom: 1px solid var(--border);
-}
-td {
- padding: var(--td-pad);
- border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
- vertical-align: top;
-}
-tbody tr:hover td { background: color-mix(in srgb, var(--hover) 55%, transparent); }
-td.id,
-td.num {
- font-family: var(--mono);
-}
-td.value { font-family: var(--font); }
-td.label { color: var(--fg); }
-td.type { color: var(--muted); font-size: 11px; }
-td.num { text-align: right; color: var(--muted); }
-td.value { word-break: break-word; white-space: pre-wrap; }
-td.field { color: var(--fg); }
-tr.overridden td {
- background: color-mix(in srgb, var(--accent) 7%, transparent);
-}
-.override-badge {
- display: inline-block;
- margin-left: 5px;
- padding: 0 4px;
- border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
- border-radius: 2px;
- color: var(--accent);
- font-size: 10px;
- font-family: var(--vscode-font-family, sans-serif);
- font-weight: 600;
-}
-.category-row td {
- position: sticky;
- top: var(--table-header-h);
- z-index: 1;
- padding: var(--cat-pad);
- background: var(--vscode-editorGroupHeader-tabsBackground, var(--sidebar));
- color: var(--muted);
- font-size: 11px;
- font-weight: 600;
- text-transform: uppercase;
- border-bottom: 1px solid var(--border);
-}
-.source-pill {
- display: inline-block;
- max-width: 150px;
- margin-left: 5px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- vertical-align: -2px;
- border: 1px solid var(--border);
- border-radius: 2px;
- padding: 0 4px;
- background: color-mix(in srgb, var(--input-bg) 78%, transparent);
- color: var(--vscode-textLink-foreground, var(--muted));
- font-family: var(--mono);
- font-size: 10px;
-}
-.source-pill.missing {
- color: var(--vscode-errorForeground, #f14c4c);
-}
-.empty-state,
-.details-loading {
- flex: 1;
- display: grid;
- place-items: center;
- color: var(--muted);
- text-align: center;
-}
-.empty-state { padding: 18px; }
-.details-error { text-align: center; }
-.details-error-title { color: var(--fg); margin-bottom: 4px; }
-.details-error-reason {
- color: var(--muted);
- font-family: var(--mono);
- font-size: 11px;
- max-width: 360px;
- margin-bottom: 10px;
- word-break: break-word;
-}
-.details-error .browse-btn { padding: 4px 14px; }
-/* Stacked fallback, only below NARROW_LAYOUT_PX (see objModEditorWebview.ts) — the class is driven by
- a ResizeObserver on the editor element itself, which is what actually matters here, so there is no
- parallel @media (max-width) copy of these rules to keep in sync with that threshold. */
-.object-editor.narrow {
- /* minmax(0, 1fr), not plain 1fr, matching the side-by-side rule above: an fr track's implicit
- minimum is auto (its content's min-content), so a wide unbreakable child could push the single
- column past the editor's own width. Pinned to 0 it can't, and this container's overflow: auto
- scrolls instead. */
- grid-template-columns: minmax(0, 1fr);
- grid-template-rows: minmax(120px, 34%) minmax(240px, 1fr);
- overflow: auto;
-}
-.object-editor.narrow .object-list { border-right: 0; border-bottom: 1px solid var(--border); min-height: 120px; }
-.object-editor.narrow .details { min-height: 240px; }
-.object-editor.narrow .splitter { display: none; }
-${tooltipFontCss}
-`,
+ extraCss: `${OBJMOD_EDITOR_CSS}
+:root { --wc3-tip-width: ${tooltipWidthPx}px; }
+${tooltipFontCss}`,
body: `