Turn Script Rehearser's Stage character strip into a sidebar (v1.13.0)

Replaces the horizontal avatar strip above the transport bar with a
sidebar next to the script page, reusing Read Aloud's Casting sidebar
classes (.ab-cv-side/.ab-char-item) directly instead of a separate
look. Same collapse-to-avatars control, now shows each character's
line count, and clicking a character scrolls the script to their
first line. Removed the CSS/HTML this replaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-07-06 00:58:04 +02:00
parent 8922496ec9
commit 04531b25f8
6 changed files with 57 additions and 38 deletions

View File

@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
## [1.13.0] — 2026-07-06
### Changed
- **Script Rehearser's Stage character list is now a sidebar**, matching Read Aloud's Casting sidebar look exactly (same `.ab-cv-side`/`.ab-char-item` styling: colored dot avatar, name, line count) instead of the old horizontal avatar strip above the transport bar. It sits alongside the script page, collapses to just the avatar dots via the same chevron control, and clicking a character scrolls the script to their first line.
---
## [1.12.99] — 2026-07-05
### Fixed

View File

@ -1 +1 @@
1.12.99
1.13.0

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.12.99">
<meta name="app-version" content="1.13.0">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,7 +27,7 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.12.99">
<link rel="stylesheet" href="/static/style.css?v=1.13.0">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.12.99"></script>
<script src="/static/loader.js?v=1.13.0"></script>
</body>
</html>

View File

