Files
sst/docs/design/tokens-preview.html
T

381 lines
40 KiB
HTML

<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sst Ontwerp-tokens</title>
<style>
* { box-sizing: border-box; margin: 0; }
html { -webkit-font-smoothing: antialiased; }
body { background: var(--c-surface-chassis); color: var(--c-ink-primary); font: 13px/18px var(--f-text); min-height: 100vh; }
.wrap { max-width: 1180px; margin: 0 auto; padding: var(--s-7) var(--s-5) 96px; }
.brushed { background-image: var(--tex-brushed), linear-gradient(180deg, var(--c-surface-metalHigh), var(--c-surface-metalLow)); }
/* silkscreen */
.silk { font: 500 10px/12px var(--f-chrome); letter-spacing: .09em; text-transform: uppercase; color: var(--c-ink-tertiary); }
.silk-s { font: 500 9px/11px var(--f-chrome); letter-spacing: .10em; text-transform: uppercase; color: var(--c-ink-tertiary); }
.head { font: 500 12px/16px var(--f-chrome); letter-spacing: .12em; text-transform: uppercase; color: var(--c-ink-primary); }
.mono { font: 500 12px/16px var(--f-mono); font-variant-numeric: tabular-nums; }
/* masthead */
.mast { display: flex; align-items: stretch; margin-bottom: var(--s-6); background: var(--c-surface-panel); box-shadow: var(--sh-panel); border-top: 1px solid var(--c-line-seamDark); }
.mast .id { padding: var(--s-5); flex: 1; }
.mast h1 { font: 500 22px/26px var(--f-chrome); letter-spacing: .12em; text-transform: uppercase; margin-top: var(--s-2); }
.mast p { color: var(--c-ink-secondary); max-width: 62ch; margin-top: var(--s-3); }
.mast .stamp { padding: var(--s-5); border-left: 2px solid var(--c-line-seamDark); box-shadow: -1px 0 0 var(--c-line-seamLight); display: flex; flex-direction: column; justify-content: center; gap: var(--s-3); min-width: 220px; }
.stamp .row { display: flex; align-items: center; gap: var(--s-3); }
section { margin-top: var(--s-7); }
section > .sec-head { display: flex; align-items: baseline; gap: var(--s-4); margin-bottom: var(--s-4); padding-bottom: var(--s-3); border-bottom: 1px solid var(--c-line-seamDark); box-shadow: 0 1px 0 var(--c-line-seamLight); }
.sec-head .no { font: 500 12px var(--f-mono); color: var(--c-ink-tertiary); }
.sec-head .tk { margin-left: auto; font: 11px var(--f-mono); color: var(--c-ink-tertiary); }
.two { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-5); }
@media (max-width: 860px) { .two { grid-template-columns: 1fr; } .mast { flex-direction: column; } .mast .stamp { border-left: 0; border-top: 2px solid var(--c-line-seamDark); } }
/* a finish is a complete, scoped set of variables */
.finish { background: var(--c-surface-chassis); color: var(--c-ink-primary); padding: var(--s-5); border: 1px solid var(--c-line-seamDark); }
.finish.brushed { background-image: var(--tex-brushed), linear-gradient(180deg, var(--c-surface-metalHigh), var(--c-surface-metalLow)); }
.finish > .silk.top { display: flex; justify-content: space-between; margin-bottom: var(--s-4); }
/* swatches */
.sw-group { margin-bottom: var(--s-4); }
.sw-group .silk { margin-bottom: var(--s-2); display: block; }
.sw { display: grid; grid-template-columns: 28px 1fr auto; align-items: center; gap: var(--s-3); padding: 3px 0; border-bottom: 1px solid var(--c-line-hairline); }
.sw i { width: 28px; height: 18px; border: 1px solid var(--c-line-seamDark); display: block; }
.sw b { font: 500 11px var(--f-mono); }
.sw span { font: 11px var(--f-mono); color: var(--c-ink-tertiary); }
/* type */
.ty { display: grid; grid-template-columns: 150px 1fr; gap: var(--s-4); padding: var(--s-3) 0; border-bottom: 1px solid var(--c-line-hairline); align-items: baseline; }
.ty .spec { font: 11px/15px var(--f-mono); color: var(--c-ink-tertiary); }
.ty .spec b { color: var(--c-ink-primary); display: block; font-weight: 500; }
/* spacing / radius / borders */
.spc { display: flex; align-items: flex-end; gap: var(--s-5); flex-wrap: wrap; }
.spc div { text-align: center; } .spc i { display: block; background: var(--c-ink-primary); height: 14px; margin-bottom: var(--s-2); }
.rad { display: flex; gap: var(--s-5); flex-wrap: wrap; }
.rad div { text-align: center; } .rad i { display: block; width: 56px; height: 56px; background: var(--c-surface-panel); border: 1px solid var(--c-line-seamDark); margin-bottom: var(--s-2); }
/* parts */
.panel { background: var(--c-surface-panel); box-shadow: var(--sh-panel); border-top: 1px solid var(--c-line-seamDark); padding: var(--s-5); }
.well { background: var(--c-surface-well); box-shadow: var(--sh-well); padding: var(--s-4); }
.seam { border-top: 1px solid var(--c-line-seamDark); border-bottom: 1px solid var(--c-line-seamLight); height: 2px; }
.key { display: inline-flex; align-items: center; justify-content: center; gap: var(--s-3); height: var(--z-control-height); min-width: var(--z-key-minWidth); padding: 0 var(--s-4);
font: 500 11px/14px var(--f-chrome); letter-spacing: .06em; text-transform: uppercase; color: var(--c-ink-primary); cursor: pointer; user-select: none;
background: linear-gradient(180deg, var(--c-surface-keyHigh), var(--c-surface-keyLow)); border: 1px solid var(--c-line-seamDark); border-radius: var(--r-sm);
box-shadow: var(--sh-key-raised); transition: transform var(--m-key-release) var(--e-mechanical), box-shadow var(--m-key-release) var(--e-mechanical), background var(--m-key-release) var(--e-mechanical); }
.key:active, .key.down { background: var(--c-surface-keyPressed); box-shadow: var(--sh-key-pressed); transform: translateY(1px); transition-duration: var(--m-key-press); }
.key.rec { min-width: var(--z-key-recWidth); height: var(--z-key-recHeight); background: linear-gradient(180deg, var(--c-accent-record), var(--c-accent-record)); color: var(--c-ink-inverse); }
.key.rec.down { filter: brightness(.88); }
.key:focus-visible { outline: var(--b-focus) solid var(--c-line-focus); outline-offset: 2px; }
.lamp { width: var(--z-lamp-diameter); height: var(--z-lamp-diameter); border-radius: var(--r-lamp); background: radial-gradient(circle at 35% 30%, rgba(255,255,255,.55), rgba(255,255,255,0) 38%), var(--c-accent-recordUnlit);
border: 1px solid var(--c-line-seamDark); transition: background var(--m-lamp-off), box-shadow var(--m-lamp-off); flex: none; }
.lamp.on { background: radial-gradient(circle at 35% 30%, rgba(255,255,255,.6), rgba(255,255,255,0) 38%), var(--c-accent-record); box-shadow: var(--sh-lamp-on); transition-duration: var(--m-lamp-on); }
.lamp.big { width: var(--z-lamp-large); height: var(--z-lamp-large); }
.lamp.amber.on { background: radial-gradient(circle at 35% 30%, rgba(255,255,255,.6), rgba(255,255,255,0) 38%), var(--c-level-amber); box-shadow: none; }
.lamp.amber { background: radial-gradient(circle at 35% 30%, rgba(255,255,255,.3), rgba(255,255,255,0) 38%), var(--c-level-amberUnlit); }
.stack { display: flex; gap: var(--s-4); flex-wrap: wrap; align-items: center; }
.stack.col { flex-direction: column; align-items: stretch; }
.cap { margin-top: var(--s-2); }
/* paper list */
.paper { background: var(--c-surface-paper); box-shadow: var(--sh-well); }
.paper .r { display: grid; grid-template-columns: 76px 1fr; gap: var(--s-4); padding: var(--s-3) var(--s-4); min-height: var(--z-row-height); border-bottom: 1px solid var(--c-line-hairline); align-items: baseline; }
.paper .r.sel { background: var(--c-surface-selection); }
.paper .t { font: 500 12px/16px var(--f-mono); color: var(--c-ink-secondary); font-variant-numeric: tabular-nums; }
.paper .x { font: 15px/22px var(--f-text); }
mark { background: color-mix(in srgb, var(--c-level-amber) calc(var(--o-mark) * 100%), transparent); color: inherit; padding: 0 1px; }
mark s { color: var(--c-ink-tertiary); text-decoration-thickness: 1px; margin-right: 4px; }
/* instruments */
.rig { display: grid; grid-template-columns: auto 1fr; gap: var(--s-5); align-items: start; }
@media (max-width: 640px) { .rig { grid-template-columns: 1fr; } }
.vu { width: var(--m-vu-width); max-width: 100%; display: block; border-radius: var(--r-bezel); border: var(--b-bezel) solid var(--c-line-seamDark); box-shadow: var(--sh-well), 0 1px 0 var(--c-line-seamLight); background: var(--c-surface-meterFace); }
.display { background: var(--c-surface-display); box-shadow: var(--sh-well); padding: var(--s-3) var(--s-4); display: inline-block; }
.leds { display: flex; gap: var(--m-led-gap); }
.leds i { width: 6px; height: 14px; background: var(--c-level-greenUnlit); border-radius: var(--r-sharp); display: block; }
.leds i.g.on { background: var(--c-level-green); } .leds i.a { background: var(--c-level-amberUnlit); } .leds i.a.on { background: var(--c-level-amber); }
.leds i.r { background: var(--c-accent-recordUnlit); } .leds i.r.on { background: var(--c-accent-record); }
ul.rules { list-style: none; padding: 0; display: grid; gap: var(--s-3); max-width: 70ch; }
ul.rules li { padding-left: var(--s-5); position: relative; color: var(--c-ink-secondary); }
ul.rules li::before { content: ""; position: absolute; left: 0; top: 8px; width: 8px; height: 1px; background: var(--c-ink-primary); }
@media (prefers-reduced-motion: reduce) { * { transition-duration: 0ms !important; } }
</style>
</head>
<body>
<script id="tokens" type="application/json">{"meta": {"name": "Sst Design Tokens", "version": "0.1.0-draft", "status": "Ter goedkeuring — nog niet gebruikt door de app", "concept": "Draagbare veldrecorder / cassettedeck uit de jaren 80 (Sony TC-D5, Marantz PMD, Nakamichi, Braun). Twee afwerkingen: ALUMINIUM (licht) en GRAPHITE (donker). Eén accent: het rode opnamelampje. Amber en groen alleen voor niveau-indicatoren."}, "color": {"surface.chassis": {"light": "#CBC8C0", "dark": "#1C1B19", "use": "Venster-achtergrond; het 'huis' van het apparaat"}, "surface.metalHigh": {"light": "#E3E1DB", "dark": "#3B3A36", "use": "Geborsteld metaal, bovenkant van de verloop"}, "surface.metalLow": {"light": "#BDBAB2", "dark": "#262522", "use": "Geborsteld metaal, onderkant van de verloop"}, "surface.panel": {"light": "#E2DFD5", "dark": "#2B2A27", "use": "Frontplaat van mat plastic; zijbalk en bedieningspanelen"}, "surface.well": {"light": "#B4B1A8", "dark": "#131211", "use": "Verzonken vlak: invoervelden, display-vensters"}, "surface.paper": {"light": "#EEE9D9", "dark": "#232220", "use": "Cassette-label crème: lijsten met transcripties en woordenboek"}, "surface.keyHigh": {"light": "#E6E3DA", "dark": "#44433F", "use": "Toetsdop, bovenkant"}, "surface.keyLow": {"light": "#CBC8BF", "dark": "#33322F", "use": "Toetsdop, onderkant"}, "surface.keyPressed": {"light": "#BEBBB2", "dark": "#2A2927", "use": "Ingedrukte toets"}, "surface.meterFace": {"light": "#EFE6C8", "dark": "#D8CEA6", "use": "Wijzerplaat van de VU-meter (blijft crème in beide afwerkingen)"}, "surface.selection": {"light": "#D4CDB2", "dark": "#3A3828", "use": "Geselecteerde rij in een lijst"}, "surface.display": {"light": "#1B1A18", "dark": "#0E0D0C", "use": "Displayglas van de teller; donker in beide afwerkingen"}, "display.segmentUnlit": {"light": "#3A3320", "dark": "#2A2415", "use": "Gedoofd segment op het displayglas"}, "ink.primary": {"light": "#1E1D1B", "dark": "#ECE8DC", "use": "Hoofdtekst en wijzer"}, "ink.secondary": {"light": "#403D38", "dark": "#C4BFB2", "use": "Secundaire tekst"}, "ink.tertiary": {"light": "#4D4A44", "dark": "#ABA79B", "use": "Zeefdruk-labels en hulptekst"}, "ink.disabled": {"light": "#8F8B82", "dark": "#6B685F", "use": "Uitgeschakeld; nooit voor essentiële tekst"}, "ink.inverse": {"light": "#F4EFE3", "dark": "#F4EFE3", "use": "Tekst op rode of donkere vulling; crème in beide afwerkingen"}, "ink.onMeter": {"light": "#1E1D1B", "dark": "#1E1D1B", "use": "Schaal, cijfers en wijzer op de wijzerplaat; vast donker in beide afwerkingen"}, "accent.record": {"light": "#C8281E", "dark": "#CC2A20", "use": "ENIGE accent: opnamelampje, REC-toets, piek-LED. Nooit voor lopende tekst"}, "accent.recordUnlit": {"light": "#7A2E28", "dark": "#4A1F1B", "use": "Lampje uit"}, "level.green": {"light": "#5E9E48", "dark": "#6DB556", "use": "Niveau-segment, brandend"}, "level.amber": {"light": "#D9A023", "dark": "#E5B032", "use": "Niveau-segment, waarschuwing, gecorrigeerd woord"}, "level.greenUnlit": {"light": "#A8B79C", "dark": "#26331F", "use": "Groen segment, uit"}, "level.amberUnlit": {"light": "#CDBA8A", "dark": "#3D3418", "use": "Amber segment, uit"}, "line.seamDark": {"light": "#8C8981", "dark": "#0A0A09", "use": "Naad tussen panelen, donkere helft"}, "line.seamLight": {"light": "#F3F1EB", "dark": "#45433F", "use": "Naad tussen panelen, lichte helft"}, "line.hairline": {"light": "#A9A69D", "dark": "#3A3834", "use": "Fijne scheidingslijn, zeefdruk-kader"}, "line.focus": {"light": "#1E1D1B", "dark": "#ECE8DC", "use": "Toetsenbord-focus"}}, "opacity": {"mark": {"value": 0.3, "use": "Amber markering achter een gecorrigeerd woord"}, "disabled": {"value": 0.45, "use": "Uitgeschakelde bediening"}, "unlit": {"value": 1.0, "use": "Gedoofde lampjes gebruiken eigen 'unlit'-kleuren, geen transparantie"}, "pressedRecord": {"value": 0.85, "use": "Ingedrukte REC-toets: het rood dooft licht"}, "specularOn": {"value": 0.6, "use": "Lichtvlekje op een brandend lampje"}, "specularOff": {"value": 0.3, "use": "Lichtvlekje op een gedoofd lampje"}, "caption": {"value": 0.75, "use": "Kleine opschriften op de wijzerplaat"}}, "font": {"family.chrome": {"value": "Helvetica Neue", "fallback": "Helvetica, Arial", "use": "Zeefdruk, toetsen, koppen: neutrale grotesk"}, "family.text": {"value": "SF Pro Text", "fallback": "system-ui", "use": "Leesbare tekst: transcripties, woordenboek"}, "family.mono": {"value": "SF Mono", "fallback": "Menlo, monospace", "use": "Teller, tijden, woordenboekbestand"}}, "type": {"label": {"family": "chrome", "size": 10, "weight": 500, "tracking": 0.09, "line": 12, "caps": true, "use": "Zeefdruk-label"}, "labelSmall": {"family": "chrome", "size": 9, "weight": 500, "tracking": 0.1, "line": 11, "caps": true, "use": "Schaalmarkeringen en eenheden"}, "control": {"family": "chrome", "size": 11, "weight": 500, "tracking": 0.06, "line": 14, "caps": true, "use": "Opschrift op toetsen"}, "heading": {"family": "chrome", "size": 12, "weight": 500, "tracking": 0.12, "line": 16, "caps": true, "use": "Paneelkop"}, "body": {"family": "text", "size": 13, "weight": 400, "tracking": 0, "line": 18, "caps": false, "use": "Standaardtekst"}, "reading": {"family": "text", "size": 15, "weight": 400, "tracking": 0, "line": 22, "caps": false, "use": "Transcript"}, "meta": {"family": "text", "size": 11, "weight": 400, "tracking": 0, "line": 14, "caps": false, "use": "Bijschriften"}, "counter": {"family": "mono", "size": 22, "weight": 500, "tracking": 0, "line": 26, "caps": false, "tabular": true, "use": "Teller"}, "counterSmall": {"family": "mono", "size": 12, "weight": 500, "tracking": 0, "line": 16, "caps": false, "tabular": true, "use": "Tijdstip in een rij"}, "code": {"family": "mono", "size": 12, "weight": 400, "tracking": 0, "line": 18, "caps": false, "use": "Woordenboekregels"}}, "segment": {"digitHeight": {"value": 36, "use": "Hoogte van een getekend segmentcijfer"}, "digitWidth": {"value": 20, "use": "Breedte van een segmentcijfer"}, "thickness": {"value": 4, "use": "Dikte van één segment"}, "gap": {"value": 1, "use": "Ruimte tussen segmenten"}, "slant": {"value": 0, "use": "Schuinte in graden; 0 = rechtop"}, "advance": {"value": 26, "use": "Afstand van cijfer tot cijfer"}, "colonWidth": {"value": 10, "use": "Breedte van een dubbele punt of punt"}}, "space": {"0": 0, "1": 2, "2": 4, "3": 8, "4": 12, "5": 16, "6": 24, "7": 32, "8": 48}, "layout": {"window.minWidth": {"value": 800, "use": "Minimale vensterbreedte"}, "window.minHeight": {"value": 560, "use": "Minimale vensterhoogte"}, "window.defaultWidth": {"value": 1080, "use": "Standaard vensterbreedte"}, "window.defaultHeight": {"value": 700, "use": "Standaard vensterhoogte"}, "sidebar.width": {"value": 232, "use": "Breedte zijbalk"}, "settings.width": {"value": 560, "use": "Breedte instellingenvenster"}, "panel.padding": {"value": 16, "use": "Binnenmarge van een paneel (= space.5)"}, "panel.gap": {"value": 12, "use": "Afstand tussen panelen (= space.4)"}}, "size": {"control.height": {"value": 28, "use": "Standaardhoogte van toets of veld"}, "key.minWidth": {"value": 56, "use": "Minimale breedte van een toets"}, "key.recWidth": {"value": 64, "use": "REC-toets, breedte"}, "key.recHeight": {"value": 44, "use": "REC-toets, hoogte"}, "lamp.diameter": {"value": 10, "use": "Standaard lampje"}, "lamp.large": {"value": 14, "use": "Opnamelampje"}, "switch.width": {"value": 40, "use": "Tuimelschakelaar, breedte"}, "switch.height": {"value": 22, "use": "Tuimelschakelaar, hoogte"}, "row.height": {"value": 40, "use": "Rij in transcriptielijst"}, "icon.small": {"value": 16, "use": "Klein pictogram"}, "icon.medium": {"value": 20, "use": "Normaal pictogram"}, "hit.min": {"value": 24, "use": "Minimaal klikdoel"}}, "radius": {"none": {"value": 0, "use": "Panelen en velden: scherpe randen"}, "sharp": {"value": 1, "use": "Toetsranden: bijna scherp"}, "sm": {"value": 2, "use": "Toetsen, velden"}, "md": {"value": 4, "use": "Grote panelen"}, "bezel": {"value": 6, "use": "Rand van de VU-meter"}, "lamp": {"value": 999, "use": "Lampjes: rond"}}, "border": {"hairline": {"value": 1, "use": "Fijne lijn"}, "seam": {"value": 2, "use": "Naad: 1 donkere lijn boven, 1 lichte lijn eronder, leest als gegraveerde groef"}, "bezel": {"value": 3, "use": "Rand om de VU-meter"}, "focus": {"value": 2, "use": "Focusring, 2 pt offset"}}, "shadow": {"key.raised": {"use": "Toets in rust: lichtvang boven, donkere onderrand, valschaduw", "layers": [{"inset": true, "x": 0, "y": 1, "blur": 0, "spread": 0, "color": "#FFFFFF", "alpha": {"light": 0.65, "dark": 0.1}}, {"inset": true, "x": 0, "y": -1, "blur": 0, "spread": 0, "color": "#000000", "alpha": {"light": 0.25, "dark": 0.55}}, {"inset": false, "x": 0, "y": 2, "blur": 2, "spread": 0, "color": "#000000", "alpha": {"light": 0.35, "dark": 0.6}}]}, "key.pressed": {"use": "Ingedrukte toets: dop 1 pt omlaag, geen valschaduw", "layers": [{"inset": true, "x": 0, "y": 2, "blur": 4, "spread": 0, "color": "#000000", "alpha": {"light": 0.5, "dark": 0.7}}, {"inset": true, "x": 0, "y": 0, "blur": 0, "spread": 1, "color": "#000000", "alpha": {"light": 0.35, "dark": 0.6}}], "translateY": 1}, "well": {"use": "Verzonken vlak met lichtvang op de onderlip", "layers": [{"inset": true, "x": 0, "y": 1, "blur": 3, "spread": 0, "color": "#000000", "alpha": {"light": 0.4, "dark": 0.7}}, {"inset": false, "x": 0, "y": 1, "blur": 0, "spread": 0, "color": "#FFFFFF", "alpha": {"light": 0.55, "dark": 0.07}}]}, "panel": {"use": "Frontplaat boven het chassis", "layers": [{"inset": true, "x": 0, "y": 1, "blur": 0, "spread": 0, "color": "#FFFFFF", "alpha": {"light": 0.6, "dark": 0.08}}, {"inset": false, "x": 0, "y": 3, "blur": 8, "spread": 0, "color": "#000000", "alpha": {"light": 0.22, "dark": 0.55}}]}, "lamp.on": {"use": "Brandend lampje: kleine gloed, alleen voor lampjes en nooit voor tekst", "layers": [{"inset": false, "x": 0, "y": 0, "blur": 6, "spread": 1, "color": "#CC2A20", "alpha": {"light": 0.45, "dark": 0.55}}, {"inset": true, "x": 0, "y": -1, "blur": 1, "spread": 0, "color": "#000000", "alpha": {"light": 0.35, "dark": 0.45}}]}}, "texture": {"brushed": {"use": "Geborsteld aluminium voor chassis en zijbalk", "direction": "horizontaal", "grain": {"light": 0.035, "dark": 0.05}, "gradient": "surface.metalHigh naar surface.metalLow, van boven naar beneden"}}, "meter": {"vu.referenceDbfs": {"value": -20, "use": "0 VU = -20 dBFS (RMS)"}, "vu.min": {"value": -20, "use": "Onderkant van de schaal in VU"}, "vu.max": {"value": 3, "use": "Bovenkant van de schaal in VU; vanaf 0 VU rood"}, "vu.arcDegrees": {"value": 90, "use": "Hoek tussen minimale en maximale stand"}, "vu.width": {"value": 260, "use": "Breedte van de meter"}, "vu.height": {"value": 150, "use": "Hoogte van de meter"}, "vu.needleWidth": {"value": 1.5, "use": "Dikte van de wijzer"}, "vu.ticks": {"value": "-20 -10 -7 -5 -3 0 +3", "use": "Genummerde schaalmarkeringen"}, "vu.pivotOffset": {"value": 22, "use": "Draaipunt van de wijzer, onder de onderrand van de wijzerplaat"}, "vu.scaleRadius": {"value": 142, "use": "Straal van de schaalboog vanaf het draaipunt"}, "vu.needleLength": {"value": 146, "use": "Lengte van de wijzer"}, "vu.tickMajor": {"value": 11, "use": "Lengte van een genummerde streep"}, "vu.tickMinor": {"value": 6, "use": "Lengte van een gewone streep"}, "vu.labelInset": {"value": 24, "use": "Afstand van de schaalboog tot de cijfers"}, "vu.redBandWidth": {"value": 5, "use": "Dikte van de rode band vanaf 0 VU"}, "led.segments": {"value": 12, "use": "Aantal segmenten: 8 groen, 3 amber, 1 rood"}, "led.segmentSize": {"value": "6x14", "use": "Segment in pt (breedte x hoogte)"}, "led.gap": {"value": 2, "use": "Ruimte tussen segmenten"}, "peak.holdMs": {"value": 1000, "use": "Piek-LED blijft zo lang branden"}}, "motion": {"duration.instant": {"value": 0, "unit": "ms", "use": "Geen overgang"}, "duration.fast": {"value": 60, "unit": "ms", "use": "Toets in/uit"}, "duration.base": {"value": 120, "unit": "ms", "use": "Standaardovergang"}, "duration.slow": {"value": 220, "unit": "ms", "use": "Paneel of venster"}, "easing.mechanical": {"value": "cubic-bezier(0.2, 0, 0, 1)", "use": "Toetsen: direct, zonder veer"}, "easing.standard": {"value": "cubic-bezier(0.4, 0, 0.2, 1)", "use": "Overige overgangen"}, "key.press": {"value": 50, "unit": "ms", "use": "Toets indrukken"}, "key.release": {"value": 90, "unit": "ms", "use": "Toets loslaten"}, "lamp.on": {"value": 30, "unit": "ms", "use": "Lampje aan"}, "lamp.off": {"value": 200, "unit": "ms", "use": "Lampje uit: dooft langzaam, als een gloeidraad"}, "needle.response": {"value": 0.3, "unit": "s", "use": "VU-ballistiek: 300 ms om de stand te bereiken"}, "needle.damping": {"value": 0.8, "unit": "ratio", "use": "Veerdemping; geeft ongeveer 1,5 % overschot"}, "needle.updateHz": {"value": 60, "unit": "Hz", "use": "Verversing van de wijzer"}, "reducedMotion": {"value": "Overgangen 0 ms; wijzer blijft bewegen maar met demping 1.0 (geen overschot)", "use": "Systeeminstelling 'Verminder beweging'"}}}</script>
<div class="wrap" id="root">
<header class="mast">
<div class="id">
<div class="silk">Ontwerpsysteem · Sst</div>
<h1>Ontwerp-tokens</h1>
<p id="concept"></p>
</div>
<div class="stamp">
<div class="row"><span class="lamp amber on"></span><span class="silk">Ter goedkeuring</span></div>
<div class="mono" id="ver"></div>
<div class="silk-s">Bron · Design/tokens.json</div>
</div>
</header>
<section>
<div class="sec-head"><span class="no">01</span><span class="head">Instrumenten in gebruik</span><span class="tk">meter.* · motion.needle.* · segment.*</span></div>
<div class="two">
<div class="finish" data-finish="light"><div class="silk top"><span>Afwerking · Aluminium</span><span>licht</span></div><div class="rig-host"></div></div>
<div class="finish" data-finish="dark"><div class="silk top"><span>Afwerking · Graphite</span><span>donker</span></div><div class="rig-host"></div></div>
</div>
</section>
<section>
<div class="sec-head"><span class="no">02</span><span class="head">Kleur</span><span class="tk">color.*</span></div>
<div class="two" id="colors"></div>
</section>
<section>
<div class="sec-head"><span class="no">03</span><span class="head">Diepte en materiaal</span><span class="tk">shadow.* · border.* · texture.*</span></div>
<div class="two">
<div class="finish brushed" data-finish="light" data-depth></div>
<div class="finish brushed" data-finish="dark" data-depth></div>
</div>
</section>
<section>
<div class="sec-head"><span class="no">04</span><span class="head">Typografie</span><span class="tk">font.* · type.*</span></div>
<div id="type" class="panel"></div>
</section>
<section>
<div class="sec-head"><span class="no">05</span><span class="head">Ruimte, hoeken, randen</span><span class="tk">space.* · radius.* · border.*</span></div>
<div class="panel stack col" style="gap:24px">
<div><div class="silk cap">Ruimte (pt)</div><div class="spc" id="spc"></div></div>
<div class="seam"></div>
<div><div class="silk cap">Hoekradius</div><div class="rad" id="rad"></div></div>
</div>
</section>
<section>
<div class="sec-head"><span class="no">06</span><span class="head">Beweging</span><span class="tk">motion.*</span></div>
<div class="two">
<div class="panel">
<div class="silk cap">Toets en lampje</div>
<div class="stack" style="margin-top:12px"><button class="key" type="button">Stop</button><button class="key" type="button" id="lampbtn">Lampje</button><span class="lamp big" id="lampdemo"></span></div>
<p style="margin-top:12px;color:var(--c-ink-secondary)">Toets: 50 ms in, 90 ms uit, zonder veer. Lampje: 30 ms aan, 200 ms uit — dooft als een gloeidraad.</p>
</div>
<div class="panel">
<div class="silk cap">Wijzer-ballistiek</div>
<div class="stack" style="margin-top:12px"><button class="key" type="button" id="step">Stap naar 0 VU</button><button class="key" type="button" id="stepHigh">Stap naar +3 VU</button></div>
<p style="margin-top:12px;color:var(--c-ink-secondary)">Veer: 300 ms tot 99 %, demping 0,80 → ongeveer 1,5 % overschot. De meters bij 01 gebruiken dezelfde waarden.</p>
</div>
</div>
</section>
<section>
<div class="sec-head"><span class="no">07</span><span class="head">Wat het niet is</span></div>
<ul class="rules">
<li>Geen neon, vaporwave, synthwave, paars-roze verlopen, gloeiende tekst, chromen letters of rasterhorizonten.</li>
<li>Eén accent: het rode opnamelampje. Amber en groen alleen als niveau-indicator of markering, nooit als versiering.</li>
<li>Lampjes mogen een kleine gloed hebben. Tekst nooit.</li>
<li>Geen eenmalige waarden in componenten: elke afmeting, kleur en tijd komt uit deze tokens.</li>
</ul>
</section>
</div>
<script>
const T = JSON.parse(document.getElementById('tokens').textContent);
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const kebab = n => n.replace(/\./g, '-');
const rgba = (hex, a) => { const h = hex.slice(1); return `rgba(${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)},${a})`; };
const shadow = (tok, th) => tok.layers.map(l => `${l.inset ? 'inset ' : ''}${l.x}px ${l.y}px ${l.blur}px ${l.spread}px ${rgba(l.color, l.alpha[th])}`).join(', ');
const grain = th => {
const g = T.texture.brushed.grain[th];
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='320' height='320'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.004 0.85' numOctaves='2' seed='7'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1.4 -0.2'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='${g * 9}'/></svg>`;
return `url("data:image/svg+xml;utf8,${svg}")`;
};
function vars(th) {
const v = {};
for (const [n, c] of Object.entries(T.color)) v['--c-' + kebab(n)] = c[th];
for (const [n, s] of Object.entries(T.shadow)) v['--sh-' + kebab(n)] = shadow(s, th);
for (const [n, o] of Object.entries(T.opacity)) v['--o-' + n] = o.value;
for (const [n, s] of Object.entries(T.space)) v['--s-' + n] = s + 'px';
for (const [n, r] of Object.entries(T.radius)) v['--r-' + n] = r.value + 'px';
for (const [n, b] of Object.entries(T.border)) v['--b-' + n] = b.value + 'px';
for (const [n, s] of Object.entries(T.size)) v['--z-' + kebab(n)] = s.value + 'px';
for (const [n, f] of Object.entries(T.font)) v['--f-' + n.replace('family.', '')] = `"${f.value}", ${f.fallback}`;
for (const [n, m] of Object.entries(T.motion)) {
if (m.unit === 'ms') v['--m-' + kebab(n)] = m.value + 'ms'; else if (typeof m.value === 'string' && m.value.startsWith('cubic')) v['--e-' + n.replace('easing.', '')] = m.value;
}
v['--m-vu-width'] = T.meter['vu.width'].value + 'px';
v['--m-led-gap'] = T.meter['led.gap'].value + 'px';
v['--tex-brushed'] = grain(th);
return v;
}
function apply(el, th) { for (const [k, val] of Object.entries(vars(th))) el.style.setProperty(k, val); el.dataset.finish = th; }
apply(document.body, 'light');
$$('.finish').forEach(el => apply(el, el.dataset.finish));
$('#concept').textContent = T.meta.concept;
$('#ver').textContent = 'v' + T.meta.version;
/* ---- 02 colour swatches ---- */
const groups = {};
for (const [n, c] of Object.entries(T.color)) (groups[n.split('.')[0]] ??= []).push([n, c]);
const groupLabel = { surface: 'Oppervlak', ink: 'Inkt', accent: 'Accent', level: 'Niveau', line: 'Lijnen', display: 'Display' };
for (const th of ['light', 'dark']) {
const box = document.createElement('div');
box.className = 'finish'; apply(box, th);
box.innerHTML = `<div class="silk top"><span>Afwerking · ${th === 'light' ? 'Aluminium' : 'Graphite'}</span><span>${th === 'light' ? 'licht' : 'donker'}</span></div>` +
Object.entries(groups).map(([g, items]) => `<div class="sw-group"><span class="silk">${groupLabel[g] || g}</span>` +
items.map(([n, c]) => `<div class="sw" title="${c.use}"><i style="background:${c[th]}"></i><b>${n}</b><span>${c[th]}</span></div>`).join('') + '</div>').join('');
$('#colors').appendChild(box);
}
/* ---- 03 depth ---- */
$$('[data-depth]').forEach(el => {
el.innerHTML = `
<div class="silk top"><span>Afwerking · ${el.dataset.finish === 'light' ? 'Aluminium' : 'Graphite'}</span><span>geborsteld metaal</span></div>
<div class="panel stack col" style="gap:20px">
<div class="stack"><button class="key" type="button">Toets</button><button class="key down" type="button">Ingedrukt</button><button class="key rec" type="button"><span class="lamp on"></span>Rec</button><button class="key rec down" type="button">Rec</button></div>
<div class="seam"></div>
<div class="stack" style="align-items:stretch"><div class="well" style="flex:1"><div class="silk">Verzonken vlak</div><div class="mono" style="margin-top:6px">well</div></div><div class="stack col" style="gap:8px;justify-content:center"><div class="stack"><span class="lamp big on"></span><span class="silk-s">Lamp aan</span></div><div class="stack"><span class="lamp big"></span><span class="silk-s">Lamp uit</span></div></div></div>
<div class="paper"><div class="r"><span class="t">14:02:11</span><span class="x">Ik wil de meeting verplaatsen naar tomorrow morning.</span></div>
<div class="r sel"><span class="t">14:03:40</span><span class="x">Hij stuurt het door naar <mark><s>cloud code</s>Claude Code</mark> vandaag.</span></div></div>
</div>`;
});
/* ---- 04 type ---- */
const fam = f => `'${T.font['family.' + f].value}', ${T.font['family.' + f].fallback}`;
const samples = { label: 'Opnameniveau', labelSmall: 'VU · dB', control: 'Start opname', heading: 'Transcripties', body: 'Doorzoek je eerdere transcripties en woordenboek.', reading: 'Ik wil de meeting verplaatsen naar tomorrow morning, want de deadline komt eraan.', meta: 'Gisteren, 14:02 · 8,4 s', counter: '00:12.4', counterSmall: '14:02:11', code: 'cloud code => Claude Code' };
$('#type').innerHTML = Object.entries(T.type).map(([n, t]) => `
<div class="ty"><div class="spec"><b>type.${n}</b>${t.family} ${t.size}/${t.line} · ${t.weight}${t.caps ? ' · CAPS' : ''}${t.tracking ? ' · +' + t.tracking + 'em' : ''}</div>
<div style="font:${t.weight} ${t.size}px/${t.line}px ${fam(t.family)};letter-spacing:${t.tracking}em;${t.caps ? 'text-transform:uppercase;' : ''}${t.tabular ? 'font-variant-numeric:tabular-nums;' : ''}">${samples[n] || n}</div></div>`).join('');
/* ---- 05 spacing / radius ---- */
$('#spc').innerHTML = Object.entries(T.space).map(([n, v]) => `<div><i style="width:${v || 1}px"></i><span class="mono">${n} · ${v}</span></div>`).join('');
$('#rad').innerHTML = Object.entries(T.radius).map(([n, r]) => `<div><i style="border-radius:${Math.min(r.value, 28)}px"></i><span class="mono">${n} · ${r.value >= 999 ? 'rond' : r.value}</span></div>`).join('');
/* ---- instruments ---- */
const VU = T.meter;
const vuMin = VU['vu.min'].value, vuMax = VU['vu.max'].value, arc = VU['vu.arcDegrees'].value;
const W = VU['vu.width'].value, H = VU['vu.height'].value;
const pivot = { x: W / 2, y: H + VU['vu.pivotOffset'].value };
const frac = v => Math.max(0, Math.min(1.03, (v - vuMin) / (vuMax - vuMin)));
const ang = f => (-arc / 2 + arc * f) * Math.PI / 180;
const pt = (r, a) => [pivot.x + r * Math.sin(a), pivot.y - r * Math.cos(a)];
const f2 = n => n.toFixed(2);
function meterSvg() {
const R = VU['vu.scaleRadius'].value;
let s = `<svg class="vu" viewBox="0 0 ${W} ${H}" role="img" aria-label="VU-meter" style="aspect-ratio:${W}/${H}">`;
s += `<rect width="${W}" height="${H}" fill="var(--c-surface-meterFace)"/>`;
const redFrom = frac(0);
const arcPath = (r0, f0, f1) => { const [x0, y0] = pt(r0, ang(f0)), [x1, y1] = pt(r0, ang(f1)); return `M${f2(x0)} ${f2(y0)} A${r0} ${r0} 0 0 1 ${f2(x1)} ${f2(y1)}`; };
s += `<path d="${arcPath(R, 0, redFrom)}" stroke="var(--c-ink-onMeter)" stroke-width="1" fill="none"/>`;
s += `<path d="${arcPath(R - 2, redFrom, 1)}" stroke="var(--c-accent-record)" stroke-width="${VU['vu.redBandWidth'].value}" fill="none"/>`;
for (let v = vuMin; v <= vuMax; v++) {
const major = [-20, -10, -7, -5, -3, 0, 3].includes(v), a = ang(frac(v));
const [x0, y0] = pt(R, a), [x1, y1] = pt(R - (major ? VU['vu.tickMajor'].value : VU['vu.tickMinor'].value), a);
s += `<line x1="${f2(x0)}" y1="${f2(y0)}" x2="${f2(x1)}" y2="${f2(y1)}" stroke="${v > 0 ? 'var(--c-accent-record)' : 'var(--c-ink-onMeter)'}" stroke-width="${major ? 1.4 : 1}"/>`;
if (major) { const [tx, ty] = pt(R - VU['vu.labelInset'].value, a); s += `<text x="${f2(tx)}" y="${f2(ty + 3)}" text-anchor="middle" font-family="${fam('chrome')}" font-weight="500" font-size="9" letter-spacing=".5" fill="${v > 0 ? 'var(--c-accent-record)' : 'var(--c-ink-onMeter)'}">${v > 0 ? '+' + v : v}</text>`; }
}
s += `<text x="${W / 2}" y="${H - 30}" text-anchor="middle" font-family="${fam('chrome')}" font-weight="500" font-size="15" letter-spacing="3" fill="var(--c-ink-onMeter)">VU</text>`;
s += `<text x="14" y="${H - 10}" font-family="${fam('chrome')}" font-weight="500" font-size="8" letter-spacing="1.2" fill="var(--c-ink-onMeter)" opacity=".75">SST · FIELD RECORDER</text>`;
s += `<line class="needle" x1="${pivot.x}" y1="${pivot.y}" x2="${pivot.x}" y2="${pivot.y - VU['vu.needleLength'].value}" stroke="var(--c-ink-onMeter)" stroke-width="${VU['vu.needleWidth'].value}" stroke-linecap="butt"/>`;
s += `<circle cx="${W - 18}" cy="18" r="5" class="peak" fill="var(--c-accent-recordUnlit)" stroke="var(--c-ink-onMeter)" stroke-width=".8"/><text x="${W - 18}" y="34" text-anchor="middle" font-family="${fam('chrome')}" font-size="7" font-weight="500" letter-spacing=".8" fill="var(--c-ink-onMeter)">PEAK</text>`;
s += `</svg>`;
return s;
}
const SEG = T.segment, DW = SEG.digitWidth.value, DH = SEG.digitHeight.value, TH = SEG.thickness.value, GP = SEG.gap.value;
const MAP = { 0: 'abcdef', 1: 'bc', 2: 'abdeg', 3: 'abcdg', 4: 'bcfg', 5: 'acdfg', 6: 'acdefg', 7: 'abc', 8: 'abcdefg', 9: 'abcdfg' };
function hbar(x0, x1, cy) { const h = TH / 2; return `${x0},${cy} ${x0 + h},${cy - h} ${x1 - h},${cy - h} ${x1},${cy} ${x1 - h},${cy + h} ${x0 + h},${cy + h}`; }
function vbar(cx, y0, y1) { const h = TH / 2; return `${cx},${y0} ${cx + h},${y0 + h} ${cx + h},${y1 - h} ${cx},${y1} ${cx - h},${y1 - h} ${cx - h},${y0 + h}`; }
function digit(ch, ox, lit) {
const h = TH / 2, L = h, Rr = DW - h, mid = DH / 2, g = GP;
const P = { a: hbar(L + h + g, Rr - h - g, h), g: hbar(L + h + g, Rr - h - g, mid), d: hbar(L + h + g, Rr - h - g, DH - h),
f: vbar(L, h + h + g, mid - h - g), b: vbar(Rr, h + h + g, mid - h - g), e: vbar(L, mid + h + g, DH - h - h - g), c: vbar(Rr, mid + h + g, DH - h - h - g) };
const on = MAP[ch] || '';
return `<g transform="translate(${ox},0)">` + Object.entries(P).map(([k, pts]) => `<polygon points="${pts}" fill="${lit && on.includes(k) ? 'var(--c-level-amber)' : 'var(--c-display-segmentUnlit)'}"/>`).join('') + `</g>`;
}
function counterSvg(text) {
const adv = SEG.advance.value; let x = 0, out = '';
for (const ch of text) {
if (ch === ':' || ch === '.') { out += `<rect x="${x + 2}" y="${ch === ':' ? DH * .28 : DH - 5}" width="4" height="4" fill="var(--c-level-amber)"/>` + (ch === ':' ? `<rect x="${x + 2}" y="${DH * .64}" width="4" height="4" fill="var(--c-level-amber)"/>` : ''); x += SEG.colonWidth.value; }
else { out += digit(ch, x, true); x += adv; }
}
return `<svg width="${x}" height="${DH}" viewBox="0 0 ${x} ${DH}" role="img" aria-label="Teller ${text}">${out}</svg>`;
}
const ghost8 = () => counterSvg('88:88.8').replace(/var\(--c-level-amber\)/g, 'var(--c-display-segmentUnlit)');
const rigs = [];
$$('.rig-host').forEach(host => {
host.innerHTML = `
<div class="rig">
<div>${meterSvg()}<div class="silk-s" style="margin-top:6px">Niveau · 0 VU = ${VU['vu.referenceDbfs'].value} dBFS</div></div>
<div class="stack col" style="gap:14px">
<div><div class="silk-s" style="margin-bottom:6px">Tijd</div>
<div class="display"><div style="position:relative"><div class="ghost" style="position:absolute;inset:0">${ghost8()}</div><div class="live" style="position:relative"></div></div></div></div>
<div><div class="silk-s" style="margin-bottom:6px">Niveau</div><div class="leds">${Array.from({length: 12}, (_, i) => `<i class="${i < 8 ? 'g' : i < 11 ? 'a' : 'r'}"></i>`).join('')}</div></div>
<div class="stack"><button class="key rec down" type="button" data-rec><span class="lamp on"></span>Rec</button><button class="key" type="button" data-stop>Stop</button></div>
</div>
</div>`;
rigs.push({ host, needle: $('.needle', host), peak: $('.peak', host), live: $('.live', host), leds: $$('.leds i', host), rec: $('[data-rec]', host), stop: $('[data-stop]', host), lamp: $('[data-rec] .lamp', host), x: 0, v: 0, target: -40, peakUntil: 0, running: true, t0: performance.now(), mode: 'speech' });
});
/* spring: critically-parametrised second-order system, 99% settling = needle.response */
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const zeta = reduce ? 1 : T.motion['needle.damping'].value, resp = T.motion['needle.response'].value;
const wn = 4.6 / (zeta * resp);
function envelope(t) {
const cyc = t % 9;
if (cyc > 6.2) return -40;
const syl = Math.floor(t * 4.2);
const r = Math.sin(syl * 12.9898) * 43758.5453; const rnd = r - Math.floor(r);
const gap = Math.sin(t * 4.2 * Math.PI) > -0.55 ? 0 : -25;
return rnd > .12 ? (-15 + rnd * 16.5 + gap) : -40;
}
let last = performance.now();
function frame(now) {
const dt = Math.min(.05, (now - last) / 1000); last = now;
for (const r of rigs) {
const t = (now - r.t0) / 1000;
if (r.mode === 'speech') r.target = r.running ? envelope(t) : -40;
const tgt = frac(r.target);
const a = wn * wn * (tgt - r.x) - 2 * zeta * wn * r.v;
r.v += a * dt; r.x += r.v * dt;
const ang_ = ang(Math.max(-0.02, Math.min(1.05, r.x)));
const [nx, ny] = pt(VU['vu.needleLength'].value, ang_);
r.needle.setAttribute('x2', f2(nx)); r.needle.setAttribute('y2', f2(ny));
const vuNow = vuMin + (vuMax - vuMin) * r.x;
if (vuNow > 0 && r.running) r.peakUntil = now + VU['peak.holdMs'].value;
r.peak.setAttribute('fill', now < r.peakUntil ? 'var(--c-accent-record)' : 'var(--c-accent-recordUnlit)');
const lit = Math.round(Math.max(0, Math.min(1, (vuNow - vuMin) / (vuMax - vuMin))) * 12);
r.leds.forEach((l, i) => l.classList.toggle('on', i < lit));
const secs = r.running ? t : (r.frozen ?? 0); if (r.running) r.frozen = t;
const mm = String(Math.floor(secs / 60) % 100).padStart(2, '0'), ss = String(Math.floor(secs % 60)).padStart(2, '0'), d = Math.floor((secs * 10) % 10);
const txt = `${mm}:${ss}.${d}`;
if (r.txt !== txt) { r.live.innerHTML = counterSvg(txt); r.txt = txt; }
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
rigs.forEach(r => {
r.stop.onclick = () => { r.running = false; r.rec.classList.remove('down'); r.lamp.classList.remove('on'); };
r.rec.onclick = () => { if (!r.running) { r.running = true; r.t0 = performance.now(); r.rec.classList.add('down'); r.lamp.classList.add('on'); } };
});
/* ---- 06 motion demos ---- */
$('#lampbtn').onclick = () => $('#lampdemo').classList.toggle('on');
const stepTo = v => rigs.forEach(r => { r.mode = 'manual'; r.running = false; r.target = v; setTimeout(() => { r.target = -40; setTimeout(() => { r.mode = 'speech'; r.running = true; r.t0 = performance.now(); r.rec.classList.add('down'); r.lamp.classList.add('on'); }, 1800); }, 1800); r.rec.classList.remove('down'); r.lamp.classList.remove('on'); });
$('#step').onclick = () => stepTo(0);
$('#stepHigh').onclick = () => stepTo(3);
</script>
</body>
</html>