hung doan
Style Guide

hung doan — Style Guide

The shared component system for every /video/ page. Built from three stylesheets: css/tokens.css (design tokens + the sharp-corner reset), css/shell.css (chrome, buttons, forms, pills, modals), and css/editor.css (workspace, panels, layer/job rows, timeline). No build step — author HTML against these classes and it ships. This page itself uses only those classes.

This guide is self-contained — CSS, JS, the icon sprite and placeholder art are vendored into styleguide/, so the whole folder zips and runs anywhere with no ../ dependencies. Full-page layout references live in Reference pages.

Design principles

The core idea: this is a desktop application, not a web page. Every page wears native-app chrome so the product feels like a tool you install, not a site you visit. Seven rules carry that through:

1 · Desktop-app feel — the defining principle. Each page is a fixed-height .vshell (no page scroll) with a titlebar menu bar (File / Edit / Actions ribbon with drop-downs, .menus + .menu-drop), a statusbar at the bottom, and content organised into panels with collapsible sections (.panel-section + .panel-toggle) and tabs — exactly the affordances of a native editor (Photoshop, an NLE, a DAW). Right-click opens a .ctx-menu. Everything else here serves this feel.
2 · Authoring is desktop-only; front-facing views are responsive. The editing/authoring UI is desktop-only by design — the workspace, timeline and multi-panel tools are too complex to collapse onto a phone, so editors must never render at a mobile breakpoint. The front-facing, end-user views — browsing the template gallery, the media gallery, render-queue status — are responsive. The split is producer vs. consumer: an editor creates a reel and publishes a template on desktop; an end-user or stakeholder then browses and uses that template, or checks render status, on their phone.
3 · Sharp corners everywhere. A global reset zeroes border-radius on all elements — square, defined edges read as "application". The only exemption is inside [data-live-stage] (real artwork canvases, where payload radii must render). Don't add radii — if you must, qualify your selector to out-specify the reset (see the scene-map brief card).
4 · Graphite surfaces, blue accent. Layered greys (--bg--panel-3), one blue accent for primary/active, status colors reserved for meaning.
5 · No log modals. Logs and status live in persistent panels (queue bottom panel, jobs strip), never popups — like a docked output panel in an IDE.
6 · Mark unbuilt controls .future. Yellow-green + a data-stub toast — see Conventions below.
7 · Relative paths only. Every asset/script reference is relative so a page works at any depth.
See the desktop chrome assembled in the mock pages — the Mock Editor shows the menu bar, collapsible panel sections, tabs, and statusbar working together.

Shell & chrome — the desktop frame

The native-app feel comes from the frame every page wears. The titlebar at the top of this page is live — click Go to, File, Edit or View to open a drop-down ribbon (the File/Edit/View menus are inert — there for the desktop feel), and right-click the Context-menu demo below. The pieces:

Desktop menu-bar drop-down: the hung doan brand, a Go to menu opened with AI Studio / Asset Library / Help / Back to Dashboard items
The menu-bar ribbon in action — desktop-style drop-down navigation (.menus.menu-drop), opened by shell.js.
.vshell — full-viewport grid, grid-template-rows: 44px 1fr (titlebar / content). Add a third 26px row for a statusbar, and .with-timeline for a bottom strip. The shell never page-scrolls — content panels scroll internally, like an app window.
.titlebar holds: .brand (the wordmark) · .menus.menu.menu-drop (the File/Edit/Actions ribbon; .kbd for shortcuts, hr separators, .danger items) · .project-title (center) · .top-actions (right). Menu open/close, drop-downs, and data-modal / data-stub are wired by shell.js — no per-page JS.
.statusbar — the 26px footer (left message + right meta), an app-style status line.
.ctx-menu — right-click menu via VideoShell.contextMenu(x, y, items); see Overlays.
Assembled and interactive in the Mock Editor (menu bar + collapsible panel sections + tabs + statusbar) and the other mock pages.

Layout anatomy

How the desktop frame nests. The ASCII map shows the regions; the mermaid block is the same structure as a diagram you can paste into any mermaid renderer (kept as source so this bundle stays dependency-free).

