Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions design/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ REC / cut / skip / trim / transcript-highlight states; amber covers warnings.
| `--fs-app-sm` | 12px | Secondary |
| `--fs-app-lg` | 14px | Primary UI |
| `--fs-title` | 16px | Panel titles |
| `--fs-section` | 11px | Section headers (uppercase, tracked) |
| `--fs-section` | 13px | Section labels: 600, sentence case (`.sectionLabel`) |
| `--fs-display` | 24px | Hero numerals |

Nothing below 11px.
Expand All @@ -85,7 +85,7 @@ Nothing below 11px.

## Layout

- **Radius:** 8px standard; `--r-xs: 4px`, `--r-sm: 6px`, `--r-md: 8px`, `--r-lg: 12px`, `--r-pill: 9999px`
- **Radius:** `--r-xs: 4px`, `--r-sm: 8px`, `--r-md: 10px`, `--r-lg: 12px`, `--r-pill: 9999px` (see `tokens/v4.css`)
- **Border weight:** 1px
- **Spacing:** 4px baseline grid (`--sp-1` through `--sp-6`)
- **Editor wireframe:**
Expand All @@ -104,6 +104,16 @@ Nothing below 11px.
- **Off-white canvas, tiered surfaces.** Never use pure `#000` or pure `#fff` for editorial chrome.
- **Soft elevation, slate-based shadows.** Two `--elev-card` and `--elev-pop` are enough.
- **System-ui type, mono numerics.** No web fonts loaded; ships the declared fallback stack.
- **One click for a fixed choice.** A ChoiceRow of equal buttons, never a dropdown; a select only
for a long list (15 translation targets).
- **Named levels over meaningless numbers** (shadow None/Light/Medium/Strong); sliders only for
continuous amounts.
- **Free rows, no hints.** Settings are rows grouped by sentence-case section labels, not cards.
Nothing under a setting explains it; an option that does nothing right now is hidden.
- **Candy timeline.** The clip is the brand green, zooms blue, every lane solid with dark ink
`#0b1220`. The playhead is violet. No transport seek bar: the ruler seeks.

The full rule set, with sources in the app, is **EDITOR CONTROLS** in `readme.md`.

---

Expand Down Expand Up @@ -140,17 +150,16 @@ Nothing below 11px.

| File | Role |
|---|---|
| `openscreen-editor-2.html` | Latest editor (light + dark themes in one file). Canonical source of tokens. |
| `openscreen-editor.html` | First editor pass. Light-only. |
| `editor.html` | Earlier experimental editor with red REC accent. |
| `openscreen-landing.html` | Marketing landing page. |
| `index.html` | Launcher / overview. |
| `openscreen-editor-v2.html` | Later editor pass (bundled page, light + dark). Historical. |
| `openscreen-editor.html` | First editor pass. Its timeline, inspector rows and pills are kept in line with the app's direction. |
| `openscreen-widget.html` | Recording HUD widget (bundled page). |
| `tokens/v4.css` | The shipped token vocabulary, a re-export of `src/styles/design-tokens.css`. Canonical. |
| `readme.md` | System overview, foundations and **EDITOR CONTROLS**. |
| `DESIGN.md` | This document. |

---

## Open questions / follow-ups

