Migrating through 0.x
tremolo-ui is still on 0.x, so breaking changes ship in minor releases. This page collects them in one place, newest first, with what to write instead.
Every release also has generated notes per package: @tremolo-ui/functions, @tremolo-ui/dom, @tremolo-ui/react. All three packages are released together and always share a version.
Unreleased
styleHelper is gone
It turned a number into a pixel length, with an optional bit of arithmetic:
styleHelper(10) // '10px'
styleHelper(10, '/', 2) // '5px'
Nothing in the package uses it any more. Its one caller was the border radius
of Slider.Track, which is calc(var(--thickness) / 2) in CSS now that the
track no longer paints itself.
Write the two cases directly:
typeof value === 'number' ? `${value}px` : value
`calc(${value} / 2)`
PointsEditor selects points
Points can be selected, and a selection moves as one. Turn it on with
selectable — it is off by default, since it changes what a press and a
drag mean, and an editor whose points each mean something different (the four
handles of an ADSR envelope, say) has nothing to gain from moving them
together.
<PointsEditor.Root selectable>
- a press selects the point it landed on
- ctrl or ⌘ adds to the selection — not shift, which is the fine-adjustment key on every control here and cannot be both
- a drag on empty space draws a rubber band and selects what it covers; hold ctrl or ⌘ to add to what is already selected
- dragging or arrow-keying one selected point moves the whole selection
The editor keeps the selection itself unless you take it over with
selection / onSelectionChange. Points are named by their id prop, or by
one generated to last as long as the point is mounted.
<PointsEditor.Root selectable selection={selected} onSelectionChange={setSelected}>
Update each point from the previous state. A selection calls onChange on
several points in the same tick, so a handler that rebuilds its state from a
value captured in the render keeps only the last one, and every point but one
appears stuck:
// good
onChange={(v) => setPoints((prev) => ({ ...prev, [id]: v }))}
// throws away every call but the last
onChange={(v) => setPoints({ ...points, [id]: v })}
A selected point carries data-selected="true", and the rubber band is
.tremolo-points-editor-marquee. Both are in the theme in
Styling.
A drag now moves a point rather than putting it under the pointer. Grabbing
a point at its edge used to shift it under the cursor on the first movement;
it keeps the offset it was grabbed at now, which is what makes moving several
at once mean anything. min and max still hold, and a selection stops as a
whole when any one of its points reaches a limit — clamping each on its own
would pull the selection out of shape.
Deleting and duplicating are not here: the points are yours, and only you know what the array behind them is.
Appearance props write custom properties
Every prop that decides what a component looks like now sets a custom property and nothing else. The default moved with it, from the component into the CSS.
<Slider.Track thickness={16} />
// is exactly
<Slider.Track style={{ '--thickness': '16px' }} />
What changes for you. These props used to be written into the element as
inline styles, which a stylesheet could not override without !important.
Now they are the same cascade as everything else, and a rule of yours is enough
to change the default everywhere:
.tremolo-slider-track {
--thickness: 16px;
}
The properties are listed in Styling. A number is
taken as pixels; a string is written through, so '3rem' and 'auto' work.
Slider.Track no longer paints itself, and defaultStyle is gone. The
track publishes --percent — where the value sits, the one number CSS cannot
work out — and the fill is a rule in the theme:
.tremolo-slider-track {
background: linear-gradient(
var(--axis),
var(--active) var(--percent),
var(--inactive) var(--percent)
);
}
That is what defaultStyle={false} was for, and there is nothing left to turn
off: to draw the track yourself, write the rule you want instead of ours. The
Slider.css in Styling is the starting point.
If you were not using our CSS at all, Slider.Track, XYPad.Area,
PointsEditor, PointsEditor.Point and Piano no longer carry a size of
their own, so give them one — either in your CSS or through the props, which
now feed it.
Two new state attributes come with it: data-flipped on Slider.Track, for a
value that grows from the far end, and data-fill on Piano.
Arrow keys leave an IME alone
An arrow key no longer steps the value of a NumberInput while an IME is
converting. There it belongs to the candidate list, and stepping fought the
conversion.
Nothing else changed about the arrow keys. NumberInput.InputField takes a new
keepCaretOnStep if you want the caret held on its column while stepping; it
is off by default.
Shift is the fine-adjustment key
Holding shift with an arrow key now moves the value by a tenth of a step, on every component. Nothing had to be written to get it, and nothing breaks if you were not expecting it — a plain press behaves exactly as before, and the next one snaps back onto the grid.
wheel and keyboard take a map if you want something else. A plain tuple,
which is what they took before, opts out of modifiers entirely.
keyboard={{ default: ['raw', 1], alt: ['raw', 0.5] }}
keyboard={['raw', 1]}
A modifier entry is not snapped to step. Naming one says you want to move
off the grid, and without that a finer amount would round straight back to
where it started.
Nothing is bound on the wheel. Browsers turn shift+wheel into horizontal scrolling, so shift is not available there.
Shift makes a drag fine
Holding shift while dragging now moves the value a tenth as far, the same as it
does on the arrow keys, on Knob, Slider, XYPad and PointsEditor.
Pressing or releasing it partway through does not disturb the value.
dragSensitivity rebinds it, and a bare number uses no modifier at all.
<Slider.Root dragSensitivity={{ default: 1, alt: 0.25 }} … />
<Slider.Root dragSensitivity={1} … />
On Slider, XYPad and PointsEditor this changes what a drag is. Their
value is normally the position pointed at, so a fine drag cannot stay under the
pointer — it moves a tenth as fast, and the two drift apart. They stay apart
when the key is released: pulling the value back under the pointer would move
it by however far the two had drifted, which is a jump nobody asked for. Let go
and grab again to line them up.
Knob is unaffected by that: it was already relative.
NumberInput.Stepper takes it too. There a drag is already relative, so the
sensitivity simply divides the amount: drag pixels move a tenth of a step
instead of a whole one.
<NumberInput.Root dragSensitivity={{ default: 1, shift: 0.1 }} … />
pointerLock hides the cursor while dragging
Knob and NumberInput.Stepper take pointerLock. With it on, the cursor
disappears for the length of the drag and the pointer movement is read
directly.
<Knob.Root pointerLock … />
The reason is not tidiness. A relative drag does not care where the pointer
is, but it still stops at the edge of the screen: the operating system pins
the pointer there and the coordinates stop changing, so the value stops moving
however far you keep dragging. A fine drag — shift held, or a low
dragSensitivity — reaches that edge quickly.
It is off by default. The browser shows a notice of its own, Esc takes the lock back, and the request can be refused; a refused request is not an error, and the drag carries on as an ordinary one. Losing the lock ends the drag, since no pointerup is coming.
Not available on Slider, XYPad or PointsEditor: their value is the
position pointed at, and there is no position while the pointer is locked.
Shift and the wheel move XYPad and PointsEditor both ways
Shift+wheel could only ever raise the x value. The direction came from
deltaY, which browsers empty once they move the scroll onto deltaX. Both
components now read whichever axis moved, so scrolling back lowers the value.
A trackpad's own horizontal gesture moves x as well now, with no modifier held.
The package ships no CSS
@tremolo-ui/react/styles/index.css and every other ./styles/*.css export
are removed, and dist/index.css is no longer built. Importing one now fails
to resolve rather than silently doing nothing, so the break is visible at build
time.
- import '@tremolo-ui/react/styles/index.css'
+ import './tremolo-theme.css'
Nothing about the markup changed: the same tremolo- class names and the same
aria-* / data-* state attributes are still there, so a stylesheet that
already overrides them keeps working. What is gone is the one underneath it.
To keep the current appearance, copy the theme from
Styling — the six files that style every example in
these docs — into your project and import that instead. It is plain CSS with no
build step, and it is where customisation was heading anyway: previously the
only way to change a default was to load dist/index.css and then out-specify
it.
@tremolo-ui/react/styles/global.css is removed with them, along with the
.tremolo-user-select-none and .tremolo-cursor-* classes it held. This one
needs nothing done. The page-wide user-select: none applied during a drag
is now an inline style the component sets, so it works with no stylesheet at
all, and the cursor classes had already stopped being used — a dragged element
takes its cursor from createDrag.
NumberInput formats through format and parse only
units and digit are removed. They were a second way to say the same thing,
and format silently won whenever both were given.
// before
<NumberInput.Root units={[['Hz', 1], ['kHz', 1000]]} digit={2} ... >
// after
<NumberInput.Root {...unitFormat('Hz', { digits: 2 })} ... >
unitFormat comes from @tremolo-ui/functions and returns the format and
parse pair together, so it spreads straight into the input. It picks an SI
prefix by magnitude; pass prefixes: false for a unit that takes none, and
base for a value stored in a prefixed unit.
// a single symbol appended as-is, which is what `units='dB'` used to do
<NumberInput.Root {...unitFormat('dB', { prefixes: false, digits: 1 })} ... >
// the value is in milliseconds: 1500 shows as 1.5s
<NumberInput.Root {...unitFormat('s', { base: 'm' })} ... >
Units, formatValue, parseValue and selectUnit are removed from
@tremolo-ui/functions with them.
Text with no number in it now reads as NaN rather than 0, and the input keeps
the value it had. Emptying the field and leaving it used to commit a 0. A
hand-written parse should do the same for text it cannot read.
MIDI input takes a handlers object
useMIDIInput grew from three handlers to seven, which positional arguments
no longer carry.
// before
useMIDIInput(midiAccess, onNoteOn, onNoteOff, onPitchBend)
// after
useMIDIInput(midiAccess, {
onNoteOnEvent: onNoteOn,
onNoteOffEvent: onNoteOff,
onPitchBendEvent: onPitchBend,
})
Every handler is now given the channel as its last argument, as 0–15. Existing handlers ignore it and keep working.
onPitchBendEvent receives one 14-bit value rather than two raw bytes, and the
two bytes it used to be given were named the wrong way round — pitch bend sends
the low 7 bits first.
// before: the arguments were labelled (msb, lsb), but arrived as (lsb, msb)
onPitchBendEvent: (msb, lsb) => setBend(((lsb << 7) | msb) - 8192)
// after
onPitchBendEvent: (value) => setBend(value - PITCH_BEND_CENTER)
useMIDIAccess().request takes options now, so passing it straight to an event
handler sends the click event in as the options object.
// before
<button onClick={request}>Connect</button>
// after
<button onClick={() => request()}>Connect</button>
PointsEditor
grid is removed — it was never implemented, and leaked onto the DOM as a
grid attribute. children is now required, matching the other components.
readonly and disabled on PointsEditor.Root used to be ignored by the
points. They now take effect, so an editor that set readonly and relied on
the points still moving has to drop it.
0.5.0
Piano is no longer built from key components
Piano.WhiteKey, Piano.BlackKey and Piano.KeyLabel are removed, along with
the KeyProps / KeyMethods / KeyLabelProps types. Piano.Root draws the
keys itself, and per-key customization is two callbacks.
// before
<Piano.Root noteRange={range} onPlayNote={play} onStopNote={stop}>
<Piano.WhiteKey />
<Piano.BlackKey />
</Piano.Root>
// after
<Piano.Root
noteRange={range}
onPlayNote={play}
onStopNote={stop}
keyProps={(note, { keyType }) => ({ 'data-in-scale': inScale(note, root, 'major') })}
/>
Each key carries data-note, data-note-key, data-active and
aria-disabled, so static styling needs no callback at all — plain CSS on
.tremolo-piano-white-key / .tremolo-piano-black-key reaches everything.
Other changes to Piano:
labeltakes(note, state)rather than(note, index); the index isstate.index. A label of'',nullorundefineddraws nothing rather than an empty boxwhiteNoteWidthiswhiteKeyWidth. The deadblackNoteWidthis replaced byblackKeyWidthRatio/blackKeyHeightRatio/keyGapgetNoteRangeArrayandNoteRangemoved to@tremolo-ui/functionsKeyboardShortcuts.flagsis removed. It was declared but never implemented;SHORTCUTS.HOME_ROW_NATURALcovers whatnaturalOnlywas for
Slider.Scale is Slider.Marks
The name clashed with the value-curve Scale introduced in the same release.
// before
<Slider.Scale options={[...]} />
<Slider.ScaleOption value={50} />
// after
<Slider.Marks options={[...]} />
<Slider.MarksOption value={50} />
ScaleProps and ScaleOptionProps become MarksProps and MarksOptionProps.
skew is replaced by scale
A skew factor is now one of a set of named scales, which behave properly at the ends of the range.
// before
<Slider.Root skew={0.5} ... />
// after
<Slider.Root scale={skewScale(0.5)} ... />
linearScale is the default. exponentialScale suits frequency, curveScale(n)
takes an exponent, symmetricSkewScale(n) bends both halves of a bipolar range,
and skewScale(n) matches a JUCE parameter exactly.
NumberInput is redesigned
variant,activeColor,wrapperClassName,keepWithinRangeandclampValueOnBlurare removed fromRoot.clampValuereplaces the last twoselectWithFocus,blurOnEnter,onFocusandonBlurmove toInputField;selectWithFocusis nowselectOnFocus- The tab stop is the
<input>, which carriesrole="spinbutton"and thearia-value*attributes.data-errorisdata-out-of-range Stepper'sdynamic, andsizeonIncrementStepper/DecrementStepper, are replaced by CSS variables
The wheel only acts while the focus is inside
Every component used to change value on a wheel event whether or not it had focus, so scrolling a page past one silently changed it. They now require the focus to be inside. Nothing to change in your code; the behaviour is different.
0.4.0
useDragWithElement is replaced by useDragValue
// before
const ref = useDragWithElement(({ x, y }) => setValue(x * 100))
// after
const { refCallback, dragging } = useDragValue({
axis: { min: 0, max: 100 },
baseElementRef: trackRef,
onChange: ([x]) => setValue(x),
})
useDragValue reports values rather than normalized coordinates, and covers
both mappings: baseElementRef normalizes the pointer against an element, and
getValue moves the value away from where it stood by the distance dragged.
useDrag returns a ref callback
// before
const [ref] = useDrag(handlers)
// after
const ref = useDrag(handlers)
DragObserver and WheelObserver are removed
Use useDrag and useWheel, or createDrag and createWheel from
@tremolo-ui/dom outside React.
XYPad's XYOrSingle is XYInput
Its pair form is a readonly tuple. Where the value could itself be an array —
scale, for one — only the pair form is accepted, since a lone array would be
read as a pair.
Still to come
The CSS is not headless yet: @tremolo-ui/react ships index.css files and
you are expected to import them. That will change before 1.0, and this page
will carry the steps.