Region map — a .vshell.with-timeline page
┌────────────────────────────────────────────────────────────────────────────────────────────┐
│  .titlebar (44px)                                                                          │
│  .brand     .menus (File / Actions)     .project-title     .top-actions                    │
├──────────────────────────┬──────────────────────────────────────┬──────────────────────────┤
│  .left-panel             │  .stage-area                         │  .right-panel            │
│   tray tabs              │   .viewer-frame                      │   .tabs                  │
│   > Cast                 │   (canvas)                           │   > .panel-section [v]   │
│   > Frames               │                                      │     (collapsible)        │
│                          │  .workspace - 3-col grid             │                          │
├──────────────────────────┴──────────────────────────────────────┴──────────────────────────┤
│  .timeline-wrap - jobs strip / timeline   (.with-timeline)                                 │
├────────────────────────────────────────────────────────────────────────────────────────────┤
│  .statusbar (26px)   left message ......... right meta                                     │
└────────────────────────────────────────────────────────────────────────────────────────────┘
Region map — rendered
.brand hung doan .menus (File / Actions ribbon) .project-title .top-actions .left-panel tray tabs > Cast / Frames .stage-area .viewer-frame (canvas) .right-panel .tabs > .panel-section [v] (collapsible) .workspace — 3-column CSS grid .timeline-wrap jobs strip / timeline · .with-timeline .statusbar — left message right meta
DOM hierarchy — mermaid source
mermaid
graph TD
  vshell[".vshell · CSS grid"] --> titlebar[".titlebar · 44px"]
  vshell --> workspace[".workspace · 3-col"]
  vshell --> timeline[".timeline-wrap · optional"]
  vshell --> statusbar[".statusbar · 26px"]
  titlebar --> brand[".brand"]
  titlebar --> menus[".menus → .menu → .menu-drop"]
  titlebar --> ptitle[".project-title"]
  titlebar --> actions[".top-actions"]
  workspace --> left[".left-panel · tray tabs"]
  workspace --> stage[".stage-area · .viewer-frame"]
  workspace --> right[".right-panel · .tabs + .panel-section ▾"]

Color tokens

CSS custom properties on :root in tokens.css. Always reference the variable, never the hex.

Surfaces
Lines
Text
Accents & status

Typography

Inter for UI (--font, 14px/1.4 base), monospace for IDs/timecodes/code (--mono). Readable content — documentation, front-facing/end-user copy, prose — uses the 14px base. Editor & tooling UIs drop to 12–13px to fit dense, panel-heavy workflows within the browser; that density is intentional there and shouldn't leak into readable text (this page itself follows the rule — its prose is 14px).

Page heading 24px · .page-head h1
Section heading 16–17px · modal h3
Panel head 15px · .panel-head strong
Body text — the 14px base --font
Label / secondary 12px · label, .card span
Section title (overline) 12px · .section-title
mono 0:42 · #4521 --mono · .timecode

Icons

Lucide-adapted SVG sprite at assets/icons/icons.svg, all currentColor. Place <i data-icon="name"></i>icons.js hydrates it on load; call VideoIcons.hydrate(root) after injecting markup. Sizes: .ic (16) · .ic-lg (20) · .ic-xl (24). Click any icon to copy its markup.

<i data-icon="play"></i>

Full-page layouts (mock)

Standalone pages assembling the components into the real app layouts — open each to study structure, spacing and the responsive grids. All self-contained in this folder.

Buttons

Bare <button> and .btn (for <a>) share the graphite base. Variants layer on intent.

