- 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 and Videorc's shared primitives: 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, confirmations, empty or locked states), 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.
The words on these surfaces are not in this file. How to write them is
[`docs/ui-copy.md`](../../../docs/ui-copy.md) (voice, casing, ellipsis,
toasts, errors, disabled and locked copy), and the one name for each thing is
the glossary in [`CONTEXT.md`](../../../CONTEXT.md).
**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. Plan 174 (the UI/UX pass, 2026-10) gave every job one shared
primitive ([Primitives](#primitives)) and added the guards that hold them.
## 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, and never colour a status
word: the tone rides on an icon, a dot, a chip or an Alert.
5. **Keyboard-first, quietly.** Primary actions show their shortcut as a key
chip. Global hints live in the status bar, not on every surface.
6. **One primitive per job.** Confirmations, locks, disabled reasons,
shortcuts, device pickers, status words, numbers and times each have one
shared primitive or helper ([Primitives](#primitives)). Use it. If it
lacks something, extend it minimally; never hand-roll a competing one.
## Owner calls
Binding. Follow them as written; a change that would undo one is a question
for the owner, not a design choice.
- **The toolbar holds only the page title** (2026-09-23). Record and Go
Live… sit at the top of the Studio inspector, and a page's own actions sit
in its body.
- **Buddy stays second in the sidebar, directly under Studio, on ⌘9**
(2026-10-10, plan 174 D6 and D8). It keeps Clean cut and every other
feature, because the page sells Premium. Never move it or a feature out of
it.
- **A click on a live chat row puts the message on stream**, and a click on
the live row takes it off (2026-10-10, D10; plans 055 and 151). There is no
select-first step and no separate Show button.
- **Toasts are never tinted** (2026-09-29). A toast's type colours its icon
only, never the panel, its rim or its text.
- **No success toast for a scene, layout or source change** (2026-07-16):
the stage is the confirmation.
- **The platform mark closes a chat or Activity row on the far right**, at
20 px, with YouTube's official icon (plan 165).
- **The Stream Manager's live chip reads ON AIR** (2026-09-24), the
broadcast convention. Everywhere else the state is "On air".
- **On macOS the brand lives in About and the Dock**, never in the sidebar
(2026-09-23).
- **The Buddy mark is the real artwork**, never a redrawn glyph (2026-10-04;
see [Icons](#icons)).
## 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: on macOS it pads `pl-[88px]`
(`TRAFFIC_LIGHT_GUTTER_CLASS`, through `useTrafficLightGutter()` in
`components/window-frame.tsx`), **in native fullscreen too**. macOS draws
the lights over the header when the title bar reveals on hover, and a
window filling the screen keeps them showing; dropping the gutter there put
the lights on the Stream Manager title (2026-09-25). Off macOS the native
frame owns the title bar and the row pads `pl-3`. Header rows are drag
regions, and their controls opt out with `[-webkit-app-region:no-drag]`.
- **Single-pane windows** (Stream Manager, Notes, the Captions reader) wrap
their body in `WindowFrame` and lead with one `WindowHeader`: the title as
the window's `h1` (14 px / 600), an optional quiet `meta` fact after it,
and the window's few controls trailing. Stream Manager keeps its row to the
title.
- **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.
- **Providers.** Each window is its own React root and wraps its tree in
`<WindowProviders window="main|stream-manager|captions|notes">`: one
TooltipProvider (600 ms, the desktop delay) and one Toaster, inset above
that window's status bar.
- **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 and tertiary text
tiers.
- **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.
- Inset `bg-inset`: foreground 3%, for a well inside a section (a preview
frame, a code line). Never a card.
Text (three tiers, nothing else)
- Primary `text-foreground`: titles and labels, weight 500–600.
- Secondary `text-muted-foreground`: inline context, metadata, placeholders,
descriptions, and every fact the streamer needs.
- Tertiary `text-subtle`: section labels, quiet hints, disabled.
**Essential information never uses `text-subtle`** (plan 174 D12): a
reason, a limit, a status, a value or a rule a streamer needs is secondary
or primary. `--subtle` passes AA on the solid (0.62 dark · 0.54 light) and
rises under `prefers-contrast: more`, but over a bright desktop it is
still the faintest tier.
- Sizes are tokens: `text-label` 11 px / 600 (section labels,
`SectionLabel`), `text-section` 13 px / 600 (section headers), `text-meta`
12 px (descriptions and metadata). **No text below 11 px** (a guard fails
on `text-[9px]` and `text-[10px]`).
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.
- `--reward`: a muted teal for Twitch channel point rewards. It paints only
the reward row tint in Activity (`bg-reward/8`, plan 177); 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: the status colour guard fails on a
coloured status word.
## 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, the window's
`h1`) on the window's drag band, and nothing else (owner call,
2026-09-23). Actions live with the content they act on. The Studio's
Record, Go Live… and the stop button ("Stop recording", or "End
livestream" while on air) and its clock sit at the top of the 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** (plan 174 D8).
- The top row holds the traffic lights and the ⌘K button.
- Studio leads, with a live dot and the clock while a recording or
livestream runs. Buddy sits directly under it on ⌘9 (owner call).
- "Setup" labels the pages you configure between sessions: Sources ⌘2,
Scene ⌘3, Assets ⌘4, Livestream ⌘5, Captions ⌘6, Output ⌘7. Library ⌘8
follows under no label: a group of one row carries none.
- Settings (⌘,) sits in a bottom block above the account row. Health has
no row: the status bar's state is its door (D9).
- Section labels are `SectionLabel` (11 px / 600, tertiary). Rows are
28 px, with 14 px text and 16 px icons.
- On Windows the native title bar carries the app's name, so the sidebar
shows no mark (D18).
- **Status bar** (`StatusBar`, 26 px in the main window): the session state
on the left, as a button that opens Health ("● On air 12:04",
`StatusBarSessionState`). On the right, quiet 11 px shortcut hints
(`StatusBarHint`), each still clickable and pressed while its window is
open: ⌘K Search, ⌘P Preview, ⇧⌘N Notes, ⇧⌘J Stream Manager. Every
clickable thing in a status bar is a `StatusBarHint` or a
`StatusBarAction` (20 px, 11 px tertiary text that brightens on hover).
- **Sections** (`PanelSection`): flush. A 13 px / 600 header (an `h2`; a
nested section passes `headingLevel={3}`), 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; Buddy, plan 150; Livestream,
plan 174): `PageTabs`, one segmented `TabsList` in a `PaneStrip` pinned
under the toolbar (`border-b px-gutter py-2`). Only the region under it
scrolls: the page spreads `PANE_OWNS_SCROLL`, so `PaneBody` stops
scrolling for it. The last tab used is remembered on this device, deep
links open a named tab (`openWorkspaceTab(tab, subTab?)`), and ⌘K lists
every tab. A filter bar pinned under the toolbar is a `PaneStrip` too.
- 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 `SettingsRow` (`label`, `description`, `error`, the
control as its child) in a `FieldGroup variant="grouped"`: label and
12 px description on the left, the control on the right.
- **Locked means disabled, with one reason.** One `LockAlert` (or
`SessionLockAlert` while recording or live, `OfflineAlert` while
Videorc reconnects) 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". A Premium feature stays on screen behind its
lock, never hidden: the locked page is what sells it.
- 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. A row is a fact unless it says
otherwise: `interactive` adds the hover, `as="button"` makes the whole row
one button, and a row that holds its own controls stretches a
`ListRowButton` over itself. A fact row that jumps to the page owning its
value is a `NavigableRow` (icon, label, value, chevron).
Cards remain only for objects with a picture (scene thumbnails, takeovers,
Buddies, library items): `MediaCard`, 8 px radius, white 4% fill, a 1 px
hairline, no shadow; `selected` is a brighter rim, never a colour.
Sources (plan 173) is the reference for rows that carry controls: one
`SourceItem` per source (`components/sources/source-item.tsx`), a header
row (icon, title, one status chip, one quick control, More), a body
indented to the title, and the rare settings folded into More.
- **Overflow menus.** Low-frequency per-item actions go in one `KebabMenu`
(⋯): items grouped with a hairline between groups, destructive items last,
each item's keys right-aligned, a spinner while an item's work runs.
`reveal="hover"` hides the trigger until its row (`group/kebab-host`) is
hovered or focused.
- **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.
- `rounded-tag`: 7 px. Tags. Status chips are round.
- Floating surfaces add the popup tier (`rounded-lg`, 10 px; see
Floating surfaces). Nothing else: no `rounded-sm`/`md`/`xl` or
`rounded-[Npx]` in chrome (a guard checks).
- Window corners belong to the OS.
- **Controls.** The default height is 28 px for Button, Toggle, Select
trigger, and Input. Retune it once in `components/ui`, never per screen: no
`h-*` or `size-*` on a control at its call site (a guard checks).
- **One compact size** (plan 174 S4.1, J1-11): `xs`, 24 px. Button `xs`
and `icon-xs`, Toggle and ToggleGroup `xs`, SelectTrigger `xs`
(SegmentedControl `sm` is the same 24 px). Use it only where the control
sits inside something denser than a page: an Alert's action; a row's
trailing or hover actions (`ListRow`, `SourceItem`, chat, activity,
View on GitHub