@ -2252,12 +2252,21 @@ function rehStageFontStep(delta) {
}
function rehApplyCastCollapsed() {
// Same collapse pattern as Read Aloud's Casting sidebar (.ab-cv-side.is-collapsed
// shrinks to just the avatar dots); the stage area's grid column narrows to match.
const row = $('reh-cast-row');
const stage = $('reh-stage-area');
if (!row) return;
const collapsed = localStorage.getItem(REH_CAST_COLLAPSED_KEY) === '1';
row.classList.toggle('reh-cast-collapsed', collapsed);
row.classList.toggle('is-collapsed', collapsed);
if (stage) stage.classList.toggle('side-collapsed', collapsed);
const btn = $('reh-cast-toggle');
if (btn) btn.setAttribute('aria-expanded', String(!collapsed));
if (btn) {
btn.setAttribute('aria-expanded', String(!collapsed));
btn.title = collapsed ? 'Expand character list' : 'Collapse to avatars';
const icon = btn.querySelector('.mdi');
if (icon) icon.className = 'mdi ' + (collapsed ? 'mdi-chevron-right' : 'mdi-chevron-left');
}
}
function rehToggleCast() {
const collapsed = localStorage.getItem(REH_CAST_COLLAPSED_KEY) === '1';
@ -2275,18 +2284,27 @@ function buildScriptPage() {
const titleEl = $('reh-page-title');
if (titleEl) titleEl.textContent = $('reh-script-title')?.value.trim() || 'Script';
// Cast strip
// Cast sidebar — same look (.ab-char-item rows) as Read Aloud's Casting sidebar.
const castStrip = $('reh-cast-strip');
if (castStrip) {
const names = Object.keys(rehState.cast);
const lineCountFor = sp => rehState.lines.filter(l => l.speaker === sp && l.type === 'dialog').length;
castStrip.innerHTML = names.map(sp => {
const c = rehState.cast[sp], isMe = c.voice === 'me';
return `<div class="reh-strip-char" title="${escHtml(sp)}${isMe?'me':(c.voice||'no voice')}">
${voiceAvatarHtml(isMe?'':c.voice, c.color, 28)}
<span style="font-size:10px;font-weight:700;color:${c.color};max-width:56px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(sp)}</span>
${isMe?'<span class="mdi mdi-microphone" style="font-size:10px;color:var(--subtext)"></span>':''}
const displayName = (sp === REH_NARRATOR_KEY) ? 'Narrator' : sp;
return `<div class="ab-char-item" data-speaker="${escHtml(sp)}" title="${escHtml(displayName)}${isMe?'me':(c.voice||'no voice')}">
<span class="ab-char-dot" style="background:${c.color}">${escHtml((displayName||'?')[0].toUpperCase())}</span>
<span class="ab-char-name">${escHtml(displayName)}</span>
${isMe?'<span class="mdi mdi-microphone" style="font-size:11px;color:var(--subtext)"></span>':''}
<b class="ab-char-count">${lineCountFor(sp)}</b>
</div>`;
}).join('');
castStrip.querySelectorAll('.ab-char-item').forEach(item => {
item.addEventListener('click', () => {
const line = document.querySelector(`.reh-block[data-speaker="${CSS.escape(item.dataset.speaker)}"]`);
if (line) line.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
});
const lbl = $('reh-cast-toggle-label');
if (lbl) lbl.textContent = `Characters (${names.length})`;
}

View File

@ -293,15 +293,6 @@
<!-- Director's console -->
<div class="reh-console" id="reh-transport-bar">
<div class="reh-cast-row" id="reh-cast-row">
<button class="reh-cast-toggle" id="reh-cast-toggle" title="Show or hide the character list" aria-expanded="true">
<span class="mdi mdi-account-group"></span>
<span id="reh-cast-toggle-label">Characters</span>
<span class="mdi mdi-chevron-down reh-cast-chev"></span>
</button>
<div class="reh-cast-strip" id="reh-cast-strip"></div>
</div>
<div class="reh-console-transport">
<button class="reh-tb-btn" id="reh-tb-prev" title="Previous line"><span class="mdi mdi-skip-previous"></span></button>
<button class="reh-tb-btn reh-tb-play" id="reh-tb-play" title="Play all"><span class="mdi mdi-play"></span></button>
@ -390,7 +381,7 @@
</div>
<!-- Stage -->
<div class="reh-stage-area">
<div class="reh-stage-area" id="reh-stage-area">
<!-- A4 script page -->
<div class="reh-page-wrap">
<div class="reh-a4-page" id="reh-a4-page">
@ -398,6 +389,15 @@
<div class="reh-script-lines" id="reh-script-lines"></div>
</div>
</div>
<!-- Character sidebar — same look as Read Aloud's Casting sidebar -->
<div class="ab-cv-side" id="reh-cast-row">
<div class="ab-cv-side-head">
<span class="ab-cv-side-title" id="reh-cast-toggle-label">Characters</span>
<button class="ab-cv-side-collapse" id="reh-cast-toggle" type="button" title="Collapse to avatars" aria-expanded="true"><span class="mdi mdi-chevron-left"></span></button>
</div>
<div class="ab-cv-chars" id="reh-cast-strip"></div>
</div>
</div>
<!-- TTS now-playing bar -->

View File

@ -4058,9 +4058,6 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.reh-input-row textarea { min-height: 220px; font-family: monospace; font-size: 13px; width: 100%; resize: vertical; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px; color: var(--text); }
.reh-input-side { display: flex; flex-direction: column; gap: 8px; }
/* Phase 2 cast */
.reh-cast-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: var(--panel); border-radius: 8px; flex-wrap: wrap; border: 1px solid var(--border); }
/* ── Cast list: toolbar + Card/List view toggle ───────────────────────────── */
.reh-cast-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.reh-cast-count { font-size: 13px; color: var(--subtext); font-weight: 600; }
@ -4183,8 +4180,6 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.reh-cc-soul-text:focus { outline: none; border-color: var(--accent); }
.reh-cast-strip { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.reh-strip-char { display: flex; flex-direction: column; align-items: center; gap: 2px; cursor: default; }
.reh-tb-btn { width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--border); background: var(--panel); color: var(--text); cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; transition: background .15s; }
.reh-tb-btn:hover { background: var(--border); }
.reh-tb-play { background: var(--accent); color: #fff; border-color: var(--accent); font-size: 18px; }
@ -4194,6 +4189,17 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.reh-tb-progress { height: 100%; background: var(--accent); width: 0%; transition: width .3s ease; }
.reh-tb-label { font-size: 12px; color: var(--subtext); white-space: nowrap; font-variant-numeric: tabular-nums; }
/* Stage: script page + character sidebar (same .ab-cv-side look Read Aloud's
Casting view uses). The sidebar sticks in place while the script scrolls. */
.reh-stage-area { display: flex; gap: 12px; align-items: flex-start; transition: none; }
.reh-stage-area .reh-page-wrap { flex: 1; min-width: 0; }
.reh-stage-area .ab-cv-side {
width: 220px; flex-shrink: 0; position: sticky; top: 12px;
max-height: calc(100vh - 160px);
}
.reh-stage-area.side-collapsed .ab-cv-side { width: 44px; }
.reh-stage-area .ab-cv-side .ab-cv-chars { flex: 1; overflow-y: auto; }
/* A4 page */
.reh-page-wrap { padding: 24px 16px; background: var(--panel); min-height: 60vh; }
.reh-a4-page {
@ -4435,18 +4441,6 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
transition: background .15s;
}
/* Cast strip takes its own full row inside the console */
.reh-console .reh-cast-strip { flex-basis: 100%; order: -1; padding-bottom: 5px; border-bottom: 1px solid var(--border); margin-bottom: 0; }
/* Collapsible character list (Stage) */
.reh-console .reh-cast-row { flex-basis: 100%; order: -1; display: flex; align-items: flex-start; gap: 10px; padding-bottom: 5px; border-bottom: 1px solid var(--border); }
.reh-console .reh-cast-row .reh-cast-strip { flex-basis: auto; order: 0; flex: 1; padding-bottom: 0; border-bottom: none; min-width: 0; }
.reh-cast-toggle { flex-shrink: 0; display: inline-flex; align-items: center; gap: 5px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 16px; background: var(--panel); color: var(--text); font-size: 11px; font-weight: 700; cursor: pointer; white-space: nowrap; height: 28px; }
.reh-cast-toggle:hover { background: var(--border); }
.reh-cast-toggle .reh-cast-chev { transition: transform .2s; font-size: 14px; }
.reh-cast-row.reh-cast-collapsed .reh-cast-strip { display: none; }
.reh-cast-row.reh-cast-collapsed .reh-cast-chev { transform: rotate(-90deg); }
/* Play font-size stepper (+A / -A) */
.reh-fontsize-ctl { display: inline-flex; gap: 2px; }
.reh-fontsize-btn { font-weight: 700; padding: 2px 7px; }