Add tabbed line-numbered overlay editor
Client checks / checks (push) Canceled after 0s
Client dependency and security checks / audit (push) Canceled after 0s

This commit is contained in:
2026-10-25 06:29:01 +01:00
parent 5920055473
commit ed30446136
2 changed files with 32 additions and 1 deletions
+6 -1
View File
@@ -212,6 +212,7 @@
let overlayAutosaveAttemptedSignature = '';
let pendingOverlayDraft: { overlayName: string; overlaySlug: string; overlayHtml: string; overlayCss: string; overlayJavascript: string; overlayEnabled: boolean; overlayDisabledDesign: string; overlayAlertGroupWindowMs: number; overlayAlertGroupSize: number; overlayAlertDisplayMs: number } | null = null;
let overlayDiagnosticsOpen = false;
let overlayEditorTab: 'HTML' | 'CSS' | 'JAVASCRIPT' = 'HTML';
let overlayAutosaveTimer: number | undefined;
let oauthScopes: Record<'streamer' | 'bot', { connected: boolean; missing_scopes: string[]; reauthorization_required: boolean }> = {
streamer: { connected: false, missing_scopes: [], reauthorization_required: false },
@@ -1281,6 +1282,10 @@
return warnings;
}
function overlayLineNumbers(source: string) {
return Array.from({ length: Math.max(1, source.split('\n').length) }, (_, index) => index + 1);
}
function overlayDraftCacheKey(id: string) {
return `trixie.overlay.draft.${id}`;
}
@@ -1577,7 +1582,7 @@
</section>
{/if}
{:else if activeView === 'Overlays'}
<section class="overlay-page panel"><div class="panel-header"><span>OVERLAYS</span><div class="overlay-toolbar"><button class="secondary" onclick={createOverlay}>NEW</button><button class="secondary" onclick={loadOverlays}>REFRESH</button></div></div><div class="overlay-layout"><aside class="overlay-list">{#each overlays as overlay}<button class:active={selectedOverlayId === overlay.id} class="overlay-list-item" onclick={() => selectOverlay(overlay)}><strong>{overlay.name}</strong><span>{overlay.published ? 'PUBLISHED' : 'DRAFT'}</span></button>{/each}{#if !overlays.length}<div class="empty-row">No overlays yet.</div>{/if}</aside><div class="overlay-editor">{#if selectedOverlayId || overlayName}<div class="overlay-fields"><input class="control" bind:value={overlayName} maxlength="100" placeholder="Overlay name" /><input class="control" bind:value={overlaySlug} maxlength="100" placeholder="stable-slug" /><label class="overlay-enabled"><input type="checkbox" bind:checked={overlayEnabled} /> ENABLED</label><input class="control" bind:value={overlayDisabledDesign} maxlength="10000" placeholder="Disabled design" /></div><div class="overlay-tabs"><label class="micro" for="overlay-html">HTML</label><textarea id="overlay-html" class="overlay-code" bind:value={overlayHtml} spellcheck="false"></textarea><label class="micro" for="overlay-css">CSS</label><textarea id="overlay-css" class="overlay-code" bind:value={overlayCss} spellcheck="false"></textarea><label class="micro" for="overlay-javascript">JAVASCRIPT</label><textarea id="overlay-javascript" class="overlay-code" bind:value={overlayJavascript} spellcheck="false"></textarea></div><div class="overlay-preview-label micro">LIVE PREVIEW</div><iframe class="overlay-preview" title="Overlay preview" sandbox="allow-scripts" srcdoc={`<style>${overlayCss}</style>${overlayEnabled ? overlayHtml : `<div>${overlayDisabledDesign}</div>`}\u003cscript\u003e${overlayJavascript}\u003c/script\u003e`}></iframe><div class="overlay-controls"><button class="primary" onclick={saveOverlay} disabled={overlayBusy || !overlayName || !overlaySlug}>SAVE DRAFT</button><button class="secondary" onclick={publishOverlay} disabled={overlayBusy || !selectedOverlayId}>PUBLISH</button><button class="secondary" onclick={undoOverlay} disabled={overlayBusy || !selectedOverlayId}>UNDO PUBLISHED</button><button class="secondary" onclick={copyOverlayUrl} disabled={!overlaySlug}>COPY OBS URL</button></div>{#if overlayError}<div class="auth-error">{overlayError}</div>{/if}{:else}<div class="empty-state"><strong>Select or create an overlay</strong><span>Drafts autosave when explicitly saved; publishing controls the browser source.</span></div>{/if}</div></div></section>
<section class="overlay-page panel"><div class="panel-header"><span>OVERLAYS</span><div class="overlay-toolbar"><button class="secondary" onclick={createOverlay}>NEW</button><button class="secondary" onclick={loadOverlays}>REFRESH</button></div></div><div class="overlay-layout"><aside class="overlay-list">{#each overlays as overlay}<button class:active={selectedOverlayId === overlay.id} class="overlay-list-item" onclick={() => selectOverlay(overlay)}><strong>{overlay.name}</strong><span>{overlay.published ? 'PUBLISHED' : 'DRAFT'}</span></button>{/each}{#if !overlays.length}<div class="empty-row">No overlays yet.</div>{/if}</aside><div class="overlay-editor">{#if selectedOverlayId || overlayName}<div class="overlay-fields"><input class="control" bind:value={overlayName} maxlength="100" placeholder="Overlay name" /><input class="control" bind:value={overlaySlug} maxlength="100" placeholder="stable-slug" /><label class="overlay-enabled"><input type="checkbox" bind:checked={overlayEnabled} /> ENABLED</label><input class="control" bind:value={overlayDisabledDesign} maxlength="10000" placeholder="Disabled design" /></div><div class="overlay-source-tabs" role="tablist" aria-label="Overlay source editors">{#each ['HTML', 'CSS', 'JAVASCRIPT'] as tab}<button class:active={overlayEditorTab === tab} class="tab" role="tab" aria-selected={overlayEditorTab === tab} onclick={() => (overlayEditorTab = tab as typeof overlayEditorTab)}>{tab}</button>{/each}</div>{#if overlayEditorTab === 'HTML'}<div class="overlay-code-wrap"><div class="overlay-line-numbers" aria-hidden="true">{#each overlayLineNumbers(overlayHtml) as line}<span>{line}</span>{/each}</div><textarea id="overlay-html" class="overlay-code" bind:value={overlayHtml} spellcheck="false" aria-label="Overlay HTML source"></textarea></div>{:else if overlayEditorTab === 'CSS'}<div class="overlay-code-wrap"><div class="overlay-line-numbers" aria-hidden="true">{#each overlayLineNumbers(overlayCss) as line}<span>{line}</span>{/each}</div><textarea id="overlay-css" class="overlay-code" bind:value={overlayCss} spellcheck="false" aria-label="Overlay CSS source"></textarea></div>{:else}<div class="overlay-code-wrap"><div class="overlay-line-numbers" aria-hidden="true">{#each overlayLineNumbers(overlayJavascript) as line}<span>{line}</span>{/each}</div><textarea id="overlay-javascript" class="overlay-code" bind:value={overlayJavascript} spellcheck="false" aria-label="Overlay JavaScript source"></textarea></div>{/if}<div class="overlay-preview-label micro">LIVE PREVIEW</div><iframe class="overlay-preview" title="Overlay preview" sandbox="allow-scripts" srcdoc={`<style>${overlayCss}</style>${overlayEnabled ? overlayHtml : `<div>${overlayDisabledDesign}</div>`}\u003cscript\u003e${overlayJavascript}\u003c/script\u003e`}></iframe><div class="overlay-controls"><button class="primary" onclick={saveOverlay} disabled={overlayBusy || !overlayName || !overlaySlug}>SAVE DRAFT</button><button class="secondary" onclick={publishOverlay} disabled={overlayBusy || !selectedOverlayId}>PUBLISH</button><button class="secondary" onclick={undoOverlay} disabled={overlayBusy || !selectedOverlayId}>UNDO PUBLISHED</button><button class="secondary" onclick={copyOverlayUrl} disabled={!overlaySlug}>COPY OBS URL</button></div>{#if overlayError}<div class="auth-error">{overlayError}</div>{/if}{:else}<div class="empty-state"><strong>Select or create an overlay</strong><span>Drafts autosave when explicitly saved; publishing controls the browser source.</span></div>{/if}</div></div></section>
<section class="panel overlay-alert-settings-panel"><div class="panel-header"><span>ALERT PLAYBACK</span></div><div class="settings-body"><p class="settings-note">Configure grouping and playback for the selected browser-source overlay. Changes are saved with the next draft save.</p><div class="command-form-grid"><label class="micro" for="overlay-alert-window">GROUP WINDOW (MS)</label><input id="overlay-alert-window" class="control" type="number" min="0" max="5000" bind:value={overlayAlertGroupWindowMs} /><label class="micro" for="overlay-alert-size">MAX GROUP SIZE</label><input id="overlay-alert-size" class="control" type="number" min="1" max="20" bind:value={overlayAlertGroupSize} /><label class="micro" for="overlay-alert-display">DISPLAY TIME (MS)</label><input id="overlay-alert-display" class="control" type="number" min="500" max="60000" bind:value={overlayAlertDisplayMs} /></div></div></section>
{:else if activeView === 'Settings'}
<section class="settings-page panel"><div class="panel-header"><span>SERVER SETTINGS</span><button class="secondary" onclick={loadBackups}>REFRESH</button></div><div class="settings-body"><div class="micro">DATABASE BACKUPS</div><p class="settings-note">Daily backups use the server's local schedule. Restore replaces the current database and is permanently destructive.</p><button class="primary control-button" onclick={runBackup} disabled={backupBusy || restoreBusy}>{backupBusy ? 'RUNNING…' : 'RUN BACKUP NOW'}</button>{#if backupError}<div class="auth-error">{backupError}</div>{/if}{#if backups.length}<div class="backup-list">{#each backups as backup}<div class="backup-row"><span class="mono">{backup.filename}</span><span>{Math.round(backup.size_bytes / 1024)} KiB</span><span class="micro">{backup.modified_at ? new Date(backup.modified_at).toLocaleString() : 'UNKNOWN TIME'}</span><button class="danger" onclick={() => restoreBackup(backup.filename)} disabled={backupBusy || restoreBusy}>RESTORE</button></div>{/each}</div>{:else}<div class="empty-row">No backup archives are available.</div>{/if}</div></section>
+26
View File
@@ -662,9 +662,35 @@ label {
display: grid;
gap: 7px;
}
.overlay-source-tabs {
display: flex;
gap: 4px;
border-bottom: 1px solid var(--ui-divider);
}
.overlay-code-wrap {
display: grid;
grid-template-columns: 42px minmax(0, 1fr);
min-height: 190px;
border: 1px solid var(--ui-line);
background: var(--ui-surface-low);
}
.overlay-line-numbers {
display: grid;
align-content: start;
justify-items: end;
gap: 0;
padding: 9px 8px;
color: var(--ed-dim);
background: var(--ui-surface-hi);
font: 12px/1.4 var(--font-data);
user-select: none;
}
.overlay-code {
min-height: 110px;
resize: vertical;
width: 100%;
min-width: 0;
border: 0;
color: var(--ed-dim);
background: var(--ui-surface-low);
font: 12px/1.4 var(--font-data);