mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Turn Live Lab into a UI workbench
AI-assisted: OpenAI Codex.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
---
|
||||
import '../styles/live-ui-gallery.css';
|
||||
import { LIVE_COMMANDS } from '../../skill/scripts/live/vocabulary.mjs';
|
||||
|
||||
const STATE_GROUPS = [
|
||||
{
|
||||
label: 'Global chrome',
|
||||
states: [
|
||||
['global-ready', 'Ready'],
|
||||
['global-disconnected', 'Agent disconnected'],
|
||||
['global-tools', 'Detect + DESIGN.md'],
|
||||
['steer-expanded', 'Steer composing'],
|
||||
['steer-processing', 'Steer processing'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Selection',
|
||||
states: [
|
||||
['configure-replace', 'Configure selection'],
|
||||
['action-picker', 'Action picker'],
|
||||
['configure-listening', 'Voice input'],
|
||||
['configure-locked', 'Apply locked'],
|
||||
['annotation', 'Annotations'],
|
||||
['insert-placeholder', 'Insert placeholder'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Generation + review',
|
||||
states: [
|
||||
['generating', 'Generating'],
|
||||
['generation-recovery', 'Recovery'],
|
||||
['cycling-progressive', 'Variant 1 arrived'],
|
||||
['cycling-second', 'Variant 2 arrived'],
|
||||
['tune-open', 'Tune panel'],
|
||||
['applying', 'Applying variant'],
|
||||
['confirmed', 'Variant applied'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Copy + support',
|
||||
states: [
|
||||
['edit-copy', 'Edit copy'],
|
||||
['copy-pending', 'Copy edits pending'],
|
||||
['copy-applying', 'Copy edits applying'],
|
||||
['copy-attention', 'Apply needs attention'],
|
||||
['design-panel', 'DESIGN.md panel'],
|
||||
['toast-error', 'Error toast'],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const statePayload = STATE_GROUPS.flatMap((group) =>
|
||||
group.states.map(([key, label]) => ({ key, label, group: group.label })),
|
||||
);
|
||||
const commandPayload = JSON.stringify(LIVE_COMMANDS).replace(/</g, '\\u003c');
|
||||
const statesJson = JSON.stringify(statePayload).replace(/</g, '\\u003c');
|
||||
---
|
||||
|
||||
<!--
|
||||
Dev-only, embeddable state harness for Impeccable Live. The chrome mirrors:
|
||||
- skill/scripts/live-browser.js (standalone browser UI and exact wording)
|
||||
- skill/scripts/live/ui-core.mjs (surface/state inventory)
|
||||
- skill/scripts/live/vocabulary.mjs (canonical command labels and SVGs)
|
||||
|
||||
Production Live deliberately uses the same dark Neo Kinpaku chrome on light
|
||||
and dark host pages (barPaletteForTheme, live-browser.js). The two columns
|
||||
below therefore vary the host canvas, not the chrome palette.
|
||||
-->
|
||||
<section class="live-ui-gallery" data-live-ui-gallery data-dev-only="true" aria-labelledby="live-ui-gallery-title">
|
||||
<header class="live-ui-gallery__header">
|
||||
<div class="live-ui-gallery__intro">
|
||||
<p class="live-ui-gallery__eyebrow">Dev-only · Live chrome</p>
|
||||
<h2 id="live-ui-gallery-title">Live UI state gallery</h2>
|
||||
<p>
|
||||
Inspect the real picker, progressive review, copy-apply, and recovery states without starting Live.
|
||||
Every state renders against both host themes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="live-ui-gallery__control-panel" aria-label="Gallery controls">
|
||||
<label for="live-ui-gallery-state">State</label>
|
||||
<div class="live-ui-gallery__select-row">
|
||||
<button type="button" class="live-ui-gallery__step" data-gallery-step="-1" aria-label="Previous state">←</button>
|
||||
<select id="live-ui-gallery-state" data-gallery-state>
|
||||
{STATE_GROUPS.map((group) => (
|
||||
<optgroup label={group.label}>
|
||||
{group.states.map(([key, label]) => <option value={key}>{label}</option>)}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" class="live-ui-gallery__step" data-gallery-step="1" aria-label="Next state">→</button>
|
||||
</div>
|
||||
<p class="live-ui-gallery__shortcut">Use ←/→ while this panel is focused to step through states.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="live-ui-gallery__quick-nav" aria-label="Quick state selection">
|
||||
{STATE_GROUPS.map((group) => (
|
||||
<div class="live-ui-gallery__quick-group">
|
||||
<span>{group.label}</span>
|
||||
<div>
|
||||
{group.states.map(([key, label]) => (
|
||||
<button type="button" data-gallery-state-button={key}>{label}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div class="live-ui-gallery__state-readout" aria-live="polite" aria-atomic="true">
|
||||
<span data-gallery-state-group>Global chrome</span>
|
||||
<strong data-gallery-state-label>Ready</strong>
|
||||
</div>
|
||||
|
||||
<div class="live-ui-gallery__previews">
|
||||
<section class="live-ui-gallery__preview" aria-labelledby="live-ui-gallery-light-title">
|
||||
<header>
|
||||
<span class="live-ui-gallery__theme-dot" aria-hidden="true"></span>
|
||||
<h3 id="live-ui-gallery-light-title">Light host</h3>
|
||||
<code>host: light</code>
|
||||
</header>
|
||||
<div class="live-ui-stage is-light" data-live-gallery-preview="light"></div>
|
||||
</section>
|
||||
|
||||
<section class="live-ui-gallery__preview" aria-labelledby="live-ui-gallery-dark-title">
|
||||
<header>
|
||||
<span class="live-ui-gallery__theme-dot" aria-hidden="true"></span>
|
||||
<h3 id="live-ui-gallery-dark-title">Dark host</h3>
|
||||
<code>host: dark</code>
|
||||
</header>
|
||||
<div class="live-ui-stage is-dark" data-live-gallery-preview="dark"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<p class="live-ui-gallery__fidelity-note">
|
||||
Fidelity anchors: <code>live-browser.js</code> configure row, generation row, cycling row, Tune panel,
|
||||
pending-copy dock, global bar, annotation overlay, and DESIGN.md panel. Command icons and order are imported
|
||||
from <code>live/vocabulary.mjs</code>.
|
||||
</p>
|
||||
|
||||
<script is:inline type="application/json" data-live-gallery-states set:html={statesJson}></script>
|
||||
<script is:inline type="application/json" data-live-gallery-vocabulary set:html={commandPayload}></script>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
import '../scripts/live-ui-gallery.js';
|
||||
</script>
|
||||
+127
-38
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import LiveUiGallery from '../../components/LiveUiGallery.astro';
|
||||
import benchmarkData from '../../data/live-performance.json';
|
||||
import { currentHarnessProbe, harnessPaths, liveExperiments } from '../../data/live-harnesses';
|
||||
import { progressiveDeliveryResult } from '../../data/live-progressive-result';
|
||||
@@ -11,6 +12,7 @@ import { liveCodexWorkerResult } from '../../data/live-codex-worker-result';
|
||||
import { liveAnnotatedResult } from '../../data/live-annotated-result';
|
||||
import '../../styles/sub-pages.css';
|
||||
import '../../styles/live-performance.css';
|
||||
import '../../styles/live-lab-workbench.css';
|
||||
|
||||
const reports = 'reports' in benchmarkData ? benchmarkData.reports : [benchmarkData];
|
||||
const plainReports = reports.filter(report => report.benchmark.scenario === 'plain' && report.benchmark.agent === 'fake');
|
||||
@@ -73,40 +75,88 @@ const providerColumns = [
|
||||
<Base
|
||||
title="Live Latency Lab | Impeccable"
|
||||
description="Measured latency for Impeccable Live, from Go to the first usable variant."
|
||||
bodyClass="sub-page live-performance-page"
|
||||
mainClass="live-performance-main"
|
||||
bodyClass="sub-page live-performance-page live-lab-page"
|
||||
mainClass="live-performance-main live-lab-main-shell"
|
||||
noIndex
|
||||
hideHeader
|
||||
hideFooter
|
||||
>
|
||||
<header class="live-lab-header">
|
||||
<a class="live-lab-brand ks-brand" href="/live" aria-label="Back to Live Mode">
|
||||
<span class="ks-mark" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"></path>
|
||||
<path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="ks-wordmark">Impeccable</span>
|
||||
</a>
|
||||
<div>
|
||||
<strong>Live Latency Lab</strong>
|
||||
<span>Development benchmark</span>
|
||||
</div>
|
||||
</header>
|
||||
<article
|
||||
class="live-performance"
|
||||
data-live-performance
|
||||
data-protocol-floor={baselineProductFloor}
|
||||
data-overlap-floor={productFloor}
|
||||
>
|
||||
<header class="live-performance-hero ks-section">
|
||||
<p class="live-performance-kicker">Live latency lab · optimization result</p>
|
||||
<h1>Capture left<br /><span>the critical path.</span></h1>
|
||||
<section class="live-lab-workbench" aria-label="Live performance workbench" data-live-lab-workbench>
|
||||
<aside class="live-lab-nav" aria-label="Live lab navigation">
|
||||
<div class="live-lab-nav-head">
|
||||
<a class="live-lab-brand ks-brand" href="/live" aria-label="Back to Live Mode">
|
||||
<span class="ks-mark" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"></path>
|
||||
<path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="ks-wordmark">Impeccable</span>
|
||||
</a>
|
||||
<h1>Live Lab</h1>
|
||||
<p>Internal performance, harness, and UI state workbench.</p>
|
||||
<dl class="live-lab-summary-grid" aria-label="Live benchmark summary">
|
||||
<div><dt>Product floor</dt><dd>{displayMs(protocolFloor)}</dd></div>
|
||||
<div><dt>Accept clean</dt><dd>{displayMs(liveProviderResult.cleanupControl.acceptToCleanPickingMs)}</dd></div>
|
||||
<div><dt>Worker wake</dt><dd>{displayMs(liveCodexWorkerResult.timings.wakeToTurnStartedMs)}</dd></div>
|
||||
<div><dt>Provider pass</dt><dd>{liveProviderResult.totals.passingRuns}/{liveProviderResult.totals.runs}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<nav class="live-lab-nav-scroll" aria-label="Workbench views">
|
||||
<div class="live-lab-nav-group">
|
||||
<h2>Views</h2>
|
||||
<div class="live-lab-nav-list">
|
||||
<button type="button" data-lab-view="overview" aria-pressed="true"><span>Overview</span><span>5</span></button>
|
||||
<button type="button" data-lab-view="providers" aria-pressed="false"><span>Providers</span><span>2</span></button>
|
||||
<button type="button" data-lab-view="harness" aria-pressed="false"><span>Harnesses</span><span>3</span></button>
|
||||
<button type="button" data-lab-view="ui" aria-pressed="false"><span>Live UI states</span><span>new</span></button>
|
||||
<button type="button" data-lab-view="all" aria-pressed="false"><span>All evidence</span><span>13</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-lab-nav-group">
|
||||
<h2>Actions</h2>
|
||||
<div class="live-lab-action-list">
|
||||
<a href="/live">Open Live docs</a>
|
||||
<a href="/detector">Open Detector Lab</a>
|
||||
<button type="button" data-lab-reset>Reset simulator</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="live-lab-workspace">
|
||||
<header class="live-lab-toolbar">
|
||||
<div>
|
||||
<p class="live-lab-panel-label" data-lab-view-kicker>Runtime overview</p>
|
||||
<h2 data-lab-view-title>Critical path and interaction latency</h2>
|
||||
<p data-lab-view-summary>Model-free product overhead, progressive review, initialization, and the latency simulator.</p>
|
||||
</div>
|
||||
<div class="live-lab-toolbar-actions">
|
||||
<span class="live-lab-status"><i aria-hidden="true"></i> benchmarks loaded</span>
|
||||
<a href="/live">Open Live</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="live-lab-workspace-scroll">
|
||||
<div class="live-lab-content-grid">
|
||||
<article
|
||||
class="live-performance live-lab-canvas"
|
||||
data-live-performance
|
||||
data-protocol-floor={baselineProductFloor}
|
||||
data-overlap-floor={productFloor}
|
||||
>
|
||||
<header class="live-performance-hero ks-section" data-lab-panel="overview">
|
||||
<p class="live-performance-kicker">Latest matched run · first usable variant</p>
|
||||
<h1><span>{displayMs(protocolFloor)}</span> product floor</h1>
|
||||
<p class="live-performance-lede">
|
||||
The same model-free run fell from {displayMs(baselineFloor)} to {displayMs(protocolFloor)}, a{' '}
|
||||
{(improvement * 100).toFixed(1)}% reduction. Live now starts the generate fetch in {displayMs(browserDispatch)} median.
|
||||
Down {(improvement * 100).toFixed(1)}% from {displayMs(baselineFloor)} on the same fixture. Generate dispatch now starts in {displayMs(browserDispatch)} median.
|
||||
</p>
|
||||
<dl class="live-lab-run-stats" aria-label="Latest Live run stages">
|
||||
<div><dt>Browser dispatch</dt><dd>{displayMs(browserDispatch)}</dd></div>
|
||||
<div><dt>Server pickup</dt><dd>{displayMs(serverPickup)}</dd></div>
|
||||
<div><dt>Source scaffold</dt><dd>{displayMs(scaffold)}</dd></div>
|
||||
<div><dt>Write + settle</dt><dd>{displayMs(writeAndRender)}</dd></div>
|
||||
</dl>
|
||||
<div class="live-performance-meta" aria-label="Benchmark context">
|
||||
<span>{plain.summary.count} warm runs</span>
|
||||
<span>{plain.benchmark.fixture}</span>
|
||||
@@ -115,7 +165,7 @@ const providerColumns = [
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="live-performance-section ks-section" aria-labelledby="critical-path-title">
|
||||
<section class="live-performance-section ks-section" aria-labelledby="critical-path-title" data-lab-panel="overview">
|
||||
<div class="live-performance-section-head">
|
||||
<div>
|
||||
<p class="live-performance-label">Click → first usable variant</p>
|
||||
@@ -172,7 +222,7 @@ const providerColumns = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-performance-section ks-section" aria-labelledby="progressive-title">
|
||||
<section class="live-performance-section ks-section" aria-labelledby="progressive-title" data-lab-panel="overview">
|
||||
<div class="live-performance-section-head">
|
||||
<div>
|
||||
<p class="live-performance-label">Progressive delivery · Codex path</p>
|
||||
@@ -212,7 +262,7 @@ const providerColumns = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-performance-section ks-section" aria-labelledby="provider-title">
|
||||
<section class="live-performance-section ks-section" aria-labelledby="provider-title" data-lab-panel="providers">
|
||||
<div class="live-performance-section-head">
|
||||
<div>
|
||||
<p class="live-performance-label">Paid provider matrix · five runs per candidate</p>
|
||||
@@ -255,7 +305,7 @@ const providerColumns = [
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="live-performance-section ks-section" aria-labelledby="init-title">
|
||||
<section class="live-performance-section ks-section" aria-labelledby="init-title" data-lab-panel="overview">
|
||||
<div class="live-performance-section-head">
|
||||
<div>
|
||||
<p class="live-performance-label">Configured cold initialization</p>
|
||||
@@ -286,7 +336,7 @@ const providerColumns = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-performance-section ks-section" aria-labelledby="paths-title">
|
||||
<section class="live-performance-section ks-section" aria-labelledby="paths-title" data-lab-panel="providers">
|
||||
<div class="live-performance-section-head">
|
||||
<div>
|
||||
<p class="live-performance-label">Before / after / control</p>
|
||||
@@ -321,7 +371,7 @@ const providerColumns = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-performance-section live-simulator-section ks-section" aria-labelledby="simulator-title">
|
||||
<section class="live-performance-section live-simulator-section ks-section" aria-labelledby="simulator-title" data-lab-panel="overview">
|
||||
<div class="live-performance-section-head">
|
||||
<div>
|
||||
<p class="live-performance-label">Latency simulator</p>
|
||||
@@ -347,7 +397,11 @@ const providerColumns = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-performance-section ks-section" aria-labelledby="harness-title">
|
||||
<section class="live-lab-ui-panel" data-lab-panel="ui" aria-label="Live UI state gallery">
|
||||
<LiveUiGallery />
|
||||
</section>
|
||||
|
||||
<section class="live-performance-section ks-section" aria-labelledby="harness-title" data-lab-panel="harness">
|
||||
<div class="live-performance-section-head">
|
||||
<div>
|
||||
<p class="live-performance-label">Harness delivery</p>
|
||||
@@ -384,7 +438,7 @@ const providerColumns = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="live-performance-section live-experiments-section ks-section" aria-labelledby="experiments-title">
|
||||
<section class="live-performance-section live-experiments-section ks-section" aria-labelledby="experiments-title" data-lab-panel="harness">
|
||||
<div class="live-performance-section-head">
|
||||
<div>
|
||||
<p class="live-performance-label">Evidence-ranked decisions</p>
|
||||
@@ -409,7 +463,7 @@ const providerColumns = [
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="live-performance-method ks-section" aria-labelledby="method-title">
|
||||
<section class="live-performance-method ks-section" aria-labelledby="method-title" data-lab-panel="harness">
|
||||
<h2 id="method-title">Measurement contract</h2>
|
||||
<p>
|
||||
<code>bun run bench:live</code> boots a real framework fixture and Chromium, drives Pick → Go → Cycle,
|
||||
@@ -418,10 +472,45 @@ const providerColumns = [
|
||||
The deterministic agent makes Impeccable overhead visible; model-backed runs remain opt-in because they send fixture context to an external provider.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
</article>
|
||||
|
||||
<aside class="live-lab-diagnostics" aria-label="Live lab diagnostics">
|
||||
<section class="live-lab-diagnostic-panel">
|
||||
<header><p class="live-lab-panel-label">Current runtime</p><h2>Interaction floor</h2></header>
|
||||
<dl>
|
||||
<div><dt>Go → first</dt><dd>{displayMs(protocolFloor)}</dd></div>
|
||||
<div><dt>Dispatch</dt><dd>{displayMs(browserDispatch)}</dd></div>
|
||||
<div><dt>Scaffold</dt><dd>{displayMs(scaffold)}</dd></div>
|
||||
<div><dt>Accept → Pick</dt><dd>{displayMs(liveControlResult.acceptToPicking.medianMs)}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="live-lab-diagnostic-panel">
|
||||
<header><p class="live-lab-panel-label">Strategy</p><h2>Measured defaults</h2></header>
|
||||
<ul class="live-lab-strategy-list">
|
||||
<li><strong>Codex / OpenAI</strong><span>progressive compact</span></li>
|
||||
<li><strong>Anthropic</strong><span>progressive compact</span></li>
|
||||
<li><strong>Gemini</strong><span>parallel compact</span></li>
|
||||
<li><strong>Unmeasured harness</strong><span>atomic fallback</span></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="live-lab-diagnostic-panel">
|
||||
<header><p class="live-lab-panel-label">Worker path</p><h2>Dedicated Codex turn</h2></header>
|
||||
<dl>
|
||||
<div><dt>Wake</dt><dd>{displayMs(liveCodexWorkerResult.timings.wakeToTurnStartedMs)}</dd></div>
|
||||
<div><dt>Cold setup</dt><dd>{displayMs(liveCodexWorkerResult.timings.coldHandshakeMs + liveCodexWorkerResult.timings.coldThreadStartMs)}</dd></div>
|
||||
</dl>
|
||||
<p>Separate Live-owned thread; never a competing resume of the desktop task.</p>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
import { initLivePerformance } from '../../scripts/live-performance.js';
|
||||
import { initLiveLabWorkbench } from '../../scripts/live-lab-workbench.js';
|
||||
initLivePerformance();
|
||||
initLiveLabWorkbench();
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
const VIEW_COPY = {
|
||||
overview: {
|
||||
kicker: 'Runtime overview',
|
||||
title: 'Critical path and interaction latency',
|
||||
summary: 'Model-free product overhead, progressive review, initialization, and the latency simulator.',
|
||||
},
|
||||
providers: {
|
||||
kicker: 'Generation strategy',
|
||||
title: 'Provider reliability and review latency',
|
||||
summary: 'Five-run strict gates across full, compact, progressive, and parallel delivery.',
|
||||
},
|
||||
harness: {
|
||||
kicker: 'Harness architecture',
|
||||
title: 'Control lanes, worker wake, and tested decisions',
|
||||
summary: 'Foreground fallback, dedicated Codex worker evidence, and the ideas that survived testing.',
|
||||
},
|
||||
ui: {
|
||||
kicker: 'Live chrome gallery',
|
||||
title: 'Every Live UI state without a running session',
|
||||
summary: 'Trigger and compare the real control states in light and dark mode.',
|
||||
},
|
||||
all: {
|
||||
kicker: 'Complete evidence',
|
||||
title: 'All Live measurements and UI fixtures',
|
||||
summary: 'The full workbench in one scrollable surface.',
|
||||
},
|
||||
};
|
||||
|
||||
export function initLiveLabWorkbench(root = document) {
|
||||
const workbench = root.querySelector('[data-live-lab-workbench]');
|
||||
if (!workbench || workbench.dataset.initialized === 'true') return;
|
||||
workbench.dataset.initialized = 'true';
|
||||
|
||||
const buttons = [...workbench.querySelectorAll('[data-lab-view]')];
|
||||
const panels = [...workbench.querySelectorAll('[data-lab-panel]')];
|
||||
const kicker = workbench.querySelector('[data-lab-view-kicker]');
|
||||
const title = workbench.querySelector('[data-lab-view-title]');
|
||||
const summary = workbench.querySelector('[data-lab-view-summary]');
|
||||
const scroller = workbench.querySelector('.live-lab-workspace-scroll');
|
||||
|
||||
const setView = (requested, { updateHash = true } = {}) => {
|
||||
const view = VIEW_COPY[requested] ? requested : 'overview';
|
||||
for (const button of buttons) {
|
||||
button.setAttribute('aria-pressed', String(button.dataset.labView === view));
|
||||
}
|
||||
for (const panel of panels) {
|
||||
panel.hidden = view !== 'all' && panel.dataset.labPanel !== view;
|
||||
}
|
||||
workbench.dataset.activeView = view;
|
||||
if (kicker) kicker.textContent = VIEW_COPY[view].kicker;
|
||||
if (title) title.textContent = VIEW_COPY[view].title;
|
||||
if (summary) summary.textContent = VIEW_COPY[view].summary;
|
||||
if (scroller) scroller.scrollTop = 0;
|
||||
if (updateHash && window.location.hash !== `#${view}`) {
|
||||
window.history.replaceState(null, '', `#${view}`);
|
||||
}
|
||||
};
|
||||
|
||||
for (const button of buttons) {
|
||||
button.addEventListener('click', () => setView(button.dataset.labView));
|
||||
}
|
||||
|
||||
workbench.querySelector('[data-lab-reset]')?.addEventListener('click', () => {
|
||||
const slider = workbench.querySelector('[data-model-latency]');
|
||||
if (!(slider instanceof HTMLInputElement)) return;
|
||||
slider.value = '15000';
|
||||
slider.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
setView('overview');
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', () => setView(window.location.hash.slice(1), { updateHash: false }));
|
||||
setView(window.location.hash.slice(1) || 'overview', { updateHash: false });
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
* Dev-only Impeccable Live state gallery.
|
||||
*
|
||||
* This is a static state harness, not a second implementation of Live. It
|
||||
* mirrors the exact chrome vocabulary and state wording from:
|
||||
* skill/scripts/live-browser.js
|
||||
* skill/scripts/live/ui-core.mjs
|
||||
* Command labels/icons are injected by LiveUiGallery.astro from the canonical
|
||||
* skill/scripts/live/vocabulary.mjs export.
|
||||
*/
|
||||
|
||||
const ICONS = Object.freeze({
|
||||
pick: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>',
|
||||
insert: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg>',
|
||||
detect: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
|
||||
chat: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
||||
voice: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>',
|
||||
submit: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>',
|
||||
tune: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><line x1="4" y1="8" x2="20" y2="8"/><circle cx="14" cy="8" r="2.4" fill="currentColor" stroke="none"/><line x1="4" y1="16" x2="20" y2="16"/><circle cx="10" cy="16" r="2.4" fill="currentColor" stroke="none"/></svg>',
|
||||
edit: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>',
|
||||
trash: '<svg width="12" height="12" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4h8"/><path d="M5 4V3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1"/><path d="M4 4l.5 7a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1L10 4"/></svg>',
|
||||
exit: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>',
|
||||
});
|
||||
|
||||
function parseJson(root, selector, fallback = []) {
|
||||
try {
|
||||
return JSON.parse(root.querySelector(selector)?.textContent || '[]');
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function brandMark() {
|
||||
return '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"/><path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"/></svg>';
|
||||
}
|
||||
|
||||
function designMark() {
|
||||
return '<span class="lvg-design-mark" aria-hidden="true"><i></i><i></i><i></i><i></i></span>';
|
||||
}
|
||||
|
||||
function hostPage({ edited = false, insert = false } = {}) {
|
||||
return `
|
||||
<div class="lvg-host-page${insert ? ' has-insert' : ''}">
|
||||
<p class="lvg-host-kicker">Northstar field journal · Edition 08</p>
|
||||
<article class="lvg-host-target"${edited ? ' data-edited="true"' : ''}>
|
||||
<h4>${edited ? 'Useful observations, refined.' : 'Useful observations from the long way around.'}</h4>
|
||||
<p>Four routes, annotated maps, and practical details for unhurried weekends.</p>
|
||||
</article>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function selectionOutline() {
|
||||
return '<div class="lvg-selection-outline" aria-hidden="true"></div>';
|
||||
}
|
||||
|
||||
function dots({ arrived = 0, visible = 0, expected = 3, clickable = false } = {}) {
|
||||
let html = '<span class="lvg-dots" aria-label="Variant progress">';
|
||||
for (let index = 1; index <= expected; index += 1) {
|
||||
const active = index === visible;
|
||||
const pending = index > arrived;
|
||||
const className = `lvg-dot${active ? ' is-active' : ''}${pending ? ' is-pending' : ''}`;
|
||||
if (clickable && !pending) {
|
||||
const target = index === 1 ? 'cycling-progressive' : 'cycling-second';
|
||||
html += `<button type="button" class="${className}" data-gallery-go="${target}" aria-label="Show variant ${index}"${active ? ' aria-current="true"' : ''}></button>`;
|
||||
} else {
|
||||
html += `<i class="${className}" aria-hidden="true"></i>`;
|
||||
}
|
||||
}
|
||||
return html + '</span>';
|
||||
}
|
||||
|
||||
function configureBar({ actionLabel, count, listening = false, locked = false, insert = false, picker = '' } = {}) {
|
||||
const inputValue = insert
|
||||
? 'Add a compact proof strip'
|
||||
: listening
|
||||
? 'Tighten the hierarchy'
|
||||
: locked
|
||||
? ''
|
||||
: 'Make this feel more confident';
|
||||
const actionControl = insert ? '' : `
|
||||
<button type="button" class="lvg-configure-modifier" data-gallery-go="action-picker" aria-haspopup="listbox" aria-expanded="${picker ? 'true' : 'false'}">
|
||||
${escapeHtml(actionLabel)} <span aria-hidden="true">▾</span>
|
||||
</button>`;
|
||||
return `
|
||||
<div class="lvg-live-context is-configure${picker ? ' has-picker' : ''}"${locked ? ' data-locked="true"' : ''}>
|
||||
<div class="lvg-configure-row">
|
||||
<div class="lvg-configure-input-shell">
|
||||
<button type="button" class="lvg-selection-pill" data-gallery-go="global-ready" aria-label="Clear selection">${insert ? 'slot' : 'article'}</button>
|
||||
<input class="lvg-configure-input" aria-label="${insert ? 'Describe the new element' : 'Describe the change'}" value="${escapeHtml(inputValue)}"${locked ? ' placeholder="apply is running..." disabled' : ''} />
|
||||
</div>
|
||||
<div class="lvg-configure-trailing">
|
||||
<div class="lvg-configure-modifiers">
|
||||
${actionControl}
|
||||
<button type="button" class="lvg-configure-modifier is-count" data-gallery-count aria-label="Change variant count">×${count}</button>
|
||||
</div>
|
||||
<button type="button" class="lvg-configure-voice" data-gallery-go="${listening ? 'configure-replace' : 'configure-listening'}" aria-label="${listening ? 'Stop voice input' : 'Voice input'}" aria-pressed="${listening}">${ICONS.voice}</button>
|
||||
<button type="button" class="lvg-configure-submit" data-gallery-go="generating" aria-label="${insert ? 'Create variants' : 'Generate variants'}">${ICONS.submit}</button>
|
||||
</div>
|
||||
</div>
|
||||
${picker}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function actionPicker(commands, selectedAction) {
|
||||
return `
|
||||
<div class="lvg-action-picker" role="listbox" aria-label="Design action">
|
||||
<div class="lvg-action-grid">
|
||||
${commands.map((command) => `
|
||||
<button type="button" class="lvg-action-chip" role="option" data-gallery-action="${escapeHtml(command.value)}" aria-pressed="${command.value === selectedAction}">
|
||||
<span>${command.icon}</span><span>${escapeHtml(command.label)}</span>
|
||||
</button>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function generatingBar(recovery = false, actionLabel = 'Freeform') {
|
||||
return `
|
||||
<div class="lvg-live-context">
|
||||
<div class="lvg-generation-row">
|
||||
<span class="lvg-generation-label">${escapeHtml(actionLabel)}</span>
|
||||
${dots({ expected: 3 })}
|
||||
<span class="lvg-generation-status">${recovery ? 'Variants ready. Reveal the selected element to resume.' : 'Source ready. Generating...'}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function tuneButton(open) {
|
||||
return `
|
||||
<button type="button" class="lvg-tune-button" data-gallery-go="${open ? 'cycling-second' : 'tune-open'}" aria-expanded="${open}">
|
||||
${ICONS.tune}<span>Tune</span><span class="lvg-tune-badge">3</span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
function cyclingBar({ arrived = 1, visible = 1, tune = false } = {}) {
|
||||
const remaining = 3 - arrived;
|
||||
return `
|
||||
<div class="lvg-live-context${tune ? ' has-tune' : ''}">
|
||||
<div class="lvg-cycling-row">
|
||||
<button type="button" class="lvg-nav-button" data-gallery-go="cycling-progressive" aria-label="Previous variant"${visible <= 1 ? ' disabled' : ''}>←</button>
|
||||
${dots({ arrived, visible, expected: 3, clickable: true })}
|
||||
<span class="lvg-variant-counter">${visible}/3</span>
|
||||
<button type="button" class="lvg-nav-button" data-gallery-go="cycling-second" aria-label="Next variant"${visible >= arrived ? ' disabled' : ''}>→</button>
|
||||
${tuneButton(tune)}
|
||||
<span class="lvg-cycling-spacer"></span>
|
||||
${remaining > 0 ? `<span class="lvg-arrival-progress">${remaining} more arriving...</span>` : ''}
|
||||
<button type="button" class="lvg-accept" data-gallery-go="applying">✓ Accept</button>
|
||||
<button type="button" class="lvg-discard" data-gallery-go="global-ready" aria-label="Discard all variants" title="Discard all variants">✕</button>
|
||||
</div>
|
||||
${tune ? tunePanel() : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function tunePanel() {
|
||||
return `
|
||||
<div class="lvg-tune-panel">
|
||||
<div class="lvg-tune-grid">
|
||||
<div class="lvg-param">
|
||||
<div class="lvg-param-header"><strong>Color amount</strong><output data-gallery-range-output>0.60</output></div>
|
||||
<input type="range" min="0" max="1" step="0.05" value="0.6" data-gallery-range aria-label="Color amount" />
|
||||
</div>
|
||||
<div class="lvg-param">
|
||||
<div class="lvg-param-header"><strong>Density</strong><output data-gallery-density-output>Snug</output></div>
|
||||
<div class="lvg-param-steps" role="group" aria-label="Density">
|
||||
<button type="button" data-gallery-density="Airy" aria-pressed="false">Airy</button>
|
||||
<button type="button" data-gallery-density="Snug" aria-pressed="true">Snug</button>
|
||||
<button type="button" data-gallery-density="Packed" aria-pressed="false">Packed</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lvg-param">
|
||||
<div class="lvg-param-header"><strong>Motion</strong><output data-gallery-toggle-output>Off</output></div>
|
||||
<button type="button" class="lvg-param-toggle" data-gallery-param-toggle aria-label="Motion" aria-pressed="false"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function statusBar(kind) {
|
||||
if (kind === 'confirmed') {
|
||||
return '<div class="lvg-live-context is-confirmed"><div class="lvg-status-row"><span aria-hidden="true">✓</span><span>Variant applied</span></div></div>';
|
||||
}
|
||||
return '<div class="lvg-live-context"><div class="lvg-status-row"><i class="lvg-spinner" aria-hidden="true"></i><span>Applying variant...</span></div></div>';
|
||||
}
|
||||
|
||||
function editBadge(editing = false) {
|
||||
if (!editing) {
|
||||
return `<div class="lvg-edit-badge"><button type="button" class="is-icon" data-gallery-go="edit-copy" aria-label="Edit copy" title="Edit copy">${ICONS.edit}</button></div>`;
|
||||
}
|
||||
return '<div class="lvg-edit-badge"><button type="button" data-gallery-go="configure-replace">Cancel</button><button type="button" class="is-primary" data-gallery-go="copy-pending">Save</button></div>';
|
||||
}
|
||||
|
||||
function annotationLayer() {
|
||||
return `
|
||||
<div class="lvg-annotation-layer">
|
||||
<svg viewBox="0 0 404 150" aria-hidden="true"><path d="M44 106 C82 74, 136 83, 175 105 S267 135, 322 90"/></svg>
|
||||
<button type="button" class="lvg-annotation-clear" data-gallery-go="configure-replace">Clear</button>
|
||||
<div class="lvg-annotation-pin"><span>Keep this line on one row</span></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function pendingDock(kind) {
|
||||
if (kind === 'attention') {
|
||||
return `
|
||||
<div class="lvg-pending-dock">
|
||||
<button type="button" class="lvg-pending-pill" disabled>Apply needs attention</button>
|
||||
<button type="button" class="lvg-pending-decision is-primary" data-gallery-go="copy-applying">Keep fixing</button>
|
||||
<button type="button" class="lvg-pending-decision" data-gallery-go="copy-pending">Rollback</button>
|
||||
</div>`;
|
||||
}
|
||||
const applying = kind === 'applying';
|
||||
return `
|
||||
<div class="lvg-pending-dock">
|
||||
<button type="button" class="lvg-pending-pill" data-gallery-go="${applying ? 'copy-attention' : 'copy-applying'}" aria-busy="${applying}"${applying ? ' disabled' : ''}>
|
||||
${applying ? '<i class="lvg-pending-spinner" aria-hidden="true"></i><span>Applying 3 copy edits</span>' : '<span>Apply copy edits</span><span class="lvg-pending-count">3</span>'}
|
||||
</button>
|
||||
<button type="button" class="lvg-pending-trash" data-gallery-go="global-ready" aria-label="Discard copy edits on this page">${ICONS.trash}</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function designPanel(tab = 'visual') {
|
||||
const raw = tab === 'raw';
|
||||
return `
|
||||
<aside class="lvg-design-panel" aria-label="DESIGN.md panel">
|
||||
<header class="lvg-design-header">
|
||||
<span class="lvg-design-title">DESIGN.md</span>
|
||||
<div class="lvg-design-tabs" role="tablist" aria-label="Design system view">
|
||||
<button type="button" role="tab" data-gallery-design-tab="visual" aria-selected="${!raw}">Visual</button>
|
||||
<button type="button" role="tab" data-gallery-design-tab="raw" aria-selected="${raw}">Raw</button>
|
||||
</div>
|
||||
<button type="button" class="lvg-design-close" data-gallery-go="global-tools" aria-label="Close panel">✕</button>
|
||||
</header>
|
||||
<div class="lvg-design-body">
|
||||
${raw ? `
|
||||
<div class="lvg-design-tile">
|
||||
<div class="lvg-design-meta"><strong>Neo Kinpaku</strong><span>Raw</span></div>
|
||||
<p class="lvg-design-copy"># Design System: Impeccable<br><br>Dark lacquer, kinpaku gold, and precise technical geometry.</p>
|
||||
</div>` : `
|
||||
<div class="lvg-design-tile">
|
||||
<div class="lvg-design-meta"><strong>Kinpaku Gold</strong><span>Primary</span></div>
|
||||
<div class="lvg-design-swatch"></div>
|
||||
<p class="lvg-design-copy">Primary accent for commitment, active controls, and the Impeccable mark.</p>
|
||||
</div>
|
||||
<div class="lvg-design-tile">
|
||||
<div class="lvg-design-meta"><strong>Display</strong><span>Typography</span></div>
|
||||
<p class="lvg-design-type">Useful observations</p>
|
||||
<p class="lvg-design-copy">Alumni Sans · precise, light, and deliberately geometric.</p>
|
||||
</div>`}
|
||||
</div>
|
||||
</aside>`;
|
||||
}
|
||||
|
||||
function globalBar({ connected = true, active = 'pick', steer = 'collapsed', detectCount = 0, designActive = false } = {}) {
|
||||
const modeButton = (key, icon, label, target, extra = '') => {
|
||||
const isActive = active === key || (key === 'design' && designActive);
|
||||
return `<button type="button" class="lvg-global-mode" data-active="${isActive}" data-gallery-go="${target}" aria-label="${escapeHtml(label)}">${icon}${isActive ? `<span>${escapeHtml(label)}</span>` : ''}${extra}</button>`;
|
||||
};
|
||||
const steerHtml = steer === 'processing'
|
||||
? `<div class="lvg-steer" data-expanded="true" data-processing="true" aria-busy="true" aria-label="Processing steer request"><span class="lvg-steer-icon">${ICONS.chat}</span><span class="lvg-steer-dots" aria-hidden="true"><i></i><i></i><i></i></span></div>`
|
||||
: steer === 'expanded'
|
||||
? `<div class="lvg-steer" data-expanded="true"><span class="lvg-steer-icon">${ICONS.chat}</span><input type="text" value="Make the page hierarchy more decisive" data-gallery-steer-input aria-label="Steer the page"/><button type="button" class="lvg-steer-voice" aria-label="Voice input">${ICONS.voice}</button></div>`
|
||||
: `<button type="button" class="lvg-steer" data-gallery-go="steer-expanded" aria-label="Steer the page"><span class="lvg-steer-icon">${ICONS.chat}</span><span class="lvg-steer-hint">Steer</span><span class="lvg-steer-voice">${ICONS.voice}</span></button>`;
|
||||
|
||||
return `
|
||||
<div class="lvg-live-global">
|
||||
<span class="lvg-live-brand" data-connected="${connected}" role="img" aria-label="Impeccable live mode${connected ? '' : ' - agent not polling'}">
|
||||
${brandMark()}${connected ? '' : '<i class="lvg-agent-dot" aria-hidden="true"></i>'}
|
||||
</span>
|
||||
<div class="lvg-global-inner">
|
||||
${modeButton('pick', ICONS.pick, 'Pick', 'configure-replace')}
|
||||
${modeButton('insert', ICONS.insert, 'Insert', 'insert-placeholder')}
|
||||
${modeButton('detect', ICONS.detect, 'Detect', 'global-tools', detectCount ? `<span class="lvg-detect-badge">${detectCount}</span>` : '')}
|
||||
${modeButton('design', designMark(), 'DESIGN.md', 'design-panel')}
|
||||
${steerHtml}
|
||||
<span class="lvg-global-divider" aria-hidden="true"></span>
|
||||
<button type="button" class="lvg-global-exit" data-gallery-go="global-ready" aria-label="Exit live mode" title="Exit live mode">${ICONS.exit}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function sceneFor(state, context) {
|
||||
const actionLabel = context.commands.find((command) => command.value === context.selectedAction)?.label || 'Freeform';
|
||||
const commonGlobal = (opts = {}) => globalBar({ active: 'pick', ...opts });
|
||||
switch (state) {
|
||||
case 'global-disconnected':
|
||||
return hostPage() + '<div class="lvg-agent-tooltip" role="tooltip">Agent disconnected - run live-poll.mjs to connect</div>' + commonGlobal({ connected: false });
|
||||
case 'global-tools':
|
||||
return hostPage() + commonGlobal({ active: 'detect', detectCount: 7, designActive: true });
|
||||
case 'steer-expanded':
|
||||
return hostPage() + commonGlobal({ steer: 'expanded' });
|
||||
case 'steer-processing':
|
||||
return hostPage() + commonGlobal({ steer: 'processing' });
|
||||
case 'configure-replace':
|
||||
return hostPage() + selectionOutline() + editBadge(false) + configureBar({ actionLabel, count: context.count }) + commonGlobal();
|
||||
case 'action-picker':
|
||||
return hostPage() + selectionOutline() + editBadge(false) + configureBar({ actionLabel, count: context.count, picker: actionPicker(context.commands, context.selectedAction) }) + commonGlobal();
|
||||
case 'configure-listening':
|
||||
return hostPage() + selectionOutline() + editBadge(false) + configureBar({ actionLabel, count: context.count, listening: true }) + commonGlobal();
|
||||
case 'configure-locked':
|
||||
return hostPage({ edited: true }) + selectionOutline() + editBadge(false) + configureBar({ actionLabel, count: context.count, locked: true }) + pendingDock('applying') + commonGlobal();
|
||||
case 'annotation':
|
||||
return hostPage() + selectionOutline() + annotationLayer() + configureBar({ actionLabel, count: context.count }) + commonGlobal();
|
||||
case 'insert-placeholder':
|
||||
return hostPage({ insert: true }) + '<div class="lvg-insert-placeholder" aria-label="Insert placeholder"></div>' + configureBar({ count: context.count, insert: true }) + globalBar({ active: 'insert' });
|
||||
case 'generating':
|
||||
return hostPage() + selectionOutline() + generatingBar(false, actionLabel) + commonGlobal();
|
||||
case 'generation-recovery':
|
||||
return hostPage() + generatingBar(true, actionLabel) + commonGlobal();
|
||||
case 'cycling-progressive':
|
||||
return hostPage() + selectionOutline() + cyclingBar({ arrived: 1, visible: 1 }) + commonGlobal();
|
||||
case 'cycling-second':
|
||||
return hostPage() + selectionOutline() + cyclingBar({ arrived: 2, visible: 2 }) + commonGlobal();
|
||||
case 'tune-open':
|
||||
return hostPage() + selectionOutline() + cyclingBar({ arrived: 3, visible: 2, tune: true }) + commonGlobal();
|
||||
case 'applying':
|
||||
return hostPage() + selectionOutline() + statusBar('applying') + commonGlobal();
|
||||
case 'confirmed':
|
||||
return hostPage({ edited: true }) + statusBar('confirmed') + commonGlobal();
|
||||
case 'edit-copy':
|
||||
return hostPage({ edited: true }) + selectionOutline() + editBadge(true) + configureBar({ actionLabel, count: context.count }) + commonGlobal();
|
||||
case 'copy-pending':
|
||||
return hostPage({ edited: true }) + pendingDock('pending') + commonGlobal();
|
||||
case 'copy-applying':
|
||||
return hostPage({ edited: true }) + pendingDock('applying') + commonGlobal();
|
||||
case 'copy-attention':
|
||||
return hostPage({ edited: true }) + pendingDock('attention') + commonGlobal();
|
||||
case 'design-panel':
|
||||
return hostPage() + designPanel(context.designTab) + globalBar({ active: 'design', designActive: true });
|
||||
case 'toast-error':
|
||||
return hostPage() + '<div class="lvg-live-toast" role="alert">No variants were mounted. Please try again.</div>' + commonGlobal();
|
||||
case 'global-ready':
|
||||
default:
|
||||
return hostPage() + commonGlobal();
|
||||
}
|
||||
}
|
||||
|
||||
function initLiveUiGallery(root) {
|
||||
if (!root || root.dataset.galleryReady === 'true') return;
|
||||
root.dataset.galleryReady = 'true';
|
||||
|
||||
const states = parseJson(root, '[data-live-gallery-states]');
|
||||
const commands = parseJson(root, '[data-live-gallery-vocabulary]');
|
||||
if (states.length === 0 || commands.length === 0) return;
|
||||
|
||||
const select = root.querySelector('[data-gallery-state]');
|
||||
const groupReadout = root.querySelector('[data-gallery-state-group]');
|
||||
const labelReadout = root.querySelector('[data-gallery-state-label]');
|
||||
const previews = [...root.querySelectorAll('[data-live-gallery-preview]')];
|
||||
let selectedAction = 'impeccable';
|
||||
let count = 3;
|
||||
let designTab = 'visual';
|
||||
|
||||
const stateIndex = (key) => Math.max(0, states.findIndex((state) => state.key === key));
|
||||
const currentState = () => select?.value || states[0].key;
|
||||
|
||||
function render(nextState = currentState(), { focusSelect = false } = {}) {
|
||||
const state = states[stateIndex(nextState)] || states[0];
|
||||
if (select) select.value = state.key;
|
||||
if (groupReadout) groupReadout.textContent = state.group;
|
||||
if (labelReadout) labelReadout.textContent = state.label;
|
||||
|
||||
root.querySelectorAll('[data-gallery-state-button]').forEach((button) => {
|
||||
const active = button.dataset.galleryStateButton === state.key;
|
||||
button.setAttribute('aria-current', active ? 'true' : 'false');
|
||||
});
|
||||
|
||||
for (const preview of previews) {
|
||||
preview.dataset.galleryState = state.key;
|
||||
preview.setAttribute('aria-label', `${preview.dataset.liveGalleryPreview} host — ${state.label}`);
|
||||
preview.innerHTML = sceneFor(state.key, { commands, selectedAction, count, designTab });
|
||||
}
|
||||
if (focusSelect) select?.focus();
|
||||
}
|
||||
|
||||
function step(delta) {
|
||||
const next = (stateIndex(currentState()) + delta + states.length) % states.length;
|
||||
render(states[next].key, { focusSelect: true });
|
||||
}
|
||||
|
||||
select?.addEventListener('change', () => render(select.value));
|
||||
|
||||
root.addEventListener('click', (event) => {
|
||||
const stepButton = event.target.closest('[data-gallery-step]');
|
||||
if (stepButton) {
|
||||
step(Number(stepButton.dataset.galleryStep) || 1);
|
||||
return;
|
||||
}
|
||||
|
||||
const stateButton = event.target.closest('[data-gallery-state-button]');
|
||||
if (stateButton) {
|
||||
render(stateButton.dataset.galleryStateButton);
|
||||
return;
|
||||
}
|
||||
|
||||
const actionButton = event.target.closest('[data-gallery-action]');
|
||||
if (actionButton) {
|
||||
selectedAction = actionButton.dataset.galleryAction || 'impeccable';
|
||||
render('configure-replace');
|
||||
return;
|
||||
}
|
||||
|
||||
const countButton = event.target.closest('[data-gallery-count]');
|
||||
if (countButton) {
|
||||
count = count >= 4 ? 1 : count + 1;
|
||||
render(currentState());
|
||||
return;
|
||||
}
|
||||
|
||||
const densityButton = event.target.closest('[data-gallery-density]');
|
||||
if (densityButton) {
|
||||
const value = densityButton.dataset.galleryDensity;
|
||||
root.querySelectorAll('[data-gallery-density]').forEach((button) => {
|
||||
button.setAttribute('aria-pressed', button.dataset.galleryDensity === value ? 'true' : 'false');
|
||||
});
|
||||
root.querySelectorAll('[data-gallery-density-output]').forEach((output) => { output.textContent = value; });
|
||||
return;
|
||||
}
|
||||
|
||||
const toggleButton = event.target.closest('[data-gallery-param-toggle]');
|
||||
if (toggleButton) {
|
||||
const next = toggleButton.getAttribute('aria-pressed') !== 'true';
|
||||
root.querySelectorAll('[data-gallery-param-toggle]').forEach((button) => button.setAttribute('aria-pressed', String(next)));
|
||||
root.querySelectorAll('[data-gallery-toggle-output]').forEach((output) => { output.textContent = next ? 'On' : 'Off'; });
|
||||
return;
|
||||
}
|
||||
|
||||
const designTabButton = event.target.closest('[data-gallery-design-tab]');
|
||||
if (designTabButton) {
|
||||
designTab = designTabButton.dataset.galleryDesignTab === 'raw' ? 'raw' : 'visual';
|
||||
render('design-panel');
|
||||
return;
|
||||
}
|
||||
|
||||
const trigger = event.target.closest('[data-gallery-go]');
|
||||
if (trigger && !trigger.disabled) render(trigger.dataset.galleryGo);
|
||||
});
|
||||
|
||||
root.addEventListener('input', (event) => {
|
||||
const range = event.target.closest('[data-gallery-range]');
|
||||
if (!range) return;
|
||||
const value = Number(range.value).toFixed(2);
|
||||
root.querySelectorAll('[data-gallery-range]').forEach((input) => { if (input !== range) input.value = range.value; });
|
||||
root.querySelectorAll('[data-gallery-range-output]').forEach((output) => { output.textContent = value; });
|
||||
});
|
||||
|
||||
root.addEventListener('keydown', (event) => {
|
||||
if (event.target.matches('[data-gallery-steer-input]') && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
render('steer-processing');
|
||||
return;
|
||||
}
|
||||
const panel = event.target.closest('.live-ui-gallery__control-panel');
|
||||
if (!panel || event.target.matches('input')) return;
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
step(-1);
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
step(1);
|
||||
}
|
||||
});
|
||||
|
||||
render(states[0].key);
|
||||
}
|
||||
|
||||
function initLiveUiGalleries() {
|
||||
document.querySelectorAll('[data-live-ui-gallery]').forEach(initLiveUiGallery);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initLiveUiGalleries, { once: true });
|
||||
} else {
|
||||
initLiveUiGalleries();
|
||||
}
|
||||
|
||||
document.addEventListener('astro:page-load', initLiveUiGalleries);
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
/* Live Lab workbench: detector-style navigation, active workspace, diagnostics. */
|
||||
|
||||
.live-lab-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.live-lab-main-shell,
|
||||
.live-lab-workbench,
|
||||
.live-lab-workspace {
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.live-lab-workbench {
|
||||
--live-lab-sidebar: 300px;
|
||||
background: var(--ks-lacquer);
|
||||
}
|
||||
|
||||
.live-lab-nav {
|
||||
position: fixed;
|
||||
inset: 0 auto 0 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--live-lab-sidebar);
|
||||
border-right: 1px solid var(--ks-rule);
|
||||
background: var(--ks-lacquer-deep);
|
||||
}
|
||||
|
||||
.live-lab-nav-head {
|
||||
flex-shrink: 0;
|
||||
padding: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.live-lab-brand {
|
||||
color: var(--ks-kinpaku);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.live-lab-brand .ks-mark { width: 30px; height: 30px; }
|
||||
.live-lab-brand .ks-mark svg { width: 26px; height: 26px; }
|
||||
|
||||
.live-lab-brand .ks-wordmark {
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
|
||||
.live-lab-nav-head h1 {
|
||||
margin: var(--spacing-md) 0 0;
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font);
|
||||
font-size: var(--ks-type-title-size);
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.live-lab-nav-head > p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: var(--ks-type-body-size);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.live-lab-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin: var(--spacing-md) 0 0;
|
||||
border: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.live-lab-summary-grid div {
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border-right: 1px solid var(--ks-rule);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.live-lab-summary-grid div:nth-child(2n) { border-right: 0; }
|
||||
.live-lab-summary-grid div:nth-last-child(-n + 2) { border-bottom: 0; }
|
||||
|
||||
.live-lab-summary-grid dt,
|
||||
.live-lab-panel-label,
|
||||
.live-lab-nav-group h2,
|
||||
.live-lab-diagnostic-panel dt {
|
||||
color: var(--ks-text-muted);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.live-lab-summary-grid dd {
|
||||
margin: 4px 0 0;
|
||||
color: var(--ks-champagne);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.live-lab-nav-scroll {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.live-lab-nav-group {
|
||||
padding: var(--spacing-sm);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.live-lab-nav-group h2 {
|
||||
margin: 0;
|
||||
color: var(--ks-kinpaku-deep);
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
.live-lab-nav-list,
|
||||
.live-lab-action-list {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.live-lab-nav-list button,
|
||||
.live-lab-action-list button,
|
||||
.live-lab-action-list a {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--ks-text-muted);
|
||||
font: 500 0.875rem/1.3 var(--ks-font);
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.live-lab-action-list button,
|
||||
.live-lab-action-list a {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.live-lab-nav-list button:hover,
|
||||
.live-lab-action-list button:hover,
|
||||
.live-lab-action-list a:hover {
|
||||
background: var(--ks-graphite);
|
||||
color: var(--ks-champagne);
|
||||
}
|
||||
|
||||
.live-lab-nav-list button:focus-visible,
|
||||
.live-lab-action-list button:focus-visible,
|
||||
.live-lab-action-list a:focus-visible {
|
||||
outline: 2px solid var(--ks-kinpaku);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.live-lab-nav-list button[aria-pressed='true'] {
|
||||
border-color: var(--ks-kinpaku);
|
||||
background: var(--ks-kinpaku);
|
||||
color: var(--ks-lacquer-deep);
|
||||
}
|
||||
|
||||
.live-lab-nav-list button span:last-child {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.live-lab-workspace {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
margin-left: var(--live-lab-sidebar);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.live-lab-toolbar {
|
||||
position: relative;
|
||||
z-index: 8;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-md);
|
||||
align-items: center;
|
||||
min-height: 80px;
|
||||
padding: 12px var(--spacing-md);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
background: var(--ks-lacquer-deep);
|
||||
}
|
||||
|
||||
.live-lab-toolbar h2,
|
||||
.live-lab-diagnostic-panel h2 {
|
||||
margin: 2px 0 0;
|
||||
color: var(--ks-champagne);
|
||||
font: 600 1rem/1.2 var(--ks-font);
|
||||
}
|
||||
|
||||
.live-lab-toolbar p:not(.live-lab-panel-label) {
|
||||
margin: 4px 0 0;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: var(--ks-type-body-size);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.live-lab-toolbar-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.live-lab-status {
|
||||
display: inline-flex;
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
color: var(--ks-text-muted);
|
||||
font: 0.75rem/1.2 var(--ks-mono);
|
||||
}
|
||||
|
||||
.live-lab-status i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--ks-patina);
|
||||
}
|
||||
|
||||
.live-lab-toolbar-actions a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 38px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid var(--ks-kinpaku);
|
||||
border-radius: 2px;
|
||||
background: var(--ks-kinpaku);
|
||||
color: var(--ks-lacquer-deep);
|
||||
font-size: var(--ks-type-body-size);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.live-lab-workspace-scroll {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.live-lab-content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: start;
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.live-lab-canvas,
|
||||
.live-lab-diagnostic-panel {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--ks-rule);
|
||||
background: var(--ks-lacquer-raised);
|
||||
}
|
||||
|
||||
.live-lab-ui-panel {
|
||||
min-width: 0;
|
||||
padding: clamp(24px, 4vw, 48px);
|
||||
}
|
||||
|
||||
.live-lab-canvas {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.live-lab-canvas [hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.live-lab-canvas .ks-section {
|
||||
width: auto;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding-right: clamp(20px, 3vw, 40px);
|
||||
padding-left: clamp(20px, 3vw, 40px);
|
||||
}
|
||||
|
||||
.live-lab-canvas .live-performance-hero {
|
||||
padding-top: clamp(32px, 5vw, 56px);
|
||||
padding-bottom: clamp(32px, 5vw, 56px);
|
||||
}
|
||||
|
||||
.live-lab-canvas .live-performance-hero::after {
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.live-lab-canvas .live-performance-hero h1 {
|
||||
max-width: 720px;
|
||||
margin-top: 14px;
|
||||
font-family: var(--ks-font);
|
||||
font-size: var(--ks-type-headline-size);
|
||||
font-weight: 600;
|
||||
line-height: var(--ks-type-headline-line);
|
||||
}
|
||||
|
||||
.live-lab-canvas .live-performance-lede {
|
||||
margin-top: 24px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.live-lab-run-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin: 28px 0 0;
|
||||
border: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.live-lab-run-stats div {
|
||||
min-width: 0;
|
||||
padding: var(--spacing-sm);
|
||||
border-right: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.live-lab-run-stats div:last-child { border-right: 0; }
|
||||
|
||||
.live-lab-run-stats dt {
|
||||
color: var(--ks-text-muted);
|
||||
font: 500 var(--ks-type-eyebrow-size)/1.4 var(--ks-mono);
|
||||
letter-spacing: var(--ks-type-eyebrow-track);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.live-lab-run-stats dd {
|
||||
margin: 6px 0 0;
|
||||
color: var(--ks-champagne);
|
||||
font-size: var(--ks-type-title-size);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.live-lab-canvas .live-performance-meta {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.live-lab-canvas .live-performance-section {
|
||||
padding-top: clamp(30px, 4vw, 52px);
|
||||
padding-bottom: clamp(30px, 4vw, 52px);
|
||||
}
|
||||
|
||||
.live-lab-canvas .live-performance-section-head {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 360px);
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.live-lab-canvas .live-performance-section-head h2,
|
||||
.live-lab-canvas .live-performance-method h2 {
|
||||
margin-top: 8px;
|
||||
font-size: clamp(1.65rem, 3vw, 2.6rem);
|
||||
}
|
||||
|
||||
.live-lab-canvas .latency-tape,
|
||||
.live-lab-canvas .scenario-comparison,
|
||||
.live-lab-canvas .harness-table-wrap,
|
||||
.live-lab-canvas .experiment-list,
|
||||
.live-lab-canvas .live-simulator {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.live-lab-canvas .live-performance-finding {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.live-lab-canvas .live-performance-method {
|
||||
gap: 28px;
|
||||
padding-top: 36px;
|
||||
padding-bottom: 36px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.live-lab-diagnostics {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: grid;
|
||||
gap: var(--spacing-sm);
|
||||
max-height: calc(100vh - 106px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.live-lab-diagnostic-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.live-lab-diagnostic-panel header {
|
||||
padding: var(--spacing-sm);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.live-lab-diagnostic-panel dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.live-lab-diagnostic-panel dl div {
|
||||
padding: var(--spacing-sm);
|
||||
border-right: 1px solid var(--ks-rule);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.live-lab-diagnostic-panel dl div:nth-child(2n) { border-right: 0; }
|
||||
|
||||
.live-lab-diagnostic-panel dd {
|
||||
margin: 5px 0 0;
|
||||
color: var(--ks-champagne);
|
||||
font-size: var(--ks-type-title-size);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.live-lab-diagnostic-panel > p {
|
||||
margin: 0;
|
||||
padding: var(--spacing-sm);
|
||||
color: var(--ks-text-muted);
|
||||
font-size: var(--ks-type-body-size);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.live-lab-strategy-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.live-lab-strategy-list li {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 11px var(--spacing-sm);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.live-lab-strategy-list strong {
|
||||
color: var(--ks-champagne);
|
||||
font-size: var(--ks-type-body-size);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.live-lab-strategy-list span {
|
||||
color: var(--ks-patina);
|
||||
font: 0.75rem/1.4 var(--ks-mono);
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.live-lab-workbench { --live-lab-sidebar: 270px; }
|
||||
.live-lab-content-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.live-lab-diagnostics {
|
||||
position: static;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.live-lab-page { overflow: auto; }
|
||||
|
||||
.live-lab-main-shell,
|
||||
.live-lab-workbench,
|
||||
.live-lab-workspace {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.live-lab-nav {
|
||||
position: static;
|
||||
width: auto;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.live-lab-nav-scroll {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.live-lab-workspace { margin-left: 0; }
|
||||
.live-lab-workspace-scroll { overflow: visible; }
|
||||
|
||||
.live-lab-toolbar {
|
||||
position: static;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.live-lab-diagnostics { grid-template-columns: 1fr; }
|
||||
.live-lab-canvas .live-performance-section-head { grid-template-columns: 1fr; }
|
||||
.live-lab-run-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.live-lab-run-stats div:nth-child(2) { border-right: 0; }
|
||||
.live-lab-run-stats div:nth-child(-n + 2) { border-bottom: 1px solid var(--ks-rule); }
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.live-lab-summary-grid,
|
||||
.live-lab-diagnostic-panel dl,
|
||||
.live-lab-nav-scroll,
|
||||
.live-lab-run-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.live-lab-summary-grid div,
|
||||
.live-lab-diagnostic-panel dl div,
|
||||
.live-lab-run-stats div {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.live-lab-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.live-lab-toolbar-actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.live-lab-content-grid {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.live-lab-canvas,
|
||||
.live-lab-diagnostic-panel {
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.live-lab-status i { animation: none; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user