From d69bafe4d7d2efa102b43e668dd9f3216be5b4db Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 17:42:47 +0200 Subject: [PATCH 1/2] Move viewer stylesheets out of the host modules into .css files The object editor's ~1,380 lines of CSS, the MPQ viewer's and the shared map-data page styles were template literals inside TypeScript host modules, where nothing could lint or format them. They now live as real stylesheets under src/webview/, inlined into the host bundle by a webpack asset/source rule (and by the test loader), with the two runtime values the object editor needs (--wc3-tip-width and the optional tooltip @font-face) injected as a trailing rule. The extracted text is byte-identical to what the template literals produced. --- e2e/harness/tsLoader.js | 2 + src/features/mapDataPreview.ts | 181 +-- src/features/mpqViewer.ts | 158 +-- src/features/objModPreview.ts | 1383 +-------------------- src/types/assets.d.ts | 9 + src/webview/mapData.css | 180 +++ src/webview/mpqViewer.css | 156 +++ src/webview/objModEditor/objModEditor.css | 1380 ++++++++++++++++++++ webpack.config.js | 2 + 9 files changed, 1736 insertions(+), 1715 deletions(-) create mode 100644 src/types/assets.d.ts create mode 100644 src/webview/mapData.css create mode 100644 src/webview/mpqViewer.css create mode 100644 src/webview/objModEditor/objModEditor.css 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/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: `
${escapeHtml(fileName)} diff --git a/src/types/assets.d.ts b/src/types/assets.d.ts new file mode 100644 index 0000000..e47946b --- /dev/null +++ b/src/types/assets.d.ts @@ -0,0 +1,9 @@ +/** + * Stylesheets imported from host code are inlined as strings by webpack (`type: 'asset/source'`, + * see webpack.config.js) and by the test loader (e2e/harness/tsLoader.js). Keeping viewer CSS in + * real .css files keeps it out of the TypeScript modules and editable/lintable as CSS. + */ +declare module '*.css' { + const css: string; + export default css; +} diff --git a/src/webview/mapData.css b/src/webview/mapData.css new file mode 100644 index 0000000..ba06a4e --- /dev/null +++ b/src/webview/mapData.css @@ -0,0 +1,180 @@ +/* Shared stylesheet for the map-data viewers/editors (.w3i, .w3c, .w3r, .mmp, .imp, .shd, .w3e, ...); see mapDataPreview.ts page(). */ +.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; } diff --git a/src/webview/mpqViewer.css b/src/webview/mpqViewer.css new file mode 100644 index 0000000..752ae64 --- /dev/null +++ b/src/webview/mpqViewer.css @@ -0,0 +1,156 @@ +/* MPQ archive viewer webview stylesheet (see mpqViewer.ts / mpqViewerWebview.ts). */ +/* ── 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; } diff --git a/src/webview/objModEditor/objModEditor.css b/src/webview/objModEditor/objModEditor.css new file mode 100644 index 0000000..582a6ef --- /dev/null +++ b/src/webview/objModEditor/objModEditor.css @@ -0,0 +1,1380 @@ +/* + * Object editor (.w3u/.w3t/.w3a/...) webview stylesheet. Inlined into the host bundle via webpack + * `asset/source` and appended after WEBVIEW_BASE_CSS by objModPreview.ts, which also injects the two + * runtime values: `--wc3-tip-width` (wurst.objModTooltipWidth) and the optional tooltip @font-face. + */ +: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. */ + /* 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; } diff --git a/webpack.config.js b/webpack.config.js index daa8126..e9dc322 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -131,6 +131,8 @@ const nodeExtensionConfig = { exclude: /node_modules/, use: [{ loader: 'ts-loader' }], }, + // Viewer stylesheets live in .css files and are inlined into the host bundle as strings. + { test: /\.css$/, type: 'asset/source' }, ], }, externals: { From ded501285b4b04a12d20393c8fde3a850af1a191 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 17:54:08 +0200 Subject: [PATCH 2/2] Let the density/tree styling guards read the extracted stylesheet --- scripts/test-webview.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) 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');