track 1: foundation polish — splitter, custom scrollbars, micro-states
Adds docs/roadmap.md outlining the 4-track plan (foundation, surface aesthetics, organizable panel, theming) and ships the first track. - Resizable splitter between editor and control surface (drag the seam, double-click to reset). Width persists in localStorage. - Custom scrollbars (WebKit pseudo-elements + Firefox scrollbar-color) applied globally so CodeMirror's .cm-scroller, the right pane and the piano roll all match the dark theme. - Header separators between transport / gain / info groups. - Smooth transitions on buttons, range thumbs and surface borders. - :focus-visible rings using accent-glow color token. - Pulse animation on the live status dot. - New design-system tokens for layout (--right-w, --splitter-w), timing (--t-fast/base/slow), and panel hierarchy (--panel-hi, --gutter-hi, --divider, --fg-mute, --accent-glow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
157
web/index.html
157
web/index.html
@@ -27,11 +27,16 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* === design tokens === */
|
||||
:root {
|
||||
--bg: #0e0f12;
|
||||
--panel: #14161a;
|
||||
--panel-hi: #1a1d22;
|
||||
--gutter: #2a2f38;
|
||||
--gutter-hi: #404652;
|
||||
--divider: #1c1f25;
|
||||
--fg: #d6dae0;
|
||||
--fg-mute: #aab0b8;
|
||||
--comment: #5a6470;
|
||||
--kw: #c678dd;
|
||||
--num: #d19a66;
|
||||
@@ -41,36 +46,111 @@
|
||||
--wave-bg: #161a1f;
|
||||
--error: #f08080;
|
||||
--accent: #7af0c0;
|
||||
--accent-glow: rgba(122, 240, 192, 0.30);
|
||||
--knob-track: #2a2f38;
|
||||
|
||||
/* scrollbar */
|
||||
--scrollbar-thumb: #2a2f38;
|
||||
--scrollbar-thumb-hover: #3a4048;
|
||||
|
||||
/* layout */
|
||||
--right-w: 360px;
|
||||
--splitter-w: 4px;
|
||||
|
||||
/* timing */
|
||||
--t-fast: 80ms;
|
||||
--t-base: 160ms;
|
||||
--t-slow: 280ms;
|
||||
}
|
||||
|
||||
/* === reset / base === */
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; margin: 0; background: var(--bg); color: var(--fg);
|
||||
font-family: 'JetBrains Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 14px; }
|
||||
body { display: flex; flex-direction: column; }
|
||||
|
||||
/* === scrollbars (global, also apply to CodeMirror's .cm-scroller) === */
|
||||
* { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) transparent; }
|
||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb);
|
||||
border-radius: 6px; border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
transition: background var(--t-base); }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover);
|
||||
background-clip: padding-box;
|
||||
border: 2px solid transparent; }
|
||||
::-webkit-scrollbar-corner { background: transparent; }
|
||||
|
||||
/* === header === */
|
||||
header { padding: 8px 14px; font-size: 12px; color: var(--comment);
|
||||
border-bottom: 1px solid #1c1f25; display: flex; gap: 14px;
|
||||
border-bottom: 1px solid var(--divider); display: flex; gap: 12px;
|
||||
flex-wrap: wrap; align-items: center; }
|
||||
header button { background: var(--panel); border: 1px solid var(--gutter);
|
||||
color: var(--fg); padding: 5px 14px; cursor: pointer;
|
||||
font-family: inherit; font-size: 12px; border-radius: 3px; }
|
||||
font-family: inherit; font-size: 12px; border-radius: 3px;
|
||||
transition: border-color var(--t-fast), color var(--t-fast),
|
||||
background var(--t-fast), transform var(--t-fast); }
|
||||
header button:hover { border-color: var(--accent); color: var(--accent); }
|
||||
header button:focus-visible { outline: 2px solid var(--accent-glow);
|
||||
outline-offset: 2px; }
|
||||
header button:active:not(:disabled) { transform: translateY(1px); }
|
||||
header button:disabled { opacity: 0.4; cursor: default; }
|
||||
.header-sep { width: 1px; height: 16px; background: var(--gutter);
|
||||
opacity: 0.7; }
|
||||
|
||||
/* range input (gain slider in header + everywhere) */
|
||||
input[type=range] { -webkit-appearance: none; appearance: none;
|
||||
height: 4px; background: var(--knob-track);
|
||||
border-radius: 2px; vertical-align: middle;
|
||||
width: 90px; cursor: pointer; outline: none; }
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; appearance: none;
|
||||
width: 12px; height: 12px; border-radius: 50%; background: var(--fg);
|
||||
cursor: grab; transition: background var(--t-fast),
|
||||
box-shadow var(--t-fast); }
|
||||
input[type=range]:hover::-webkit-slider-thumb { background: var(--accent); }
|
||||
input[type=range]:active::-webkit-slider-thumb { cursor: grabbing;
|
||||
box-shadow: 0 0 0 4px var(--accent-glow); }
|
||||
input[type=range]::-moz-range-thumb {
|
||||
width: 12px; height: 12px; border-radius: 50%; background: var(--fg);
|
||||
border: none; cursor: grab; transition: background var(--t-fast); }
|
||||
input[type=range]:focus-visible::-webkit-slider-thumb {
|
||||
box-shadow: 0 0 0 3px var(--accent-glow); }
|
||||
|
||||
header .dot { width: 8px; height: 8px; border-radius: 50%; background: #555;
|
||||
display: inline-block; vertical-align: middle; margin-right: 6px; }
|
||||
header .dot.live { background: var(--accent); box-shadow: 0 0 8px var(--accent); }
|
||||
display: inline-block; vertical-align: middle; margin-right: 6px;
|
||||
transition: background var(--t-base), box-shadow var(--t-base); }
|
||||
header .dot.live { background: var(--accent);
|
||||
animation: pulse 1.6s ease-in-out infinite; }
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 6px var(--accent); }
|
||||
50% { box-shadow: 0 0 14px var(--accent); }
|
||||
}
|
||||
#error { color: var(--error); flex: 1; min-width: 0; overflow: hidden;
|
||||
text-overflow: ellipsis; white-space: nowrap; }
|
||||
main { flex: 1; display: grid; grid-template-columns: 1fr 320px; min-height: 0; }
|
||||
#editor { background: var(--panel); border-right: 1px solid #1c1f25;
|
||||
overflow: hidden; }
|
||||
|
||||
/* === main layout: editor | splitter | controls === */
|
||||
main { flex: 1; display: grid;
|
||||
grid-template-columns: minmax(320px, 1fr) var(--splitter-w) minmax(260px, var(--right-w));
|
||||
min-height: 0; }
|
||||
#editor { background: var(--panel); overflow: hidden; }
|
||||
.cm-editor { height: 100%; font-size: 14px; }
|
||||
.cm-editor.cm-focused { outline: none; }
|
||||
.cm-content { font-family: inherit; }
|
||||
|
||||
/* draggable seam between editor and control surface */
|
||||
.splitter { background: var(--divider); position: relative;
|
||||
cursor: col-resize; transition: background var(--t-base); }
|
||||
.splitter::before { /* widen hit area to ~12px without changing visual width */
|
||||
content: ''; position: absolute; inset: 0 -4px; z-index: 2; }
|
||||
.splitter:hover, .splitter.dragging { background: var(--accent); }
|
||||
|
||||
.wave-widget { display: inline-block; vertical-align: middle; margin-left: 12px;
|
||||
background: var(--wave-bg); border-radius: 3px; }
|
||||
|
||||
/* control surface */
|
||||
/* === control surface === */
|
||||
#controls { padding: 16px; overflow: auto;
|
||||
display: flex; flex-wrap: wrap; gap: 18px; align-content: flex-start; }
|
||||
#controls.empty::before { content: 'declare faders or knobs in the patch to populate this surface →←';
|
||||
@@ -79,9 +159,12 @@
|
||||
.ctrl { display: flex; flex-direction: column; align-items: center;
|
||||
gap: 6px; min-width: 70px; padding: 10px;
|
||||
background: var(--panel); border: 1px solid var(--gutter);
|
||||
border-radius: 6px; }
|
||||
.ctrl-label { font-size: 11px; color: var(--fg); user-select: none;
|
||||
max-width: 80px; overflow: hidden; text-overflow: ellipsis; }
|
||||
border-radius: 6px;
|
||||
transition: border-color var(--t-base), box-shadow var(--t-base); }
|
||||
.ctrl:hover { border-color: var(--gutter-hi); }
|
||||
.ctrl-label { font-size: 10px; color: var(--fg-mute); user-select: none;
|
||||
max-width: 80px; overflow: hidden; text-overflow: ellipsis;
|
||||
text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.ctrl-value { font-size: 10px; color: var(--accent); font-variant-numeric: tabular-nums;
|
||||
user-select: none; }
|
||||
.knob-canvas { cursor: grab; touch-action: none; }
|
||||
@@ -132,12 +215,15 @@
|
||||
<header>
|
||||
<button id="start">Start audio</button>
|
||||
<span><span id="dot" class="dot"></span><span id="status">stopped</span></span>
|
||||
<span class="header-sep"></span>
|
||||
<span>gain <input id="gain" type="range" min="0" max="1" step="0.01" value="0.3"></span>
|
||||
<span class="header-sep"></span>
|
||||
<span id="info"></span>
|
||||
<span id="error"></span>
|
||||
</header>
|
||||
<main>
|
||||
<div id="editor"></div>
|
||||
<div class="splitter" id="splitter" title="drag to resize · double-click to reset"></div>
|
||||
<div id="controls" class="empty"></div>
|
||||
</main>
|
||||
|
||||
@@ -838,6 +924,55 @@ function tick() {
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
|
||||
// =====================================================================
|
||||
// splitter — drag the seam between editor and surface to rebalance.
|
||||
// width persists in localStorage; double-click resets to default.
|
||||
// =====================================================================
|
||||
{
|
||||
const splitter = document.getElementById('splitter');
|
||||
const root = document.documentElement;
|
||||
const KEY = 'cs.right-w';
|
||||
const MIN_LEFT = 320;
|
||||
const MIN_RIGHT = 260;
|
||||
|
||||
try {
|
||||
const saved = localStorage.getItem(KEY);
|
||||
if (saved) root.style.setProperty('--right-w', saved);
|
||||
} catch {}
|
||||
|
||||
splitter.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault();
|
||||
splitter.setPointerCapture(e.pointerId);
|
||||
splitter.classList.add('dragging');
|
||||
document.body.style.cursor = 'col-resize';
|
||||
const onMove = (ev) => {
|
||||
const winW = root.clientWidth;
|
||||
const want = winW - ev.clientX;
|
||||
const w = Math.max(MIN_RIGHT, Math.min(winW - MIN_LEFT, want));
|
||||
root.style.setProperty('--right-w', w + 'px');
|
||||
};
|
||||
const onUp = () => {
|
||||
splitter.classList.remove('dragging');
|
||||
document.body.style.cursor = '';
|
||||
splitter.removeEventListener('pointermove', onMove);
|
||||
splitter.removeEventListener('pointerup', onUp);
|
||||
splitter.removeEventListener('pointercancel', onUp);
|
||||
try {
|
||||
const w = getComputedStyle(root).getPropertyValue('--right-w').trim();
|
||||
if (w) localStorage.setItem(KEY, w);
|
||||
} catch {}
|
||||
};
|
||||
splitter.addEventListener('pointermove', onMove);
|
||||
splitter.addEventListener('pointerup', onUp);
|
||||
splitter.addEventListener('pointercancel', onUp);
|
||||
});
|
||||
|
||||
splitter.addEventListener('dblclick', () => {
|
||||
root.style.removeProperty('--right-w');
|
||||
try { localStorage.removeItem(KEY); } catch {}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user