Skip to content

Commit 1a5a49a

Browse files
committed
docs(design): align the design system with the editor's new direction
ChoiceRow for fixed choices, named levels, free rows, no hint text under settings, candy timeline pills with the clip in green and zooms in blue. tokens/v4.css re-exports the app's tokens verbatim again.
1 parent 8b2a860 commit 1a5a49a

15 files changed

Lines changed: 396 additions & 213 deletions

File tree

‎design/DESIGN.md‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ REC / cut / skip / trim / transcript-highlight states; amber covers warnings.
7676
| `--fs-app-sm` | 12px | Secondary |
7777
| `--fs-app-lg` | 14px | Primary UI |
7878
| `--fs-title` | 16px | Panel titles |
79-
| `--fs-section` | 11px | Section headers (uppercase, tracked) |
79+
| `--fs-section` | 13px | Section labels: 600, sentence case (`.sectionLabel`) |
8080
| `--fs-display` | 24px | Hero numerals |
8181

8282
Nothing below 11px.
@@ -85,7 +85,7 @@ Nothing below 11px.
8585

8686
## Layout
8787

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

108118
---
109119

@@ -140,17 +150,16 @@ Nothing below 11px.
140150

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

150160
---
151161

152162
## Open questions / follow-ups
153163

154-
- `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.
155-
- Landing page (`openscreen-landing.html`) is light-only; consider a dark variant once product photography is sourced.
164+
- The token tables above predate `tokens/v4.css`; where they disagree, `tokens/v4.css` (and the app's `src/styles/design-tokens.css`) wins.
156165
- No logo asset is committed yet. Brand mark should be a simple wordmark or geometric mark in mint on the dark canvas.

‎design/SKILL.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ Quick orientation:
1414
- One brand hue: emerald `--accent`. Ration it. Timeline lanes add amber/orange/red semantic accents.
1515
- Components live in `components/` (forms, display, editor); full-screen recreations in `ui_kits/editor/`. Icons are Lucide.
1616
- No emoji, no gradients-as-decoration, no photographic backgrounds. Dense pro-tool density, glassy floating panels, hairline borders.
17+
- 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.
18+
- Timeline is "candy": the clip is the brand green, zooms blue, every lane solid with dark ink `#0b1220`; playhead violet; no seek bar.

‎design/_ds_manifest.json‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎design/components/editor/TimelinePill.jsx‎

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,55 @@
11
import React from 'react';
22

