Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -4,7 +4,19 @@
|
||||
import type { ConfigInspection, ConfigServer, ConfigSource } from '$lib/config-workbench.js';
|
||||
import Button from './primitives/Button.svelte';
|
||||
import { saveLibraryContext } from '$ui/library-context.js';
|
||||
let { artifactId, draftText, scopeType, scopeValue, fixedServer }: { fixedServer?: string; artifactId?: number; draftText?: string; scopeType?: string | null; scopeValue?: string | null } = $props();
|
||||
let {
|
||||
artifactId,
|
||||
draftText,
|
||||
scopeType,
|
||||
scopeValue,
|
||||
fixedServer
|
||||
}: {
|
||||
fixedServer?: string;
|
||||
artifactId?: number;
|
||||
draftText?: string;
|
||||
scopeType?: string | null;
|
||||
scopeValue?: string | null;
|
||||
} = $props();
|
||||
let servers = $state<ConfigServer[]>([]);
|
||||
let selected = $state('');
|
||||
let inspection = $state<ConfigInspection | null>(null);
|
||||
@@ -19,143 +31,587 @@
|
||||
let reload = $state(0);
|
||||
let ready = $state(false);
|
||||
let desiredServer = '';
|
||||
const eligible = $derived(servers.filter(s => (!fixedServer || s.name === fixedServer) && (!artifactId || (scopeType === 'game' ? s.game === scopeValue : scopeType === 'map' ? s.map === scopeValue : false))));
|
||||
const settings = $derived((inspection?.settings ?? []).filter(s => (!onlyOverrides || s.history.length > 1) && `${s.key} ${s.value} ${s.source.name}`.toLowerCase().includes(query.toLowerCase())));
|
||||
const overlaps = $derived(inspection?.settings.filter(s => s.history.length > 1).length ?? 0);
|
||||
const previousValues = $derived(new Map((promoted?.settings ?? []).map(s => [s.key, s.value])));
|
||||
const changes = $derived(promoted && inspection ? inspection.settings.filter(s => previousValues.get(s.key) !== s.value) : []);
|
||||
const removed = $derived(promoted?.settings.filter(s => inspection && !inspection.settings.some(current => current.key === s.key)) ?? []);
|
||||
function label(source: ConfigSource): string { return `${source.name}${source.draft ? ' · draft' : source.version ? ` · v${source.version}` : ''}`; }
|
||||
function overriddenCount(source: ConfigSource) { return inspection?.settings.filter(s => (s.source.id !== source.id || s.source.kind !== source.kind) && s.history.some(h => h.source.id === source.id && h.source.kind === source.kind)).length ?? 0; }
|
||||
const eligible = $derived(
|
||||
servers.filter(
|
||||
(s) =>
|
||||
(!fixedServer || s.name === fixedServer) &&
|
||||
(!artifactId ||
|
||||
(scopeType === 'game'
|
||||
? s.game === scopeValue
|
||||
: scopeType === 'map'
|
||||
? s.map === scopeValue
|
||||
: false))
|
||||
)
|
||||
);
|
||||
const settings = $derived(
|
||||
(inspection?.settings ?? []).filter(
|
||||
(s) =>
|
||||
(!onlyOverrides || s.history.length > 1) &&
|
||||
`${s.key} ${s.value} ${s.source.name}`.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
);
|
||||
const overlaps = $derived(inspection?.settings.filter((s) => s.history.length > 1).length ?? 0);
|
||||
const previousValues = $derived(
|
||||
new Map((promoted?.settings ?? []).map((s) => [s.key, s.value]))
|
||||
);
|
||||
const changes = $derived(
|
||||
promoted && inspection
|
||||
? inspection.settings.filter((s) => previousValues.get(s.key) !== s.value)
|
||||
: []
|
||||
);
|
||||
const removed = $derived(
|
||||
promoted?.settings.filter(
|
||||
(s) => inspection && !inspection.settings.some((current) => current.key === s.key)
|
||||
) ?? []
|
||||
);
|
||||
function label(source: ConfigSource): string {
|
||||
return `${source.name}${source.draft ? ' · draft' : source.version ? ` · v${source.version}` : ''}`;
|
||||
}
|
||||
function overriddenCount(source: ConfigSource) {
|
||||
return (
|
||||
inspection?.settings.filter(
|
||||
(s) =>
|
||||
(s.source.id !== source.id || s.source.kind !== source.kind) &&
|
||||
s.history.some(
|
||||
(h) => h.source.id === source.id && h.source.kind === source.kind
|
||||
)
|
||||
).length ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
async function loadServers() {
|
||||
loading = true; error = '';
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const response = await fetch('/api/configs/inspect');
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error ?? 'Could not load servers.');
|
||||
servers = body.servers;
|
||||
ready = true;
|
||||
} catch (err) { error = (err as Error).message; }
|
||||
finally { loading = false; }
|
||||
} catch (err) {
|
||||
error = (err as Error).message;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
onMount(() => { desiredServer = new URLSearchParams(window.location.search).get('server') ?? ''; void loadServers(); });
|
||||
$effect(() => { if (!eligible.some(s => s.name === selected)) selected = eligible.find(s => s.name === desiredServer)?.name ?? eligible[0]?.name ?? ''; });
|
||||
$effect(() => { if (ready && selected && !fixedServer) saveLibraryContext({ server: selected }); });
|
||||
onMount(() => {
|
||||
desiredServer = new URLSearchParams(window.location.search).get('server') ?? '';
|
||||
void loadServers();
|
||||
});
|
||||
$effect(() => {
|
||||
if (!eligible.some((s) => s.name === selected))
|
||||
selected =
|
||||
eligible.find((s) => s.name === desiredServer)?.name ?? eligible[0]?.name ?? '';
|
||||
});
|
||||
$effect(() => {
|
||||
if (ready && selected && !fixedServer) saveLibraryContext({ server: selected });
|
||||
});
|
||||
$effect(() => {
|
||||
const server = selected;
|
||||
const text = draftText;
|
||||
const id = artifactId;
|
||||
const refresh = reload;
|
||||
if (!ready || !server) { inspection = null; if (ready) loading = false; return; }
|
||||
void reload;
|
||||
if (!ready || !server) {
|
||||
inspection = null;
|
||||
if (ready) loading = false;
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
loading = true; error = '';
|
||||
if (inspectedServer !== server) { inspection = null; promoted = null; expanded = []; inspectedServer = server; }
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const response = await fetch(id !== undefined && text !== undefined ? '/api/configs/inspect' : `/api/configs/inspect?server=${encodeURIComponent(server)}`, {
|
||||
signal: controller.signal,
|
||||
...(id !== undefined && text !== undefined ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ server, artifactId: id, text }) } : {})
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error ?? 'Could not inspect this config.');
|
||||
if (!controller.signal.aborted) { inspection = body.inspection; promoted = body.promoted ?? null; }
|
||||
} catch (err) { if (!controller.signal.aborted) error = (err as Error).message; }
|
||||
finally { if (!controller.signal.aborted) loading = false; }
|
||||
}, text === undefined ? 0 : 350);
|
||||
return () => { clearTimeout(timer); controller.abort(); };
|
||||
loading = true;
|
||||
error = '';
|
||||
if (inspectedServer !== server) {
|
||||
inspection = null;
|
||||
promoted = null;
|
||||
expanded = [];
|
||||
inspectedServer = server;
|
||||
}
|
||||
const timer = setTimeout(
|
||||
async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
id !== undefined && text !== undefined
|
||||
? '/api/configs/inspect'
|
||||
: `/api/configs/inspect?server=${encodeURIComponent(server)}`,
|
||||
{
|
||||
signal: controller.signal,
|
||||
...(id !== undefined && text !== undefined
|
||||
? {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ server, artifactId: id, text })
|
||||
}
|
||||
: {})
|
||||
}
|
||||
);
|
||||
const body = await response.json();
|
||||
if (!response.ok)
|
||||
throw new Error(body.error ?? 'Could not inspect this config.');
|
||||
if (!controller.signal.aborted) {
|
||||
inspection = body.inspection;
|
||||
promoted = body.promoted ?? null;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!controller.signal.aborted) error = (err as Error).message;
|
||||
} finally {
|
||||
if (!controller.signal.aborted) loading = false;
|
||||
}
|
||||
},
|
||||
text === undefined ? 0 : 350
|
||||
);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
controller.abort();
|
||||
};
|
||||
});
|
||||
function download() {
|
||||
if (!inspection) return;
|
||||
const url = URL.createObjectURL(new Blob([inspection.text], { type: 'text/plain' }));
|
||||
const anchor = document.createElement('a'); anchor.href = url; anchor.download = `${selected}${artifactId ? '-draft' : ''}.cfg`; anchor.click();
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = `${selected}${artifactId ? '-draft' : ''}.cfg`;
|
||||
anchor.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="inspector" aria-label="Effective config inspector" aria-busy={loading}>
|
||||
<div class="heading"><div>{#if !fixedServer}<span class="eyebrow">{artifactId ? 'Draft preview' : 'Follow every value to its source'}</span>{/if}<h2>{artifactId ? 'On this server' : 'Promoted configuration'}</h2></div>
|
||||
<Button size="sm" variant="ghost" onclick={() => ready ? reload++ : loadServers()}>Refresh</Button>
|
||||
<div class="heading">
|
||||
<div>
|
||||
{#if !fixedServer}<span class="eyebrow"
|
||||
>{artifactId ? 'Draft preview' : 'Follow every value to its source'}</span
|
||||
>{/if}
|
||||
<h2>{artifactId ? 'On this server' : 'Promoted configuration'}</h2>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onclick={() => (ready ? reload++ : loadServers())}
|
||||
>Refresh</Button
|
||||
>
|
||||
</div>
|
||||
<p class="intro">{artifactId ? 'Your draft composed with the other promoted configs. Saving creates an untested version.' : 'The intended file from promoted configs and the selected map. Live files and active betas may differ.'}</p>
|
||||
{#if !fixedServer}<label class="server-label">Server<select aria-label="Server" bind:value={selected} disabled={!!fixedServer || !eligible.length}>
|
||||
{#each eligible as server}<option value={server.name}>{serverTitle(server.name)}</option>{/each}
|
||||
{#if !eligible.length}<option value="">No matching servers</option>{/if}
|
||||
</select></label>{/if}
|
||||
{#if error}<div class="error" role="alert">{error}{inspection ? ' Showing the last completed preview.' : ''}</div>{/if}
|
||||
{#if !fixedServer || loading}<p class="preview-status" role="status">{loading ? inspection ? 'Updating preview… Showing the last completed result.' : 'Composing configuration…' : inspection ? 'Preview up to date.' : ''}</p>{/if}
|
||||
{#if !eligible.length && !loading}<p class="empty">{artifactId ? 'No registered servers match this scope.' : 'Register a server to inspect its configuration.'}</p>
|
||||
{:else if inspection?.reason}<p class="empty">{fixedServer && inspection.reason.startsWith('No promoted baseline') ? `No promoted baseline for ${servers.find(item => item.name === fixedServer)?.game === 't6' ? 'Black Ops II' : servers.find(item => item.name === fixedServer)?.game === 't5' ? 'Black Ops I' : 'this game'}.` : inspection.reason}</p>{#if fixedServer}<a href="/configs">Open config library →</a>{/if}
|
||||
<p class="intro">
|
||||
{artifactId
|
||||
? 'Your draft composed with the other promoted configs. Saving creates an untested version.'
|
||||
: 'The intended file from promoted configs and the selected map. Live files and active betas may differ.'}
|
||||
</p>
|
||||
{#if !fixedServer}<label class="server-label"
|
||||
>Server<select
|
||||
aria-label="Server"
|
||||
bind:value={selected}
|
||||
disabled={!!fixedServer || !eligible.length}
|
||||
>
|
||||
{#each eligible as server}<option value={server.name}
|
||||
>{serverTitle(server.name)}</option
|
||||
>{/each}
|
||||
{#if !eligible.length}<option value="">No matching servers</option>{/if}
|
||||
</select></label
|
||||
>{/if}
|
||||
{#if error}<div class="error" role="alert">
|
||||
{error}{inspection ? ' Showing the last completed preview.' : ''}
|
||||
</div>{/if}
|
||||
{#if !fixedServer || loading}<p class="preview-status" role="status">
|
||||
{loading
|
||||
? inspection
|
||||
? 'Updating preview… Showing the last completed result.'
|
||||
: 'Composing configuration…'
|
||||
: inspection
|
||||
? 'Preview up to date.'
|
||||
: ''}
|
||||
</p>{/if}
|
||||
{#if !eligible.length && !loading}<p class="empty">
|
||||
{artifactId
|
||||
? 'No registered servers match this scope.'
|
||||
: 'Register a server to inspect its configuration.'}
|
||||
</p>
|
||||
{:else if inspection?.reason}<p class="empty">
|
||||
{fixedServer && inspection.reason.startsWith('No promoted baseline')
|
||||
? `No promoted baseline for ${servers.find((item) => item.name === fixedServer)?.game === 't6' ? 'Black Ops II' : servers.find((item) => item.name === fixedServer)?.game === 't5' ? 'Black Ops I' : 'this game'}.`
|
||||
: inspection.reason}
|
||||
</p>
|
||||
{#if fixedServer}<a href="/configs">Open config library →</a>{/if}
|
||||
{:else if inspection}
|
||||
<ol class="sources" aria-label="Composition order, later layers take precedence">
|
||||
{#each inspection.sources as source, index}
|
||||
<li><span class="layer-number">{index + 1}.</span>{#if source.id}<a class:draft={source.draft} href="/configs/{source.id}">{label(source)}</a>
|
||||
{:else}<span class="map-source">{source.name}</span>{/if}<span>{overriddenCount(source)} settings superseded later</span></li>
|
||||
<li>
|
||||
<span class="layer-number">{index + 1}.</span>{#if source.id}<a
|
||||
class:draft={source.draft}
|
||||
href="/configs/{source.id}">{label(source)}</a
|
||||
>
|
||||
{:else}<span class="map-source">{source.name}</span>{/if}<span
|
||||
>{overriddenCount(source)} settings superseded later</span
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
<p class="hint">Overlays apply in creation order; later overlays win for the same key. The selected map takes precedence over map rotation.</p>
|
||||
{#if promoted}<p class="change-summary">Compared with promoted settings: {changes.length} changed or added, {removed.length} removed.</p>{#if removed.length}<details><summary>Removed settings</summary><ul>{#each removed as setting}<li><code>{setting.key}</code> · previously {setting.value}</li>{/each}</ul></details>{/if}{/if}
|
||||
<div class="toolbar"><div class="switch"><button class:active={!raw} aria-pressed={!raw} onclick={() => raw = false}>Settings <span>{inspection.settings.length}</span></button><button class:active={raw} aria-pressed={raw} onclick={() => raw = true}>Composed .cfg</button></div><Button size="sm" variant="ghost" onclick={download}>Download</Button></div>
|
||||
{#if raw}<textarea class="composed" readonly aria-label="Composed config" value={inspection.text} rows="18"></textarea>
|
||||
<p class="hint">
|
||||
Overlays apply in creation order; later overlays win for the same key. The selected map
|
||||
takes precedence over map rotation.
|
||||
</p>
|
||||
{#if promoted}<p class="change-summary">
|
||||
Compared with promoted settings: {changes.length} changed or added, {removed.length} removed.
|
||||
</p>
|
||||
{#if removed.length}<details>
|
||||
<summary>Removed settings</summary>
|
||||
<ul>
|
||||
{#each removed as setting}<li>
|
||||
<code>{setting.key}</code> · previously {setting.value}
|
||||
</li>{/each}
|
||||
</ul>
|
||||
</details>{/if}{/if}
|
||||
<div class="toolbar">
|
||||
<div class="switch">
|
||||
<button class:active={!raw} aria-pressed={!raw} onclick={() => (raw = false)}
|
||||
>Settings <span>{inspection.settings.length}</span></button
|
||||
><button class:active={raw} aria-pressed={raw} onclick={() => (raw = true)}
|
||||
>Composed .cfg</button
|
||||
>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onclick={download}>Download</Button>
|
||||
</div>
|
||||
{#if raw}<textarea
|
||||
class="composed"
|
||||
readonly
|
||||
aria-label="Composed config"
|
||||
value={inspection.text}
|
||||
rows="18"
|
||||
></textarea>
|
||||
{:else}
|
||||
<div class="filters"><input aria-label="Search effective settings" bind:value={query} placeholder="Search settings or sources…" /><label class="check"><input type="checkbox" bind:checked={onlyOverrides} /> Overrides ({overlaps})</label></div>
|
||||
<div class="filters">
|
||||
<input
|
||||
aria-label="Search effective settings"
|
||||
bind:value={query}
|
||||
placeholder="Search settings or sources…"
|
||||
/><label class="check"
|
||||
><input type="checkbox" bind:checked={onlyOverrides} /> Overrides ({overlaps})</label
|
||||
>
|
||||
</div>
|
||||
<div class="settings">
|
||||
{#each settings as setting (setting.key)}
|
||||
<details class="setting" open={expanded.includes(setting.key)} ontoggle={event => { const open = event.currentTarget.open; expanded = open ? [...new Set([...expanded, setting.key])] : expanded.filter(key => key !== setting.key); }}><summary><div class="setting-main"><code>{setting.key}</code><strong>{setting.value}</strong></div><span class="source-label">{label(setting.source)} {#if setting.history.length > 1}<b>{setting.history.length - 1} overridden</b>{/if}</span>{#if promoted && previousValues.get(setting.key) !== setting.value}<span class="change-summary">{previousValues.has(setting.key) ? `Changed from ${previousValues.get(setting.key)}` : 'Added by draft'}</span>{/if}</summary>
|
||||
<ol>{#each setting.history as entry, index}<li class:winner={index === setting.history.length - 1}><div><span>{label(entry.source)}</span><small>{entry.source.kind === 'map' ? 'Automatic' : `Line ${entry.line}`} · {index === setting.history.length - 1 ? 'Final value' : 'Overridden'}</small></div><code>{entry.value}</code></li>{/each}</ol>
|
||||
<details
|
||||
class="setting"
|
||||
open={expanded.includes(setting.key)}
|
||||
ontoggle={(event) => {
|
||||
const open = event.currentTarget.open;
|
||||
expanded = open
|
||||
? [...new Set([...expanded, setting.key])]
|
||||
: expanded.filter((key) => key !== setting.key);
|
||||
}}
|
||||
>
|
||||
<summary
|
||||
><div class="setting-main">
|
||||
<code>{setting.key}</code><strong>{setting.value}</strong>
|
||||
</div>
|
||||
<span class="source-label"
|
||||
>{label(setting.source)}
|
||||
{#if setting.history.length > 1}<b
|
||||
>{setting.history.length - 1} overridden</b
|
||||
>{/if}</span
|
||||
>{#if promoted && previousValues.get(setting.key) !== setting.value}<span
|
||||
class="change-summary"
|
||||
>{previousValues.has(setting.key)
|
||||
? `Changed from ${previousValues.get(setting.key)}`
|
||||
: 'Added by draft'}</span
|
||||
>{/if}</summary
|
||||
>
|
||||
<ol>
|
||||
{#each setting.history as entry, index}<li
|
||||
class:winner={index === setting.history.length - 1}
|
||||
>
|
||||
<div>
|
||||
<span>{label(entry.source)}</span><small
|
||||
>{entry.source.kind === 'map'
|
||||
? 'Automatic'
|
||||
: `Line ${entry.line}`} · {index ===
|
||||
setting.history.length - 1
|
||||
? 'Final value'
|
||||
: 'Overridden'}</small
|
||||
>
|
||||
</div>
|
||||
<code>{entry.value}</code>
|
||||
</li>{/each}
|
||||
</ol>
|
||||
</details>
|
||||
{:else}<p class="empty">No matching settings.</p>{/each}
|
||||
</div>
|
||||
<p class="hint">Shows set / seta assignments and map rotation. Other commands remain visible in the composed file.</p>
|
||||
<p class="hint">
|
||||
Shows set / seta assignments and map rotation. Other commands remain visible in the
|
||||
composed file.
|
||||
</p>
|
||||
{/if}
|
||||
{#if inspection.warnings.length}<details class="notes"><summary>{inspection.warnings.length} configuration {inspection.warnings.length === 1 ? 'note' : 'notes'}</summary><ul>{#each inspection.warnings as warning}<li>{warning}</li>{/each}</ul></details>{/if}
|
||||
{#if inspection.warnings.length}<details class="notes">
|
||||
<summary
|
||||
>{inspection.warnings.length} configuration {inspection.warnings.length === 1
|
||||
? 'note'
|
||||
: 'notes'}</summary
|
||||
>
|
||||
<ul>
|
||||
{#each inspection.warnings as warning}<li>{warning}</li>{/each}
|
||||
</ul>
|
||||
</details>{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.inspector { min-width: 0; padding: var(--space-6); border: 1px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface); }
|
||||
.heading, .toolbar, .filters { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
||||
.eyebrow { font-size: var(--text-xs); color: var(--accent-primary-hover); }
|
||||
h2 { margin: 5px 0 0; font-size: var(--text-lg); letter-spacing: var(--tracking-tight); }
|
||||
.intro { color: var(--text-secondary); font-size: var(--text-sm); margin: 12px 0 20px; }
|
||||
.server-label { display: grid; gap: 8px; font-size: var(--text-xs); color: var(--text-secondary); }
|
||||
select, input:not([type='checkbox']) { width: 100%; box-sizing: border-box; background: var(--surface-sunken); color: var(--text-primary); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 10px 12px; font: var(--text-sm) var(--font-body); }
|
||||
select:focus-visible, input:focus-visible, button:focus-visible, summary:focus-visible, a:focus-visible, .composed:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
|
||||
.sources { display: grid; gap: 8px; margin: 20px 0 0; padding: 0; border: 0; font-size: var(--text-xs); list-style: none; }
|
||||
.sources li { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 0; }
|
||||
.layer-number { color: var(--text-primary); font-weight: 600; }
|
||||
.preview-status { font-size: var(--text-xs); color: var(--text-secondary); min-height: 20px; }
|
||||
.change-summary { display: block; font-size: var(--text-xs); color: var(--state-warning); margin-top: 8px; }
|
||||
.sources a, .map-source { background: var(--surface-raised); border-radius: 6px; padding: 6px 9px; text-decoration: none; }
|
||||
.sources a.draft { color: var(--state-warning); }
|
||||
.map-source { color: var(--text-secondary); }
|
||||
.hint { color: var(--text-secondary); font-size: var(--text-xs); line-height: 1.6; }
|
||||
.toolbar { border-top: 1px solid var(--border-subtle); padding-top: 12px; margin-top: 20px; }
|
||||
.switch { display: flex; gap: 4px; }
|
||||
button { border: 0; border-radius: 6px; background: transparent; color: var(--text-secondary); font: var(--text-sm) var(--font-body); padding: 8px 10px; cursor: pointer; }
|
||||
button.active { color: var(--text-primary); background: var(--surface-raised); }
|
||||
button span { color: var(--text-secondary); margin-left: 6px; }
|
||||
.filters { margin: 16px 0 8px; }
|
||||
.filters > input { flex: 1 1 180px; }
|
||||
.check { font-size: var(--text-xs); color: var(--text-secondary); display: flex; align-items: center; gap: 6px; }
|
||||
.settings { max-height: 620px; overflow: auto; }
|
||||
.setting { border-bottom: 1px solid var(--border-subtle); padding: 14px 0; }
|
||||
.setting summary { cursor: pointer; list-style: none; }
|
||||
.setting summary::-webkit-details-marker { display: none; }
|
||||
.setting-main { display: flex; gap: 12px; justify-content: space-between; flex-wrap: wrap; }
|
||||
code, strong { font: var(--text-xs)/1.6 var(--font-mono); overflow-wrap: anywhere; white-space: pre-wrap; }
|
||||
strong { color: var(--text-primary); }
|
||||
.source-label { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: var(--text-xs); color: var(--text-secondary); margin-top: 6px; }
|
||||
.source-label::before { content: '▸'; } .setting[open] .source-label::before { content: '▾'; }
|
||||
b { font-weight: 400; color: var(--state-warning); }
|
||||
ol { list-style: none; padding: 0 0 0 14px; margin: 16px 0 0 4px; border-left: 2px solid var(--border-strong); }
|
||||
ol li { display: flex; justify-content: space-between; gap: 16px; padding: 8px 0; color: var(--text-secondary); font-size: var(--text-xs); }
|
||||
ol li.winner { color: var(--text-primary); }
|
||||
small { display: block; font-size: var(--text-xs); color: var(--text-secondary); margin-top: 3px; }
|
||||
.composed { display: block; width: 100%; box-sizing: border-box; margin-top: 16px; padding: 20px; background: var(--surface-sunken); color: var(--text-primary); border: 0; border-radius: var(--radius-sm); min-height: 260px; max-height: 600px; resize: vertical; overflow: auto; font: var(--text-xs)/1.8 var(--font-mono); white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.notes { margin-top: 16px; color: var(--state-warning); font-size: var(--text-xs); } .notes summary { cursor: pointer; } .notes li { margin: 8px 0; }
|
||||
.empty { color: var(--text-secondary); font-size: var(--text-sm); padding: 8px 0; }
|
||||
.error { color: var(--state-danger); margin-top: 16px; font-size: var(--text-sm); }
|
||||
@media (max-width: 600px) { .inspector { padding: 16px; } }
|
||||
input:not([type='checkbox']):not([type='file']), select, textarea { border-color: var(--border-control); }
|
||||
.inspector {
|
||||
min-width: 0;
|
||||
padding: var(--space-6);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface);
|
||||
}
|
||||
.heading,
|
||||
.toolbar,
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--accent-primary-hover);
|
||||
}
|
||||
h2 {
|
||||
margin: 5px 0 0;
|
||||
font-size: var(--text-lg);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
.intro {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
margin: 12px 0 20px;
|
||||
}
|
||||
.server-label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
select,
|
||||
input:not([type='checkbox']) {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--surface-sunken);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px 12px;
|
||||
font: var(--text-sm) var(--font-body);
|
||||
}
|
||||
select:focus-visible,
|
||||
input:focus-visible,
|
||||
button:focus-visible,
|
||||
summary:focus-visible,
|
||||
a:focus-visible,
|
||||
.composed:focus-visible {
|
||||
outline: 2px solid var(--accent-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.sources {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 20px 0 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: var(--text-xs);
|
||||
list-style: none;
|
||||
}
|
||||
.sources li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
.layer-number {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.preview-status {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
min-height: 20px;
|
||||
}
|
||||
.change-summary {
|
||||
display: block;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--state-warning);
|
||||
margin-top: 8px;
|
||||
}
|
||||
.sources a,
|
||||
.map-source {
|
||||
background: var(--surface-raised);
|
||||
border-radius: 6px;
|
||||
padding: 6px 9px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.sources a.draft {
|
||||
color: var(--state-warning);
|
||||
}
|
||||
.map-source {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.hint {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.toolbar {
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
padding-top: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.switch {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font: var(--text-sm) var(--font-body);
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.active {
|
||||
color: var(--text-primary);
|
||||
background: var(--surface-raised);
|
||||
}
|
||||
button span {
|
||||
color: var(--text-secondary);
|
||||
margin-left: 6px;
|
||||
}
|
||||
.filters {
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
.filters > input {
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
.check {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.settings {
|
||||
max-height: 620px;
|
||||
overflow: auto;
|
||||
}
|
||||
.setting {
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
padding: 14px 0;
|
||||
}
|
||||
.setting summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.setting summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.setting-main {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
code,
|
||||
strong {
|
||||
font: var(--text-xs)/1.6 var(--font-mono);
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.source-label {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
margin-top: 6px;
|
||||
}
|
||||
.source-label::before {
|
||||
content: '▸';
|
||||
}
|
||||
.setting[open] .source-label::before {
|
||||
content: '▾';
|
||||
}
|
||||
b {
|
||||
font-weight: 400;
|
||||
color: var(--state-warning);
|
||||
}
|
||||
ol {
|
||||
list-style: none;
|
||||
padding: 0 0 0 14px;
|
||||
margin: 16px 0 0 4px;
|
||||
border-left: 2px solid var(--border-strong);
|
||||
}
|
||||
ol li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 8px 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
ol li.winner {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
small {
|
||||
display: block;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
margin-top: 3px;
|
||||
}
|
||||
.composed {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-top: 16px;
|
||||
padding: 20px;
|
||||
background: var(--surface-sunken);
|
||||
color: var(--text-primary);
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
min-height: 260px;
|
||||
max-height: 600px;
|
||||
resize: vertical;
|
||||
overflow: auto;
|
||||
font: var(--text-xs)/1.8 var(--font-mono);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.notes {
|
||||
margin-top: 16px;
|
||||
color: var(--state-warning);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.notes summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
.notes li {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.empty {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
padding: 8px 0;
|
||||
}
|
||||
.error {
|
||||
color: var(--state-danger);
|
||||
margin-top: 16px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.inspector {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
input:not([type='checkbox']):not([type='file']),
|
||||
select,
|
||||
textarea {
|
||||
border-color: var(--border-control);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user