feat(schedule): EPG stylesheet + impeccable context (PRODUCT/DESIGN.md)
The EPG JSX components in screens-ingest.jsx ship with the YouTube branch but the matching stylesheet got lost during the parallel-branch shuffle. This adds the missing .epg-* block to styles-rest.css and replaces the dead .cal-* (month-calendar) rules left over from the previous design. What the styles cover: - .epg-page / .epg-toolbar — top-level flex layout + date nav row - .epg-status — sticky "on air" strip with pulse halo on the live dot - .epg / .epg-corner / .epg-gutter / .epg-canvas-head / .epg-canvas — the 2x2 sticky grid (top ruler + left gutter both sticky) - .epg-ruler / .epg-ruler-tick — hour ticks - .epg-row + .epg-block + .epg-block.live/.failed/.past — event blocks with project-color 4px inner bar (no side-stripes; impeccable ban) - .epg-now / .epg-now-pip — vertical hot-red now-line with broadcast glow - .epg-week + .epg-week-day — stacked 7-day sections for week view - .epg-empty — recorder-less / loading empty state Also adds PRODUCT.md and DESIGN.md so future design passes have the context files the impeccable skill requires. Both drafted from the existing codebase (tokens, screen patterns) rather than synthesised from a prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c0d1251c1f
commit
5882c68217
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; }
|
.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; }
|
.token-row.revoked { opacity: 0.5; }
|
||||||
|
|
||||||
/* ========== Schedule calendar ========== */
|
/* ========== Schedule (EPG timeline) ==========
|
||||||
.cal-toolbar {
|
Broadcast-control-room schedule. Recorders are rows, time is the horizontal
|
||||||
display: flex; align-items: center; gap: 6px;
|
axis. The single scrollable .epg container uses a 2-column / 2-row grid
|
||||||
margin-bottom: 12px;
|
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;
|
/* ---- Status strip (always on top of the schedule screen) -------------- */
|
||||||
min-width: 140px; text-align: center;
|
.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 {
|
.epg-status-row {
|
||||||
background: var(--bg-1);
|
display: flex; align-items: center; gap: 10px;
|
||||||
border: 1px solid var(--border);
|
min-height: 22px;
|
||||||
border-radius: var(--r-lg);
|
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;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.cal-weekheads {
|
.epg-status-pill-bar {
|
||||||
display: grid;
|
width: 3px; align-self: stretch;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
background: var(--text-3);
|
||||||
background: var(--bg-2);
|
}
|
||||||
|
.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);
|
border-bottom: 1px solid var(--border);
|
||||||
font-size: 10.5px;
|
background: var(--bg-1);
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--text-4);
|
|
||||||
}
|
}
|
||||||
.cal-weekheads > div { padding: 8px 10px; }
|
.epg-toolbar .spacer { flex: 1; }
|
||||||
.cal-grid {
|
.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;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: var(--epg-gutter-w) 1fr;
|
||||||
grid-auto-rows: minmax(110px, 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-right: 1px solid var(--border);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
padding: 6px;
|
display: flex; align-items: center;
|
||||||
display: flex; flex-direction: column;
|
padding: 0 14px;
|
||||||
gap: 4px;
|
font-size: 11px; color: var(--text-3);
|
||||||
cursor: pointer;
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.epg-gutter {
|
||||||
|
position: sticky; left: 0;
|
||||||
|
grid-row: 2; grid-column: 1;
|
||||||
|
z-index: 2;
|
||||||
background: var(--bg-1);
|
background: var(--bg-1);
|
||||||
transition: background 80ms;
|
border-right: 1px solid var(--border);
|
||||||
min-height: 110px;
|
|
||||||
}
|
}
|
||||||
.cal-cell:hover { background: var(--bg-2); }
|
.epg-gutter-rows { display: flex; flex-direction: column; }
|
||||||
.cal-cell:nth-child(7n) { border-right: 0; }
|
.epg-gutter-row {
|
||||||
.cal-cell.off-month { background: var(--bg-0); }
|
display: flex; align-items: center; gap: 10px;
|
||||||
.cal-cell.off-month .cal-daynum { color: var(--text-4); }
|
height: var(--epg-row-h);
|
||||||
.cal-cell.today { background: var(--accent-soft); }
|
padding: 0 14px;
|
||||||
.cal-cell.today:hover { background: var(--accent-soft-2); }
|
border-bottom: 1px solid var(--border);
|
||||||
.cal-cell-head {
|
position: relative;
|
||||||
display: flex; align-items: center; gap: 6px;
|
|
||||||
}
|
}
|
||||||
.cal-daynum {
|
.epg-gutter-row:last-child { border-bottom: 0; }
|
||||||
font-size: 12px;
|
.epg-gutter-status {
|
||||||
font-weight: 600;
|
width: 8px; height: 8px; border-radius: 50%;
|
||||||
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 {
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 10.5px;
|
background: var(--text-4);
|
||||||
color: var(--text-3);
|
|
||||||
}
|
}
|
||||||
.cal-event-name {
|
.epg-gutter-status.live {
|
||||||
flex: 1;
|
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;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
transition: background 80ms, border-color 80ms, transform 80ms;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
.cal-event-overflow {
|
.epg-block:hover {
|
||||||
font-size: 10.5px;
|
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);
|
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 ========== */
|
/* ========== Cluster ========== */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue