@kolkrabbi/kol-hardware 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@ A different TIER from `kol-component`'s app atoms: a `kol-component` Slider is a
18
18
  | | `IconButton` | 1px-bordered icon key, momentary pulse |
19
19
  | indicators | `LED` | 6 / 8px lamp — red · yellow · green · white · blue, optional hit pad |
20
20
  | | `JackSocket`, `LabeledJack` | the 3.5mm jack, presentational: ring, hole, label, a rim that glows with `signalRef`. **Routing stays in the consumer** |
21
- | | `SignalScope` | the oscilloscope — a trace of any `sample(t)`, the knob range dashed, a live playhead; zoom X/Y, drag to pan, a clock `rate`, `loop` or one-shot on `trigger`, `height="fill"` |
21
+ | | `SignalScope` | the oscilloscope — a trace of any `sample(t)`, the knob range dashed, a live playhead; zoom X/Y, drag to pan, a clock `rate` or a host's `now`, `loop` or one-shot on `trigger`, the played part in the accent, `height="fill"` |
22
22
  | panel | `PanelLabel` | label wrapper, four positions |
23
23
  | | `ModuleHeader` | Toggle + module name + edit-mode remove dot / bypass dot |
24
24
  | | `SignalReference` | an expression / envelope reference — `variant` `panel` (mirror's box) · `tabs` · `popover` (EX / REF) · `sheet` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-hardware",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "description": "Panel controls for instruments",
6
6
  "license": "MIT",
@@ -13,16 +13,16 @@
13
13
  "./*": "./src/*"
14
14
  },
15
15
  "peerDependencies": {
16
- "@kolkrabbi/kol-component": ">=0.238.0",
16
+ "@kolkrabbi/kol-component": ">=0.262.0",
17
17
  "@kolkrabbi/kol-icons": ">=0.25.0",
18
18
  "@kolkrabbi/kol-theme": ">=0.164.0",
19
19
  "react": "^18.3.0 || ^19.0.0",
20
20
  "react-dom": "^18.3.0 || ^19.0.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@kolkrabbi/kol-component": "^0.238.0",
24
- "@kolkrabbi/kol-icons": "^0.33.0",
25
- "@kolkrabbi/kol-theme": "^0.164.0"
23
+ "@kolkrabbi/kol-icons": "^0.41.0",
24
+ "@kolkrabbi/kol-component": "^0.287.0",
25
+ "@kolkrabbi/kol-theme": "^0.183.0"
26
26
  },