Height30px (content-driven, no fixed height)
Padding4px 8px (v / h)
Border1px solid (#45494f · primary --accent-edge · danger #7d3445)
Font14px / 600 · line-height 19.6px
Icon gap7px (inline-flex, centered)
Radius0 (sharp)
Disabledopacity 0.45 · not-allowed
.btn-primary blue (the workflow CTA) · .btn-ghost transparent · .btn-danger red outline. For an AI-assist CTA the scene-map adds a page-local green .btn-ai — promote it to shell.css if a second page needs it.

Form controls

Inset background, --line border, full-width by default. label is a block element above its control.

.mock-input — mono, for read-only command/path display
Input height34px (6px pad + 14px/19.6 + 1px border)
Select height32px
Text padding (inside)6px 8px (v / h)
Border / bg1px solid --line · bg --inset
Font14px / 400 · color #e4e8ed
Width100% (fills its container)
Labelblock · 12px · margin-bottom 3px · --text-soft
Field spacing.control-group 9px · .two-col gap 8px
Textareasame padding · height = rows × line-height
.two-col for paired fields · .control-group spaces stacked fields · .selectish styles a button as a faux-select.

File upload

One upload pattern, two variants. All share the same three intake routes: click → file picker (a hidden <input type="file">), drag-and-drop (a .dragover highlight on hover), and — where a library exists — pick from existing assets. Live in the Mock Form (drop a file on a slot).

Variant 1 — reference / frame slots (the generate-tab "Advanced" frames, scene references, cast views)
Variant 2 — drop-zone bar (bulk file drops — uploads tab)
…or drop files anywhere in this bar
.slot-thumb132 × 92 · 1px dashed --line-strong · inset bg
.slot-thumb hover / .dragoverborder --accent · bg --accent-soft
.slot-actions buttonpadding 3px 8px (Upload · Pick · Clear)
.upload-barflex · 1px dashed --line-strong · padding 14px
Intakehidden file input · drag-drop · library pick
Wiring: a single hidden <input type="file"> the buttons trigger; dragover/drop handlers toggle .dragover and read e.dataTransfer.files; uploaded files post to the staged-frames API and the returned URL fills the slot. Real uses: storyboard editor & scene-map reference slots, the AI Studio Advanced frames.

Status pills

.pill.<state> — small caps-weight status chips with a semantic color per state.

running queued encoding complete failed published draft
Padding4px 9px (v / h)
Font11px / 800
Border1px solid (per-state color)
Height~25px (content-driven)
Displayinline-block · radius 0
Storyboard statuses map to these via STATUS_PILL: scripting/compiling→encoding, ready→queued, running→running, paused→draft, completed→complete, published→published, failed→failed. Animate an active pill by adding the job-pulse keyframe (1.6s).

Badges & chips

.badge — neutral metadata. .chip — selectable filter (toggle .active); group in a .chip-row.

1080 × 1920 · 30 fps 9:16
Badgepadding 4px 10px · 12px / 400 · 1px border
Chippadding 5px 10px · 12px / 600 · 1px border
Chip · activebg --accent-soft · border --accent · #fff
Chip rowflex-wrap · gap 7px

Tabs

Two flavors. .tabs — equal-width segmented control (right panels). .tray-tabs-cloud + .tray-tab — wrapping pill cloud (left panels); active is .is-active.

.tabs (segmented)
.tray-tab (cloud)
.tabsequal-width grid · gap 4px
.tabs buttonpadding 5px 0 · 14px / 600 · height 32px
.tabs .activebg #23406d · border #3479df
.tray-tabpadding 4px 9px · 11px / 700 · UPPERCASE
.tray-tab.is-activebg --accent-soft · border --accent

Panels & layout

.panel is the base surface. .panel-section + .panel-toggle make a collapsible block (toggle .is-collapsed on the section). The editor workspace is a 3-column grid: .workspace.left-panel / .stage-area / .right-panel.

Click the header to collapse. Body hides when the parent has .is-collapsed.

.vshell rows44px / 1fr (/ 26px statusbar · / 226px with-timeline)
.workspace colse.g. 300px / minmax(0,1fr) / 330px
.panel-sectionpadding-bottom 12px · 1px bottom border
.panel-toggle13px / 800 · 9px bottom pad · chevron rotates
.is-collapsedhides .panel-body · chevron −90°
Full-height app pages use the .vshell grid (44px titlebar / content / optional 26px statusbar); add .with-timeline for a bottom strip. Gallery/scroll pages use .page + .page-head.

Cards & galleries

.card-grid auto-fills .cards (240px min). A card has a .thumb (16:9, or .tall 9:16) and a .meta block. Selection: .selected/.active.

0:12
Asset title1080×1920 · © credit
Selected (.tall thumb)9:16
.card-gridauto-fill minmax(240px, 1fr) · gap 14px
.card .thumbaspect 16:9 (.tall 9:16, max-h 240px)
.card .metapadding 10px 12px · b 13px / span 12px
.card:hoverborder --line-strong
.selected / .activeborder --accent + 2px ring
Empty states: .grid-empty (centered, full-width). Picker/gallery cards reuse .lib-card (defined page-local where the picker is mounted).

Media asset cards

The library pattern: a grid of media cards, each opening a preview modal on click. Cards carry their data on data-* attributes; the click handler populates and opens #preview-modal. (Thumbnails here are self-contained placeholder SVGs.)

Click any card → the preview modal opens the full-size media.
Preview modal — the shared media viewer

One modal (#preview-modal) serves every tab via library/preview.jsopenPreview(asset) for catalog/upload rows, openStockPreview(item, opts) for Pexels results. It picks the element by kind — <img> / <video controls autoplay muted loop> / <audio controls> — and the footer carries contextual actions: Copy Path, Download (video) or Raw File (other), and conditionally Add to Live Assets / Delete. It stops playback on close (a MutationObserver pauses any video,audio when .open is removed) so audio doesn't keep playing behind the gallery.

See it in the Mock Gallery →

Tables

Plain <table> picks up borders/padding from editor.css (bottom-border rows, dim th). .mini badges flag per-row state.

JobKindStatusAsset
#84stitchcomplete4621
#83video_genrunning
#82image_genfailed
live wait encode
th / tdpadding 10px 8px · bottom border --line-soft
th12px / 700 · --text-soft · left-aligned
td13px / 400
.mini badgepadding 4px 8px · 11px / 800 (live·wait·encode)

List & layer rows

.layer-row — selectable row with hover + .is-active (layers panel, scene lists). Used with a thumb + main column + trailing pill.

VIDScene 1 — The Empty Corridordone
VIDScene 2 — The Encounter (active)rendering

Jobs & progress

Job rows use the queue .job skin. In-flight work gets the live-progress pattern: a pulsating info line + an indeterminate creep bar (providers give no real %, so the bar creeps elapsed / expected and caps at ~96%). A 1s ticker re-reads data-since/data-expected between polls.

#127 video_gen⟳ rendering · scene 3 · 1:07 · ~1–3 min
running
#126 video_genopenrouter · scene 2
completed
Pipeline strip (sb-pipeline)
Script ✓ Ref frames 4/4 S3 CompilePublish

Modals

.modal-backdrop (toggle .open) holds a .modal with .modal-head (eyebrow + title + .modal-close), a scrolling body, and .modal-foot. shell.js wires data-modal="#id" to open and data-modal-close/backdrop to close. Sits at z-50 — layer above app overlays with a higher local z-index (e.g. the picker on the scene map). Reserve modals for dialogs/pickers, never logs.

Context menu

Right-click menus reinforce the desktop-app feel. Open one with VideoShell.contextMenu(x, y, items) (from shell.js) — each item is { label, icon?, danger?, action }, rendered with the .ctx-menu styling (auto-positioned, closes on outside-click / Esc). danger: true tints an item red.

Right-click any row below → Copy · Duplicate · Expand · Delete (each fires a toast):
Scene 1 — The Empty Corridor right-click
Scene 2 — The Encounter right-click
Inspector Whiskers (cast) right-click
The real pattern — scope a contextmenu listener to specific surfaces
from js/typography.js — right-click stage nodes & layer rows
element.addEventListener("contextmenu", (e) => {
  const row = e.target.closest("[data-id]");        // a surface that has a menu
  if (!row) return;                                 // otherwise: native menu
  e.preventDefault();
  VideoShell.contextMenu(e.clientX, e.clientY, [
    { label: "Copy",      icon: "save",   action: () => copy(row.dataset.id) },
    { label: "Duplicate", icon: "layers", action: () => duplicate(row.dataset.id) },
    { label: "Expand",    icon: "zoom-in", action: () => expand(row.dataset.id) },
    { label: "Delete", icon: "trash", danger: true, action: () => remove(row.dataset.id) },
  ]);
});
Always preventDefault() only on surfaces you handle, so the browser's native menu still works elsewhere. Used in the Typography workbench (right-click a stage node or a .layer-row).

Stage & preview

A plain media preview uses .viewer-frame (16:9; .tall 9:16, .square 1:1). A live render canvas uses .reels-stage-frame with the data-live-stage attribute (the one place radii are allowed).

.viewer-frame.tall
.viewer-frame (16:9)

Conventions

Future / stub controls. Anything not yet wired gets .future (yellow-green) + data-stub="message"; shell.js shows a toast on click. Remove the class when wired.

Error banners. Provider/pipeline failures parse through friendlyError() (shared.js) — it pulls the message out of raw HTTP/JSON, adds an actionable hint, and links to a matching /video/help/#anchor section from help/errors.json. Render loud, above the stage.

⚠ Scene 1 submit failed The request failed because the input image may contain a real person.
→ Switch the video model, or regenerate the reference frame with a less photoreal look.
📖 Help: about this error

Code / payload view. Raw JSON or prompts render in a mono <pre> on the inset surface (green-on-dark), with a Copy affordance — never a modal.

{
  "logline": "Three kids on bikes blur past a sleeping relic…",
  "scenes": [ { "idx": 0, "shot": "wide establishing" } ]
}
hung doan
Style Guide — a self-contained, sharp-edged desktop-app design system. Drop the styleguide/ folder into any project; it runs with no build step and no external dependencies.
Built from css/tokens.css · css/shell.css · css/editor.css — author HTML against the classes documented above.
hung doan
component system