@zylem/ui 0.8.14 → 0.9.1
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/dist/chunk-YJVLOHJL.js +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.js +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +6 -1
- package/src/components/BuildLabel/BuildLabel.tsx +0 -1
- package/src/components/BuildLabel/mount-build-label.ts +0 -1
- package/src/components/Button/Button.css.ts +85 -66
- package/src/components/Button/Button.tsx +9 -3
- package/src/components/Callout/Callout.css.ts +1 -1
- package/src/components/Callout/Callout.tsx +1 -1
- package/src/components/Card/Card.css.ts +1 -0
- package/src/components/Card/Card.tsx +11 -2
- package/src/components/Checkbox/Checkbox.tsx +4 -2
- package/src/components/ColorPalette/ColorPalette.tsx +1 -1
- package/src/components/Console/Console.css.ts +1 -1
- package/src/components/Console/Console.tsx +3 -0
- package/src/components/Dialog/Dialog.css.ts +41 -17
- package/src/components/Dialog/Dialog.tsx +134 -53
- package/src/components/FrameStrip/FrameStrip.css.ts +1 -0
- package/src/components/FrameStrip/FrameStrip.tsx +2 -3
- package/src/components/ItemPicker/ItemPicker.tsx +46 -33
- package/src/components/MenuButton/MenuButton.css.ts +49 -46
- package/src/components/MenuButton/MenuButton.tsx +17 -5
- package/src/components/Menubar/Menubar.css.ts +1 -1
- package/src/components/Mixer/Knob.tsx +10 -3
- package/src/components/Mixer/Mixer.css.ts +21 -0
- package/src/components/Mixer/MixerStrip.tsx +29 -8
- package/src/components/NumberField/NumberField.css.ts +32 -26
- package/src/components/NumberField/NumberField.tsx +7 -2
- package/src/components/Panel/Panel.css.ts +1 -0
- package/src/components/Panel/Panel.tsx +9 -1
- package/src/components/PianoRoll/PianoRoll.tsx +1 -0
- package/src/components/Progress/Progress.css.ts +1 -1
- package/src/components/Progress/Progress.tsx +3 -0
- package/src/components/Property/Property.css.ts +13 -0
- package/src/components/Property/Property.tsx +3 -2
- package/src/components/SearchInput/SearchInput.tsx +1 -1
- package/src/components/Select/Select.css.ts +7 -1
- package/src/components/Select/Select.tsx +7 -2
- package/src/components/Sidebar/Sidebar.css.ts +1 -0
- package/src/components/Sidebar/Sidebar.tsx +23 -5
- package/src/components/Slider/Slider.css.ts +1 -1
- package/src/components/Slider/Slider.tsx +4 -2
- package/src/components/SpriteSizePicker/SpriteSizePicker.css.ts +2 -0
- package/src/components/Switch/Switch.tsx +3 -1
- package/src/components/Tabs/Tabs.css.ts +0 -5
- package/src/components/Tabs/Tabs.tsx +23 -12
- package/src/components/TextField/TextField.tsx +4 -0
- package/src/components/TimelineLanes/TimelineLanes.css.ts +7 -0
- package/src/components/TimelineLanes/TimelineLanes.tsx +52 -4
- package/src/components/TimelineLanes/lanes-math.ts +51 -0
- package/src/components/TimelineTrack/TimelineTrack.css.ts +3 -2
- package/src/components/TimelineTrack/TimelineTrack.tsx +16 -3
- package/src/components/TitleBar/TitleBar.css.ts +1 -0
- package/src/components/TitleBar/TitleBar.tsx +7 -1
- package/src/components/Toast/Toast.tsx +1 -2
- package/src/components/ToolButton/ToolButton.tsx +1 -1
- package/src/components/Toolbar/Toolbar.tsx +6 -1
- package/src/components/ToolbarButton/ToolbarButton.tsx +9 -2
- package/src/components/ToolboxButton/ToolboxButton.tsx +2 -1
- package/src/components/TrackHeader/TrackHeader.css.ts +21 -0
- package/src/components/TrackHeader/TrackHeader.tsx +37 -12
- package/src/components/TransportBar/TransportBar.css.ts +5 -0
- package/src/components/TransportBar/TransportBar.tsx +4 -1
- package/src/components/Waveform/Waveform.css.ts +10 -0
- package/src/components/Waveform/Waveform.tsx +59 -27
- package/src/components/WindowControls/WindowControls.css.ts +46 -63
- package/src/components/WindowControls/WindowControls.tsx +7 -6
- package/src/components/index.ts +1 -0
- package/src/components/shared/Heading.tsx +30 -0
- package/src/fonts.css +1 -1
- package/src/global/accordion.css.ts +9 -5
- package/src/global/common.css.ts +13 -0
- package/src/global/editor-base.css.ts +9 -0
- package/src/layers/LayerContext.tsx +2 -2
- package/src/sprinkles.css.ts +4 -1
- package/src/theme.css.ts +6 -0
- package/dist/chunk-V47CGFWX.js +0 -1
|
@@ -46,12 +46,12 @@ export interface TabsTriggerProps {
|
|
|
46
46
|
disabled?: boolean;
|
|
47
47
|
class?: string;
|
|
48
48
|
/**
|
|
49
|
-
* Document-style tabs:
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* Document-style tabs: Delete (Backspace on a Mac keyboard) closes the
|
|
50
|
+
* focused tab, and a close affordance after the label does the same for the
|
|
51
|
+
* pointer. Closing does not select the tab.
|
|
52
52
|
*/
|
|
53
53
|
onClose?: () => void;
|
|
54
|
-
/**
|
|
54
|
+
/** Tooltip of the close affordance; defaults to `Close tab`. */
|
|
55
55
|
closeLabel?: string;
|
|
56
56
|
/** Unsaved-changes marker before the close affordance. */
|
|
57
57
|
dirty?: boolean;
|
|
@@ -59,10 +59,22 @@ export interface TabsTriggerProps {
|
|
|
59
59
|
|
|
60
60
|
export function TabsTrigger(props: TabsTriggerProps) {
|
|
61
61
|
const close = (event: Event) => {
|
|
62
|
+
// Stopped so a page-level Delete shortcut does not also act on the key.
|
|
62
63
|
event.stopPropagation();
|
|
63
64
|
event.preventDefault();
|
|
64
65
|
props.onClose?.();
|
|
65
66
|
};
|
|
67
|
+
const closeFromKeyboard = (event: KeyboardEvent & { currentTarget: HTMLButtonElement }) => {
|
|
68
|
+
if (!props.onClose || (event.key !== 'Delete' && event.key !== 'Backspace')) return;
|
|
69
|
+
const trigger = event.currentTarget;
|
|
70
|
+
const list = trigger.closest('[role="tablist"]');
|
|
71
|
+
close(event);
|
|
72
|
+
// A closed tab usually leaves the document, and focus with it.
|
|
73
|
+
queueMicrotask(() => {
|
|
74
|
+
if (trigger.isConnected) return;
|
|
75
|
+
list?.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]')?.focus();
|
|
76
|
+
});
|
|
77
|
+
};
|
|
66
78
|
return (
|
|
67
79
|
<KTabs.Trigger
|
|
68
80
|
class={
|
|
@@ -70,25 +82,24 @@ export function TabsTrigger(props: TabsTriggerProps) {
|
|
|
70
82
|
}
|
|
71
83
|
value={props.value}
|
|
72
84
|
disabled={props.disabled}
|
|
85
|
+
aria-keyshortcuts={props.onClose ? 'Delete Backspace' : undefined}
|
|
86
|
+
onKeyDown={closeFromKeyboard}
|
|
73
87
|
>
|
|
74
88
|
<span class="zylem-tabs-trigger-label">{props.children}</span>
|
|
75
89
|
<Show when={props.dirty}>
|
|
76
|
-
<span class="zylem-tabs-dirty" aria-
|
|
90
|
+
<span class="zylem-tabs-dirty" aria-hidden="true" title="Unsaved changes" />
|
|
91
|
+
<span class="zylem-visually-hidden">(unsaved changes)</span>
|
|
77
92
|
</Show>
|
|
78
93
|
<Show when={props.onClose}>
|
|
94
|
+
{/* Pointer only: a button may not nest in the trigger button, and the key covers it. */}
|
|
79
95
|
<span
|
|
80
96
|
class="zylem-tabs-close"
|
|
81
|
-
|
|
82
|
-
tabIndex={0}
|
|
83
|
-
aria-label={props.closeLabel ?? 'Close tab'}
|
|
97
|
+
aria-hidden="true"
|
|
84
98
|
title={props.closeLabel ?? 'Close tab'}
|
|
85
99
|
onClick={close}
|
|
86
100
|
onPointerDown={(event) => event.stopPropagation()}
|
|
87
|
-
onKeyDown={(event) => {
|
|
88
|
-
if (event.key === 'Enter' || event.key === ' ') close(event);
|
|
89
|
-
}}
|
|
90
101
|
>
|
|
91
|
-
<X
|
|
102
|
+
<X />
|
|
92
103
|
</span>
|
|
93
104
|
</Show>
|
|
94
105
|
</KTabs.Trigger>
|
|
@@ -4,6 +4,8 @@ import type { JSX } from 'solid-js';
|
|
|
4
4
|
|
|
5
5
|
export interface TextFieldProps {
|
|
6
6
|
label?: string;
|
|
7
|
+
/** Accessible name when there is no visible `label`. */
|
|
8
|
+
'aria-label'?: string;
|
|
7
9
|
error?: string;
|
|
8
10
|
placeholder?: string;
|
|
9
11
|
value?: string;
|
|
@@ -92,6 +94,7 @@ export function TextField(props: TextFieldProps) {
|
|
|
92
94
|
<KTextField.Input
|
|
93
95
|
class="zylem-field zylem-textfield-input"
|
|
94
96
|
type={props.type ?? 'text'}
|
|
97
|
+
aria-label={props['aria-label']}
|
|
95
98
|
placeholder={props.placeholder}
|
|
96
99
|
autofocus={props.autofocus}
|
|
97
100
|
onInput={props.onInput as JSX.EventHandlerUnion<HTMLInputElement, InputEvent>}
|
|
@@ -102,6 +105,7 @@ export function TextField(props: TextFieldProps) {
|
|
|
102
105
|
>
|
|
103
106
|
<KTextField.TextArea
|
|
104
107
|
class="zylem-field zylem-textfield-textarea"
|
|
108
|
+
aria-label={props['aria-label']}
|
|
105
109
|
placeholder={props.placeholder}
|
|
106
110
|
rows={props.rows}
|
|
107
111
|
autofocus={props.autofocus}
|
|
@@ -66,6 +66,12 @@ globalStyle('.zylem-lanes-ruler', {
|
|
|
66
66
|
cursor: 'pointer',
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
// Inset: the scrolling lanes would clip a ring drawn outside the sticky ruler.
|
|
70
|
+
globalStyle('.zylem-lanes-ruler:focus-visible', {
|
|
71
|
+
outline: 'none',
|
|
72
|
+
boxShadow: 'inset 0 0 0 2px rgba(97, 166, 232, 0.6)',
|
|
73
|
+
});
|
|
74
|
+
|
|
69
75
|
globalStyle('.zylem-lanes-range', {
|
|
70
76
|
position: 'absolute',
|
|
71
77
|
top: 0,
|
|
@@ -94,6 +100,7 @@ globalStyle('.zylem-lanes-tick-label', {
|
|
|
94
100
|
position: 'absolute',
|
|
95
101
|
left: '4px',
|
|
96
102
|
bottom: '8px',
|
|
103
|
+
fontFamily: vars.typography.fontFamilyMono,
|
|
97
104
|
fontSize: '10px',
|
|
98
105
|
fontVariantNumeric: 'tabular-nums',
|
|
99
106
|
whiteSpace: 'nowrap',
|
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
type ClipBounds,
|
|
14
14
|
clamp,
|
|
15
|
+
describeTime,
|
|
15
16
|
resolveClipDrag,
|
|
16
17
|
rulerTicks,
|
|
18
|
+
sliderKeyValue,
|
|
17
19
|
snapTime,
|
|
18
20
|
type TimeScale,
|
|
19
21
|
} from './lanes-math';
|
|
@@ -59,7 +61,11 @@ export interface TimelineLanesProps {
|
|
|
59
61
|
/** Timeline length in units. */
|
|
60
62
|
length: number;
|
|
61
63
|
playhead?: number;
|
|
62
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Fires while the ruler is clicked or dragged, and for its keys once it has
|
|
66
|
+
* focus: the arrows step by the ruler's ticks (by its labelled ticks with
|
|
67
|
+
* Shift), Home and End jump to the ends.
|
|
68
|
+
*/
|
|
63
69
|
onSeek?: (time: number) => void;
|
|
64
70
|
/** Drag snap in units, applied to clips and markers (not to seeking). */
|
|
65
71
|
snap?: number;
|
|
@@ -134,6 +140,24 @@ export function TimelineLanes(props: TimelineLanesProps) {
|
|
|
134
140
|
window.addEventListener('pointercancel', stop);
|
|
135
141
|
};
|
|
136
142
|
|
|
143
|
+
const seekFromKey = (event: KeyboardEvent) => {
|
|
144
|
+
if (props.disabled || !props.onSeek) return;
|
|
145
|
+
const step = minorStep() > 0 ? minorStep() / ppu() : 1;
|
|
146
|
+
const major = majorStep() / ppu();
|
|
147
|
+
const next = sliderKeyValue(event, {
|
|
148
|
+
value: props.playhead ?? 0,
|
|
149
|
+
min: 0,
|
|
150
|
+
max: props.length,
|
|
151
|
+
step,
|
|
152
|
+
bigStep: major > step ? major : step * 10,
|
|
153
|
+
});
|
|
154
|
+
if (next === null) return;
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
// Page-level transport shortcuts (Home, arrows) must not seek a second time.
|
|
157
|
+
event.stopPropagation();
|
|
158
|
+
props.onSeek(next);
|
|
159
|
+
};
|
|
160
|
+
|
|
137
161
|
const handleWheel = (event: WheelEvent) => {
|
|
138
162
|
if (!props.onZoom || !(event.ctrlKey || event.metaKey)) return;
|
|
139
163
|
event.preventDefault();
|
|
@@ -171,11 +195,17 @@ export function TimelineLanes(props: TimelineLanesProps) {
|
|
|
171
195
|
class="zylem-lanes-ruler"
|
|
172
196
|
style={{ width: `${bodyWidth()}px` }}
|
|
173
197
|
onPointerDown={startSeek}
|
|
198
|
+
onKeyDown={seekFromKey}
|
|
174
199
|
role={props.onSeek ? 'slider' : undefined}
|
|
200
|
+
tabIndex={props.onSeek && !props.disabled ? 0 : undefined}
|
|
175
201
|
aria-label={props.onSeek ? 'Playhead' : undefined}
|
|
176
202
|
aria-valuemin={props.onSeek ? 0 : undefined}
|
|
177
203
|
aria-valuemax={props.onSeek ? props.length : undefined}
|
|
178
|
-
aria-valuenow={props.onSeek ? props.playhead : undefined}
|
|
204
|
+
aria-valuenow={props.onSeek ? (props.playhead ?? 0) : undefined}
|
|
205
|
+
aria-valuetext={
|
|
206
|
+
props.onSeek ? describeTime(props.playhead ?? 0, props.scale) : undefined
|
|
207
|
+
}
|
|
208
|
+
aria-disabled={props.onSeek && props.disabled ? 'true' : undefined}
|
|
179
209
|
>
|
|
180
210
|
<Show when={props.range}>
|
|
181
211
|
{(range) => (
|
|
@@ -298,7 +328,8 @@ export interface TimelineClipProps {
|
|
|
298
328
|
movable?: boolean;
|
|
299
329
|
resizable?: boolean;
|
|
300
330
|
disabled?: boolean;
|
|
301
|
-
|
|
331
|
+
/** Pointer down, or Enter / Space while focused. */
|
|
332
|
+
onSelect?: (event: MouseEvent | KeyboardEvent) => void;
|
|
302
333
|
/** Fires continuously while dragging. */
|
|
303
334
|
onChange?: (change: TimelineClipChange) => void;
|
|
304
335
|
/** Fires once when a drag ends, with the final bounds. */
|
|
@@ -378,6 +409,17 @@ export function TimelineClip(props: TimelineClipProps) {
|
|
|
378
409
|
aria-label={props.label}
|
|
379
410
|
aria-pressed={props.selected ? 'true' : 'false'}
|
|
380
411
|
onPointerDown={beginDrag('move')}
|
|
412
|
+
onKeyDown={(event) => {
|
|
413
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
414
|
+
event.preventDefault();
|
|
415
|
+
// Space is also a page-level play shortcut; on a focused clip it selects.
|
|
416
|
+
event.stopPropagation();
|
|
417
|
+
props.onSelect?.(event);
|
|
418
|
+
}}
|
|
419
|
+
onClick={(event) => {
|
|
420
|
+
// Assistive tech activates with a click and no pointer down before it.
|
|
421
|
+
if (event.detail === 0) props.onSelect?.(event);
|
|
422
|
+
}}
|
|
381
423
|
onDblClick={(event) => {
|
|
382
424
|
event.stopPropagation();
|
|
383
425
|
props.onDoubleClick?.();
|
|
@@ -412,7 +454,8 @@ export interface KeyframeMarkerProps {
|
|
|
412
454
|
color?: string;
|
|
413
455
|
selected?: boolean;
|
|
414
456
|
disabled?: boolean;
|
|
415
|
-
|
|
457
|
+
/** Pointer down, or a click from the keyboard or assistive tech. */
|
|
458
|
+
onSelect?: (event: MouseEvent | KeyboardEvent) => void;
|
|
416
459
|
/** Fires continuously while dragging. */
|
|
417
460
|
onChange?: (time: number) => void;
|
|
418
461
|
/** Fires once when a drag ends. */
|
|
@@ -476,6 +519,11 @@ export function KeyframeMarker(props: KeyframeMarkerProps) {
|
|
|
476
519
|
aria-pressed={props.selected ? 'true' : 'false'}
|
|
477
520
|
title={props.label}
|
|
478
521
|
onPointerDown={beginDrag}
|
|
522
|
+
onClick={(event) => {
|
|
523
|
+
// The pointer already selected on pointer down; keys and assistive tech
|
|
524
|
+
// click without one.
|
|
525
|
+
if (event.detail === 0) props.onSelect?.(event);
|
|
526
|
+
}}
|
|
479
527
|
onDblClick={(event) => {
|
|
480
528
|
event.stopPropagation();
|
|
481
529
|
props.onDoubleClick?.();
|
|
@@ -30,6 +30,57 @@ export function clamp(value: number, min: number, max: number): number {
|
|
|
30
30
|
return Math.min(max, Math.max(min, value));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface SliderKeyOptions {
|
|
34
|
+
value: number;
|
|
35
|
+
min: number;
|
|
36
|
+
max: number;
|
|
37
|
+
step: number;
|
|
38
|
+
/** Shift + arrow; defaults to ten steps. */
|
|
39
|
+
bigStep?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where a slider key moves `value`: arrows step (Shift for a big step), Home
|
|
44
|
+
* and End jump to the ends. Null for any other key, which the slider should
|
|
45
|
+
* let through.
|
|
46
|
+
*/
|
|
47
|
+
export function sliderKeyValue(
|
|
48
|
+
event: Pick<KeyboardEvent, 'key' | 'shiftKey'>,
|
|
49
|
+
options: SliderKeyOptions,
|
|
50
|
+
): number | null {
|
|
51
|
+
const step = event.shiftKey ? (options.bigStep ?? options.step * 10) : options.step;
|
|
52
|
+
switch (event.key) {
|
|
53
|
+
case 'ArrowRight':
|
|
54
|
+
case 'ArrowUp':
|
|
55
|
+
return clamp(options.value + step, options.min, options.max);
|
|
56
|
+
case 'ArrowLeft':
|
|
57
|
+
case 'ArrowDown':
|
|
58
|
+
return clamp(options.value - step, options.min, options.max);
|
|
59
|
+
case 'Home':
|
|
60
|
+
return options.min;
|
|
61
|
+
case 'End':
|
|
62
|
+
return options.max;
|
|
63
|
+
default:
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Seconds as spoken text for `aria-valuetext`: `1.5 seconds`. */
|
|
69
|
+
export function describeSeconds(seconds: number): string {
|
|
70
|
+
const rounded = Math.round(Math.max(0, seconds) * 100) / 100;
|
|
71
|
+
return `${rounded} ${rounded === 1 ? 'second' : 'seconds'}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A time on `scale` as spoken text: `1.5 seconds`, or `bar 2, beat 1` (both 1-based). */
|
|
75
|
+
export function describeTime(time: number, scale: Pick<TimeScale, 'unit' | 'beatsPerBar'>): string {
|
|
76
|
+
if (scale.unit === 'seconds') return describeSeconds(time);
|
|
77
|
+
const bpb = Math.max(1, scale.beatsPerBar ?? 4);
|
|
78
|
+
const total = Math.round(Math.max(0, time) * 100) / 100;
|
|
79
|
+
const bar = Math.floor(total / bpb) + 1;
|
|
80
|
+
const beat = Math.round((total - (bar - 1) * bpb + 1) * 100) / 100;
|
|
81
|
+
return `bar ${bar}, beat ${beat}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
33
84
|
export interface ClipBounds {
|
|
34
85
|
start: number;
|
|
35
86
|
length: number;
|
|
@@ -142,16 +142,17 @@ globalStyle('.zylem-timeline-thumb-input', {
|
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
// Fixed box so the readout doesn't reflow the track as digits change:
|
|
145
|
-
// wide enough for `59.99s / 120s
|
|
145
|
+
// wide enough for `59.99s / 120s` in the mono face, one track-height tall.
|
|
146
146
|
globalStyle('.zylem-timeline-time', {
|
|
147
147
|
boxSizing: 'border-box',
|
|
148
148
|
flex: 'none',
|
|
149
149
|
display: 'inline-flex',
|
|
150
150
|
alignItems: 'center',
|
|
151
151
|
justifyContent: 'flex-end',
|
|
152
|
-
width: '
|
|
152
|
+
width: '14ch',
|
|
153
153
|
height: TRACK_HEIGHT,
|
|
154
154
|
lineHeight: TRACK_HEIGHT,
|
|
155
|
+
fontFamily: vars.typography.fontFamilyMono,
|
|
155
156
|
fontSize: `calc(${vars.typography.fontSize} - 1px)`,
|
|
156
157
|
fontWeight: 600,
|
|
157
158
|
fontVariantNumeric: 'tabular-nums',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Slider as KSlider } from '@kobalte/core';
|
|
2
2
|
import { For, Show, createMemo } from 'solid-js';
|
|
3
|
+
import { describeSeconds } from '../TimelineLanes/lanes-math';
|
|
3
4
|
import { formatSeconds, intervalTicks, markerPercent } from './timeline-math';
|
|
4
5
|
|
|
5
6
|
export interface TimelineTrackProps {
|
|
@@ -19,6 +20,10 @@ export interface TimelineTrackProps {
|
|
|
19
20
|
showTime?: boolean;
|
|
20
21
|
disabled?: boolean;
|
|
21
22
|
label?: string;
|
|
23
|
+
/** Accessible name when there is no visible `label`. */
|
|
24
|
+
'aria-label'?: string;
|
|
25
|
+
/** The value as a screen reader reads it (default: seconds). */
|
|
26
|
+
valueText?: (value: number) => string;
|
|
22
27
|
class?: string;
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -34,6 +39,7 @@ export function TimelineTrack(props: TimelineTrackProps) {
|
|
|
34
39
|
return [];
|
|
35
40
|
});
|
|
36
41
|
const pct = (t: number) => `${markerPercent(t, min(), props.max)}%`;
|
|
42
|
+
const valueText = () => (props.valueText ?? describeSeconds)(props.value);
|
|
37
43
|
|
|
38
44
|
return (
|
|
39
45
|
<div class={props.class ? `zylem-timeline ${props.class}` : 'zylem-timeline'}>
|
|
@@ -45,7 +51,6 @@ export function TimelineTrack(props: TimelineTrackProps) {
|
|
|
45
51
|
maxValue={props.max}
|
|
46
52
|
step={props.step ?? 0.01}
|
|
47
53
|
disabled={props.disabled}
|
|
48
|
-
getValueLabel={(params) => formatSeconds(params.values[0] ?? 0)}
|
|
49
54
|
>
|
|
50
55
|
<Show when={props.label}>
|
|
51
56
|
<KSlider.Label class="zylem-timeline-label">{props.label}</KSlider.Label>
|
|
@@ -75,8 +80,16 @@ export function TimelineTrack(props: TimelineTrackProps) {
|
|
|
75
80
|
</For>
|
|
76
81
|
</div>
|
|
77
82
|
</Show>
|
|
78
|
-
<KSlider.Thumb
|
|
79
|
-
|
|
83
|
+
<KSlider.Thumb
|
|
84
|
+
class="zylem-timeline-thumb"
|
|
85
|
+
aria-label={props['aria-label']}
|
|
86
|
+
aria-valuetext={valueText()}
|
|
87
|
+
>
|
|
88
|
+
<KSlider.Input
|
|
89
|
+
class="zylem-timeline-thumb-input"
|
|
90
|
+
aria-label={props['aria-label']}
|
|
91
|
+
aria-valuetext={valueText()}
|
|
92
|
+
/>
|
|
80
93
|
</KSlider.Thumb>
|
|
81
94
|
</KSlider.Track>
|
|
82
95
|
</KSlider.Root>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Show, splitProps } from 'solid-js';
|
|
2
2
|
import type { JSX } from 'solid-js';
|
|
3
|
+
import { Heading, type HeadingLevel } from '../shared/Heading';
|
|
3
4
|
import { WindowControls, type WindowControlsProps } from '../WindowControls/WindowControls';
|
|
4
5
|
|
|
5
6
|
export interface TitleBarProps
|
|
6
7
|
extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'title' | 'onClose'>,
|
|
7
8
|
WindowControlsProps {
|
|
8
9
|
title?: JSX.Element;
|
|
10
|
+
/** Heading level of the title (default 2). */
|
|
11
|
+
headingLevel?: HeadingLevel;
|
|
9
12
|
/** Smaller 32px bar for panel headers. */
|
|
10
13
|
compact?: boolean;
|
|
11
14
|
/** Show grab cursor for draggable bars. */
|
|
@@ -21,6 +24,7 @@ export interface TitleBarProps
|
|
|
21
24
|
export function TitleBar(props: TitleBarProps) {
|
|
22
25
|
const [local, rest] = splitProps(props, [
|
|
23
26
|
'title',
|
|
27
|
+
'headingLevel',
|
|
24
28
|
'compact',
|
|
25
29
|
'draggable',
|
|
26
30
|
'children',
|
|
@@ -45,7 +49,9 @@ export function TitleBar(props: TitleBarProps) {
|
|
|
45
49
|
return (
|
|
46
50
|
<div class={classes()} {...rest}>
|
|
47
51
|
<Show when={local.title}>
|
|
48
|
-
<
|
|
52
|
+
<Heading level={local.headingLevel ?? 2} class="zylem-titlebar-title">
|
|
53
|
+
{local.title}
|
|
54
|
+
</Heading>
|
|
49
55
|
</Show>
|
|
50
56
|
{local.children}
|
|
51
57
|
<WindowControls
|
|
@@ -25,8 +25,7 @@ export function Toast(props: ToastProps) {
|
|
|
25
25
|
<Show when={props.message}>
|
|
26
26
|
<div
|
|
27
27
|
class={props.class ? `zylem-toast ${props.class}` : 'zylem-toast'}
|
|
28
|
-
role=
|
|
29
|
-
aria-live="polite"
|
|
28
|
+
role={props.tone === 'error' ? 'alert' : 'status'}
|
|
30
29
|
data-tone={props.tone}
|
|
31
30
|
data-position={props.position}
|
|
32
31
|
>
|
|
@@ -81,7 +81,7 @@ export function ToolButton(props: ToolButtonProps) {
|
|
|
81
81
|
return (
|
|
82
82
|
<ToolbarButton
|
|
83
83
|
label={label()}
|
|
84
|
-
|
|
84
|
+
pressed={props.selected ?? false}
|
|
85
85
|
disabled={props.disabled}
|
|
86
86
|
onClick={props.onClick}
|
|
87
87
|
class={props.class ? `zylem-tool-btn ${props.class}` : 'zylem-tool-btn'}
|
|
@@ -23,7 +23,12 @@ export function Toolbar(props: ToolbarProps) {
|
|
|
23
23
|
.join(' ');
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
|
-
<div
|
|
26
|
+
<div
|
|
27
|
+
role="toolbar"
|
|
28
|
+
aria-orientation={local.orientation === 'vertical' ? 'vertical' : undefined}
|
|
29
|
+
class={classes()}
|
|
30
|
+
{...rest}
|
|
31
|
+
>
|
|
27
32
|
{local.children}
|
|
28
33
|
</div>
|
|
29
34
|
);
|
|
@@ -5,7 +5,13 @@ import { useLayer } from '../../layers';
|
|
|
5
5
|
export interface ToolbarButtonProps {
|
|
6
6
|
label: string;
|
|
7
7
|
onClick?: (event: MouseEvent) => void;
|
|
8
|
+
/**
|
|
9
|
+
* The selected look alone, for a button whose label already says its state
|
|
10
|
+
* (Play / Pause). For a toggle with a constant label, use `pressed`.
|
|
11
|
+
*/
|
|
8
12
|
selected?: boolean;
|
|
13
|
+
/** Makes the button a toggle: the state is exposed as `aria-pressed` and drawn as selected. */
|
|
14
|
+
pressed?: boolean;
|
|
9
15
|
disabled?: boolean;
|
|
10
16
|
children: JSX.Element;
|
|
11
17
|
class?: string;
|
|
@@ -13,7 +19,7 @@ export interface ToolbarButtonProps {
|
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
21
|
* HyperGlass toolbar button: square glass icon button with a tooltip.
|
|
16
|
-
* Selected
|
|
22
|
+
* Selected and pressed states render as a live-green jewel.
|
|
17
23
|
*
|
|
18
24
|
* The tooltip portals into the layer container for its own tree, so it stays
|
|
19
25
|
* styled when the toolbar lives inside a web component's shadow root.
|
|
@@ -26,6 +32,7 @@ export function ToolbarButton(props: ToolbarButtonProps) {
|
|
|
26
32
|
<KTooltip.Trigger as="span" ref={layer.anchorRef}>
|
|
27
33
|
<KButton.Root
|
|
28
34
|
aria-label={props.label}
|
|
35
|
+
aria-pressed={props.pressed}
|
|
29
36
|
onClick={props.onClick}
|
|
30
37
|
disabled={props.disabled}
|
|
31
38
|
class={
|
|
@@ -33,7 +40,7 @@ export function ToolbarButton(props: ToolbarButtonProps) {
|
|
|
33
40
|
? `zylem-toolbar-btn ${props.class}`
|
|
34
41
|
: 'zylem-toolbar-btn'
|
|
35
42
|
}
|
|
36
|
-
data-selected={props.selected ? '' : undefined}
|
|
43
|
+
data-selected={props.selected || props.pressed ? '' : undefined}
|
|
37
44
|
>
|
|
38
45
|
{props.children}
|
|
39
46
|
</KButton.Root>
|
|
@@ -11,6 +11,7 @@ export interface ToolboxButtonProps {
|
|
|
11
11
|
label: string;
|
|
12
12
|
/** Icon rendered above the label. */
|
|
13
13
|
children: JSX.Element;
|
|
14
|
+
/** Given at all, the tile is a toggle and exposes this as `aria-pressed`. */
|
|
14
15
|
selected?: boolean;
|
|
15
16
|
disabled?: boolean;
|
|
16
17
|
/** Overrides the size inherited from the enclosing `ToolboxGrid`. */
|
|
@@ -29,7 +30,7 @@ export function ToolboxButton(props: ToolboxButtonProps) {
|
|
|
29
30
|
class={props.class ? `zylem-toolbox-btn ${props.class}` : 'zylem-toolbox-btn'}
|
|
30
31
|
data-size={props.size}
|
|
31
32
|
data-selected={props.selected ? '' : undefined}
|
|
32
|
-
aria-pressed={props.selected
|
|
33
|
+
aria-pressed={props.selected}
|
|
33
34
|
disabled={props.disabled}
|
|
34
35
|
onClick={props.onClick}
|
|
35
36
|
>
|
|
@@ -61,6 +61,27 @@ globalStyle('.zylem-track-header-name', {
|
|
|
61
61
|
textOverflow: 'ellipsis',
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
// A selectable header's name is a button that still reads as the name.
|
|
65
|
+
globalStyle('button.zylem-track-header-name', {
|
|
66
|
+
appearance: 'none',
|
|
67
|
+
minWidth: 0,
|
|
68
|
+
margin: 0,
|
|
69
|
+
padding: 0,
|
|
70
|
+
border: 0,
|
|
71
|
+
background: 'none',
|
|
72
|
+
color: 'inherit',
|
|
73
|
+
fontFamily: 'inherit',
|
|
74
|
+
lineHeight: 'inherit',
|
|
75
|
+
textAlign: 'start',
|
|
76
|
+
cursor: 'inherit',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
globalStyle('button.zylem-track-header-name:focus-visible', {
|
|
80
|
+
outline: 'none',
|
|
81
|
+
borderRadius: '3px',
|
|
82
|
+
boxShadow: vars.effects.focusRing,
|
|
83
|
+
});
|
|
84
|
+
|
|
64
85
|
globalStyle('.zylem-track-header[data-muted] .zylem-track-header-name', {
|
|
65
86
|
color: vars.colors.textSecondary,
|
|
66
87
|
});
|
|
@@ -11,6 +11,7 @@ export interface TrackHeaderProps {
|
|
|
11
11
|
/** Secondary line under the name (instrument preset, camera kind). */
|
|
12
12
|
detail?: string;
|
|
13
13
|
selected?: boolean;
|
|
14
|
+
/** Pointer down anywhere on the header; given, the name is also a button for the keyboard. */
|
|
14
15
|
onSelect?: () => void;
|
|
15
16
|
/** Double-click the name to rename; the new name arrives here on Enter/blur. */
|
|
16
17
|
onRename?: (name: string) => void;
|
|
@@ -47,6 +48,31 @@ export function TrackHeader(props: TrackHeaderProps) {
|
|
|
47
48
|
if (next && next !== props.name) props.onRename?.(next);
|
|
48
49
|
};
|
|
49
50
|
|
|
51
|
+
const name = () => (
|
|
52
|
+
<Show
|
|
53
|
+
when={props.onSelect}
|
|
54
|
+
fallback={
|
|
55
|
+
<span class="zylem-track-header-name" onDblClick={startRename} title={props.name}>
|
|
56
|
+
{props.name}
|
|
57
|
+
</span>
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
class="zylem-track-header-name"
|
|
63
|
+
aria-current={props.selected ? 'true' : undefined}
|
|
64
|
+
title={props.name}
|
|
65
|
+
onClick={(event) => {
|
|
66
|
+
// The pointer selected on pointer down; keys and assistive tech click without one.
|
|
67
|
+
if (event.detail === 0) props.onSelect?.();
|
|
68
|
+
}}
|
|
69
|
+
onDblClick={startRename}
|
|
70
|
+
>
|
|
71
|
+
{props.name}
|
|
72
|
+
</button>
|
|
73
|
+
</Show>
|
|
74
|
+
);
|
|
75
|
+
|
|
50
76
|
return (
|
|
51
77
|
<div
|
|
52
78
|
class={props.class ? `zylem-track-header ${props.class}` : 'zylem-track-header'}
|
|
@@ -54,6 +80,8 @@ export function TrackHeader(props: TrackHeaderProps) {
|
|
|
54
80
|
data-selected={props.selected ? '' : undefined}
|
|
55
81
|
data-muted={props.muted ? '' : undefined}
|
|
56
82
|
data-disabled={props.disabled ? '' : undefined}
|
|
83
|
+
role="group"
|
|
84
|
+
aria-label={props.name}
|
|
57
85
|
onPointerDown={() => props.onSelect?.()}
|
|
58
86
|
>
|
|
59
87
|
<span class="zylem-track-header-swatch" aria-hidden="true" />
|
|
@@ -62,14 +90,7 @@ export function TrackHeader(props: TrackHeaderProps) {
|
|
|
62
90
|
<Show when={props.icon}>
|
|
63
91
|
<span class="zylem-track-header-icon">{props.icon}</span>
|
|
64
92
|
</Show>
|
|
65
|
-
<Show
|
|
66
|
-
when={editing()}
|
|
67
|
-
fallback={
|
|
68
|
-
<span class="zylem-track-header-name" onDblClick={startRename} title={props.name}>
|
|
69
|
-
{props.name}
|
|
70
|
-
</span>
|
|
71
|
-
}
|
|
72
|
-
>
|
|
93
|
+
<Show when={editing()} fallback={name()}>
|
|
73
94
|
<input
|
|
74
95
|
class="zylem-track-header-input"
|
|
75
96
|
value={draft()}
|
|
@@ -95,7 +116,7 @@ export function TrackHeader(props: TrackHeaderProps) {
|
|
|
95
116
|
class="zylem-track-toggle"
|
|
96
117
|
data-kind="mute"
|
|
97
118
|
aria-pressed={props.muted ? 'true' : 'false'}
|
|
98
|
-
aria-label=
|
|
119
|
+
aria-label="Mute"
|
|
99
120
|
title={props.muted ? 'Unmute' : 'Mute'}
|
|
100
121
|
disabled={props.disabled}
|
|
101
122
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -110,7 +131,7 @@ export function TrackHeader(props: TrackHeaderProps) {
|
|
|
110
131
|
class="zylem-track-toggle"
|
|
111
132
|
data-kind="solo"
|
|
112
133
|
aria-pressed={props.solo ? 'true' : 'false'}
|
|
113
|
-
aria-label=
|
|
134
|
+
aria-label="Solo"
|
|
114
135
|
title={props.solo ? 'Unsolo' : 'Solo'}
|
|
115
136
|
disabled={props.disabled}
|
|
116
137
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -125,13 +146,17 @@ export function TrackHeader(props: TrackHeaderProps) {
|
|
|
125
146
|
class="zylem-track-toggle"
|
|
126
147
|
data-kind="lock"
|
|
127
148
|
aria-pressed={props.locked ? 'true' : 'false'}
|
|
128
|
-
aria-label=
|
|
149
|
+
aria-label="Lock"
|
|
129
150
|
title={props.locked ? 'Unlock' : 'Lock'}
|
|
130
151
|
disabled={props.disabled}
|
|
131
152
|
onPointerDown={(event) => event.stopPropagation()}
|
|
132
153
|
onClick={() => props.onToggleLock?.()}
|
|
133
154
|
>
|
|
134
|
-
{props.locked ?
|
|
155
|
+
{props.locked ? (
|
|
156
|
+
<Lock class="zylem-icon" aria-hidden="true" />
|
|
157
|
+
) : (
|
|
158
|
+
<LockOpen class="zylem-icon" aria-hidden="true" />
|
|
159
|
+
)}
|
|
135
160
|
</button>
|
|
136
161
|
</Show>
|
|
137
162
|
<Show when={props.actions}>
|
|
@@ -29,6 +29,7 @@ globalStyle('.zylem-transport-track', {
|
|
|
29
29
|
globalStyle('.zylem-transport-time', {
|
|
30
30
|
flex: 'none',
|
|
31
31
|
minWidth: '9ch',
|
|
32
|
+
fontFamily: vars.typography.fontFamilyMono,
|
|
32
33
|
fontSize: `calc(${vars.typography.fontSize} - 1px)`,
|
|
33
34
|
fontWeight: 600,
|
|
34
35
|
fontVariantNumeric: 'tabular-nums',
|
|
@@ -63,3 +64,7 @@ globalStyle('.zylem-transport-signature', {
|
|
|
63
64
|
width: '84px',
|
|
64
65
|
flex: 'none',
|
|
65
66
|
});
|
|
67
|
+
|
|
68
|
+
globalStyle('.zylem-transport-signature .zylem-field', {
|
|
69
|
+
fontFamily: vars.typography.fontFamilyMono,
|
|
70
|
+
});
|