dragonflight/services/web-ui/public/styles-rest.css

1417 lines
38 KiB
CSS
Raw Normal View History

/* ========== Projects ========== */
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 14px;
}
.project-card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
cursor: pointer;
transition: transform 120ms, border 80ms;
}
.project-card:hover {
transform: translateY(-2px);
border-color: var(--border-stronger);
}
.project-thumb-grid {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 2px;
aspect-ratio: 16 / 9;
background: var(--bg-2);
}
.project-thumb-grid .project-thumb-cell:first-child { grid-row: span 2; }
.project-thumb-cell { position: relative; overflow: hidden; background: var(--bg-2); }
.project-thumb-cell .thumb-svg { width: 100%; height: 100%; }
.project-card-body { padding: 12px 14px; }
.project-meta {
display: flex; gap: 6px;
font-size: 11.5px; color: var(--text-3);
margin-top: 4px;
}
.project-bar {
margin-top: 10px;
display: flex;
height: 4px;
border-radius: 99px;
overflow: hidden;
background: var(--bg-3);
gap: 1px;
}
/* ========== Upload ========== */
.field-label {
font-size: 10.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-4);
display: block;
margin-bottom: 6px;
}
.select-faux, .field-input {
height: 34px;
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-sm);
padding: 0 10px;
display: flex; align-items: center; gap: 8px;
color: var(--text-1);
font-size: 12.5px;
width: 100%;
cursor: pointer;
}
.field-input { font-size: 12.5px; outline: 0; }
.field-input:focus { border-color: var(--accent); background: var(--bg-2); }
.field-input.select { justify-content: space-between; }
.dropzone {
background: var(--bg-1);
border: 1.5px dashed var(--border-stronger);
border-radius: var(--r-lg);
padding: 40px 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
transition: background 120ms, border 120ms;
}
.dropzone:hover { background: var(--bg-2); border-color: var(--accent); }
.dropzone-formats { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; justify-content: center; }
.upload-row {
display: flex; align-items: center; gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid var(--border);
}
.upload-row:last-child { border-bottom: 0; }
/* ========== Recorders ========== */
.recorders-list { display: flex; flex-direction: column; gap: 10px; }
.recorder-row {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 12px;
display: grid;
grid-template-columns: 220px 1fr 380px auto;
align-items: center;
gap: 16px;
transition: border 80ms;
}
.recorder-row:hover { border-color: var(--border-stronger); }
.recorder-row.recording { border-color: rgba(255,59,48,0.25); }
.recorder-row.error { border-color: rgba(255,91,91,0.25); }
.recorder-preview {
height: 56px;
border-radius: var(--r-sm);
background: var(--bg-2);
overflow: hidden;
position: relative;
}
.recorder-empty {
height: 100%;
display: grid; place-items: center;
color: var(--text-3);
}
.recorder-audio-prev {
height: 100%;
display: flex; align-items: center; gap: 10px;
padding: 0 12px;
}
.recorder-audio-prev .waveform { flex: 1; height: 80%; }
.recorder-info { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.recorder-sub {
font-size: 11.5px;
color: var(--text-3);
display: flex; gap: 6px;
}
.recorder-sub.mono { font-family: var(--font-mono); font-size: 11px; }
.recorder-stats {
display: grid;
grid-template-columns: 90px 90px 1fr;
gap: 12px;
}
.recorder-stat .stat-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
color: var(--text-4);
margin-bottom: 3px;
}
.recorder-stat .stat-val { font-size: 12.5px; }
.recorder-actions { display: flex; align-items: center; gap: 4px; }
.rec-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: currentColor;
}
/* ========== Capture / DeckLink ========== */
.decklink-card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
}
.decklink-head {
padding: 14px 16px;
display: flex; justify-content: space-between; align-items: center;
border-bottom: 1px solid var(--border);
}
.decklink-body {
padding: 24px;
display: grid;
grid-template-columns: 60px 1fr;
gap: 20px;
align-items: stretch;
background:
radial-gradient(ellipse at top, rgba(91,124,250,0.05), transparent 60%),
var(--bg-0);
}
.decklink-card-face {
background: linear-gradient(180deg, #1c1f28, #0d0e13);
border: 1px solid var(--border-strong);
border-radius: 6px;
display: flex; flex-direction: column;
justify-content: space-between;
padding: 12px 6px;
position: relative;
}
.decklink-label {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-family: var(--font-mono);
font-size: 8.5px;
letter-spacing: 0.12em;
color: var(--text-3);
text-align: center;
margin: 0 auto;
}
.decklink-leds {
display: flex; gap: 6px;
justify-content: center;
}
.decklink-led {
width: 6px; height: 6px;
border-radius: 50%;
background: rgba(255,255,255,0.1);
}
.decklink-led.on {
background: var(--success);
box-shadow: 0 0 6px var(--success);
}
.bnc-ports { display: flex; flex-direction: column; gap: 10px; }
.bnc-port {
display: grid;
grid-template-columns: 32px 1fr auto;
align-items: center;
gap: 12px;
padding: 10px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--r-sm);
text-align: left;
cursor: pointer;
position: relative;
transition: background 80ms, border 80ms;
}
.bnc-port:hover { border-color: var(--border-stronger); }
.bnc-port.active { background: var(--bg-3); }
.bnc-port.active.live::before {
content: "";
position: absolute;
left: 0; top: 0; bottom: 0;
width: 2px;
background: var(--success);
}
.bnc-port.recording::before { background: var(--live); }
.bnc-port.active { box-shadow: inset 0 0 0 1px var(--accent); border-color: var(--accent); }
.bnc-connector {
width: 32px; height: 32px;
border-radius: 50%;
background: radial-gradient(circle, #1a1d24, #0a0c11);
display: grid; place-items: center;
border: 1.5px solid var(--border-stronger);
position: relative;
}
.bnc-pin {
width: 6px; height: 6px;
background: linear-gradient(135deg, #6b7280, #2a2f3a);
border-radius: 50%;
}
.bnc-ring {
position: absolute;
inset: 4px;
border: 1px dashed rgba(255,255,255,0.08);
border-radius: 50%;
}
.bnc-port.live .bnc-pin { background: linear-gradient(135deg, #FFC857, #C8862D); box-shadow: 0 0 8px rgba(255,200,87,0.4); }
.bnc-port.recording .bnc-pin { background: linear-gradient(135deg, #FF5C5C, #C8362D); box-shadow: 0 0 8px rgba(255,92,92,0.5); }
.bnc-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bnc-num { font-size: 12.5px; font-weight: 600; }
.bnc-label { font-size: 11px; color: var(--text-2); }
.bnc-sig { font-size: 10.5px; color: var(--text-3); }
.bnc-port.live .bnc-sig { color: var(--success); }
.bnc-rec {
width: 10px; height: 10px;
border-radius: 50%;
background: var(--live);
animation: pulse 1.2s ease-in-out infinite;
}
.bnc-signal-bar {
grid-column: 2 / 4;
height: 3px;
background: var(--bg-3);
border-radius: 99px;
overflow: hidden;
margin-top: 4px;
}
.bnc-signal-fill {
height: 100%;
background: linear-gradient(90deg, var(--success), var(--warning), var(--danger));
background-size: 200% 100%;
background-position: 0% 0;
transition: width 200ms;
}
.decklink-foot {
padding: 10px 16px;
border-top: 1px solid var(--border);
display: flex; gap: 20px;
font-size: 11.5px;
color: var(--text-3);
}
.decklink-foot strong { color: var(--text-1); font-weight: 600; }
.capture-detail {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 16px;
}
.capture-detail.empty {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 80px 24px;
color: var(--text-3);
}
.capture-preview {
position: relative;
aspect-ratio: 16 / 9;
border-radius: var(--r-md);
overflow: hidden;
background: #000;
}
.capture-overlay-meters {
position: absolute;
right: 8px; top: 8px;
display: flex; gap: 4px;
}
.capture-tc {
position: absolute;
bottom: 8px; right: 8px;
background: rgba(0,0,0,0.7);
padding: 3px 8px;
border-radius: 3px;
font-family: var(--font-mono);
font-size: 11px;
color: white;
}
.capture-stats {
margin-top: 16px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.capture-stat {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--r-sm);
padding: 8px 10px;
}
.capture-stat-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
color: var(--text-4);
}
.capture-stat-value { font-size: 13px; margin-top: 2px; }
/* ========== Monitors ========== */
.monitors-grid {
display: grid;
gap: 10px;
background: var(--bg-2);
border-radius: var(--r-lg);
padding: 10px;
}
.monitor-tile {
position: relative;
aspect-ratio: 16 / 9;
background: #000;
border-radius: var(--r-sm);
overflow: hidden;
border: 1px solid var(--border);
display: flex; flex-direction: column;
cursor: pointer;
}
ui: full audit pass (fixes #146, #147, #148, #149, #151, #152, #153, #154, #155) Sweep of 9 web-ui audit findings from tracker #156. Issue #150 (modal codec stubs) deferred per user request. ## #146 sweep em-dashes (186 to 0) - Replace placeholder '—' with '·' across all jsx - Convert ' — ' to ': ' or '. ' in copy where context permits - Comment-only em-dashes converted to ASCII dash - Sweep css files too (16 comments) ## #147 remove glassmorphism + accent gradients - Strip 8 backdrop-filter declarations from styles-screens.css and styles-asset.css. Only legit modal scrim in styles-modal.css remains. - Replace .job-progress-fill gradient with solid var(--accent) - Replace .monitor-tile.audio gradient with flat var(--bg-1) ## #148 extract Jobs inline styles to CSS - Cut 19 inline style={{...}} blocks in screens-jobs.jsx to 1 (dynamic width on progress bar). Live DOM was 487 inline-styled elements due to per-row repetition; now ~0. - Added job-row-kind, job-row-asset, job-row-node, job-row-time, job-row-actions, job-row-status-* utility classes in styles-screens.css ## #149 sidebar IA reorganized - Replace flat NAV_TREE + ADMIN_TREE with NAV_SECTIONS: Workspace / Ingest / Operations / Admin - Move Capture out of Ingest into Operations (it's a live-signal monitor, not an ingest action) - Drop the 0/N capture badge from nav (belongs in topbar) - Add BETA badge to Editor ## #151 redesign Editor 'Coming Soon' bumper - Replace fullscreen glassmorphism + gradient + glow overlay with a flat beta banner across the top of the editor area - New .editor-beta-banner CSS class (flat, accent-soft tint, no blur) ## #152 hide Tokens parody, restore real API token mgmt - New top-level Tokens admin page wraps existing ApiTokensSection - Old parody renamed to TokensParody, accessible at /tokens-parody route - Add window-level df:nav event for cross-component routing ## #153 make Home actually useful - New activity strip below the launcher grid: 'Recording now' tiles for live recorders, 'Last 24 hours' tiles for newly created assets, plus an attention strip when there are failed jobs or errored recorders - Each item is clickable and routes to the relevant screen ## #154 aria-labels on icon-only buttons - Projects + Library grid/list view toggles now have aria-label + title ## #155 page-header pattern - Dashboard now renders a proper .page-header h1 with subtitle + alert badge + cluster status pip - Library toolbar-title promoted to h1 for screen-reader hierarchy - Document Home/Library/Editor full-bleed exceptions in DESIGN.md - Editor's chrome is the beta banner (covered by #151)
2026-05-28 19:50:07 -04:00
.monitor-tile.audio { background: var(--bg-1); }
.monitor-tile-label {
position: absolute;
bottom: 0; left: 0; right: 0;
padding: 8px 10px;
background: linear-gradient(180deg, transparent, rgba(0,0,0,0.8));
display: flex; align-items: center; gap: 6px;
}
.monitor-tile-label .name {
color: white;
font-size: 11.5px;
font-weight: 500;
}
.monitor-tile-label .time {
margin-left: auto;
color: white;
font-family: var(--font-mono);
font-size: 10.5px;
}
/* ========== Jobs ========== */
.jobs-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.job-row {
display: grid;
/* status · Job · Asset · Node · Progress · Time · Priority · Actions
Time needs room for "done May 22 · 2:23 PM · 6h ago"; Progress hosts
ui: full audit pass (fixes #146, #147, #148, #149, #151, #152, #153, #154, #155) Sweep of 9 web-ui audit findings from tracker #156. Issue #150 (modal codec stubs) deferred per user request. ## #146 sweep em-dashes (186 to 0) - Replace placeholder '—' with '·' across all jsx - Convert ' — ' to ': ' or '. ' in copy where context permits - Comment-only em-dashes converted to ASCII dash - Sweep css files too (16 comments) ## #147 remove glassmorphism + accent gradients - Strip 8 backdrop-filter declarations from styles-screens.css and styles-asset.css. Only legit modal scrim in styles-modal.css remains. - Replace .job-progress-fill gradient with solid var(--accent) - Replace .monitor-tile.audio gradient with flat var(--bg-1) ## #148 extract Jobs inline styles to CSS - Cut 19 inline style={{...}} blocks in screens-jobs.jsx to 1 (dynamic width on progress bar). Live DOM was 487 inline-styled elements due to per-row repetition; now ~0. - Added job-row-kind, job-row-asset, job-row-node, job-row-time, job-row-actions, job-row-status-* utility classes in styles-screens.css ## #149 sidebar IA reorganized - Replace flat NAV_TREE + ADMIN_TREE with NAV_SECTIONS: Workspace / Ingest / Operations / Admin - Move Capture out of Ingest into Operations (it's a live-signal monitor, not an ingest action) - Drop the 0/N capture badge from nav (belongs in topbar) - Add BETA badge to Editor ## #151 redesign Editor 'Coming Soon' bumper - Replace fullscreen glassmorphism + gradient + glow overlay with a flat beta banner across the top of the editor area - New .editor-beta-banner CSS class (flat, accent-soft tint, no blur) ## #152 hide Tokens parody, restore real API token mgmt - New top-level Tokens admin page wraps existing ApiTokensSection - Old parody renamed to TokensParody, accessible at /tokens-parody route - Add window-level df:nav event for cross-component routing ## #153 make Home actually useful - New activity strip below the launcher grid: 'Recording now' tiles for live recorders, 'Last 24 hours' tiles for newly created assets, plus an attention strip when there are failed jobs or errored recorders - Each item is clickable and routes to the relevant screen ## #154 aria-labels on icon-only buttons - Projects + Library grid/list view toggles now have aria-label + title ## #155 page-header pattern - Dashboard now renders a proper .page-header h1 with subtitle + alert badge + cluster status pip - Library toolbar-title promoted to h1 for screen-reader hierarchy - Document Home/Library/Editor full-bleed exceptions in DESIGN.md - Editor's chrome is the beta banner (covered by #151)
2026-05-28 19:50:07 -04:00
the bar + percent; Node is just "primary" or "-" so it can be tight. */
grid-template-columns: 20px 110px 1fr 60px 240px 240px 70px 90px;
align-items: center;
gap: 12px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
font-size: 12.5px;
}
.job-row:last-child { border-bottom: 0; }
.job-row.head {
font-size: 10.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-4);
}
.job-progress-wrap {
display: flex; align-items: center; gap: 8px;
}
.job-progress-bar {
flex: 1;
height: 5px;
background: var(--bg-3);
border-radius: 99px;
overflow: hidden;
position: relative;
}
.job-progress-fill {
height: 100%;
ui: full audit pass (fixes #146, #147, #148, #149, #151, #152, #153, #154, #155) Sweep of 9 web-ui audit findings from tracker #156. Issue #150 (modal codec stubs) deferred per user request. ## #146 sweep em-dashes (186 to 0) - Replace placeholder '—' with '·' across all jsx - Convert ' — ' to ': ' or '. ' in copy where context permits - Comment-only em-dashes converted to ASCII dash - Sweep css files too (16 comments) ## #147 remove glassmorphism + accent gradients - Strip 8 backdrop-filter declarations from styles-screens.css and styles-asset.css. Only legit modal scrim in styles-modal.css remains. - Replace .job-progress-fill gradient with solid var(--accent) - Replace .monitor-tile.audio gradient with flat var(--bg-1) ## #148 extract Jobs inline styles to CSS - Cut 19 inline style={{...}} blocks in screens-jobs.jsx to 1 (dynamic width on progress bar). Live DOM was 487 inline-styled elements due to per-row repetition; now ~0. - Added job-row-kind, job-row-asset, job-row-node, job-row-time, job-row-actions, job-row-status-* utility classes in styles-screens.css ## #149 sidebar IA reorganized - Replace flat NAV_TREE + ADMIN_TREE with NAV_SECTIONS: Workspace / Ingest / Operations / Admin - Move Capture out of Ingest into Operations (it's a live-signal monitor, not an ingest action) - Drop the 0/N capture badge from nav (belongs in topbar) - Add BETA badge to Editor ## #151 redesign Editor 'Coming Soon' bumper - Replace fullscreen glassmorphism + gradient + glow overlay with a flat beta banner across the top of the editor area - New .editor-beta-banner CSS class (flat, accent-soft tint, no blur) ## #152 hide Tokens parody, restore real API token mgmt - New top-level Tokens admin page wraps existing ApiTokensSection - Old parody renamed to TokensParody, accessible at /tokens-parody route - Add window-level df:nav event for cross-component routing ## #153 make Home actually useful - New activity strip below the launcher grid: 'Recording now' tiles for live recorders, 'Last 24 hours' tiles for newly created assets, plus an attention strip when there are failed jobs or errored recorders - Each item is clickable and routes to the relevant screen ## #154 aria-labels on icon-only buttons - Projects + Library grid/list view toggles now have aria-label + title ## #155 page-header pattern - Dashboard now renders a proper .page-header h1 with subtitle + alert badge + cluster status pip - Library toolbar-title promoted to h1 for screen-reader hierarchy - Document Home/Library/Editor full-bleed exceptions in DESIGN.md - Editor's chrome is the beta banner (covered by #151)
2026-05-28 19:50:07 -04:00
background: var(--accent);
background-size: 200% 100%;
animation: shimmer 2s linear infinite;
transition: width 300ms;
}
/* ========== Editor ========== */
.editor-shell {
display: flex; flex-direction: column;
ui: full audit pass (fixes #146, #147, #148, #149, #151, #152, #153, #154, #155) Sweep of 9 web-ui audit findings from tracker #156. Issue #150 (modal codec stubs) deferred per user request. ## #146 sweep em-dashes (186 to 0) - Replace placeholder '—' with '·' across all jsx - Convert ' — ' to ': ' or '. ' in copy where context permits - Comment-only em-dashes converted to ASCII dash - Sweep css files too (16 comments) ## #147 remove glassmorphism + accent gradients - Strip 8 backdrop-filter declarations from styles-screens.css and styles-asset.css. Only legit modal scrim in styles-modal.css remains. - Replace .job-progress-fill gradient with solid var(--accent) - Replace .monitor-tile.audio gradient with flat var(--bg-1) ## #148 extract Jobs inline styles to CSS - Cut 19 inline style={{...}} blocks in screens-jobs.jsx to 1 (dynamic width on progress bar). Live DOM was 487 inline-styled elements due to per-row repetition; now ~0. - Added job-row-kind, job-row-asset, job-row-node, job-row-time, job-row-actions, job-row-status-* utility classes in styles-screens.css ## #149 sidebar IA reorganized - Replace flat NAV_TREE + ADMIN_TREE with NAV_SECTIONS: Workspace / Ingest / Operations / Admin - Move Capture out of Ingest into Operations (it's a live-signal monitor, not an ingest action) - Drop the 0/N capture badge from nav (belongs in topbar) - Add BETA badge to Editor ## #151 redesign Editor 'Coming Soon' bumper - Replace fullscreen glassmorphism + gradient + glow overlay with a flat beta banner across the top of the editor area - New .editor-beta-banner CSS class (flat, accent-soft tint, no blur) ## #152 hide Tokens parody, restore real API token mgmt - New top-level Tokens admin page wraps existing ApiTokensSection - Old parody renamed to TokensParody, accessible at /tokens-parody route - Add window-level df:nav event for cross-component routing ## #153 make Home actually useful - New activity strip below the launcher grid: 'Recording now' tiles for live recorders, 'Last 24 hours' tiles for newly created assets, plus an attention strip when there are failed jobs or errored recorders - Each item is clickable and routes to the relevant screen ## #154 aria-labels on icon-only buttons - Projects + Library grid/list view toggles now have aria-label + title ## #155 page-header pattern - Dashboard now renders a proper .page-header h1 with subtitle + alert badge + cluster status pip - Library toolbar-title promoted to h1 for screen-reader hierarchy - Document Home/Library/Editor full-bleed exceptions in DESIGN.md - Editor's chrome is the beta banner (covered by #151)
2026-05-28 19:50:07 -04:00
flex: 1; /* parent is `.main` (flex col) - fill remaining vertical space */
chore: 1.2 ship-prep sweep — close 38 issues Frontend / UX / a11y - Sidebar collapse/expand toggle with localStorage persistence (#142) - Settings sections wrap inputs in <form> with Enter-to-submit + native validation; password autocomplete=new-password (#141, #138) - Asset thumbnails get descriptive alt text (#140) - Production deploy now precompiles JSX via esbuild and loads the production React UMD instead of dev builds + in-browser Babel (#139, #122) - Search wrapper gets role=search; global search input gets aria-label, role=combobox, aria-controls/aria-expanded/aria-activedescendant wiring (#137, #135) - Dashboard and Library no longer share the same nav icon (#136) - Sidebar collapses off-canvas with a topbar menu button below 768 px; mobile default is collapsed (#134) - --text-3 bumped to #8B92A0 for WCAG AA contrast on --bg-0 (#133) - Schedule and Library routes were rendering empty inside the .main flex container — switched to flex:1 + min-height:0 (#131, #132, editor + asset detail get the same fix) - Jobs nav badge now polls /jobs?status=active every 10 s and reflects the live count (#130, #113) - aria-label sweep on every icon-only button (#126) - Premiere panel release list moved to window.PREMIERE_RELEASES in data.jsx; Editor + Settings read from the same source (#125) - Typo setPgMclips → setPgmClips (#124) - Stray console.error / console.warn calls gated behind window.DF_LOG.{warn,error} (#123) - Hardcoded /api/v1 paths route through window.ZAMPP_API_PREFIX (#115) - Schedule rows no longer crash on null recorder_id (#117) - EditorKeyboard guards against document.activeElement === null (#116) - Unmount-safe timers for PasswordResetModal, Containers, Editor (#111) - Player seek clamps below totalMs, server-side range clamping + uncached 416 on EOF, client-side EOF-stall watchdog (#143) - Duration badge overlap fix on narrow asset cards (#52) Backend / security / reliability - GET /recorders fixed N+1: single LATERAL JOIN for live_asset_id; Docker inspects bounded to actually-recording rows (#121) - Upload disk-storage (multer.diskStorage) streams parts to S3 instead of buffering 500 MB in RAM (#120) - /assets list clamps limit to MAX_LIMIT=500 to prevent OOM (#119) - SDK upload archive listing + post-extract sanitize block zip-slip / tar-slip and symlink escapes (#118) - Migrations track applied state in schema_migrations, run in a transaction, and exit non-zero on failure (#107) - node-agent BMD_COUNT override uses BMD_DEVICE_PREFIX; filesystem detection wins (#109, #127) - GPU_COUNT override now merges with nvidia-smi enrichment (#108) - /cluster/heartbeat requires a node-bound token or admin user; tokens carry bound_hostname (#106) - /recorders/:id/start error responses no longer echo the Docker create payload — env vars stay out of client responses (#105) - /recorders/probe restricts schemes (srt/rtmp/rtsp/udp/rtp), blocks private + loopback hosts for non-admins, denies common service ports (#104) - Scheduler tick guarded by a Postgres advisory lock; pending/running rows claimed via UPDATE...RETURNING + FOR UPDATE SKIP LOCKED to survive multi-node deploys (#103) - UUID validateUuid('id') param middleware on every /:id route (#102) - Error handler scrubs Postgres error messages and 5xx detail (#101) - Graceful SIGTERM/SIGINT shutdown — stops scheduler, drains the HTTP server, ends the pool, 25 s force-exit watchdog (#100) - AMPP sync moved from fire-and-forget to a persisted retry queue (ampp_sync_status / attempts / next_attempt_at + scheduler retry loop with exponential backoff) (#77) Migrations - 019: api_tokens.bound_hostname (#106) - 020: assets.ampp_sync_status + retry bookkeeping (#77) Other - Defer #92 Growing-files per-upload toggle, #80 Audio tab, #57 Dashboard redesign, #56 Editor SPA polish phase 3, #114 S3 migration tool to v1.3
2026-05-26 22:06:14 -04:00
min-height: 0;
background: var(--bg-0);
}
.editor-topbar {
height: 44px;
border-bottom: 1px solid var(--border);
padding: 0 14px;
display: flex; align-items: center; gap: 10px;
}
.editor-body {
flex: 1;
display: grid;
grid-template-columns: 240px 1fr 240px;
min-height: 0;
}
.editor-bins, .editor-insp { background: var(--bg-1); border-right: 1px solid var(--border); overflow-y: auto; }
.editor-insp { border-right: 0; border-left: 1px solid var(--border); }
.editor-bin-item {
display: flex; align-items: center; gap: 8px;
padding: 6px;
border-radius: var(--r-sm);
cursor: pointer;
}
.editor-bin-item:hover { background: var(--hover); }
.editor-bin-thumb { width: 48px; aspect-ratio: 16/9; border-radius: 3px; overflow: hidden; background: var(--bg-2); flex-shrink: 0; }
.editor-viewer { background: #000; display: flex; flex-direction: column; }
.editor-canvas {
flex: 1;
position: relative;
background: #050608;
display: grid; place-items: center;
overflow: hidden;
max-height: 50vh;
}
.editor-canvas .thumb-svg { width: 100%; height: 100%; }
.editor-transport {
height: 44px;
display: flex; align-items: center; gap: 8px;
padding: 0 14px;
background: var(--bg-1);
border-top: 1px solid var(--border);
}
.editor-timeline {
background: var(--bg-0);
border-top: 1px solid var(--border);
height: 240px;
display: flex; flex-direction: column;
position: relative;
overflow: hidden;
}
.editor-timeline-head {
height: 32px;
display: flex; align-items: center;
padding: 0 12px;
gap: 8px;
border-bottom: 1px solid var(--border);
}
.timeline-ruler {
height: 22px;
display: flex;
padding-left: 40px;
border-bottom: 1px solid var(--border);
background: var(--bg-1);
}
.ruler-tick {
flex: 1;
border-left: 1px solid var(--border);
font-size: 9.5px;
color: var(--text-4);
padding: 4px 4px 0;
}
.timeline-track {
display: grid;
grid-template-columns: 40px 1fr;
align-items: stretch;
height: 42px;
border-bottom: 1px solid var(--border);
}
.timeline-track-label {
background: var(--bg-1);
display: grid; place-items: center;
font-size: 10.5px;
font-family: var(--font-mono);
font-weight: 600;
color: var(--text-3);
border-right: 1px solid var(--border);
}
.timeline-track-lane {
position: relative;
background:
repeating-linear-gradient(90deg, transparent 0 49px, rgba(255,255,255,0.02) 49px 50px);
}
.clip {
position: absolute;
top: 4px; bottom: 4px;
border-radius: 3px;
overflow: hidden;
display: flex; align-items: center;
cursor: pointer;
border: 1px solid rgba(0,0,0,0.3);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.15);
}
.clip-label {
position: absolute;
left: 6px; top: 3px;
font-size: 10px;
font-weight: 600;
color: white;
z-index: 2;
text-shadow: 0 1px 1px rgba(0,0,0,0.4);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.clip-film { display: flex; flex: 1; height: 100%; }
.clip-film-cell { flex: 1; overflow: hidden; }
.clip-film-cell .thumb-svg { width: 100%; height: 100%; }
.clip.audio { background: var(--success) !important; }
.clip-wave { width: 100%; height: 100%; padding: 4px; opacity: 0.6; }
.clip-wave .waveform { width: 100%; height: 100%; }
.timeline-playhead {
position: absolute;
top: 32px; bottom: 0;
width: 1.5px;
background: var(--live);
pointer-events: none;
z-index: 5;
}
.timeline-playhead::before {
content: "";
position: absolute;
top: -4px; left: 50%;
width: 10px; height: 10px;
background: var(--live);
transform: translateX(-50%) rotate(45deg);
}
/* ========== Admin tables ========== */
/* ── Row popover menu (Users, etc.) ────────────────────────────────── */
.row-menu {
position: absolute;
right: 0;
top: calc(100% + 4px);
z-index: 20;
min-width: 180px;
background: var(--bg-1);
border: 1px solid var(--border-stronger);
border-radius: 6px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
padding: 4px;
display: flex;
flex-direction: column;
}
.row-menu button {
display: flex;
align-items: center;
gap: 8px;
background: transparent;
border: 0;
color: var(--text-1);
font-size: 12.5px;
padding: 7px 10px;
border-radius: 4px;
cursor: pointer;
text-align: left;
}
.row-menu button:hover { background: var(--bg-3); }
.row-menu button.danger:hover { background: var(--danger-soft); color: var(--danger); }
.user-row, .token-row, .container-row, .schedule-row {
display: grid;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
font-size: 12.5px;
}
.user-row { grid-template-columns: 1.5fr 100px 1.5fr 120px 40px; }
.token-row { grid-template-columns: 1.4fr 1.4fr 110px 110px 100px 40px; }
.container-row { grid-template-columns: 1.4fr 1.4fr 140px 140px 100px 1.4fr 110px; }
.schedule-row { grid-template-columns: 1.6fr 1.2fr 1.2fr 90px 110px 110px 150px; }
.user-row.head, .token-row.head, .container-row.head, .schedule-row.head {
font-size: 10.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-4);
}
.user-row:last-child, .token-row:last-child, .container-row:last-child, .schedule-row:last-child { border-bottom: 0; }
.token-row.revoked { opacity: 0.5; }
/* ========== Schedule (EPG timeline) ==========
Broadcast-control-room schedule. Recorders are rows, time is the horizontal
axis. The single scrollable .epg container uses a 2-column / 2-row grid
so the gutter (left) and ruler (top) stay sticky during scroll. */
.epg-page {
display: flex; flex-direction: column;
ui: full audit pass (fixes #146, #147, #148, #149, #151, #152, #153, #154, #155) Sweep of 9 web-ui audit findings from tracker #156. Issue #150 (modal codec stubs) deferred per user request. ## #146 sweep em-dashes (186 to 0) - Replace placeholder '—' with '·' across all jsx - Convert ' — ' to ': ' or '. ' in copy where context permits - Comment-only em-dashes converted to ASCII dash - Sweep css files too (16 comments) ## #147 remove glassmorphism + accent gradients - Strip 8 backdrop-filter declarations from styles-screens.css and styles-asset.css. Only legit modal scrim in styles-modal.css remains. - Replace .job-progress-fill gradient with solid var(--accent) - Replace .monitor-tile.audio gradient with flat var(--bg-1) ## #148 extract Jobs inline styles to CSS - Cut 19 inline style={{...}} blocks in screens-jobs.jsx to 1 (dynamic width on progress bar). Live DOM was 487 inline-styled elements due to per-row repetition; now ~0. - Added job-row-kind, job-row-asset, job-row-node, job-row-time, job-row-actions, job-row-status-* utility classes in styles-screens.css ## #149 sidebar IA reorganized - Replace flat NAV_TREE + ADMIN_TREE with NAV_SECTIONS: Workspace / Ingest / Operations / Admin - Move Capture out of Ingest into Operations (it's a live-signal monitor, not an ingest action) - Drop the 0/N capture badge from nav (belongs in topbar) - Add BETA badge to Editor ## #151 redesign Editor 'Coming Soon' bumper - Replace fullscreen glassmorphism + gradient + glow overlay with a flat beta banner across the top of the editor area - New .editor-beta-banner CSS class (flat, accent-soft tint, no blur) ## #152 hide Tokens parody, restore real API token mgmt - New top-level Tokens admin page wraps existing ApiTokensSection - Old parody renamed to TokensParody, accessible at /tokens-parody route - Add window-level df:nav event for cross-component routing ## #153 make Home actually useful - New activity strip below the launcher grid: 'Recording now' tiles for live recorders, 'Last 24 hours' tiles for newly created assets, plus an attention strip when there are failed jobs or errored recorders - Each item is clickable and routes to the relevant screen ## #154 aria-labels on icon-only buttons - Projects + Library grid/list view toggles now have aria-label + title ## #155 page-header pattern - Dashboard now renders a proper .page-header h1 with subtitle + alert badge + cluster status pip - Library toolbar-title promoted to h1 for screen-reader hierarchy - Document Home/Library/Editor full-bleed exceptions in DESIGN.md - Editor's chrome is the beta banner (covered by #151)
2026-05-28 19:50:07 -04:00
flex: 1; /* parent is `.main` (flex col) - fill remaining vertical space (#132) */
min-height: 0;
--epg-pph: 88px; /* pixels per hour, overridden inline per view */
--epg-row-h: 60px;
--epg-gutter-w: 220px;
--epg-ruler-h: 32px;
}
/* ---- Status strip (always on top of the schedule screen) -------------- */
.epg-status {
padding: 10px 20px 12px;
background: linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
border-bottom: 1px solid var(--border);
}
.epg-status-row {
display: flex; align-items: center; gap: 10px;
min-height: 22px;
font-size: 13px;
}
.epg-status-row.sub { margin-top: 2px; font-size: 12px; color: var(--text-3); }
.epg-status-dot {
width: 8px; height: 8px; border-radius: 50%;
flex-shrink: 0;
}
.epg-status-dot.live {
background: var(--live);
box-shadow: 0 0 0 4px rgba(255, 59, 48, 0.16);
animation: _epg_live_pulse 1.6s ease-out infinite;
}
.epg-status-dot.idle { background: var(--text-4); }
.epg-status-label {
font-weight: 600; letter-spacing: 0.02em;
text-transform: uppercase; font-size: 10.5px;
color: var(--text-2);
}
.epg-status-label.muted { color: var(--text-4); }
.epg-status-active {
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
flex: 1; min-width: 0;
}
.epg-status-pill {
display: inline-flex; align-items: center; gap: 8px;
padding: 2px 10px 2px 0;
background: var(--bg-2);
border: 1px solid var(--border-strong);
border-radius: 6px;
font-size: 12px;
overflow: hidden;
}
.epg-status-pill-bar {
width: 3px; align-self: stretch;
background: var(--text-3);
}
.epg-status-pill-name { font-weight: 600; padding-left: 8px; }
.epg-status-pill-rec { color: var(--text-3); font-size: 11px; padding-left: 8px; border-left: 1px solid var(--border); }
.epg-status-pill-time { color: var(--text-2); font-size: 11px; padding-left: 8px; border-left: 1px solid var(--border); }
.epg-status-next { font-weight: 500; color: var(--text-1); }
.epg-status-next-rec { color: var(--text-3); padding-left: 6px; }
.epg-status-next-time { color: var(--accent-text); padding-left: 6px; }
@keyframes _epg_live_pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 59, 48, 0.55); }
70% { box-shadow: 0 0 0 10px rgba(255, 59, 48, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 59, 48, 0); }
}
/* ---- Toolbar (date nav + view tabs + CTA) ------------------------------ */
.epg-toolbar {
display: flex; align-items: center; gap: 8px;
padding: 10px 20px;
border-bottom: 1px solid var(--border);
background: var(--bg-1);
}
.epg-toolbar .spacer { flex: 1; }
.epg-date {
display: flex; align-items: center; gap: 4px;
}
.epg-date-label {
font-size: 15px; font-weight: 600;
min-width: 240px; padding: 0 6px;
letter-spacing: -0.01em;
}
/* ---- Today: scrollable timeline ---------------------------------------- */
.epg {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: var(--epg-gutter-w) 1fr;
grid-template-rows: var(--epg-ruler-h) 1fr;
overflow: auto;
background: var(--bg-0);
position: relative;
}
.epg-corner {
position: sticky; top: 0; left: 0;
grid-row: 1; grid-column: 1;
z-index: 4;
background: var(--bg-1);
border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border);
display: flex; align-items: center;
padding: 0 14px;
font-size: 11px; color: var(--text-3);
letter-spacing: 0.02em;
}
.epg-gutter {
position: sticky; left: 0;
grid-row: 2; grid-column: 1;
z-index: 2;
background: var(--bg-1);
border-right: 1px solid var(--border);
}
.epg-gutter-rows { display: flex; flex-direction: column; }
.epg-gutter-row {
display: flex; align-items: center; gap: 10px;
height: var(--epg-row-h);
padding: 0 14px;
border-bottom: 1px solid var(--border);
position: relative;
}
.epg-gutter-row:last-child { border-bottom: 0; }
.epg-gutter-status {
width: 8px; height: 8px; border-radius: 50%;
flex-shrink: 0;
background: var(--text-4);
}
.epg-gutter-status.live {
background: var(--live);
box-shadow: 0 0 0 3px rgba(255, 59, 48, 0.18);
}
.epg-gutter-status.err { background: var(--danger); }
.epg-gutter-status.idle { background: var(--text-4); }
.epg-gutter-meta { display: flex; flex-direction: column; min-width: 0; }
.epg-gutter-name {
font-size: 12.5px; font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
color: var(--text-1);
}
.epg-gutter-sub {
display: flex; align-items: center; gap: 6px;
font-size: 10.5px; color: var(--text-3);
letter-spacing: 0.06em; text-transform: uppercase;
}
.epg-gutter-dot {
display: inline-block;
width: 7px; height: 7px; border-radius: 50%;
}
.epg-canvas-head {
position: sticky; top: 0;
grid-row: 1; grid-column: 2;
z-index: 3;
background: var(--bg-1);
border-bottom: 1px solid var(--border);
}
.epg-canvas {
grid-row: 2; grid-column: 2;
position: relative;
background:
ui: full audit pass (fixes #146, #147, #148, #149, #151, #152, #153, #154, #155) Sweep of 9 web-ui audit findings from tracker #156. Issue #150 (modal codec stubs) deferred per user request. ## #146 sweep em-dashes (186 to 0) - Replace placeholder '—' with '·' across all jsx - Convert ' — ' to ': ' or '. ' in copy where context permits - Comment-only em-dashes converted to ASCII dash - Sweep css files too (16 comments) ## #147 remove glassmorphism + accent gradients - Strip 8 backdrop-filter declarations from styles-screens.css and styles-asset.css. Only legit modal scrim in styles-modal.css remains. - Replace .job-progress-fill gradient with solid var(--accent) - Replace .monitor-tile.audio gradient with flat var(--bg-1) ## #148 extract Jobs inline styles to CSS - Cut 19 inline style={{...}} blocks in screens-jobs.jsx to 1 (dynamic width on progress bar). Live DOM was 487 inline-styled elements due to per-row repetition; now ~0. - Added job-row-kind, job-row-asset, job-row-node, job-row-time, job-row-actions, job-row-status-* utility classes in styles-screens.css ## #149 sidebar IA reorganized - Replace flat NAV_TREE + ADMIN_TREE with NAV_SECTIONS: Workspace / Ingest / Operations / Admin - Move Capture out of Ingest into Operations (it's a live-signal monitor, not an ingest action) - Drop the 0/N capture badge from nav (belongs in topbar) - Add BETA badge to Editor ## #151 redesign Editor 'Coming Soon' bumper - Replace fullscreen glassmorphism + gradient + glow overlay with a flat beta banner across the top of the editor area - New .editor-beta-banner CSS class (flat, accent-soft tint, no blur) ## #152 hide Tokens parody, restore real API token mgmt - New top-level Tokens admin page wraps existing ApiTokensSection - Old parody renamed to TokensParody, accessible at /tokens-parody route - Add window-level df:nav event for cross-component routing ## #153 make Home actually useful - New activity strip below the launcher grid: 'Recording now' tiles for live recorders, 'Last 24 hours' tiles for newly created assets, plus an attention strip when there are failed jobs or errored recorders - Each item is clickable and routes to the relevant screen ## #154 aria-labels on icon-only buttons - Projects + Library grid/list view toggles now have aria-label + title ## #155 page-header pattern - Dashboard now renders a proper .page-header h1 with subtitle + alert badge + cluster status pip - Library toolbar-title promoted to h1 for screen-reader hierarchy - Document Home/Library/Editor full-bleed exceptions in DESIGN.md - Editor's chrome is the beta banner (covered by #151)
2026-05-28 19:50:07 -04:00
/* hour-band rhythm - alternating subtle stripe every other hour */
repeating-linear-gradient(
to right,
transparent 0,
transparent var(--epg-pph),
rgba(255,255,255,0.012) var(--epg-pph),
rgba(255,255,255,0.012) calc(var(--epg-pph) * 2)
),
/* hour separator lines every hour */
repeating-linear-gradient(
to right,
var(--border) 0,
var(--border) 1px,
transparent 1px,
transparent var(--epg-pph)
);
}
/* Hour ruler */
.epg-ruler {
position: relative;
height: var(--epg-ruler-h);
}
.epg-ruler-tick {
position: absolute; top: 0; bottom: 0;
padding-left: 8px;
display: flex; align-items: center;
font-size: 10.5px; font-weight: 600;
letter-spacing: 0.04em;
color: var(--text-3);
border-left: 1px solid var(--border);
}
.epg-ruler-tick.end { border-left: 0; }
/* Recorder rows + event blocks */
.epg-rows { display: flex; flex-direction: column; position: relative; }
.epg-row {
position: relative;
height: var(--epg-row-h);
border-bottom: 1px solid var(--border);
cursor: copy;
}
.epg-row:last-child { border-bottom: 0; }
.epg-block {
position: absolute;
top: 8px;
height: calc(var(--epg-row-h) - 16px);
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
display: flex; align-items: stretch;
background: var(--bg-2);
border: 1px solid var(--border-strong);
border-radius: 5px;
font-size: 11.5px;
text-align: left;
color: var(--text-1);
overflow: hidden;
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
transition: background 80ms, border-color 80ms, transform 80ms, box-shadow 80ms;
/* Don't paint text selection while the operator drags the block. */
user-select: none; -webkit-user-select: none;
}
.epg-block:hover {
background: var(--bg-3);
border-color: var(--border-stronger);
transform: translateY(-1px);
z-index: 1;
}
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
.epg-block.dragging {
transform: translateY(-1px);
z-index: 10;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 0 0 1px var(--epg-block-color, var(--accent));
transition: none; /* follow cursor 1:1, no easing during drag */
}
.epg-block-bar {
position: absolute;
left: 0; top: 0; bottom: 0;
width: 4px;
background: var(--epg-block-color, var(--accent));
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
pointer-events: none; /* let the resize handle behind catch pointerdown */
}
.epg-block-body {
flex: 1; min-width: 0;
display: flex; align-items: center; gap: 8px;
padding: 0 10px 0 14px;
cursor: pointer;
overflow: hidden;
}
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
.epg-block.resizable .epg-block-body { cursor: grab; }
.epg-block.dragging .epg-block-body { cursor: grabbing; }
.epg-block-name {
flex: 1; min-width: 0;
font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.epg-block-time {
font-size: 10px; color: var(--text-3);
flex-shrink: 0;
}
feat(schedule): right-click menu + drag-to-resize on EPG event blocks Right-click any event block to open a context menu (Edit, Cancel, Copy schedule ID, Delete) — actions per status mirror the List view so the two surfaces stay in lockstep. Menu is viewport-clamped and dismisses on outside click / scroll, same pattern as the asset menu in the Library. Drag-to-resize works for pending schedules only (the schedules PUT rejects edits to running rows, and terminal statuses are read-only): - Drag the left edge to move the start time - Drag the right edge to move the end time - Drag the body to shift the whole block in time All gestures snap to 15-minute increments to match the new-schedule click snap. Minimum duration is clamped to 5 minutes; the block clamps to the visible day on both edges. While dragging the title shows the preview range ("Start time → end time") and the block lifts with a project-tinted shadow. A short pointer click (< 4px travel) still opens the edit modal — the click and drag share the same pointerdown so the operator never has to know which gesture they made first. Implementation: replaces the <button> block with a <div> hosting three zones (left handle / body / right handle). Pointer events with setPointerCapture so drags survive losing the cursor over the block, and pointerup demotes back to click if travel was below threshold. Optimistic local update on resize, PUT /schedules/:id with just the two changed time fields, refetch to reconcile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:33:57 -04:00
/* Resize handles. 8px-wide invisible-by-default hit zones at each end;
a 2px tinted bar fades in on hover so the operator sees where the
resize affordance is. Hides for non-pending blocks (no .resizable). */
.epg-block-handle {
position: absolute;
top: 0; bottom: 0;
width: 8px;
cursor: ew-resize;
z-index: 3;
}
.epg-block-handle.left { left: 0; }
.epg-block-handle.right { right: 0; }
.epg-block-handle::after {
content: '';
position: absolute; top: 6px; bottom: 6px;
width: 2px;
background: var(--epg-block-color, var(--accent));
opacity: 0;
transition: opacity 80ms;
}
.epg-block-handle.left::after { left: 2px; }
.epg-block-handle.right::after { right: 2px; }
.epg-block-handle:hover::after,
.epg-block.dragging .epg-block-handle::after { opacity: 0.85; }
.epg-block-glyph {
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0;
width: 14px; height: 14px;
border-radius: 3px;
font-size: 9px; font-weight: 700;
font-family: var(--font-mono);
}
.epg-block-glyph.live { color: var(--live); background: rgba(255, 59, 48, 0.16); }
.epg-block-glyph.failed { color: var(--danger); background: var(--danger-soft); }
.epg-block.live {
background: linear-gradient(180deg, rgba(255,59,48,0.16) 0%, rgba(255,59,48,0.08) 100%);
border-color: var(--live);
box-shadow: 0 0 0 1px rgba(255, 59, 48, 0.25);
}
.epg-block.failed {
background: var(--danger-soft);
border-color: var(--danger);
}
.epg-block.failed .epg-block-bar { background: var(--danger); }
.epg-block.past { opacity: 0.55; }
.epg-block.past:hover { opacity: 0.85; }
/* Now-line: vertical hot-red line that ticks across the timeline */
.epg-now {
position: absolute;
top: 0; bottom: 0;
width: 1px;
background: var(--live);
pointer-events: none;
z-index: 2;
box-shadow: 0 0 6px rgba(255, 59, 48, 0.45);
}
.epg-now-pip {
position: absolute;
top: -3px; left: -4px;
width: 9px; height: 9px;
border-radius: 50%;
background: var(--live);
box-shadow: 0 0 6px rgba(255, 59, 48, 0.7);
}
/* ---- Week view: 7 day-sections stacked vertically --------------------- */
.epg-week {
flex: 1; min-height: 0;
overflow: auto;
background: var(--bg-0);
padding: 0 0 24px;
}
.epg-week-day {
border-bottom: 1px solid var(--border);
}
.epg-week-day:last-child { border-bottom: 0; }
.epg-week-day.today { background: linear-gradient(180deg, rgba(91,124,250,0.04) 0%, transparent 80%); }
.epg-week-dayhead {
display: flex; align-items: baseline; gap: 10px;
padding: 10px 20px 6px;
font-size: 12px; font-weight: 600;
color: var(--text-2);
position: sticky; left: 0;
z-index: 1;
}
.epg-week-dayname { letter-spacing: 0.02em; }
.epg-week-todaypip {
font-size: 9.5px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.08em;
color: var(--accent-text);
background: var(--accent-soft);
padding: 1px 6px; border-radius: 99px;
}
.epg-week-row-wrap {
position: relative;
padding: 0 0 8px 20px;
background:
repeating-linear-gradient(
to right,
transparent 0,
transparent var(--epg-pph),
rgba(255,255,255,0.012) var(--epg-pph),
rgba(255,255,255,0.012) calc(var(--epg-pph) * 2)
);
}
/* ---- List view (panel reuses .schedule-row from the row-tables block) - */
.epg-list { padding: 20px; flex: 1; min-height: 0; overflow: auto; }
/* ---- Empty states ----------------------------------------------------- */
.epg-empty {
flex: 1;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 10px; padding: 80px 20px;
color: var(--text-3);
}
.epg-empty-title {
font-size: 15px; font-weight: 600;
color: var(--text-2);
}
.epg-empty-sub {
font-size: 12.5px;
}
/* ========== Cluster ========== */
.cluster-canvas {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
}
.stat-row {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.stat-card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-card .label {
font-size: 10.5px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-4);
display: flex;
align-items: center;
gap: 5px;
}
.stat-card .value {
font-size: 28px;
font-weight: 600;
letter-spacing: -0.02em;
color: var(--text-1);
line-height: 1.1;
}
/* ========== Settings ========== */
.settings-nav {
display: flex; flex-direction: column; gap: 2px;
position: sticky; top: 0;
}
.settings-nav-item {
display: flex; align-items: center; gap: 8px;
padding: 0 10px;
height: 32px;
border-radius: var(--r-sm);
color: var(--text-2);
font-size: 12.5px;
cursor: pointer;
}
.settings-nav-item:hover { background: var(--hover); color: var(--text-1); }
.settings-nav-item.active { background: var(--accent-soft); color: var(--accent-text); }
.settings-card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
}
.settings-card-head {
padding: 16px;
display: flex; align-items: flex-start; gap: 12px;
border-bottom: 1px solid var(--border);
}
.settings-card-icon {
width: 36px; height: 36px;
border-radius: var(--r-sm);
background: var(--accent-soft);
color: var(--accent);
display: grid; place-items: center;
flex-shrink: 0;
}
.settings-card-body { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.field { display: flex; flex-direction: column; gap: 4px; }
.field-input-wrap { display: flex; gap: 6px; align-items: center; }
.checkbox-row {
display: flex; align-items: center; gap: 8px;
font-size: 12.5px; color: var(--text-1);
cursor: pointer;
}
.checkbox-row input { accent-color: var(--accent); }
/* ========== Tokens parody ========== */
.token-hero {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 14px;
margin-bottom: 16px;
}
.token-burn-card, .token-actual-card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 18px 20px;
position: relative;
overflow: hidden;
}
.token-burn-card {
background:
radial-gradient(ellipse at top right, rgba(255,91,91,0.12), transparent 60%),
var(--bg-1);
border-color: rgba(255,91,91,0.18);
}
.token-actual-card {
background:
radial-gradient(ellipse at bottom left, rgba(45,212,168,0.10), transparent 60%),
var(--bg-1);
border-color: rgba(45,212,168,0.18);
}
.token-card-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 700;
color: var(--text-4);
margin-bottom: 8px;
}
.token-counter {
display: flex; align-items: baseline; gap: 12px;
}
.token-flame { font-size: 32px; line-height: 1; }
.token-big {
font-size: 48px;
font-weight: 700;
letter-spacing: -0.03em;
font-variant-numeric: tabular-nums;
background: linear-gradient(180deg, #FF9D6B, #FF5B5B);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
line-height: 1.05;
}
.token-rate {
font-size: 12px;
display: flex; align-items: baseline;
font-family: var(--font-mono);
}
.token-actual-amount {
display: flex; align-items: baseline; gap: 0;
color: var(--success);
margin-bottom: 8px;
}
.token-comparison {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 0 0 12px;
margin-bottom: 16px;
overflow: hidden;
}
.token-compare-chart { padding: 0 16px; }
.token-compare-legend {
display: flex; gap: 24px;
padding: 8px 0 0;
font-size: 12px;
color: var(--text-2);
}
.token-compare-legend .dot {
display: inline-block;
width: 10px; height: 10px;
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
}
.token-grid {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 14px;
}
.token-event {
display: flex; align-items: center; gap: 12px;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
}
.token-event:last-child { border-bottom: 0; }
.token-event.fresh { animation: tokenFresh 800ms ease; }
@keyframes tokenFresh {
0% { background: rgba(255,91,91,0.15); transform: translateX(-4px); }
100% { background: transparent; transform: none; }
}
.token-tiers {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.token-tier {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 16px 14px;
display: flex; flex-direction: column;
gap: 6px;
position: relative;
}
.token-tier.popular {
border-color: var(--accent);
background: linear-gradient(180deg, var(--accent-soft) 0%, var(--bg-1) 40%);
box-shadow: 0 0 24px rgba(91,124,250,0.10);
}
.token-tier-badge {
position: absolute;
top: -8px; right: 12px;
background: var(--accent);
color: white;
font-size: 9.5px;
font-weight: 700;
letter-spacing: 0.06em;
padding: 3px 8px;
border-radius: 99px;
}
.token-tier-name { font-size: 13px; font-weight: 700; letter-spacing: -0.01em; }
.token-tier-desc { font-size: 11px; color: var(--text-3); min-height: 32px; line-height: 1.4; }
.token-tier-price { margin-top: 6px; display: flex; align-items: baseline; }
.token-tier-tokens { font-size: 11px; color: var(--text-3); padding-top: 4px; border-top: 1px solid var(--border); }
.token-footnote {
margin-top: 20px;
padding: 14px 16px;
background: var(--warning-soft);
border: 1px solid rgba(245,166,35,0.2);
border-radius: var(--r-md);
display: flex; gap: 12px;
font-size: 12.5px;
color: var(--text-2);
line-height: 1.55;
}
.token-footnote svg { color: var(--warning); flex-shrink: 0; margin-top: 2px; }
.token-footnote strong { color: var(--warning); }
/* ========== Global Search ========== */
.search-wrap {
position: relative;
flex-shrink: 0;
}
.search-wrap .search {
width: 280px;
}
.search-wrap .search.is-open {
border-color: var(--accent);
background: var(--bg-2);
}
.search-results {
position: absolute;
top: calc(100% + 6px);
right: 0;
width: 440px;
max-width: 90vw;
max-height: 460px;
overflow-y: auto;
background: var(--bg-1);
border: 1px solid var(--border-stronger);
border-radius: var(--r-md);
box-shadow: var(--shadow-pop);
padding: 4px;
z-index: 60;
}
.search-empty {
padding: 18px 12px;
text-align: center;
font-size: 12px;
color: var(--text-3);
}
.search-result {
display: grid;
grid-template-columns: 24px 1fr auto;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: var(--r-sm);
cursor: pointer;
color: var(--text-1);
}
.search-result.active {
background: var(--accent-soft);
}
.search-result-icon {
width: 24px; height: 24px;
border-radius: 5px;
background: var(--bg-3);
color: var(--text-2);
display: grid; place-items: center;
flex-shrink: 0;
}
.search-result.active .search-result-icon {
background: var(--accent-soft-2);
color: var(--accent-text);
}
.search-result-text {
min-width: 0;
display: flex;
flex-direction: column;
gap: 1px;
}
.search-result-label {
font-size: 12.5px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-result-sub {
font-size: 11px;
color: var(--text-3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-result-kind {
font-family: var(--font-mono);
font-size: 9.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-4);
background: var(--bg-2);
border: 1px solid var(--border);
padding: 2px 6px;
border-radius: 99px;
flex-shrink: 0;
}
.search-result-kind.kind-asset { color: #B4C3FF; border-color: rgba(91,124,250,0.25); }
.search-result-kind.kind-project { color: #FFD89B; border-color: rgba(245,166,35,0.25); }
.search-result-kind.kind-recorder { color: #FFAFAF; border-color: rgba(255,91,91,0.25); }
.search-result-kind.kind-job { color: #9EE7D2; border-color: rgba(45,212,168,0.25); }
.search-result-kind.kind-user { color: #D5B8FF; border-color: rgba(181,124,250,0.25); }
.search-result-kind.kind-nav { color: var(--text-2); }
/* ========== Asset right-click menu ========== */
.ctx-menu {
position: fixed;
z-index: 80;
min-width: 220px;
max-width: 280px;
background: var(--bg-1);
border: 1px solid var(--border-stronger);
border-radius: var(--r-md);
box-shadow: var(--shadow-pop);
padding: 4px;
display: flex;
flex-direction: column;
font-size: 12.5px;
}
.ctx-menu .ctx-header {
padding: 8px 10px 6px;
font-size: 11px;
font-weight: 600;
color: var(--text-3);
border-bottom: 1px solid var(--border);
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ctx-menu .ctx-divider {
height: 1px;
background: var(--border);
margin: 4px 2px;
}
.ctx-menu .ctx-section-label {
font-size: 9.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-4);
padding: 6px 10px 4px;
}
.ctx-menu .ctx-empty {
padding: 6px 10px 10px;
font-size: 11.5px;
color: var(--text-3);
font-style: italic;
}
.ctx-menu button {
display: flex;
align-items: center;
gap: 8px;
background: transparent;
border: 0;
color: var(--text-1);
font-size: 12.5px;
padding: 7px 10px;
border-radius: 4px;
cursor: pointer;
text-align: left;
width: 100%;
}
.ctx-menu button svg { color: var(--text-3); flex-shrink: 0; }
.ctx-menu button:hover:not(:disabled) { background: var(--bg-3); }
.ctx-menu button:hover:not(:disabled) svg { color: var(--text-1); }
.ctx-menu button:disabled {
opacity: 0.5;
cursor: default;
}
.ctx-menu button.danger { color: var(--danger); }
.ctx-menu button.danger svg { color: var(--danger); }
.ctx-menu button.danger:hover:not(:disabled) {
background: var(--danger-soft);
}