Merge pull request 'feat(schedule): EPG stylesheet + impeccable context (PRODUCT / DESIGN.md)' (#23) from polish/schedule-epg-styling into main
This commit is contained in:
commit
5699cff4d0
3 changed files with 533 additions and 89 deletions
143
DESIGN.md
Normal file
143
DESIGN.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# Design system
|
||||
|
||||
Documented from the live tokens in `services/web-ui/public/styles.css` and the patterns used across `screens-*.jsx`. Treat this as the source of truth for shared primitives; pages may override locally but should not invent new color or type scales.
|
||||
|
||||
## Color
|
||||
|
||||
Dark theme only. Tokens live in `:root` in `styles.css`. Tinted neutrals — no `#000`, no `#fff`.
|
||||
|
||||
### Surfaces
|
||||
|
||||
| Token | Hex | Use |
|
||||
|---|---|---|
|
||||
| `--bg-0` | `#0B0D11` | Page background, deepest surface |
|
||||
| `--bg-1` | `#14171E` | Panels, sidebars, primary chrome |
|
||||
| `--bg-2` | `#1B1F27` | Panel headers, hover state |
|
||||
| `--bg-3` | `#232833` | Inputs, raised buttons, badges |
|
||||
| `--bg-4` | `#2D3340` | Strongly raised elements (rare) |
|
||||
|
||||
### Borders + overlays
|
||||
|
||||
| Token | Use |
|
||||
|---|---|
|
||||
| `--border` (rgba white 6%) | Default 1px separator |
|
||||
| `--border-strong` (rgba white 10%) | Emphasized separator |
|
||||
| `--border-stronger` (rgba white 14%) | Hover/active border |
|
||||
| `--hover` (rgba white 4%) | Subtle hover fill |
|
||||
| `--hover-strong` (rgba white 7%) | Stronger hover fill |
|
||||
|
||||
### Text
|
||||
|
||||
| Token | Hex | Use |
|
||||
|---|---|---|
|
||||
| `--text-1` | `#F2F3F6` | Primary content |
|
||||
| `--text-2` | `#A8AEBC` | Secondary content |
|
||||
| `--text-3` | `#6B7280` | Labels, metadata |
|
||||
| `--text-4` | `#4B5260` | Section labels, off-month, disabled |
|
||||
|
||||
### Accent
|
||||
|
||||
`--accent: #5B7CFA` (Frame.io-ish blue). Soft variants `--accent-soft` (14%) and `--accent-soft-2` (22%) for fills. `--accent-text: #B4C3FF` for high-contrast accent text.
|
||||
|
||||
**Restrained strategy.** Accent is the only saturated color in chrome. Status colors below appear only where they reflect actual state. Project colors appear only on project chips.
|
||||
|
||||
### Status
|
||||
|
||||
| Token | Hex | Use |
|
||||
|---|---|---|
|
||||
| `--success` | `#2DD4A8` | Done, healthy, ready |
|
||||
| `--warning` | `#F5A623` | Processing, attention-needed |
|
||||
| `--danger` | `#FF5B5B` | Failed, error |
|
||||
| `--live` | `#FF3B30` | Currently recording (broadcast red, intentionally hotter than `--danger`) |
|
||||
| `--purple` | `#B57CFA` | Editor / dev tags |
|
||||
|
||||
Each has a `*-soft` variant at ~14% for background fills.
|
||||
|
||||
### Project palette
|
||||
|
||||
Six fixed colors cycled by index in `data.jsx` (`PROJECT_COLORS`):
|
||||
|
||||
`#5B7CFA · #2DD4A8 · #FF5B5B · #F5A623 · #B57CFA · #6B7280`
|
||||
|
||||
Used only on the project chip / rail dot. Do not reuse for status meaning.
|
||||
|
||||
## Typography
|
||||
|
||||
- **Sans:** Geist (with `cv11`, `ss01` features enabled). All UI text by default.
|
||||
- **Mono:** Geist Mono. URLs, IDs, timestamps, durations, technical metadata.
|
||||
|
||||
Body scale runs small for density:
|
||||
|
||||
| Size | Use |
|
||||
|---|---|
|
||||
| 10.5px, 600, uppercase, 0.06–0.08em letterspacing | Column heads, section labels |
|
||||
| 11–11.5px | Metadata, secondary rows |
|
||||
| 12–12.5px | Body in lists/tables |
|
||||
| 13px, 500–600 | Row labels, button text |
|
||||
| 14px | Default body |
|
||||
| 15px, 600 | Modal titles, panel heads |
|
||||
| 22–28px, 600+ | Page H1 (`.page-header h1`) |
|
||||
|
||||
Never use `gradient text` (impeccable absolute ban). Emphasis via weight and size only.
|
||||
|
||||
## Layout
|
||||
|
||||
- Sidebar: 232px fixed (`--sidebar-w`).
|
||||
- Topbar: 56px (`--topbar-h`).
|
||||
- Row height: 44px default, 36px compact (`--row-h`, `data-density="compact"`).
|
||||
- Gap unit: 16px default, 12px compact (`--gap`).
|
||||
- Border radius scale: 4 / 6 / 8 / 12 / 16 px (`--r-xs` → `--r-xl`).
|
||||
- Panels (`.panel`): `--bg-1` + 1px `--border` + `--r-lg`. Use for grouped lists, not for every section.
|
||||
- Do NOT nest panels.
|
||||
|
||||
## Shadow
|
||||
|
||||
Two tokens, used sparingly:
|
||||
|
||||
- `--shadow-card`: subtle inset highlight + soft outer. Default for raised inputs.
|
||||
- `--shadow-pop`: modal / popover / context menu.
|
||||
|
||||
No drop shadows on flat surfaces. No glow effects (except: the EPG now-line uses a tight `box-shadow` for the broadcast-red glow, and live status dots have a pulse halo; these are state cues, not decoration).
|
||||
|
||||
## Motion
|
||||
|
||||
- Default transition: 80–120ms on background/border (`transition: background 80ms, border 80ms`).
|
||||
- Heavier reveals: 200ms.
|
||||
- Easing: prefer ease-out (no bounce, no elastic).
|
||||
- Don't animate layout (width/height/top); animate transforms and opacity.
|
||||
|
||||
## Patterns
|
||||
|
||||
### Status badges (`.badge`)
|
||||
|
||||
Variants: `live` (red, animated dot), `success`, `danger`, `warning`, `accent`, `neutral`, `outline`. Tiny — 9–10px font, ~2px vertical padding. Reserved for state, not labels.
|
||||
|
||||
### Row tables (`.user-row`, `.token-row`, `.job-row`, `.schedule-row`, etc.)
|
||||
|
||||
CSS-grid with explicit columns. Header row uses `.head` (uppercase 10.5px). No card stacking — these are dense data lists.
|
||||
|
||||
### Schedule EPG (`.epg-*`)
|
||||
|
||||
Broadcast timeline pattern. Recorder rows × time-of-day axis. Single scrolling container with sticky left gutter (220px) and sticky top ruler (32px). Hour rhythm via `repeating-linear-gradient`. Now-line is a 1px hot-red vertical bar with `--live` glow, animated by re-rendering the line component every second (transform-only positioning, no layout thrash). Event blocks are absolute-positioned within each row, colored via `--epg-block-color` set per recorder's project color. Live events get a red gradient + pulse; failures get a glyph + full red border; past events fade to 0.55 opacity.
|
||||
|
||||
### Inputs
|
||||
|
||||
`.field-input` — `--bg-3` fill, 1px `--border`, `--r-sm`, 12.5px font. Focus: `--border-strong`.
|
||||
|
||||
### Status dot (`.signal-dot`, `.rec-dot`, etc.)
|
||||
|
||||
Small (~6–8px) circle, used inline with text. Recording dots pulse with a keyframe animation.
|
||||
|
||||
## Impeccable absolute bans (apply project-wide)
|
||||
|
||||
- No `border-left` / `border-right` greater than 1px as a colored accent (rewrite with full borders, leading icons, or background tint).
|
||||
- No `background-clip: text` gradient text.
|
||||
- No glassmorphism (blur + translucent) decoratively.
|
||||
- No hero-metric template (big number, small label, gradient accent, supporting stats).
|
||||
- No identical card grids.
|
||||
- Modal as last resort — exhaust inline alternatives first.
|
||||
- No em dashes in code or copy. Use commas, colons, parentheses, periods.
|
||||
|
||||
## To extend
|
||||
|
||||
When a new design need arises, prefer adding a variant to an existing primitive over inventing a new token. New tokens land in `styles.css`. New components land in the relevant `screens-*.jsx` only if reused; otherwise keep them local.
|
||||
61
PRODUCT.md
Normal file
61
PRODUCT.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Product
|
||||
|
||||
## What it is
|
||||
|
||||
Dragonflight (codebase name; UI brand: "Dragonflight · Wild Dragon Broadcast") is an on-prem broadcast media asset manager and live ingest controller. Operators capture from SRT, RTMP, and SDI sources, schedule windowed recordings against named recorders, transcode/proxy in the background, browse and clip in a library, import from YouTube, and hand off to Premiere or an in-house editor.
|
||||
|
||||
The deployable surface is a React (Babel-in-browser) single-page app served by nginx, talking to a Node/Express API backed by Postgres + Redis + BullMQ, with capture and worker containers handling the actual media.
|
||||
|
||||
Stack lives at `services/web-ui` (UI), `services/mam-api` (API), `services/capture`, `services/worker`, `services/editor`.
|
||||
|
||||
## Register
|
||||
|
||||
**Product.** This is operator UI for a working broadcast tool, not a brand site or marketing surface. Design serves the operator's job, not the brand's identity.
|
||||
|
||||
## Users
|
||||
|
||||
Primary: broadcast operators and engineers running live productions. They schedule and supervise back-to-back recordings across multiple recorders in a single shift. They care about: what's recording right now, what's about to start, what failed, and which recorder is bound to what source.
|
||||
|
||||
Secondary: editors and producers who consume the resulting library, comment on assets, request proxy regeneration. They mostly live in the Library and Asset detail screens, not the scheduler.
|
||||
|
||||
Tertiary: admins managing recorders, users, cluster nodes, and storage. Live in the Admin screens.
|
||||
|
||||
## Product purpose
|
||||
|
||||
Replace a stack of one-trick tools (NewBlue scheduler, vMix capture, ad-hoc Premiere ingest, manual S3 syncs) with a single operator surface that supervises recorders, owns the asset catalog through proxy generation, and stays out of the editor's way once footage lands.
|
||||
|
||||
## Tone
|
||||
|
||||
Function-first. Dense. Operations-room. Mono fonts where data lives. Small type. Restrained chrome. The operator should be able to glance at any screen and read the state of the system in under a second; they should never wonder "is this still happening" or "did that finish."
|
||||
|
||||
Not: marketing-warm, conversational, gamified, congratulatory.
|
||||
|
||||
## Strategic principles
|
||||
|
||||
1. **Glance-readable status.** Every list, every cell, every badge must answer "what is the state of this thing right now" without a hover.
|
||||
2. **Trust the operator.** No confirmation modals for reversible actions. No nag, no toasts for routine success. Errors stay visible until acknowledged.
|
||||
3. **Time is the spine.** This product is about time-based events (recordings, schedules, jobs). UIs should privilege time as a primary axis, not bury it under categorical filters.
|
||||
4. **Density over whitespace.** Operators run multi-monitor setups and want maximum signal per pixel. Generous whitespace is a brand-site reflex; reject it here.
|
||||
5. **No half-states.** Pending UIs disable controls; live UIs show live data; failed UIs show the failure inline, not in a separate notification feed.
|
||||
|
||||
## Anti-references
|
||||
|
||||
Steer away from:
|
||||
|
||||
- **Linear-pastel SaaS aesthetics.** Purples, mint accents, friendly empty states with cartoon line illustrations.
|
||||
- **Google-Calendar generic.** A neutral month grid with rounded event chips, no operational signal, optimized for "is Friday free" rather than "is recorder A in conflict at 14:00."
|
||||
- **Gantt-chart project-management feel.** Implies long-horizon planning of tasks with dependencies; this product is hour-scale broadcast operations.
|
||||
- **Cards-for-everything.** Identical card grids of icon+label+value. Particularly the SaaS hero-metric template.
|
||||
- **Decorative blur / glassmorphism / gradient text.** Read as decorative AI slop in a broadcast-ops context.
|
||||
- **NewBlue / Wirecast skinning.** Heavy bevels, gradient buttons, drop shadows. Read as outdated broadcast software.
|
||||
|
||||
## Decision context (Schedule v2)
|
||||
|
||||
The Schedule screen was rebuilt as an EPG (electronic program guide) timeline. Operator confirmed:
|
||||
|
||||
- Density: **heavy / back-to-back, many recorders all day** — month grid was the wrong primary.
|
||||
- At-a-glance signals: now/next, recorder bookings (conflicts), project context, failure history.
|
||||
- Aesthetic: **studio / cinematic — dark, type-led, accent moments.** DaVinci-Resolve-panel territory.
|
||||
- Scope: full rethink — replace the primary view.
|
||||
|
||||
Implementation: recorder rows × time-of-day horizontal axis, sticky gutter + ruler, vertical hot-red now-line, event blocks colored by project, status pills in a top strip. Today / Week / List views.
|
||||
|
|
@ -617,113 +617,353 @@
|
|||
.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 calendar ========== */
|
||||
.cal-toolbar {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
/* ========== 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;
|
||||
height: 100%;
|
||||
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;
|
||||
}
|
||||
.cal-month-label {
|
||||
font-size: 14px; font-weight: 600;
|
||||
min-width: 140px; text-align: center;
|
||||
|
||||
/* ---- 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);
|
||||
}
|
||||
.cal {
|
||||
background: var(--bg-1);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-lg);
|
||||
.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;
|
||||
}
|
||||
.cal-weekheads {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
background: var(--bg-2);
|
||||
.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);
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-4);
|
||||
background: var(--bg-1);
|
||||
}
|
||||
.cal-weekheads > div { padding: 8px 10px; }
|
||||
.cal-grid {
|
||||
.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: repeat(7, 1fr);
|
||||
grid-auto-rows: minmax(110px, 1fr);
|
||||
grid-template-columns: var(--epg-gutter-w) 1fr;
|
||||
grid-template-rows: var(--epg-ruler-h) 1fr;
|
||||
overflow: auto;
|
||||
background: var(--bg-0);
|
||||
position: relative;
|
||||
}
|
||||
.cal-cell {
|
||||
.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);
|
||||
padding: 6px;
|
||||
display: flex; flex-direction: column;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
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);
|
||||
transition: background 80ms;
|
||||
min-height: 110px;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.cal-cell:hover { background: var(--bg-2); }
|
||||
.cal-cell:nth-child(7n) { border-right: 0; }
|
||||
.cal-cell.off-month { background: var(--bg-0); }
|
||||
.cal-cell.off-month .cal-daynum { color: var(--text-4); }
|
||||
.cal-cell.today { background: var(--accent-soft); }
|
||||
.cal-cell.today:hover { background: var(--accent-soft-2); }
|
||||
.cal-cell-head {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
.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;
|
||||
}
|
||||
.cal-daynum {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
min-width: 18px;
|
||||
}
|
||||
.cal-today-pip {
|
||||
font-size: 9.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-text);
|
||||
background: var(--bg-1);
|
||||
padding: 1px 5px;
|
||||
border-radius: 99px;
|
||||
}
|
||||
.cal-events {
|
||||
display: flex; flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cal-event {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
background: var(--bg-3);
|
||||
color: var(--text-1);
|
||||
border-left: 2px solid var(--text-3);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cal-event:hover { background: var(--bg-2); }
|
||||
.cal-event.success { border-left-color: var(--success); }
|
||||
.cal-event.danger { border-left-color: var(--danger); }
|
||||
.cal-event.warning { border-left-color: var(--warning); }
|
||||
.cal-event.accent { border-left-color: var(--accent); }
|
||||
.cal-event.neutral { border-left-color: var(--text-3); }
|
||||
.cal-event-time {
|
||||
.epg-gutter-row:last-child { border-bottom: 0; }
|
||||
.epg-gutter-status {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
font-size: 10.5px;
|
||||
color: var(--text-3);
|
||||
background: var(--text-4);
|
||||
}
|
||||
.cal-event-name {
|
||||
flex: 1;
|
||||
.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:
|
||||
/* 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);
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 0 10px 0 14px;
|
||||
background: var(--bg-2);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 5px;
|
||||
font-size: 11.5px;
|
||||
text-align: left;
|
||||
color: var(--text-1);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: background 80ms, border-color 80ms, transform 80ms;
|
||||
}
|
||||
.cal-event-overflow {
|
||||
font-size: 10.5px;
|
||||
.epg-block:hover {
|
||||
background: var(--bg-3);
|
||||
border-color: var(--border-stronger);
|
||||
transform: translateY(-1px);
|
||||
z-index: 1;
|
||||
}
|
||||
.epg-block-bar {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
width: 4px;
|
||||
background: var(--epg-block-color, var(--accent));
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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);
|
||||
padding: 1px 6px;
|
||||
}
|
||||
.epg-empty-title {
|
||||
font-size: 15px; font-weight: 600;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.epg-empty-sub {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
/* ========== Cluster ========== */
|
||||
|
|
|
|||
Loading…
Reference in a new issue