Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 113 additions & 13 deletions crates/daemon/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3406,20 +3406,82 @@
/* Playbook session clips animate by swapping their leading glyph in JS on
the same 120ms cadence as the TUI. Keep the chip body still: no bounce,
pulse, or transform while a referenced session is active. */
/* Run shimmer: a travelling highlight band sweeps the running lines. Pure CSS
(no per-char DOM rebuild) so it never disturbs the caret while editing. */
/* Run shimmer. The TUI re-styles every character of a pending block with a
brightness taken from a travelling sine wave (crates/cli/src/ui.rs,
`apply_playbook_shimmer`): PLAYBOOK_SHIMMER_SPEED 4.2 rad/s,
PLAYBOOK_SHIMMER_DENSITY 0.18 rad/char, fg blended muted -> text through a
smoothstep so the region rests dim and the crest pops. Same wave here,
painted as a repeating gradient clipped to the glyphs rather than a
per-character DOM rebuild, so the caret, selection and typing are
untouched:
wavelength 2*pi/0.18 = 34.907ch period 2*pi/4.2 = 1.496s
The 17 stops sample smoothstep(0.5 - 0.5*sin(theta)) every 1/16 turn; 0%
and 100% match so the tile repeats seamlessly. `--pb-shim-lag` (set per
line in playbookApplyShimmer) is the negative animation-delay that
reproduces the TUI's single character index running across all pending
lines, keeping the band continuous down the block. */
.playbook-line.is-running {
background-image: linear-gradient(100deg,
transparent 20%,
color-mix(in srgb, var(--accent) 30%, transparent) 38%,
color-mix(in srgb, var(--accent) 30%, transparent) 50%,
transparent 68%);
background-size: 220% 100%;
background-repeat: no-repeat;
border-radius: 3px;
animation: playbook-shimmer 1.6s linear infinite;
--pb-shim-block-bg: transparent;
background-color: transparent;
background-image:
linear-gradient(90deg,
color-mix(in srgb, var(--fg) 50.0%, var(--fg-faint)) 0%,
color-mix(in srgb, var(--fg) 22.7%, var(--fg-faint)) 6.25%,
color-mix(in srgb, var(--fg) 5.8%, var(--fg-faint)) 12.5%,
color-mix(in srgb, var(--fg) 0.4%, var(--fg-faint)) 18.75%,
var(--fg-faint) 25%,
color-mix(in srgb, var(--fg) 0.4%, var(--fg-faint)) 31.25%,
color-mix(in srgb, var(--fg) 5.8%, var(--fg-faint)) 37.5%,
color-mix(in srgb, var(--fg) 22.7%, var(--fg-faint)) 43.75%,
color-mix(in srgb, var(--fg) 50.0%, var(--fg-faint)) 50%,
color-mix(in srgb, var(--fg) 77.3%, var(--fg-faint)) 56.25%,
color-mix(in srgb, var(--fg) 94.2%, var(--fg-faint)) 62.5%,
color-mix(in srgb, var(--fg) 99.6%, var(--fg-faint)) 68.75%,
var(--fg) 75%,
color-mix(in srgb, var(--fg) 99.6%, var(--fg-faint)) 81.25%,
color-mix(in srgb, var(--fg) 94.2%, var(--fg-faint)) 87.5%,
color-mix(in srgb, var(--fg) 77.3%, var(--fg-faint)) 93.75%,
color-mix(in srgb, var(--fg) 50.0%, var(--fg-faint)) 100%),
linear-gradient(var(--pb-shim-block-bg), var(--pb-shim-block-bg));
background-size: 34.907ch 100%, auto;
background-repeat: repeat-x, repeat;
-webkit-background-clip: text, border-box;
background-clip: text, border-box;
-webkit-text-fill-color: transparent;
animation: playbook-shimmer 1.496s linear infinite;
animation-delay: var(--pb-shim-lag, 0s);
}
/* Fenced-code rows paint their own block background. `background-clip: text`
would clip it to the glyphs, so re-issue it as the second (border-box)
layer instead of leaving it on background-color. */
.playbook-line.is-running.is-fenced-code {
--pb-shim-block-bg: color-mix(in srgb, var(--accent-alt) 22%, var(--bg-elev));
}
/* Spans that carry their own background are left alone by the TUI overlay;
keep their colors here too (transparent text fill inherits otherwise). */
.playbook-line.is-running .playbook-clip,
.playbook-line.is-running .playbook-inline-code {
-webkit-text-fill-color: currentColor;
}
/* Defensive: Chrome keeps painting the clipped gradient through a selection,
but an engine that repaints selected glyphs from the text fill colour would
draw them transparent. Pin those back to plain foreground. */
.playbook-line.is-running::selection,
.playbook-line.is-running ::selection {
-webkit-text-fill-color: var(--fg);
}
.playbook-line.is-running { cursor: help; }
@media (prefers-reduced-motion: reduce) {
/* No travelling wave: mark the pending rows with a static tint and leave
their text at full contrast. */
.playbook-line.is-running {
background-image: none;
background-color: color-mix(in srgb, var(--accent) 14%, transparent);
border-radius: 3px;
-webkit-text-fill-color: currentColor;
animation: none;
}
}
.playbook-line.is-settling:not(.is-running) {
background-image: linear-gradient(100deg,
transparent 28%,
Expand All @@ -3431,9 +3493,12 @@
border-radius: 3px;
animation: playbook-settle 300ms ease-out 1 both;
}
/* One wavelength per period: 34.907ch in 1.496s = 23.33 chars/sec, the TUI's
SPEED/DENSITY. Positive x moves the crest along the text, matching the
TUI's crest travelling toward a growing character index. */
@keyframes playbook-shimmer {
from { background-position: 140% 0; }
to { background-position: -40% 0; }
from { background-position-x: 0ch, 0; }
to { background-position-x: 34.907ch, 0; }
}
@keyframes playbook-settle {
0% { background-position: 145% 0; }
Expand Down Expand Up @@ -10449,6 +10514,24 @@ <h2 id="operatorViewTitle"></h2>
// --- run shimmer: toggle .is-running on running lines (CSS animates the wave,
// so it never rebuilds the DOM or disturbs the caret) ---

// TUI wave constants (crates/cli/src/ui.rs PLAYBOOK_SHIMMER_SPEED / _DENSITY).
const PB_SHIM_PERIOD_MS = (2 * Math.PI / 4.2) * 1000; // 1496ms per wavelength
const PB_SHIM_MS_PER_CHAR = (0.18 / 4.2) * 1000; // 42.86ms — 23.33 ch/sec

// The TUI advances ONE character index across every pending line, so the bright
// band runs continuously down a block instead of restarting per row. The CSS
// wave is periodic, so a line whose text starts `charOffset` characters into
// that index is simply the same animation lagged by charOffset / 23.33 seconds.
// `animStartPerf` is when this element's animation began (when .is-running was
// added); `runStartPerf` is the run's own origin, so lines that join a run late
// stay in phase with the ones already shimmering. Result is wrapped into
// (-period, 0] because a positive animation-delay would freeze the first frame.
function playbookShimmerLagMs(animStartPerf, runStartPerf, charOffset) {
const raw = animStartPerf - runStartPerf + charOffset * PB_SHIM_MS_PER_CHAR;
const wrapped = ((raw % PB_SHIM_PERIOD_MS) + PB_SHIM_PERIOD_MS) % PB_SHIM_PERIOD_MS;
return Math.round((wrapped - PB_SHIM_PERIOD_MS) * 10) / 10;
}

function playbookApplyShimmer() {
const id = state.playbook.mountedId;
const lineEls = playbookInputEl.querySelectorAll(":scope > div");
Expand All @@ -10467,15 +10550,32 @@ <h2 id="operatorViewTitle"></h2>
const cleanBlocks = doc && playbookNormalizeClipIds(playbookSerialize()) === doc.saved ? (doc.blocks || []) : null;
settling = cleanBlocks ? playbookSettleLines(playbookSerialize(), settledRefs, cleanBlocks) : null;
}
const runStartPerf = run && Number.isFinite(run.startPerf) ? run.startPerf : 0;
let shimChars = 0;
lineEls.forEach((el, i) => {
const running = !!(shimmer && shimmer.active[i]);
const wasRunning = el.classList.contains("is-running");
el.classList.toggle("is-running", running);
el.classList.toggle("is-settling", !running && !!(settling && settling[i]));
if (running) {
// Only lines that are actually shimmering advance the character index,
// exactly like the TUI overlay's `gidx`.
if (!wasRunning || !Number.isFinite(el.pbShimStartPerf)) el.pbShimStartPerf = performance.now();
const lag = playbookShimmerLagMs(el.pbShimStartPerf, runStartPerf, shimChars);
if (el.pbShimLagMs !== lag) {
el.pbShimLagMs = lag;
el.style.setProperty("--pb-shim-lag", `${lag}ms`);
}
shimChars += el.textContent.length;
const label = shimmer.tooltips[i] || PLAYBOOK_SHIMMER_FALLBACK_TOOLTIP;
el.dataset.tooltip = label;
el.setAttribute("aria-label", label);
} else {
if (wasRunning || el.pbShimLagMs != null) {
el.pbShimStartPerf = null;
el.pbShimLagMs = null;
el.style.removeProperty("--pb-shim-lag");
}
delete el.dataset.tooltip;
el.removeAttribute("aria-label");
const hover = state.playbook.hover;
Expand Down
Loading