33
/**
4-
* OpenScreen TimelinePill — a labelled marker on a timeline lane.
5-
* tone maps to the four lane accents. `fixedWidth` (tag mode) sizes to
6-
* content; otherwise it spans a range via left/width percentages.
4+
* OpenScreen TimelinePill — a labelled effect on a timeline lane.
5+
* Source: `.lanePill` in src/components/ai-edition/v4/EditorShellV4.module.css.
6+
*
7+
* "Candy": a solid gradient in the lane colour with a glow under it (--candy-glow,
8+
* 12% light / 35% dark), no border, no gloss line, and dark ink #0b1220 on every
9+
* colour: the light lane colours cannot carry white. Zoom is blue (--zoom) so it never
10+
* reads as the recording clip, which owns the brand green (--clip).
11+
* `widthPct` spans a range; leave it null to size to the content.
712
*/
13+
const TONES = {
14+
zoom: 'var(--zoom)',
15+
annotation: 'var(--annotation)',
16+
speed: 'var(--speed)',
17+
trim: 'var(--danger)',
18+
danger: 'var(--danger)',
19+
camera: '#a855f7',
20+
};
21+
822
export function TimelinePill({
9-
tone = 'accent',
23+
tone = 'zoom',
1024
icon = null,
1125
children,
1226
leftPct = 0,
1327
widthPct = null,
28+
selected = false,
1429
style = {},
1530
}) {
16-
const map = {
17-
accent: { c: 'var(--accent)', w: 'var(--accent-soft)' },
18-
annotation: { c: 'var(--annotation)', w: 'var(--annotation-wash)' },
19-
speed: { c: 'var(--speed)', w: 'var(--speed-wash)' },
20-
danger: { c: 'var(--danger)', w: 'var(--danger-soft)' },
21-
};
22-
const t = map[tone] || map.accent;
31+
const c = TONES[tone] || TONES.zoom;
2332
return (
2433
<span
2534
style={{
2635
position: 'absolute',
2736
top: 1,
2837
left: `${leftPct}%`,
2938
width: widthPct == null ? 'max-content' : `${widthPct}%`,
39+
minWidth: 1,
3040
height: 22,
3141
display: 'inline-flex',
3242
alignItems: 'center',
3343
gap: 5,
34-
padding: '0 9px 0 7px',
35-
borderRadius: 6,
36-
border: `1.5px solid ${t.c}`,
37-
background: t.w,
38-
color: tone === 'danger' ? 'var(--danger)' : 'var(--fg)',
39-
fontSize: 11,
44+
padding: '0 8px 0 7px',
45+
borderRadius: 8,
46+
border: '1px solid transparent',
47+
background: `linear-gradient(180deg, color-mix(in oklab, ${c} 72%, white), ${c}) border-box`,
48+
boxShadow: selected
49+
? `0 0 0 2px var(--bg), 0 0 0 3.5px ${c}`
50+
: `0 2px 6px color-mix(in oklab, ${c} var(--candy-glow), transparent)`,
51+
color: '#0b1220',
52+
fontSize: 12,
4053
fontWeight: 600,
4154
whiteSpace: 'nowrap',
4255
overflow: 'hidden',

‎design/components/editor/editor.card.html‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
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>;
2323

2424
function Demo(){
25-
const [facet,setFacet]=React.useState('bg');
25+
const [facet,setFacet]=React.useState('fx');
2626
React.useEffect(()=>{ window.lucide && window.lucide.createIcons(); });
27-
const facets=[['bg','image'],['fx','sliders-horizontal'],['layout','layout'],['cursor','mouse-pointer-2'],['cap','captions'],['crop','crop']];
27+
const facets=[['fx','sliders-horizontal'],['layout','layout'],['cursor','mouse-pointer-2'],['cap','captions'],['crop','crop']];
2828
return (
2929
<React.Fragment>
3030
<div>
@@ -47,8 +47,8 @@
4747
</div>
4848
<div style={{position:'relative',height:24,borderRadius:6,background:'var(--surface-1)'}}>
4949
<TimelinePill tone="speed" leftPct={18} widthPct={12} icon={I('clock',11)}>1.5×</TimelinePill>
50-
<TimelinePill tone="danger" leftPct={40} widthPct={18}>0:37.3</TimelinePill>
51-
<TimelinePill tone="accent" leftPct={70} widthPct={10} icon={I('zoom-in',11)}>1.8×</TimelinePill>
50+
<TimelinePill tone="trim" leftPct={40} widthPct={18}>0:37.3</TimelinePill>
51+
<TimelinePill tone="zoom" leftPct={70} widthPct={10} icon={I('zoom-in',11)} selected>1.8×</TimelinePill>
5252
</div>
5353
</div>
5454
</div>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from 'react';
2+
3+
/**
4+
* OpenScreen ChoiceRow — a fixed choice picked in one click. Replaces a select or a
5+
* dropdown whenever the options are few and known (Format, frame style, theme, focus
6+
* mode, caption font, shadow and bounce levels…): every value stays visible.
7+
* Source: `ChoiceRow` in src/components/ai-edition/RightPanes.tsx, CSS
8+
* `.choiceRow / .choiceBtn / .choiceRowTiles` in NewEditorShell.module.css.
9+
*
10+
* Equal grid cells, 4px gap. The pressed option carries the app's one selected look:
11+
* accent border on the accent tint, fg text. `tiles` makes 44px cells for icons that
12+
* draw their choice. A value that matches no option (a stored level between two named
13+
* ones) presses nothing. Only a long list (the 15 translation targets) stays a Select.
14+
*
15+
* options: string | { value, label, icon? }; `null` leaves a hole in the grid.
16+
*/
17+
export function ChoiceRow({
18+
label,
19+
options = [],
20+
value,
21+
onChange,
22+
columns,
23+
tiles = false,
24+
disabled = false,
25+
style = {},
26+
}) {
27+
const norm = options.map((o) => (o == null || typeof o === 'object' ? o : { value: o, label: o }));
28+
return (
29+
<div style={{ display: 'grid', gap: 6, ...style }}>
30+
{label && <span style={{ font: '500 13px/1.3 var(--font-body)', color: 'var(--fg-2)' }}>{label}</span>}
31+
<div
32+
role="group"
33+
aria-label={label}
34+
style={{ display: 'grid', gap: 4, gridTemplateColumns: `repeat(${columns || norm.length}, minmax(0, 1fr))` }}
35+
>
36+
{norm.map((o, i) => {
37+
if (o == null) return <span key={`hole-${i}`} aria-hidden="true" />;
38+
const pressed = o.value === value;
39+
return (
40+
<button
41+
key={String(o.value)}
42+
type="button"
43+
aria-pressed={pressed}
44+
aria-label={o.icon && !tiles ? o.label : undefined}
45+
title={o.label}
46+
disabled={disabled}
47+
onClick={() => !pressed && onChange && onChange(o.value)}
48+
style={{
49+
minWidth: 0,
50+
minHeight: tiles ? 44 : 32,
51+
padding: '0 6px',
52+
display: 'inline-flex',
53+
flexDirection: 'column',
54+
alignItems: 'center',
55+
justifyContent: 'center',
56+
gap: 3,
57+
borderRadius: 'var(--r-md)',
58+
border: `1px solid ${pressed ? 'var(--accent)' : 'transparent'}`,
59+
background: pressed ? 'var(--accent-soft)' : 'color-mix(in oklab, var(--fg) 6%, transparent)',
60+
color: pressed ? 'var(--fg)' : 'var(--fg-2)',
61+
font: '500 12.5px/1.2 var(--font-body)',
62+
fontVariantNumeric: 'tabular-nums',
63+
whiteSpace: 'nowrap',
64+
overflow: 'hidden',
65+
textOverflow: 'ellipsis',
66+
cursor: disabled ? 'not-allowed' : 'pointer',
67+
opacity: disabled ? 0.5 : 1,
68+
transition: 'background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease)',
69+
}}
70+
>
71+
{o.icon}
72+
{o.icon ? (tiles && <span style={{ fontSize: 12 }}>{o.label}</span>) : o.label}
73+
</button>
74+
);
75+
})}
76+
</div>
77+
</div>
78+
);
79+
}

‎design/components/forms/Select.jsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from 'react';
22

33
/**
4-
* OpenScreen Select — native dropdown styled to match. Used for
5-
* caption style, layout preset, transcript language, etc.
4+
* OpenScreen Select — native dropdown styled to match. Only for a LONG list
5+
* (the 15 translation targets). A fixed choice of a few values is a ChoiceRow:
6+
* one click, every value visible (commit e4c56004).
67
*/
78
export function Select({
89
options = [],

‎design/components/forms/Slider.jsx‎

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import React from 'react';
22

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

2828
const input = (
@@ -33,25 +33,24 @@ export function Slider({
3333
step={step}
3434
value={value}
3535
onChange={(e) => onChange && onChange(Number(e.target.value))}
36-
style={trackStyle}
36+
style={{
37+
width: '100%',
38+
display: 'block',
39+
backgroundImage: `linear-gradient(var(--brand),var(--brand)), linear-gradient(var(--border-hi),var(--border-hi))`,
40+
backgroundSize: `${pct}% 6px, 100% 6px`,
41+
}}
3742
/>
3843
);
3944

40-
if (!card) return input;
45+
if (bare) return input;
4146

4247
return (
43-
<div
44-
style={{
45-
border: '1px solid var(--border)',
46-
borderRadius: 12,
47-
background: 'var(--surface)',
48-
padding: '11px 13px',
49-
...style,
50-
}}
51-
>
52-
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}>
53-
<span style={{ fontSize: 11.5, fontWeight: 500, color: 'var(--fg-2)' }}>{label}</span>
54-
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--accent)', marginLeft: 'auto' }}>{display}</span>
48+
<div style={{ display: 'grid', gap: 10, padding: '12px 0', ...style }}>
49+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, minHeight: 20 }}>
50+
<span style={{ font: '500 13px/1.3 var(--font-body)', color: 'var(--fg-2)', marginRight: 'auto' }}>{label}</span>
51+
{display != null && (
52+
<span style={{ font: '500 12px/1.3 var(--font-body)', fontVariantNumeric: 'tabular-nums', color: 'var(--muted)' }}>{display}</span>
53+
)}
5554
</div>
5655
{input}
5756
</div>

‎design/components/forms/forms.card.html‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- @dsCard group="Components" viewport="700x430" subtitle="Button · IconButton · SegmentedControl · Switch · Slider · Select · TextField" name="Forms" -->
1+
<!-- @dsCard group="Components" viewport="700x520" subtitle="Button · IconButton · SegmentedControl · ChoiceRow · Switch · Slider · Select · TextField" name="Forms" -->
22
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
33
<link rel="stylesheet" href="../../styles.css">
44
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
@@ -23,13 +23,16 @@
2323
}
2424
const Button = pick('Button'), IconButton = pick('IconButton'),
2525
SegmentedControl = pick('SegmentedControl'), Switch = pick('Switch'),
26-
Slider = pick('Slider'), Select = pick('Select'), TextField = pick('TextField');
26+
Slider = pick('Slider'), Select = pick('Select'), TextField = pick('TextField'),
27+
ChoiceRow = pick('ChoiceRow');
2728
const I = (n, s=15) => <i data-lucide={n} style={{width:s,height:s}}></i>;
2829

2930
function Demo(){
3031
const [mode,setMode] = React.useState('Edit');
3132
const [on,setOn] = React.useState(true);
32-
const [sh,setSh] = React.useState(24);
33+
const [pad,setPad] = React.useState(24);
34+
const [sh,setSh] = React.useState(0.6);
35+
const [fmt,setFmt] = React.useState('16:9');
3336
const [q,setQ] = React.useState('');
3437
React.useEffect(()=>{ window.lucide && window.lucide.createIcons(); });
3538
return (
@@ -50,17 +53,22 @@
5053
<SegmentedControl options={['Media','Edit','Rec']} value={mode} onChange={setMode} />
5154
<SegmentedControl size="sm" options={['Screen','Window']} value={mode==='Rec'?'Window':'Screen'} onChange={()=>{}} />
5255
</div>
56+
<div className="grp"><span className="cap">CHOICEROW · a fixed choice, one click</span>
57+
<div className="col"><ChoiceRow label="Format" options={['Auto','16:9','9:16','1:1']} value={fmt} onChange={setFmt} /></div>
58+
<div className="col"><ChoiceRow label="Shadow" value={sh} onChange={setSh}
59+
options={[{value:0,label:'None'},{value:0.3,label:'Light'},{value:0.6,label:'Medium'},{value:0.9,label:'Strong'}]} /></div>
60+
</div>
5361
<div className="grp"><span className="cap">SWITCH</span>
5462
<Switch checked={on} onChange={setOn} />
5563
<Switch checked={!on} onChange={()=>setOn(!on)} />
5664
<span style={{fontSize:13,color:'var(--fg-2)'}}>Blur background</span>
5765
</div>
5866
<div className="grp">
5967
<div className="col"><span className="cap">SLIDER</span>
60-
<Slider label="Shadow" value={sh} onChange={setSh} format={v=>`${v}%`} />
68+
<Slider label="Padding" value={pad} onChange={setPad} format={v=>`${v}%`} />
6169
</div>
62-
<div className="col"><span className="cap">SELECT</span>
63-
<Select options={['Bold Center','Minimal Bottom','Karaoke','Boxed']} value="Bold Center" onChange={()=>{}} />
70+
<div className="col"><span className="cap">SELECT · long lists only</span>
71+
<Select options={['English','French','German','Spanish','Italian','Portuguese','Japanese']} value="French" onChange={()=>{}} />
6472
</div>
6573
<div className="col"><span className="cap">TEXTFIELD</span>
6674
<TextField leadingIcon={I('search')} placeholder="Search media…" value={q} onChange={setQ} />

0 commit comments

Comments
 (0)