@kolkrabbi/kol-hardware 0.8.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-hardware",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "description": "Panel controls for instruments",
6
6
  "license": "MIT",
@@ -23,24 +23,31 @@ import { useTransport, useTransportNow } from './useTransport.js'
23
23
  */
24
24
 
25
25
  /* The visible span: the loop window's far edge, at least four seconds, and on a
26
- free-running clock it grows in four-second steps to keep the head on screen. */
27
- function spanOf(t, now) {
26
+ free-running clock it grows in four-second steps to keep the head on screen.
27
+ `span="loop"` (TimeStripInADock, kol-fxr 2026-10-28): the loop window exactly, so a lane's `t` 0..1 is the full width;
28
+ `span={seconds}`: that many seconds from 0. Returns [origin, span] — origin is where the left edge sits, in seconds. */
29
+ function viewOf(t, now, mode = 'auto') {
30
+ if (mode === 'loop') {
31
+ const a = Math.min(t.loopIn, t.loopOut)
32
+ return [a, Math.max(Math.max(t.loopIn, t.loopOut) - a, 1e-6)]
33
+ }
34
+ if (typeof mode === 'number' && mode > 0) return [0, mode]
28
35
  const far = Math.max(t.loopIn, t.loopOut, 4)
29
36
  const free = t.mode === 'forward' && !t.looping
30
- return free && now > far ? Math.ceil(now / 4) * 4 : far
37
+ return [0, free && now > far ? Math.ceil(now / 4) * 4 : far]
31
38
  }
32
39
 
33
40
  /* The bar itself — the loop window shaded, IN marked in the accent, the head. */
34
- function Bar({ transport, t, now, span, height = 14, marker = false }) {
41
+ function Bar({ transport, t, now, span, origin = 0, height = 14, marker = false }) {
35
42
  const ref = useRef(null)
36
43
  const drag = useRef(false)
37
44
  const at = (e) => {
38
45
  const box = ref.current.getBoundingClientRect()
39
- seekToSeconds(transport, Math.max(0, Math.min(1, (e.clientX - box.left) / box.width)) * span)
46
+ seekToSeconds(transport, origin + Math.max(0, Math.min(1, (e.clientX - box.left) / box.width)) * span)
40
47
  }
41
48
  const a = Math.min(t.loopIn, t.loopOut)
42
49
  const b = Math.max(t.loopIn, t.loopOut)
43
- const pct = (x) => `${Math.max(0, Math.min(100, (x / span) * 100))}%`
50
+ const pct = (x) => `${Math.max(0, Math.min(100, ((x - origin) / span) * 100))}%`
44
51
  return (
45
52
  <div
46
53
  ref={ref}
@@ -54,7 +61,7 @@ function Bar({ transport, t, now, span, height = 14, marker = false }) {
54
61
  {/* the loop window — lit when the clock wraps inside it */}
55
62
  <div
56
63
  className="absolute top-0 bottom-0"
57
- style={{ left: pct(a), width: pct(b - a), background: t.looping || t.mode !== 'forward' ? 'var(--kol-fg-24)' : 'var(--kol-fg-12)' }}
64
+ style={{ left: pct(a), width: `${Math.max(0, Math.min(100, ((b - a) / span) * 100))}%`, background: t.looping || t.mode !== 'forward' ? 'var(--kol-fg-24)' : 'var(--kol-fg-12)' }}
58
65
  />
59
66
  {marker && (
60
67
  <div className="absolute top-0 bottom-0" style={{ left: pct(a), width: 1, background: 'var(--kol-accent-primary)' }}>
@@ -74,48 +81,87 @@ export function Scrubber({ transport, width = 232 }) {
74
81
  const now = useTransportNow(transport)
75
82
  return (
76
83
  <div style={{ width }}>
77
- <Bar transport={transport} t={t} now={now} span={spanOf(t, now)} height={10} />
84
+ <Bar transport={transport} t={t} now={now} span={viewOf(t, now)[1]} height={10} />
78
85
  </div>
79
86
  )
80
87
  }
81
88
 
82
- /** The studio's bottom strip — hidden by default, `B` shows it. `children` render under the bar, on its span (lanes). */
83
- export default function TimeStrip({ transport, onClose, children }) {
89
+ /** The studio's bottom strip — hidden by default, `B` shows it. `children` render under the bar, on its span (lanes); as a
90
+ * function they get `{ now, span, origin, state }`.
91
+ *
92
+ * For a host that frames it (TimeStripInADock, kol-fxr 2026-10-28 — fxr's TimelineDock is an in-flow panel):
93
+ * framed={false} in flow — no absolute position, border, padding, background or z; the host frames it
94
+ * span="loop" the loop window exactly (or `span={seconds}`); default `"auto"` is mirror's
95
+ * gutter={px} a left column that wide: the readout sits in it, the ruler and bar start after it, and `children`
96
+ * render full width beneath — so a lane's own label column lines up with the ruler
97
+ */
98
+ export default function TimeStrip({ transport, onClose, children, framed = true, span: spanMode = 'auto', gutter }) {
84
99
  const t = useTransport(transport)
85
100
  const now = useTransportNow(transport)
86
- const span = spanOf(t, now)
87
- const ticks = Array.from({ length: Math.floor(span) + 1 }, (_, i) => i)
101
+ const [origin, span] = viewOf(t, now, spanMode)
102
+ const first = Math.ceil(origin)
103
+ const ticks = Array.from({ length: Math.max(0, Math.floor(origin + span) - first + 1) }, (_, i) => first + i)
104
+ const x = (s) => ((s - origin) / span) * 100
105
+ const loopOn = t.looping || t.mode !== 'forward'
106
+ const lanes = typeof children === 'function' ? children({ now, span, origin, state: t }) : children
107
+ const ruler = (
108
+ <div className="relative w-full" style={{ height: 12 }}>
109
+ {ticks.map((s) => (
110
+ <span key={s} className="absolute text-fg-32" style={{ left: `${x(s)}%`, transform: s > origin ? 'translateX(-50%)' : undefined }}>
111
+ {span <= 16 || s % 4 === 0 ? s : ''}
112
+ </span>
113
+ ))}
114
+ <div className="absolute" style={{ left: `${Math.max(0, Math.min(100, x(now)))}%`, top: 0, bottom: -4, width: 1, background: 'var(--kol-fg-96)' }} />
115
+ </div>
116
+ )
117
+ const bar = <Bar transport={transport} t={t} now={now} span={span} origin={origin} height={16} marker />
118
+ const readout = <span>{now.toFixed(2)} / {span.toFixed(2)} s</span>
119
+ const loopLabel = <span className="text-fg-96">Loop {loopOn ? 'on' : 'off'}</span>
120
+ const frame = framed
121
+ ? { className: 'absolute left-0 right-0 bottom-0 flex items-stretch gap-4 px-4 py-3 border-t border-fg-08 kol-helper-10 group', style: { background: 'var(--kol-surface-primary)', zIndex: 'var(--kol-z-overlay)' } }
122
+ : { className: 'relative flex items-stretch gap-4 kol-helper-10 group', style: undefined }
123
+ const close = onClose && <button
124
+ type="button"
125
+ onClick={onClose}
126
+ className="self-start text-fg-32 hover:text-fg-96 opacity-0 group-hover:opacity-100 cursor-pointer"
127
+ aria-label="Hide timeline"
128
+ title="Hide timeline (B)"
129
+ >
130
+ ×
131
+ </button>
132
+
133
+ if (gutter != null) {
134
+ const cell = { width: gutter, fontVariantNumeric: 'tabular-nums' }
135
+ return (
136
+ <div className={frame.className} style={frame.style}>
137
+ <div className="flex flex-col flex-1 gap-2" style={{ minWidth: 0 }}>
138
+ <div className="flex items-end">
139
+ <div className="shrink-0 text-fg-64" style={cell}>{readout}</div>
140
+ <div className="flex-1" style={{ minWidth: 0 }}>{ruler}</div>
141
+ </div>
142
+ <div className="flex items-center">
143
+ <div className="shrink-0" style={cell}>{loopLabel}</div>
144
+ <div className="flex-1" style={{ minWidth: 0 }}>{bar}</div>
145
+ </div>
146
+ {lanes}
147
+ </div>
148
+ {close}
149
+ </div>
150
+ )
151
+ }
152
+
88
153
  return (
89
- <div
90
- className="absolute left-0 right-0 bottom-0 flex items-stretch gap-4 px-4 py-3 border-t border-fg-08 kol-helper-10 group"
91
- style={{ background: 'var(--kol-surface-primary)', zIndex: 'var(--kol-z-overlay)' }}
92
- >
154
+ <div className={frame.className} style={frame.style}>
93
155
  <div className="flex flex-col justify-between shrink-0 text-fg-64" style={{ width: 112, fontVariantNumeric: 'tabular-nums' }}>
94
- <span>{now.toFixed(2)} / {span.toFixed(2)} s</span>
95
- <span className="text-fg-96">Loop {t.looping || t.mode !== 'forward' ? 'on' : 'off'}</span>
156
+ {readout}
157
+ {loopLabel}
96
158
  </div>
97
159
  <div className="flex flex-col flex-1 gap-2" style={{ minWidth: 0 }}>
98
- {/* the ruler — a tick a second, a number every second until it crowds */}
99
- <div className="relative w-full" style={{ height: 12 }}>
100
- {ticks.map((s) => (
101
- <span key={s} className="absolute text-fg-32" style={{ left: `${(s / span) * 100}%`, transform: s ? 'translateX(-50%)' : undefined }}>
102
- {span <= 16 || s % 4 === 0 ? s : ''}
103
- </span>
104
- ))}
105
- <div className="absolute" style={{ left: `${Math.min(100, (now / span) * 100)}%`, top: 0, bottom: -4, width: 1, background: 'var(--kol-fg-96)' }} />
106
- </div>
107
- <Bar transport={transport} t={t} now={now} span={span} height={16} marker />
108
- {typeof children === 'function' ? children({ now, span, state: t }) : children}
160
+ {ruler}
161
+ {bar}
162
+ {lanes}
109
163
  </div>
110
- {onClose && <button
111
- type="button"
112
- onClick={onClose}
113
- className="self-start text-fg-32 hover:text-fg-96 opacity-0 group-hover:opacity-100 cursor-pointer"
114
- aria-label="Hide timeline"
115
- title="Hide timeline (B)"
116
- >
117
- ×
118
- </button>}
164
+ {close}
119
165
  </div>
120
166
  )
121
167
  }