- `editor.html` uses a different red-dominant accent and predates the mint brand color. Keep as historical reference; do not import its tokens into new work.
- Landing page (`openscreen-landing.html`) is light-only; consider a dark variant once product photography is sourced.
- The token tables above predate `tokens/v4.css`; where they disagree, `tokens/v4.css` (and the app's `src/styles/design-tokens.css`) wins.
- No logo asset is committed yet. Brand mark should be a simple wordmark or geometric mark in mint on the dark canvas.
2 changes: 2 additions & 0 deletions design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ Quick orientation:
- One brand hue: emerald `--accent`. Ration it. Timeline lanes add amber/orange/red semantic accents.
- Components live in `components/` (forms, display, editor); full-screen recreations in `ui_kits/editor/`. Icons are Lucide.
- No emoji, no gradients-as-decoration, no photographic backgrounds. Dense pro-tool density, glassy floating panels, hairline borders.
- Editor controls (full rules: "EDITOR CONTROLS" in readme.md): a fixed choice is a one-click `ChoiceRow`, not a select; named levels instead of meaningless numbers; settings are free rows under sentence-case section labels, never cards; no hint text, and an option that does nothing is hidden, not disabled.
- Timeline is "candy": the clip is the brand green, zooms blue, every lane solid with dark ink `#0b1220`; playhead violet; no seek bar.
2 changes: 1 addition & 1 deletion design/_ds_manifest.json

Large diffs are not rendered by default.

47 changes: 30 additions & 17 deletions design/components/editor/TimelinePill.jsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,55 @@
import React from 'react';

/**
* OpenScreen TimelinePill — a labelled marker on a timeline lane.
* tone maps to the four lane accents. `fixedWidth` (tag mode) sizes to
* content; otherwise it spans a range via left/width percentages.
* OpenScreen TimelinePill — a labelled effect on a timeline lane.
* Source: `.lanePill` in src/components/ai-edition/v4/EditorShellV4.module.css.
*
* "Candy": a solid gradient in the lane colour with a glow under it (--candy-glow,
* 12% light / 35% dark), no border, no gloss line, and dark ink #0b1220 on every
* colour: the light lane colours cannot carry white. Zoom is blue (--zoom) so it never
* reads as the recording clip, which owns the brand green (--clip).
* `widthPct` spans a range; leave it null to size to the content.
*/
const TONES = {
zoom: 'var(--zoom)',
annotation: 'var(--annotation)',
speed: 'var(--speed)',
trim: 'var(--danger)',
danger: 'var(--danger)',
camera: '#a855f7',
};

export function TimelinePill({
tone = 'accent',
tone = 'zoom',
icon = null,
children,
leftPct = 0,
widthPct = null,
selected = false,
style = {},
}) {
const map = {
accent: { c: 'var(--accent)', w: 'var(--accent-soft)' },
annotation: { c: 'var(--annotation)', w: 'var(--annotation-wash)' },
speed: { c: 'var(--speed)', w: 'var(--speed-wash)' },
danger: { c: 'var(--danger)', w: 'var(--danger-soft)' },
};
const t = map[tone] || map.accent;
const c = TONES[tone] || TONES.zoom;
return (
<span
style={{
position: 'absolute',
top: 1,
left: `${leftPct}%`,
width: widthPct == null ? 'max-content' : `${widthPct}%`,
minWidth: 1,
height: 22,
display: 'inline-flex',
alignItems: 'center',
gap: 5,
padding: '0 9px 0 7px',
borderRadius: 6,
border: `1.5px solid ${t.c}`,
background: t.w,
color: tone === 'danger' ? 'var(--danger)' : 'var(--fg)',
fontSize: 11,
padding: '0 8px 0 7px',
borderRadius: 8,
border: '1px solid transparent',
background: `linear-gradient(180deg, color-mix(in oklab, ${c} 72%, white), ${c}) border-box`,
boxShadow: selected
? `0 0 0 2px var(--bg), 0 0 0 3.5px ${c}`
: `0 2px 6px color-mix(in oklab, ${c} var(--candy-glow), transparent)`,
color: '#0b1220',
fontSize: 12,
fontWeight: 600,
whiteSpace: 'nowrap',
overflow: 'hidden',
Expand Down
8 changes: 4 additions & 4 deletions design/components/editor/editor.card.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
const Bot=<svg width="15" height="15" viewBox="0 0 16 16" shapeRendering="crispEdges"><rect x="7" y="0" width="2" height="2" fill="currentColor"/><rect x="3" y="3" width="10" height="9" rx="1" fill="none" stroke="currentColor" strokeWidth="1.4"/><rect x="5" y="6" width="2" height="2" fill="currentColor"/><rect x="9" y="6" width="2" height="2" fill="currentColor"/><rect x="5.5" y="9.5" width="5" height="1.4" fill="currentColor"/></svg>;

function Demo(){
const [facet,setFacet]=React.useState('bg');
const [facet,setFacet]=React.useState('fx');
React.useEffect(()=>{ window.lucide && window.lucide.createIcons(); });
const facets=[['bg','image'],['fx','sliders-horizontal'],['layout','layout'],['cursor','mouse-pointer-2'],['cap','captions'],['crop','crop']];
const facets=[['fx','sliders-horizontal'],['layout','layout'],['cursor','mouse-pointer-2'],['cap','captions'],['crop','crop']];
return (
<React.Fragment>
<div>
Expand All @@ -47,8 +47,8 @@
</div>
<div style={{position:'relative',height:24,borderRadius:6,background:'var(--surface-1)'}}>
<TimelinePill tone="speed" leftPct={18} widthPct={12} icon={I('clock',11)}>1.5×</TimelinePill>
<TimelinePill tone="danger" leftPct={40} widthPct={18}>0:37.3</TimelinePill>
<TimelinePill tone="accent" leftPct={70} widthPct={10} icon={I('zoom-in',11)}>1.8×</TimelinePill>
<TimelinePill tone="trim" leftPct={40} widthPct={18}>0:37.3</TimelinePill>
<TimelinePill tone="zoom" leftPct={70} widthPct={10} icon={I('zoom-in',11)} selected>1.8×</TimelinePill>
</div>
</div>
</div>
Expand Down
79 changes: 79 additions & 0 deletions design/components/forms/ChoiceRow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';

/**
* OpenScreen ChoiceRow — a fixed choice picked in one click. Replaces a select or a
* dropdown whenever the options are few and known (Format, frame style, theme, focus
* mode, caption font, shadow and bounce levels…): every value stays visible.
* Source: `ChoiceRow` in src/components/ai-edition/RightPanes.tsx, CSS
* `.choiceRow / .choiceBtn / .choiceRowTiles` in NewEditorShell.module.css.
*
* Equal grid cells, 4px gap. The pressed option carries the app's one selected look:
* accent border on the accent tint, fg text. `tiles` makes 44px cells for icons that
* draw their choice. A value that matches no option (a stored level between two named
* ones) presses nothing. Only a long list (the 15 translation targets) stays a Select.
*
* options: string | { value, label, icon? }; `null` leaves a hole in the grid.
*/
export function ChoiceRow({
label,
options = [],
value,
onChange,
columns,
tiles = false,
disabled = false,
style = {},
}) {
const norm = options.map((o) => (o == null || typeof o === 'object' ? o : { value: o, label: o }));
return (
<div style={{ display: 'grid', gap: 6, ...style }}>
{label && <span style={{ font: '500 13px/1.3 var(--font-body)', color: 'var(--fg-2)' }}>{label}</span>}
<div
role="group"
aria-label={label}
style={{ display: 'grid', gap: 4, gridTemplateColumns: `repeat(${columns || norm.length}, minmax(0, 1fr))` }}
>
{norm.map((o, i) => {
if (o == null) return <span key={`hole-${i}`} aria-hidden="true" />;
const pressed = o.value === value;
return (
<button
key={String(o.value)}
type="button"
aria-pressed={pressed}
aria-label={o.icon && !tiles ? o.label : undefined}
title={o.label}
disabled={disabled}
onClick={() => !pressed && onChange && onChange(o.value)}
style={{
minWidth: 0,
minHeight: tiles ? 44 : 32,
padding: '0 6px',
display: 'inline-flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 3,
borderRadius: 'var(--r-md)',
border: `1px solid ${pressed ? 'var(--accent)' : 'transparent'}`,
background: pressed ? 'var(--accent-soft)' : 'color-mix(in oklab, var(--fg) 6%, transparent)',
color: pressed ? 'var(--fg)' : 'var(--fg-2)',
font: '500 12.5px/1.2 var(--font-body)',
fontVariantNumeric: 'tabular-nums',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
transition: 'background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease)',
}}
>
{o.icon}
{o.icon ? (tiles && <span style={{ fontSize: 12 }}>{o.label}</span>) : o.label}
</button>
);
})}
</div>
</div>
);
}
5 changes: 3 additions & 2 deletions design/components/forms/Select.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';

/**
* OpenScreen Select — native dropdown styled to match. Used for
* caption style, layout preset, transcript language, etc.
* OpenScreen Select — native dropdown styled to match. Only for a LONG list
* (the 15 translation targets). A fixed choice of a few values is a ChoiceRow:
* one click, every value visible (commit e4c56004).
*/
export function Select({
options = [],
Expand Down
47 changes: 23 additions & 24 deletions design/components/forms/Slider.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import React from 'react';

/**
* OpenScreen Slider — the labelled range control inside inspector cards.
* Renders the whole card: label (left) + mono value in emerald (right)
* above a filled range track. Pass `card={false}` for a bare track.
* OpenScreen Slider — a continuous amount (padding, roundness, blur 0–100 %, cursor
* size). Source: `.sliderCell` in src/components/ai-edition/NewEditorShell.module.css.
*
* A free row, never a card: label left, the value right in --muted body text (not mint,
* not mono), a 6px track filled in the brand colour. When the number has no unit (a
* cursor size of "30.0") show no value at all: `format` returns null and the track's
* position is the value. When the number means nothing to a user (shadow, click
* bounce), use a ChoiceRow of named levels instead of a slider.
* Pass `bare` for the track alone.
*/
export function Slider({
label,
Expand All @@ -13,16 +19,10 @@ export function Slider({
step = 1,
format,
onChange,
card = true,
bare = false,
style = {},
}) {
const pct = ((value - min) / (max - min)) * 100;
const trackStyle = {
width: '100%',
display: 'block',
backgroundImage: `linear-gradient(var(--accent),var(--accent)), linear-gradient(var(--surface-3),var(--surface-3))`,
backgroundSize: `${pct}% 5px, 100% 5px`,
};
const display = format ? format(value) : value;

const input = (
Expand All @@ -33,25 +33,24 @@ export function Slider({
step={step}
value={value}
onChange={(e) => onChange && onChange(Number(e.target.value))}
style={trackStyle}
style={{
width: '100%',
display: 'block',
backgroundImage: `linear-gradient(var(--brand),var(--brand)), linear-gradient(var(--border-hi),var(--border-hi))`,
backgroundSize: `${pct}% 6px, 100% 6px`,
}}
/>
);

if (!card) return input;
if (bare) return input;

return (
<div
style={{
border: '1px solid var(--border)',
borderRadius: 12,
background: 'var(--surface)',
padding: '11px 13px',
...style,
}}
>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}>
<span style={{ fontSize: 11.5, fontWeight: 500, color: 'var(--fg-2)' }}>{label}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--accent)', marginLeft: 'auto' }}>{display}</span>
<div style={{ display: 'grid', gap: 10, padding: '12px 0', ...style }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, minHeight: 20 }}>
<span style={{ font: '500 13px/1.3 var(--font-body)', color: 'var(--fg-2)', marginRight: 'auto' }}>{label}</span>
{display != null && (
<span style={{ font: '500 12px/1.3 var(--font-body)', fontVariantNumeric: 'tabular-nums', color: 'var(--muted)' }}>{display}</span>
)}
</div>
{input}
</div>
Expand Down
20 changes: 14 additions & 6 deletions design/components/forms/forms.card.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- @dsCard group="Components" viewport="700x430" subtitle="Button · IconButton · SegmentedControl · Switch · Slider · Select · TextField" name="Forms" -->
<!-- @dsCard group="Components" viewport="700x520" subtitle="Button · IconButton · SegmentedControl · ChoiceRow · Switch · Slider · Select · TextField" name="Forms" -->
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
<link rel="stylesheet" href="../../styles.css">
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
Expand All @@ -23,13 +23,16 @@
}
const Button = pick('Button'), IconButton = pick('IconButton'),
SegmentedControl = pick('SegmentedControl'), Switch = pick('Switch'),
Slider = pick('Slider'), Select = pick('Select'), TextField = pick('TextField');
Slider = pick('Slider'), Select = pick('Select'), TextField = pick('TextField'),
ChoiceRow = pick('ChoiceRow');
const I = (n, s=15) => <i data-lucide={n} style={{width:s,height:s}}></i>;

function Demo(){
const [mode,setMode] = React.useState('Edit');
const [on,setOn] = React.useState(true);
const [sh,setSh] = React.useState(24);
const [pad,setPad] = React.useState(24);
const [sh,setSh] = React.useState(0.6);
const [fmt,setFmt] = React.useState('16:9');
const [q,setQ] = React.useState('');
React.useEffect(()=>{ window.lucide && window.lucide.createIcons(); });
return (
Expand All @@ -50,17 +53,22 @@
<SegmentedControl options={['Media','Edit','Rec']} value={mode} onChange={setMode} />
<SegmentedControl size="sm" options={['Screen','Window']} value={mode==='Rec'?'Window':'Screen'} onChange={()=>{}} />
</div>
<div className="grp"><span className="cap">CHOICEROW · a fixed choice, one click</span>
<div className="col"><ChoiceRow label="Format" options={['Auto','16:9','9:16','1:1']} value={fmt} onChange={setFmt} /></div>
<div className="col"><ChoiceRow label="Shadow" value={sh} onChange={setSh}
options={[{value:0,label:'None'},{value:0.3,label:'Light'},{value:0.6,label:'Medium'},{value:0.9,label:'Strong'}]} /></div>
</div>
<div className="grp"><span className="cap">SWITCH</span>
<Switch checked={on} onChange={setOn} />
<Switch checked={!on} onChange={()=>setOn(!on)} />
<span style={{fontSize:13,color:'var(--fg-2)'}}>Blur background</span>
</div>
<div className="grp">
<div className="col"><span className="cap">SLIDER</span>
<Slider label="Shadow" value={sh} onChange={setSh} format={v=>`${v}%`} />
<Slider label="Padding" value={pad} onChange={setPad} format={v=>`${v}%`} />
</div>
<div className="col"><span className="cap">SELECT</span>
<Select options={['Bold Center','Minimal Bottom','Karaoke','Boxed']} value="Bold Center" onChange={()=>{}} />
<div className="col"><span className="cap">SELECT · long lists only</span>
<Select options={['English','French','German','Spanish','Italian','Portuguese','Japanese']} value="French" onChange={()=>{}} />
</div>
<div className="col"><span className="cap">TEXTFIELD</span>
<TextField leadingIcon={I('search')} placeholder="Search media…" value={q} onChange={setQ} />
Expand Down
Loading
Loading