- name
- videorc-design
- description
- Videorc's UI design language — a native macOS desktop app on real window glass (vibrancy), built exclusively from shadcn/ui components: flush panes, toolbars, grouped lists, glass chips, and a Raycast-style ⌘K palette. Use whenever building, styling, reviewing, or planning ANY Videorc UI (new components, screens, dialogs, lists, toolbars, badges), when the user mentions "our design", "the design skill", "glass", "native look", or asks how something should look.
# Videorc Design Language (v2: the window family)
The single source of truth for how Videorc looks and feels. Every UI task
follows this skill. Videorc is a desktop app, not a web page: every window
sits on the real macOS window material, panes are flush and split by
hairlines, controls are desktop-sized, and colour is information. The
reference is the Videorc logo: a glossy black-glass orb with chrome detail and
one LED-red accent. Light mode is the porcelain twin of the same structure.
**Status: shipped in plan 050 (2026-09-23).** `styles.css` implements the
tokens below. `src/main/window-glass.ts` gives every window (main, Stream Manager,
Captions, Notes, Preview) its material and title bar. The Preview frame's
data-URL document mirrors the coats through `src/main/window-palette.ts`, and
a parity test fails when they drift. Fix tokens and primitives, never restyle
a screen ad hoc.
## Hard rules
1. **shadcn/ui only.** Every element is a shadcn/ui component or a composition
of them, installed and customized per the `shadcn` skill. No other
component libraries and no hand-rolled widgets when a primitive exists.
audiocn (the owner's shadcn audio registry, `@audiocn`) counts as shadcn:
its meters draw audio, fed by Videorc adapters, never by its
Web Audio hooks or blocks. Base UI enters only inside its files
(`docs/audiocn.md`).
2. **Real glass, never fake glass.** The OS draws the blur. No CSS
`backdrop-filter` / `backdrop-blur` anywhere in the renderer: it wedged the
compositor in June, and on the vibrancy windows it never reaches the screen
(Chromium's own capture shows the blur, the display does not; plan 072). A
guard test fails on it. No wallpaper underlays, no inline `color-scheme`
on the root.
3. **Dark first, light supported.** Dark is the reference. Light is the same
structure with the light token column, never a separately designed theme.
4. **Colour is information.** Chrome is monochrome. Saturated colour appears
only in source/platform icons, status dots, and emphasis chips: red for
record, live, and destructive; green for healthy; amber for attention.
Never tint panels, rows, or text decoratively.
5. **Keyboard-first, quietly.** Primary actions show their shortcut as a key
chip. Global hints live in the status bar, not on every surface.
## The window family
Every window uses one material and one title-bar recipe (`window-glass.ts`):
- **Material.** `vibrancy: 'under-window'`, with the effect state pinned
to `active` so glass stays glass when the window is inactive, stripped to
**clear glass** by the native addon (plan 091): Ghostex's strip on
Electron's own vibrancy view, no tint, no wallpaper tinting, no saturation
boost, a 60 pt blur. What shows through is the desktop as soft colour, and
the coats decide the whole tone. `VIDEORC_GLASS_STYLE=material` keeps
AppKit's own grey material as the A/B control for one release;
`VIDEORC_GLASS=0` paints the solid palette instead.
- **Title bar.** `titleBarStyle: 'hiddenInset'`. Traffic lights are centred
in the header row, which is 40 px (28 px for Preview). A header row shares
its line with the lights: it pads `pl-[88px]` through
`useTrafficLightGutter()`, which drops to `pl-3` in native fullscreen.
Header rows are drag regions, and their controls opt out with
`[-webkit-app-region:no-drag]`.
- **One dark-always window.** The Preview frames video, so main pins its
NSWindow appearance to `darkAqua` through the native addon, and a light
main window never lightens it. If the pin is unavailable, it falls back to
solid dark. Stream Manager, Captions and Notes follow the app theme like
main (#412).
- **Covers.** One cover per region, Ghostex's numbers (plan 091, D3): how
much of the solid tone a region paints over the clear blur. The sidebar
covers 88% (dark) · 93% (light), the work area 83% · 86%: the sidebar is
the heavier, more covered region, the work area shows more desktop.
- The coats are derived from the covers (D4), never set by hand: `body`
paints the lighter cover (`--glass-window`), the sidebar `<aside>` adds
its delta (`bg-glass-sidebar`), content panes add theirs
(`bg-glass-content`, 0 on macOS), so each region composites to exactly
its cover.
- Single-pane windows wrap their body in `WindowFrame`.
- Never stack a third coat.
- Windows keeps plan 050's Mica composite through its own covers.
- **Accessibility.** `prefers-reduced-transparency` swaps the window coat
for `--glass-solid` and drops the region coats (the addon leaves AppKit's
solid material whole). `prefers-contrast: more` puts both covers at 95%
and strengthens hairlines, chip rims, and the secondary text tier.
- **Proof.** `pnpm probe:ui-glass --gate` measures every window over
stand-in backdrops: transmission (the glass is real), sharpness (nothing
behind is legible), contrast, pinned-dark luminance, the native effect
view state, and since plan 091 neutrality (the stripped material passes
the backdrop colour through), `nativeClear` (the strip holds) and
`ghostexParity` (each region composites to its cover). It runs in
`smoke:local-gates`.
## Tokens
Implemented as shadcn CSS variables in
`apps/desktop/src/renderer/src/styles.css` (oklch; that file is the live
source). Values below are dark · light.
Coats and surfaces
- Base `--glass-base`: black `0.16 0.004 286` (`#0D0D0F`) · porcelain
`0.985 0.001 286` (`#FAFAFB`), the OKLCH components every coat is cut
from. Solid `--glass-solid` is that base, opaque.
- Covers `--glass-cover-sidebar` / `--glass-cover-work`: 0.88 / 0.83 ·
0.93 / 0.86 (Windows 0.34 / 0.5116 · 0.5 / 0.62). The coats
`--glass-window`, `--glass-sidebar` and `--glass-content` are derived
from them in `styles.css`.
- Floating glass, three flat opaque tiers of the solid (plan 091, D6,
Ghostex's web-modal recipe), each a 1 px rim (white 10% · black 10%) and
a drop shadow, no sheen and no highlight:
- popup `--glass-float` (menus, selects, popovers, hover cards, chart
tooltips, toasts): black `#222224` (the solid plus 6% then 3% white,
`color-mix` in sRGB) · porcelain `oklch(0.99 0 0)`;
- dialog `--glass-float-dialog` (dialogs, the palette, the error panel):
black `#1C1C1D` · porcelain;
- tooltip `--glass-float-tooltip`: the black solid · porcelain.
Opaque: without a frost, even 3% translucency lets white text under a
menu read through. Deliberately darker than the glass over a bright
desktop: `probe:ui-glass --surfaces` checks each tier reads its own token
whatever is behind it.
Text (three tiers, nothing else)
- Primary `text-foreground`: titles and labels, weight 500–600.
- Secondary `text-muted-foreground`: inline context, metadata, placeholders.
- Tertiary `text-subtle`: section labels, hints, disabled.
Hairlines and selection
- Hairlines `border-border`: white 10% · black 12%, always 1 px.
- Hover/selected row `bg-accent`: white 8% · black 6%, a full-row block with
no outline. Pressed `bg-accent-pressed`: 12% · 10%.
- Focus-visible: a 2–3 px ring, keyboard only.
Colour
- `--live` / `--destructive`: the logo's LED red. Record, on air, failed,
destructive. Never chrome.
- `--success`: healthy or connected. `--warning`: needs attention.
`--info`: rare.
- `--member`: a muted violet for paid membership (YouTube member, Twitch,
Kick or X subscriber). It paints only the member row tint in chat
(plan 154); never a chip tone, never text.
- Use them through the tone utilities (`tone-success`, `tone-warning`,
`tone-destructive`, `tone-live`, `tone-neutral`) and the chip utilities.
Never as text colour on status copy.
## Desktop scale
Everything except ⌘K uses the desktop scale.
- **Layout.** There is no page column: content fills its pane edge to edge
with a 16 px gutter. Panes are flush and split by 1 px hairlines. Inside a
window there are no floating panels, no shadows, and no big cards.
- **Toolbar** (`Toolbar`, 40 px): the page title (14 px / 600) on the
window's drag band, and nothing else. No buttons in the toolbar's top-right
corner (owner call, 2026-09-23): actions live with the content they act
on. The Studio's Record / Stream / Stop and clock sit at the top of its
inspector, right above Session; a page's own actions sit in its body (a
filter bar, a section header, or the PageHeader row). The toolbar never
scrolls; only `PaneBody` does.
- **Sidebar.**
- The top row holds the traffic lights and the ⌘K button. Windows has no
traffic lights, so there the row leads with the app icon (16 px) and
"Videorc" in 12 px regular, the Windows 11 title-bar convention.
- Section labels are 11 px / 600, tertiary.
- Rows are 28 px, with 14 px text and 16 px icons.
- The account row sits at the bottom. On macOS the brand lives in About
and the Dock, never in the sidebar (owner call, 2026-09-23).
- **Status bar** (`StatusBar`, 26 px): connection and record/live state on
the left. On the right, quiet 11 px shortcut hints, each still clickable:
`⌘K Search`, `⌘P Preview`, `⇧⌘N Notes`, `⇧⌘J Stream Manager`.
- **Sections** (`PanelSection`): flush. A 13 px / 600 header, a 12 px
secondary description, a hairline between sections, and 16 px padding. No
border, background, radius, or shadow.
- **Pages with tabs** (Settings, plan 064; Orcle, plan 150): one segmented
`TabsList` in a pinned strip under the toolbar (`border-b px-gutter py-2`).
Only the panel under it scrolls, and the shell turns `PaneBody` scroll
off for the page. The last tab used is remembered on this device, deep
links open a named tab, and ⌘K lists every tab.
- Each tab answers one question.
- A tab body is a `ConfigGrid` of two sections with `CONFIG_GRID_PAIR`,
or a `PageStack` for one.
- Settings rows are `FieldGroup variant="grouped"`: label and description
on the left, the control on the right.
- **Locked means disabled, with one reason.** One `Alert` with its one
action leads the tab, and every control under it is disabled. Never
put an enabled-looking switch or button beside "isn't available".
- Rules a streamer needs (limits, Premium versus free) go in the section's
description, never in `text-subtle` footnotes.
- **Lists, not card stacks.** Sets of like things (destinations, sources,
devices, settings) are one `GroupedList` of `ListRow`s: 32 px rows (28 px
compact) with hairline separators. Cards remain only for objects with a
picture (scene thumbnails, library items): 8 px radius, white 4% fill, a
1 px hairline, no shadow.
- **Radii.** Tiers only, never ad-hoc radius values per screen:
- `rounded-panel`: 12 px. Containers and dialogs.
- `rounded-row`: 8 px. Rows and cards.
- `rounded-chip`: 6 px. Controls and key chips.
- Tags use 7 px. Status chips are round.
- Window corners belong to the OS.
- **Controls.** The default height is 28 px for Button, Select trigger, and
Input. Retune it once in `components/ui`, never per screen. Segmented
choices use `Tabs`, a glass segmented control.
- **Type.** The system stack (SF Pro). Body 14 px, metadata 12 px, section
labels 11 px. Density comes from structure, not from smaller text.
## Stream Manager
The live dashboard window (plan 055; code name `comments`, the old Chat
window). It follows the window family and adds its own layout rules. Plan 057
made it chat first: one thin stats bar and fewer words.
- **Tiers by container query.** The body is `@container/stream-manager`,
never JS resize state (`lib/stream-manager-layout.ts`):
- Wide (1,040 px and up): the stats bar, then Chat beside a right pane
with an Activity / Orcle segmented control.
- Medium (640 to 1,039 px): the stats bar, then one pane behind
Chat / Activity / Orcle.
- Narrow (under 640 px, 320 minimum): the same, with the stats bar down to
its main slots.
- Every pane renders once; only its placement changes.
- **The title row carries the title only,** with no mode badge: the stats bar
says ON AIR, and History has its own bar. Controls live in the status bar:
each provider's chat state on the left; Keep on top, Highlight, Clear and
Open Preview as icons on the right, folded into ⋯ under 640 px.
- **One thin stats bar** (32 px, `components/stream-manager/stats-bar.tsx`),
never tiles or cards:
- The three main slots lead it: by default the ON AIR clock, viewers and
stream health. They are never clipped at any width.
- The rest follow after a hairline as a number and a short unit
("89,860 followers +83 · 8 subs · $20 · 22 msg/min"). They sit in a
clipped, wrapping row, so a stat that does not fit drops off the end
whole.
- Numbers are monochrome and tabular. Tone lives in the chip and the dots,
and the ON AIR chip is the only emphasis. Mini sparklines (the shadcn
`chart`) appear at Wide only.
- Details are one hover away (`HoverCard`): the per-platform split, peak,
fps, drops, each destination's state.
- Drag a stat to move it; right-click the bar to show or hide stats, move
one, or reset (`lib/stream-manager-stats-layout.ts`, kept in
`localStorage`). The clock and the viewer count cannot be hidden or leave
the main slots.
- **Quiet when fine, specific when not.** Mid-stream there is no time to
read. A healthy state is a dot or a number; words appear only when
something needs the streamer:
- health shows "X failed" or "12 dropped/min" instead of the bitrate;
- a provider shows "read-only" or "reconnect to send";
- the composer names only what a send skips, and a finished send says
nothing;
- every removed caption survives in a hover card or a tooltip.
- **Never an unmeasured zero.** A stat exists only when its source does, and
an unreadable number shows "–" with the reason on hover. The viewer count
is never hidden while live.
- **Chat keeps the big-text rows** (plan 055, decision 5), virtualized with
`@tanstack/react-virtual`. While live, a row's time appears on hover.
Filters are inline chips from 640 px and one Filters menu below it.
A row paints at most one background, lowest to highest: a member's own
message `bg-member/8` (plan 154), Orcle's "Talking about this"
`bg-accent`, a paid message `bg-warning/10` with its ring; an on-stream
row keeps its button fill. The Member chip stays on every tinted row and
text stays monochrome.
- **Activity rows** lead with who: a row about one named person (a follow,
sub, gift, tip or raid) shows their avatar circle, with initials until it
loads (plan 071). Counts the platform never named, announcements and
destinations keep the event glyph tile (the window-scoped registry
`components/stream-manager/activity-icons.tsx`). Either way the platform
icon sits bottom-right. Then comes the name and a short fact on one line
("Resub · 14 months"), the viewer's words, and a ⋯ menu. Filter chips carry
their counts.
- **Proof.** `pnpm probe:comments-window` sweeps 320/480/640/800/1040/1280.
It fails on any overflow, a stats bar taller than one row, main stats
apart or clipped, a stat cut at the bar's edge, a hidden viewer count, a
button in the title row, or an unreachable control.
## ⌘K palette scale
The command palette alone keeps the Raycast scale:
- `rounded-panel` float glass (the Dialog's `glass-float-dialog`; `Command`
itself paints nothing).
- An 18–20 px borderless search input with a leading 24 px icon.
- 40 px rows: icon, title, secondary context, an optional alias key chip,
then right-aligned metadata.
- A footer with the primary action and its key chip.
Nothing else in the app uses this scale.
## Glass chips
Every badge, status pill, tag, and key chip is glass (`ui/badge.tsx`,
`status-badge.tsx`, `ui/kbd.tsx`, `status-dot.tsx`). The chip utilities
(`glass-chip`, `glass-chip-tinted`, `glass-dot`, `glass-keycap`) are defined
once in `styles.css`.
| Part | Dark | Light |
| ------------- | ---------------------------------- | --------------- |
| Fill | vertical gradient white 10% → 3.5% | white 70% → 45% |
| Rim | 1 px white 13% | 1 px black 8% |
| Top highlight | `inset 0 1px 0` white 12% | white 90% |
| Drop | `0 1px 2px` black 30% | black 6% |
- **Neutral** (`Badge` default/secondary): primary text.
- **Tag** (`Badge` outline): 20 px, 7 px radius, 11 px, secondary text. For
`9:16`, counts, `Idle`, `beta`.
- **Status** (`Badge` success/warning/neutral, `StatusBadge`,
`StatusDot`):
- Monochrome text. A 6 px dot in the tone colour carries the status, with
a 2 px halo and a soft glow.
- A leading icon replaces the dot and takes the tone.
- `StatusBadge` is 22 px and fully round.
- **Emphasis** (`Badge` destructive/live, an error `StatusBadge`): tinted
glass, a tone gradient with a tone rim and tone-mixed text. Only for
failed, on air, and destructive.
- **Key chip** (`Kbd`): a glass keycap with a brighter top edge and a dark
bottom edge.
- No `backdrop-filter`, no motion, no ad-hoc pills. A rounded, bordered
`text-[10–12px]` span is a bug: use `Badge`.
## Floating surfaces
Dialogs, popovers, hover cards, menus, selects, tooltips, toasts, and the
palette are flat, opaque tiers of the window's solid tone (plan 072 made
them opaque, plan 091 D6 made them flat): `border glass-float` for the popup
tier, `border glass-float-dialog` for dialogs and the palette,
`border glass-float-tooltip` for tooltips. One surface per float: a
`Command` inside a Dialog or Popover stays transparent. Every toast is the
same neutral popup glass: a type (success, warning, error, info) colours its
View on GitHub