27
27
  "files": [
28
28
  "src",
@@ -1,4 +1,5 @@
1
1
  import { useRef, useEffect } from 'react'
2
+ import { Tooltip } from '@kolkrabbi/kol-component'
2
3
 
3
4
  /* Read a color binding once, lazily — the roles live in kol-theme's
4
5
  * kol-components-controls.css. HEX is required here: the ring appends a hex
@@ -49,7 +50,8 @@ function stopJackLoop() {
49
50
  * @param {object} signalRef `{ current }` — the live signal for the glow
50
51
  * @param {boolean} bg the fg-04 plate behind the ring (default: outputs only)
51
52
  * @param {Function} onPointerDown the consumer's routing gesture
52
- * @param {string} title
53
+ * @param {string} title the DS `Tooltip` (default `label`), on the jack itself; silent while a
54
+ * cable is pending anywhere, so a drag never flashes tooltips (HardwareNativeTitle)
53
55
  * @param {object|Function} ringRef the RING element (ControlsJackSeams, kol-monitor 2026-09-01) — the
54
56
  * consumer's drag-to-patch registers it for hit-testing (pointerup finds
55
57
  * the nearest input ring centre); a ref object or a callback
@@ -113,11 +115,11 @@ export default function JackSocket({
113
115
  const hole = size === 'sm' ? 4 : 6
114
116
 
115
117
  return (
118
+ <Tooltip label={title ?? label} asChild disabled={pending || dimPending}>
116
119
  <div
117
120
  className="inline-flex flex-col items-center gap-0.5 select-none"
118
121
  style={{ cursor: type === 'out' ? 'grab' : (dimPending ? 'pointer' : 'default'), touchAction: 'none' }}
119
122
  onPointerDown={onPointerDown}
120
- title={title ?? label}
121
123
  >
122
124
  <div
123
125
  className={(bg ?? type === 'out') ? 'bg-fg-04' : ''}
@@ -158,5 +160,6 @@ export default function JackSocket({
158
160
  </span>
159
161
  )}
160
162
  </div>
163
+ </Tooltip>
161
164
  )
162
165
  }
@@ -16,7 +16,13 @@ import { useEffect, useRef } from 'react'
16
16
  * THE CLOCK is the scope's: `rate` is time units per second (BPM / 60 for a beat clock), and
17
17
  * `playing` stops it where it is. `loop` (default) wraps the playhead over `sec`; off, the
18
18
  * playhead runs `0 → sec` once and holds at the end, and every change of `trigger` fires it
19
- * again — a one-shot envelope's gate.
19
+ * again — a one-shot envelope's gate. `now` hands the clock to the host instead: a
20
+ * `() => t` read every frame (mirror's desk transport), and `rate` / `playing` stop applying.
21
+ *
22
+ * THE PLAYED PART IS THE ACCENT (SignalScopePlayedTraceAndClock, kol-mirror): while live, the
23
+ * curve is faint and the trace up to the playhead, the playhead and its dot take
24
+ * `--kol-slider-playhead` (the accent unless a host overrides it) — where the position is in the
25
+ * signal, at a glance.
20
26
  *
21
27
  * @param {(t:number)=>number} sample
22
28
  * @param {number} min · max the view (default 0…100)
@@ -30,20 +36,21 @@ import { useEffect, useRef } from 'react'
30
36
  * @param {number} rate time units per second (default 1)
31
37
  * @param {boolean} loop wrap the playhead (default true); off = one pass per `trigger`
32
38
  * @param {number} trigger a counter — each change restarts the pass
39
+ * @param {()=>number} now an external clock (time units); overrides the scope's own
33
40
  * @param {number|'fill'} height px (default 320), or `fill` to take the parent's height
34
41
  */
35
42
  const css = (el, name, fallback) => getComputedStyle(el).getPropertyValue(name).trim() || fallback
36
43
 
37
44
  export default function SignalScope({
38
45
  sample, min = 0, max = 100, sec = 5, ofs = 0, zoomX = 1, zoomY = 1, panY = 0, onPan,
39
- refLines = [0, 100], live = true, playing = true, rate = 1, loop = true, trigger = 0,
46
+ refLines = [0, 100], live = true, playing = true, rate = 1, loop = true, trigger = 0, now,
40
47
  height = 320, className = '',
41
48
  }) {
42
49
  const ref = useRef(null)
43
50
  const args = useRef({})
44
51
  const drawRef = useRef(null)
45
52
  const clock = useRef({ t: 0, last: 0 })
46
- args.current = { sample, min, max, sec, ofs, zoomX, zoomY, panY, refLines, live, playing, rate, loop }
53
+ args.current = { sample, min, max, sec, ofs, zoomX, zoomY, panY, refLines, live, playing, rate, loop, now }
47
54
 
48
55
  // a trigger restarts the pass
49
56
  useEffect(() => { clock.current.t = 0 }, [trigger])
@@ -59,7 +66,8 @@ export default function SignalScope({
59
66
  const now = performance.now()
60
67
  const dt = (now - clock.current.last) / 1000
61
68
  clock.current.last = now
62
- if (a.live && a.playing) clock.current.t += dt * a.rate
69
+ if (a.now) clock.current.t = a.now()
70
+ else if (a.live && a.playing) clock.current.t += dt * a.rate
63
71
  const fn = a.sample
64
72
  const dpr = window.devicePixelRatio || 1
65
73
  const { width: w, height: h } = canvas.getBoundingClientRect()
@@ -71,6 +79,7 @@ export default function SignalScope({
71
79
  ctx.clearRect(0, 0, w, h)
72
80
  const ink = css(canvas, '--kol-surface-on-primary', '#e8e4dc')
73
81
  const red = css(canvas, '--kol-ctl-led-red', '#e74c3c')
82
+ const accent = css(canvas, '--kol-slider-playhead', css(canvas, '--kol-accent-primary', ink))
74
83
  const pad = 12
75
84
  const span = (a.max - a.min) / (a.zoomY || 1) || 1
76
85
  const lo = (a.min + a.max) / 2 + a.panY - span / 2
@@ -101,26 +110,35 @@ export default function SignalScope({
101
110
  ctx.fillStyle = ink
102
111
  ctx.fillText(fmt(hi), 4, pad + 9)
103
112
  ctx.fillText(fmt(lo), 4, h - pad - 3)
104
- // trace
105
- ctx.globalAlpha = 1
106
- ctx.strokeStyle = ink
107
- ctx.lineWidth = 1.5
108
- ctx.beginPath()
109
- for (let x = 0; x <= w; x++) {
110
- const y = toY(fn ? fn(a.ofs + (x / w) * dur) : lo)
111
- if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y)
113
+ const trace = (to) => {
114
+ ctx.beginPath()
115
+ for (let x = 0; x <= to; x++) {
116
+ const y = toY(fn ? fn(a.ofs + (x / w) * dur) : lo)
117
+ if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y)
118
+ }
119
+ ctx.stroke()
112
120
  }
113
- ctx.stroke()
121
+ const played = a.live && fn
122
+ // trace — faint under a playhead, the played part drawn over it
123
+ ctx.globalAlpha = played ? 0.15 : 1
124
+ ctx.strokeStyle = ink
125
+ ctx.lineWidth = played ? 1 : 1.5
126
+ trace(w)
127
+ ctx.globalAlpha = 1
114
128
  // playhead — over the base window, whatever the zoom (mirror's)
115
- if (a.live && fn) {
116
- const t = a.loop ? clock.current.t % a.sec : Math.min(clock.current.t, a.sec)
129
+ if (played) {
130
+ const t = a.loop ? ((clock.current.t % a.sec) + a.sec) % a.sec : Math.max(0, Math.min(clock.current.t, a.sec))
117
131
  const px = toX(t)
132
+ ctx.strokeStyle = accent
133
+ ctx.lineWidth = 2
134
+ if (px > 0) trace(Math.min(px, w))
118
135
  if (px >= 0 && px <= w) {
119
136
  const py = toY(fn(t))
120
- ctx.globalAlpha = 0.25
137
+ ctx.globalAlpha = 0.4
138
+ ctx.lineWidth = 1
121
139
  ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, h); ctx.stroke()
122
140
  ctx.globalAlpha = 1
123
- ctx.fillStyle = ink
141
+ ctx.fillStyle = accent
124
142
  ctx.beginPath(); ctx.arc(px, py, 3, 0, Math.PI * 2); ctx.fill()
125
143
  }
126
144
  }
@@ -1,3 +1,4 @@
1
+ import { Tooltip } from '@kolkrabbi/kol-component'
1
2
  import Toggle from '../switches/Toggle.jsx'
2
3
 
3
4
  /**
@@ -22,17 +23,19 @@ export default function ModuleHeader({ label, enabled, onToggle, powered = true,
22
23
  {label}
23
24
  </span>
24
25
  {editMode && onRemove ? (
26
+ <Tooltip label="Send module to workbench" asChild>
25
27
  <div
26
28
  onClick={(e) => { e.stopPropagation(); onRemove() }}
27
- title="Send module to workbench"
28
29
  style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: 'var(--kol-ctl-led-yellow)', cursor: 'pointer', flexShrink: 0 }}
29
30
  />
31
+ </Tooltip>
30
32
  ) : onBypass ? (
33
+ <Tooltip label={bypass ? 'Bypassed — click to re-engage' : 'Bypass'} asChild>
31
34
  <div
32
35
  onClick={(e) => { e.stopPropagation(); onBypass() }}
33
- title={bypass ? 'Bypassed — click to re-engage' : 'Bypass'}
34
36
  style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: 'var(--kol-ctl-cv-attenuate)', opacity: bypass ? 0.25 : 1, cursor: 'pointer', flexShrink: 0 }}
35
37
  />
38
+ </Tooltip>
36
39
  ) : null}
37
40
  </div>
38
41
  )
@@ -1,5 +1,6 @@
1
1
  import { useState, useRef } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
+ import { Tooltip } from '@kolkrabbi/kol-component'
3
4
 
4
5
  const PULSE_MS = 100
5
6
 
@@ -12,7 +13,8 @@ const PULSE_MS = 100
12
13
  * @param {string} icon icon name
13
14
  * @param {boolean} active · disabled · momentary
14
15
  * @param {Function} onClick
15
- * @param {string} title
16
+ * @param {string} title the DS `Tooltip`, attached to the key itself (HardwareNativeTitle,
17
+ * kol-monitor 2026-10-15 — no native `title`, no wrapper in a grid)
16
18
  * @param {number} iconSize (default 10)
17
19
  * @param {ElementType} iconComponent icon renderer seam (default kol-icons `Icon`)
18
20
  */
@@ -32,9 +34,10 @@ export default function IconButton({ icon, active, onClick, title, momentary, di
32
34
  }
33
35
 
34
36
  return (
37
+ <Tooltip label={title} asChild>
35
38
  <button
36
39
  type="button"
37
- title={title}
40
+ aria-label={title}
38
41
  onClick={handleClick}
39
42
  disabled={disabled}
40
43
  style={{
@@ -54,5 +57,6 @@ export default function IconButton({ icon, active, onClick, title, momentary, di
54
57
  >
55
58
  <IconCmp name={icon} size={iconSize} />
56
59
  </button>
60
+ </Tooltip>
57
61
  )
58
62
  }
@@ -27,11 +27,11 @@ const adsrWindow = (adsr) => envelopeLength(adsr) * 1.15
27
27
 
28
28
  /**
29
29
  * useEnvelopeGenerator(options) — the generator's state, for a host that arranges the parts.
30
- * Takes the component's own options (`default*`, `saved`, `savedAdsr`, `onSave`, `onChange`).
30
+ * Takes the component's own options (`default*`, `saved`, `savedAdsr`, `onSave`, `onChange`, `now`).
31
31
  */
32
32
  export function useEnvelopeGenerator({
33
33
  defaultMode = 'equation', defaultExpr = 'wave(t)', defaultAdsr = ADSR_DEFAULTS,
34
- saved, savedAdsr, onSave, onChange,
34
+ saved, savedAdsr, onSave, onChange, now,
35
35
  } = {}) {
36
36
  const [mode, setModeState] = useState(defaultMode)
37
37
  const [expr, setExprState] = useState(defaultExpr)
@@ -69,7 +69,7 @@ export function useEnvelopeGenerator({
69
69
  return {
70
70
  mode, setMode, expr, setExpr, compiled, adsr, setAdsr, view, setView, fit, reset,
71
71
  cycle, setCycle, playing, setPlaying, bpm, setBpm, trigger, fire: () => setTrigger((n) => n + 1),
72
- heldSec, setHeldSec, save,
72
+ heldSec, setHeldSec, save, now,
73
73
  /* spread into <SignalReference> — the current mode's data, whichever shape it takes. Key the
74
74
  * element on `mode` so its open tab resets when the data changes under it. */
75
75
  reference: {
@@ -188,7 +188,8 @@ function EnvelopeHandles({ adsr, sec, onDragStart, onDrag, onDragEnd }) {
188
188
  * itself.
189
189
  *
190
190
  * TIME is the scope's clock: `bpm` sets its speed (60 BPM = one unit a second, so `t` reads as
191
- * seconds at the default and as beats at any other). Play / pause stops it where it is.
191
+ * seconds at the default and as beats at any other). Play / pause stops it where it is. `now`
192
+ * (`() => t`) hands the clock to the host — a desk transport — and the scope reads it instead.
192
193
  *
193
194
  * ADSR: four handles on the scope — A (attack time), D (decay time), S (at the end of the hold:
194
195
  * sideways = how long the gate holds, up/down = the sustain level), R (release time). The scope
@@ -205,6 +206,7 @@ function EnvelopeHandles({ adsr, sec, onDragStart, onDrag, onDragEnd }) {
205
206
  * @param {string} defaultExpr
206
207
  * @param {object} defaultAdsr knob values 0…100, `hold` in seconds (`ADSR_DEFAULTS`)
207
208
  * @param {Array} saved · savedAdsr the host's own rows for each mode's Saved tab
209
+ * @param {()=>number} now an external clock for the scope (overrides play / BPM)
208
210
  * @param {Function} onSave `({ mode, code }) => void` — shows a Save button; `code` is the
209
211
  * expression, or the envelope as `a10 d30 s70 r50 h1`
210
212
  */
@@ -270,6 +272,7 @@ export default function EnvelopeGenerator({ generator, reference = 'panel', clas
270
272
  rate={g.bpm / 60}
271
273
  loop={mode === 'equation' || g.cycle}
272
274
  trigger={g.trigger}
275
+ now={g.now}
273
276
  className="absolute inset-0"
274
277
  />
275
278
  {mode === 'adsr' && (