Docs accuracy pass, guides, and a playable Web Audio synth demo

Docs/playground accuracy (from a full docs-vs-source audit):
- Playground: drag-feel select covers pickup/relative rotary modes, is
  disabled for faders, and generated code no longer emits props the
  target component does not accept
- API/README claims corrected: Shift-fine wheel scope, wheelStep and
  decimals defaults, trackInset row, data-part and ref-forwarding
  scope, VintageKnob readout exception, Meter peakHold tag, stale
  component lists, aria-valuetext wording, also-exported list

API consistency fixes surfaced by the audit and the tutorial:
- Keyboard gestures now fire onChangeStart (bracket parity with drag/
  wheel - fixes gesture-scoped undo patterns)
- name (hidden form input) added to Fader, LEDFader, ImageKnob
- PushButton gets a themed keyboard-only focus ring + data-focus-visible

New docs content:
- Getting Started: 7-step guide with live examples and a gotchas list
- Advanced guide: 8-step channel-strip tutorial ending in a live,
  themed, gesture-undoable strip with meter (plus headless demo)
- Synth section: playable Web Audio synth - osc/filter/env/LFO/master
  all dreamknob controls, momentary-pad keyboard, analyser-driven Meter
This commit is contained in:
Dreamodus 2026-07-12 18:27:06 -07:00
parent e9e52e43ed
commit 54189ad07a
15 changed files with 1069 additions and 53 deletions

View file

@ -0,0 +1,155 @@
import React from 'react';
import { FlatKnob, logTaper } from 'dreamknob';
import { CodeBlock } from '../components/CodeBlock';
const Step: React.FC<{ n: number; title: string; children: React.ReactNode }> = ({
n,
title,
children,
}) => (
<div style={{ marginBottom: 34 }}>
<h3 className="api-h" style={{ marginTop: 0 }}>
<span style={{ color: 'var(--accent)', fontFamily: 'var(--font-mono)', marginRight: 10 }}>
{String(n).padStart(2, '0')}
</span>
{title}
</h3>
{children}
</div>
);
export const GettingStarted: React.FC = () => (
<section className="block" id="start">
<div className="container prose">
<div className="section-kicker">Getting started</div>
<h2 className="section-title">Your first knob in five minutes</h2>
<p className="section-sub">
Everything below is copy-pasteable. The only rule to remember: every control needs
an <code>aria-label</code> (or a visible label wired via <code>aria-labelledby</code>).
</p>
<Step n={1} title="Install">
<CodeBlock code={`npm install dreamknob\n# peer deps: react >= 18, react-dom >= 18`} />
<p className="api-note" style={{ marginTop: 10 }}>
Zero runtime dependencies, ESM + CJS, full TypeScript types. No CSS file to
import everything is SVG and inline styles.
</p>
</Step>
<Step n={2} title="Drop in a knob (uncontrolled)">
<p className="api-note" style={{ marginBottom: 10 }}>
Pass <code>defaultValue</code> and the knob manages its own state.{' '}
<code>defaultValue</code> is also the double-click reset target.
</p>
<CodeBlock
code={`import { FlatKnob } from 'dreamknob'
<FlatKnob
size={90}
min={0} max={100} step={1}
defaultValue={50}
label="Level" unit="%"
aria-label="Level"
/>`}
/>
<div className="preview" style={{ minHeight: 150, marginTop: 12 }}>
<FlatKnob size={90} min={0} max={100} step={1} defaultValue={50} label="Level" unit="%" aria-label="Level" />
</div>
</Step>
<Step n={3} title="Read the value (controlled)">
<p className="api-note" style={{ marginBottom: 10 }}>
For app state, pass <code>value</code> + <code>onChange</code>. The second
argument tells you what caused the change useful later for undo/automation.
</p>
<CodeBlock
code={`const [gain, setGain] = useState(-6)
<FlatKnob
value={gain}
onChange={(v, meta) => setGain(v)} // meta.source: 'drag' | 'wheel' | 'keyboard' | 'reset' | 'api'
min={-60} max={12} step={0.5}
unit=" dB"
aria-label="Gain"
/>`}
/>
</Step>
<Step n={4} title="Pick the feel">
<p className="api-note" style={{ marginBottom: 10 }}>
The default is grab-and-rotate (the pointer angle drives the value). Prefer the
up/down plugin feel, or MIDI-style pickup? One prop each. Users always get
scroll-wheel, arrow keys, Shift-fine, Escape-cancel and double-click reset for
free.
</p>
<CodeBlock
code={`<FlatKnob interaction="vertical" ... /> // drag up/down like a plugin
<FlatKnob rotaryMode="pickup" ... /> // never jumps when grabbed off-position
<FlatKnob rotaryMode="relative" ... /> // angle deltas, no jump either`}
/>
</Step>
<Step n={5} title="Get the numbers right">
<p className="api-note" style={{ marginBottom: 10 }}>
<code>step</code> snaps and sets display precision (float-safe no{' '}
<code>0.30000000000000004</code>). For audio ranges, use a taper:{' '}
<code>logTaper</code> gives equal ratios per turn, so 20 Hz20 kHz feels right.
<code>format</code> controls display without touching the emitted value.
</p>
<CodeBlock
code={`import { FlatKnob, logTaper } from 'dreamknob'
<FlatKnob
min={20} max={20000}
taper={logTaper} // min must be > 0 for log
defaultValue={440}
decimals={0}
format={v => v >= 1000 ? \`\${(v / 1000).toFixed(1)}k\` : \`\${Math.round(v)}\`}
label="Freq"
aria-label="Frequency"
/>`}
/>
<div className="preview" style={{ minHeight: 150, marginTop: 12 }}>
<FlatKnob
size={90}
min={20}
max={20000}
taper={logTaper}
defaultValue={440}
decimals={0}
color="#3df2ad"
format={v => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${Math.round(v)}`)}
label="Freq"
aria-label="Frequency example"
/>
</div>
</Step>
<Step n={6} title="Theme once, restyle everything">
<CodeBlock
code={`import { DreamknobProvider } from 'dreamknob'
<DreamknobProvider base="dark" theme={{ accent: '#ff4d6b' }}>
{/* every knob/fader/meter below inherits the tokens */}
</DreamknobProvider>`}
/>
<p className="api-note" style={{ marginTop: 10 }}>
Use <code>base="light"</code> on light backgrounds. Per-instance color props
always win over the theme.
</p>
</Step>
<Step n={7} title="Gotchas checklist">
<p className="api-note">
Always pass <code>aria-label</code>. <code>logTaper</code> needs{' '}
<code>min &gt; 0</code> (it falls back to linear otherwise). Controlled mode
means <em>you</em> own the value if you don't call <code>setState</code> in{' '}
<code>onChange</code>, the knob won't move. Hovering captures the scroll
wheel by default; pass <code>wheelRequiresFocus</code> if your UI scrolls.
The knob is a <code>div</code>, not an input use <code>name="…"</code> if you
need it in a plain <code>&lt;form&gt;</code> post.
</p>
</Step>
</div>
</section>
);