@zylem/ui 0.8.8 → 0.8.11

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.
Files changed (81) hide show
  1. package/dist/chunk-V47CGFWX.js +1 -0
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.ts +15 -0
  4. package/dist/index.js +1 -1
  5. package/dist/styles.css +1 -1
  6. package/dist/styles.js +1 -1
  7. package/package.json +22 -11
  8. package/src/components/Accordion/Accordion.tsx +10 -1
  9. package/src/components/AddTile/AddTile.css.ts +95 -0
  10. package/src/components/AddTile/AddTile.tsx +45 -0
  11. package/src/components/BuildLabel/BuildLabel.css.ts +31 -0
  12. package/src/components/BuildLabel/BuildLabel.tsx +37 -0
  13. package/src/components/BuildLabel/mount-build-label.ts +77 -0
  14. package/src/components/BuildLabel/resolve-build-version.ts +47 -0
  15. package/src/components/Callout/Callout.css.ts +45 -0
  16. package/src/components/Callout/Callout.tsx +32 -0
  17. package/src/components/Card/Card.css.ts +18 -0
  18. package/src/components/Card/Card.tsx +25 -2
  19. package/src/components/ColorPalette/ColorPalette.css.ts +51 -0
  20. package/src/components/ColorPalette/ColorPalette.tsx +78 -0
  21. package/src/components/Dialog/Dialog.css.ts +69 -2
  22. package/src/components/Dialog/Dialog.tsx +35 -7
  23. package/src/components/DropdownMenu/DropdownMenu.css.ts +22 -0
  24. package/src/components/DropdownMenu/DropdownMenu.tsx +67 -20
  25. package/src/components/EmptyState/EmptyState.css.ts +9 -0
  26. package/src/components/EmptyState/EmptyState.tsx +3 -1
  27. package/src/components/FrameStrip/FrameStrip.css.ts +138 -0
  28. package/src/components/FrameStrip/FrameStrip.tsx +146 -0
  29. package/src/components/Menubar/Menubar.css.ts +52 -0
  30. package/src/components/Menubar/Menubar.tsx +77 -0
  31. package/src/components/Mixer/Knob.tsx +188 -0
  32. package/src/components/Mixer/Mixer.css.ts +246 -0
  33. package/src/components/Mixer/MixerStrip.tsx +150 -0
  34. package/src/components/NumberField/NumberField.tsx +5 -1
  35. package/src/components/Panel/Panel.tsx +6 -5
  36. package/src/components/PianoRoll/PianoRoll.css.ts +224 -0
  37. package/src/components/PianoRoll/PianoRoll.tsx +353 -0
  38. package/src/components/PianoRoll/piano-roll-math.ts +89 -0
  39. package/src/components/PixelGrid/PixelGrid.css.ts +30 -0
  40. package/src/components/PixelGrid/PixelGrid.tsx +48 -0
  41. package/src/components/PlaybackControls/PlaybackControls.css.ts +53 -0
  42. package/src/components/PlaybackControls/PlaybackControls.tsx +80 -0
  43. package/src/components/Property/Property.css.ts +1 -1
  44. package/src/components/Property/Property.tsx +3 -0
  45. package/src/components/Sidebar/Sidebar.css.ts +8 -0
  46. package/src/components/Sidebar/Sidebar.tsx +10 -2
  47. package/src/components/Slider/Slider.tsx +6 -0
  48. package/src/components/SpriteSizePicker/SpriteSizePicker.css.ts +103 -0
  49. package/src/components/SpriteSizePicker/SpriteSizePicker.tsx +172 -0
  50. package/src/components/SpriteSizePicker/snap-size.ts +28 -0
  51. package/src/components/Tabs/Tabs.css.ts +48 -0
  52. package/src/components/Tabs/Tabs.tsx +41 -3
  53. package/src/components/TextField/TextField.tsx +49 -5
  54. package/src/components/TimelineLanes/TimelineLanes.css.ts +365 -0
  55. package/src/components/TimelineLanes/TimelineLanes.tsx +509 -0
  56. package/src/components/TimelineLanes/lanes-math.ts +182 -0
  57. package/src/components/TimelineTrack/TimelineTrack.css.ts +163 -0
  58. package/src/components/TimelineTrack/TimelineTrack.tsx +90 -0
  59. package/src/components/TimelineTrack/timeline-math.ts +43 -0
  60. package/src/components/Toast/Toast.css.ts +48 -0
  61. package/src/components/Toast/Toast.tsx +37 -0
  62. package/src/components/ToolButton/ToolButton.tsx +92 -0
  63. package/src/components/ToolGroup/ToolGroup.css.ts +35 -0
  64. package/src/components/ToolGroup/ToolGroup.tsx +29 -0
  65. package/src/components/ToolboxButton/ToolboxButton.css.ts +131 -0
  66. package/src/components/ToolboxButton/ToolboxButton.tsx +88 -0
  67. package/src/components/TrackHeader/TrackHeader.css.ts +162 -0
  68. package/src/components/TrackHeader/TrackHeader.tsx +145 -0
  69. package/src/components/TransportBar/TransportBar.css.ts +65 -0
  70. package/src/components/TransportBar/TransportBar.tsx +154 -0
  71. package/src/components/Waveform/Waveform.css.ts +77 -0
  72. package/src/components/Waveform/Waveform.tsx +195 -0
  73. package/src/components/Waveform/peaks.ts +49 -0
  74. package/src/components/index.ts +122 -0
  75. package/src/global/accordion.css.ts +23 -0
  76. package/src/global/hyperglass-base.css.ts +9 -6
  77. package/src/styles.ts +23 -0
  78. package/src/theme.css.ts +15 -0
  79. package/vite/collect-zylem-versions.ts +82 -0
  80. package/vite/index.ts +30 -0
  81. package/dist/chunk-3TP2SNNW.js +0 -1
@@ -0,0 +1,88 @@
1
+ import { Button as KButton } from '@kobalte/core';
2
+ import { type JSX, Show, splitProps } from 'solid-js';
3
+
4
+ /**
5
+ * Tile scale. `md` is the compact rail tile; `lg` and `xl` are the extra
6
+ * large tiles sized for three- and two-column toolboxes respectively.
7
+ */
8
+ export type ToolboxSize = 'md' | 'lg' | 'xl';
9
+
10
+ export interface ToolboxButtonProps {
11
+ label: string;
12
+ /** Icon rendered above the label. */
13
+ children: JSX.Element;
14
+ selected?: boolean;
15
+ disabled?: boolean;
16
+ /** Overrides the size inherited from the enclosing `ToolboxGrid`. */
17
+ size?: ToolboxSize;
18
+ onClick?: (event: MouseEvent) => void;
19
+ class?: string;
20
+ }
21
+
22
+ /**
23
+ * Large toolbox tile: the toolbar button's glass jewel scaled up, with the
24
+ * icon stacked over a bold label. Selected renders as the live-green state.
25
+ */
26
+ export function ToolboxButton(props: ToolboxButtonProps) {
27
+ return (
28
+ <KButton.Root
29
+ class={props.class ? `zylem-toolbox-btn ${props.class}` : 'zylem-toolbox-btn'}
30
+ data-size={props.size}
31
+ data-selected={props.selected ? '' : undefined}
32
+ aria-pressed={props.selected ? 'true' : undefined}
33
+ disabled={props.disabled}
34
+ onClick={props.onClick}
35
+ >
36
+ <span class="zylem-toolbox-btn-icon" aria-hidden="true">
37
+ {props.children}
38
+ </span>
39
+ <span class="zylem-toolbox-btn-label">{props.label}</span>
40
+ </KButton.Root>
41
+ );
42
+ }
43
+
44
+ export interface ToolboxGridProps extends JSX.HTMLAttributes<HTMLDivElement> {
45
+ /** Optional uppercase caption above the grid, e.g. `Current tools`. */
46
+ label?: string;
47
+ /** Columns in the grid. Defaults to 2. */
48
+ columns?: number;
49
+ /**
50
+ * Tile size for every button in the grid that doesn't set its own. `xl`
51
+ * pairs with two columns, `lg` with three; defaults to `md`.
52
+ */
53
+ size?: ToolboxSize;
54
+ children: JSX.Element;
55
+ }
56
+
57
+ /** Captioned grid of `ToolboxButton`s. */
58
+ export function ToolboxGrid(props: ToolboxGridProps) {
59
+ const [local, rest] = splitProps(props, [
60
+ 'label',
61
+ 'columns',
62
+ 'size',
63
+ 'children',
64
+ 'class',
65
+ 'style',
66
+ ]);
67
+ const style = (): JSX.CSSProperties => ({
68
+ '--zylem-toolbox-columns': String(local.columns ?? 2),
69
+ ...(typeof local.style === 'object' ? local.style : {}),
70
+ });
71
+ return (
72
+ <div
73
+ class={local.class ? `zylem-toolbox-grid ${local.class}` : 'zylem-toolbox-grid'}
74
+ role="group"
75
+ aria-label={local.label}
76
+ data-size={local.size}
77
+ style={style()}
78
+ {...rest}
79
+ >
80
+ <Show when={local.label}>
81
+ <span class="zylem-toolbox-grid-label" aria-hidden="true">
82
+ {local.label}
83
+ </span>
84
+ </Show>
85
+ <div class="zylem-toolbox-grid-items">{local.children}</div>
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,162 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ globalStyle('.zylem-track-header', {
5
+ display: 'flex',
6
+ alignItems: 'stretch',
7
+ gap: vars.spacing.xs,
8
+ width: '100%',
9
+ minWidth: 0,
10
+ boxSizing: 'border-box',
11
+ padding: `0 ${vars.spacing.xs} 0 0`,
12
+ fontFamily: vars.typography.fontFamily,
13
+ color: vars.colors.text,
14
+ cursor: 'default',
15
+ });
16
+
17
+ globalStyle('.zylem-track-header[data-disabled]', {
18
+ opacity: 0.6,
19
+ });
20
+
21
+ globalStyle('.zylem-track-header-swatch', {
22
+ flex: 'none',
23
+ width: '4px',
24
+ background: 'var(--zylem-track-color)',
25
+ boxShadow: '0 0 6px color-mix(in srgb, var(--zylem-track-color) 60%, transparent)',
26
+ });
27
+
28
+ globalStyle('.zylem-track-header-main', {
29
+ flex: '1 1 auto',
30
+ minWidth: 0,
31
+ display: 'flex',
32
+ flexDirection: 'column',
33
+ justifyContent: 'center',
34
+ gap: '1px',
35
+ padding: `0 ${vars.spacing.xs}`,
36
+ });
37
+
38
+ globalStyle('.zylem-track-header-title', {
39
+ display: 'flex',
40
+ alignItems: 'center',
41
+ gap: vars.spacing.xs,
42
+ minWidth: 0,
43
+ });
44
+
45
+ globalStyle('.zylem-track-header-icon', {
46
+ display: 'inline-flex',
47
+ flex: 'none',
48
+ color: vars.colors.textSecondary,
49
+ });
50
+
51
+ globalStyle('.zylem-track-header-icon > svg', {
52
+ width: '13px',
53
+ height: '13px',
54
+ });
55
+
56
+ globalStyle('.zylem-track-header-name', {
57
+ fontSize: `calc(${vars.typography.fontSize} - 1px)`,
58
+ fontWeight: 600,
59
+ whiteSpace: 'nowrap',
60
+ overflow: 'hidden',
61
+ textOverflow: 'ellipsis',
62
+ });
63
+
64
+ globalStyle('.zylem-track-header[data-muted] .zylem-track-header-name', {
65
+ color: vars.colors.textSecondary,
66
+ });
67
+
68
+ globalStyle('.zylem-track-header-input', {
69
+ width: '100%',
70
+ minWidth: 0,
71
+ boxSizing: 'border-box',
72
+ padding: '1px 4px',
73
+ font: 'inherit',
74
+ fontSize: `calc(${vars.typography.fontSize} - 1px)`,
75
+ color: vars.colors.text,
76
+ background: vars.material.fieldGlass,
77
+ border: '1px solid rgba(97, 166, 232, 0.5)',
78
+ borderRadius: '4px',
79
+ outline: 'none',
80
+ });
81
+
82
+ globalStyle('.zylem-track-header-input:focus-visible', {
83
+ boxShadow: vars.effects.focusRing,
84
+ });
85
+
86
+ globalStyle('.zylem-track-header-detail', {
87
+ fontSize: '10px',
88
+ color: vars.colors.textSecondary,
89
+ whiteSpace: 'nowrap',
90
+ overflow: 'hidden',
91
+ textOverflow: 'ellipsis',
92
+ });
93
+
94
+ globalStyle('.zylem-track-header-toggles', {
95
+ flex: 'none',
96
+ display: 'flex',
97
+ alignItems: 'center',
98
+ gap: '3px',
99
+ });
100
+
101
+ globalStyle('.zylem-track-toggle', {
102
+ display: 'inline-flex',
103
+ alignItems: 'center',
104
+ justifyContent: 'center',
105
+ width: '18px',
106
+ height: '18px',
107
+ padding: 0,
108
+ fontFamily: vars.typography.fontFamily,
109
+ fontSize: '10px',
110
+ fontWeight: 700,
111
+ lineHeight: 1,
112
+ color: vars.colors.textSecondary,
113
+ background: 'rgba(8, 19, 31, 0.6)',
114
+ border: '1px solid rgba(97, 166, 232, 0.28)',
115
+ borderRadius: '4px',
116
+ boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.08)',
117
+ cursor: 'pointer',
118
+ outline: 'none',
119
+ transition: `background ${vars.motion.fast} ${vars.motion.easeOut}, color ${vars.motion.fast} ${vars.motion.easeOut}`,
120
+ });
121
+
122
+ globalStyle('.zylem-track-toggle > svg', {
123
+ width: '11px',
124
+ height: '11px',
125
+ });
126
+
127
+ globalStyle('.zylem-track-toggle:hover:not(:disabled)', {
128
+ color: vars.colors.text,
129
+ background: 'rgba(97, 166, 232, 0.22)',
130
+ });
131
+
132
+ globalStyle('.zylem-track-toggle:focus-visible', {
133
+ boxShadow: vars.effects.focusRing,
134
+ });
135
+
136
+ globalStyle('.zylem-track-toggle:disabled', {
137
+ cursor: 'default',
138
+ opacity: 0.5,
139
+ });
140
+
141
+ globalStyle('.zylem-track-toggle[data-kind="mute"][aria-pressed="true"]', {
142
+ color: '#2b1a08',
143
+ background: 'linear-gradient(180deg, rgba(255, 220, 130, 0.95), rgba(230, 150, 40, 0.95))',
144
+ borderColor: 'rgba(255, 230, 170, 0.8)',
145
+ });
146
+
147
+ globalStyle('.zylem-track-toggle[data-kind="solo"][aria-pressed="true"]', {
148
+ color: '#062010',
149
+ background: 'linear-gradient(180deg, rgba(170, 255, 190, 0.95), rgba(28, 174, 63, 0.95))',
150
+ borderColor: 'rgba(190, 255, 203, 0.8)',
151
+ });
152
+
153
+ globalStyle('.zylem-track-toggle[data-kind="lock"][aria-pressed="true"]', {
154
+ color: vars.colors.text,
155
+ background: 'rgba(97, 166, 232, 0.35)',
156
+ borderColor: 'rgba(178, 219, 255, 0.7)',
157
+ });
158
+
159
+ globalStyle('.zylem-track-header-actions', {
160
+ display: 'inline-flex',
161
+ alignItems: 'center',
162
+ });
@@ -0,0 +1,145 @@
1
+ import Lock from 'lucide-solid/icons/lock';
2
+ import LockOpen from 'lucide-solid/icons/lock-open';
3
+ import { createSignal, type JSX, Show } from 'solid-js';
4
+
5
+ export interface TrackHeaderProps {
6
+ name: string;
7
+ /** Swatch color shown as a bar on the left edge. */
8
+ color?: string;
9
+ /** Small glyph before the name (an instrument or camera icon). */
10
+ icon?: JSX.Element;
11
+ /** Secondary line under the name (instrument preset, camera kind). */
12
+ detail?: string;
13
+ selected?: boolean;
14
+ onSelect?: () => void;
15
+ /** Double-click the name to rename; the new name arrives here on Enter/blur. */
16
+ onRename?: (name: string) => void;
17
+ /** Mute / solo / lock toggles render only when their handler is given. */
18
+ muted?: boolean;
19
+ onToggleMute?: () => void;
20
+ solo?: boolean;
21
+ onToggleSolo?: () => void;
22
+ locked?: boolean;
23
+ onToggleLock?: () => void;
24
+ /** Trailing controls (a menu button, an add button). */
25
+ actions?: JSX.Element;
26
+ disabled?: boolean;
27
+ class?: string;
28
+ }
29
+
30
+ /**
31
+ * Header cell for a timeline lane: color bar, name (rename on double-click),
32
+ * an optional detail line, and M / S / lock toggles.
33
+ */
34
+ export function TrackHeader(props: TrackHeaderProps) {
35
+ const [editing, setEditing] = createSignal(false);
36
+ const [draft, setDraft] = createSignal('');
37
+
38
+ const startRename = () => {
39
+ if (!props.onRename || props.disabled) return;
40
+ setDraft(props.name);
41
+ setEditing(true);
42
+ };
43
+ const commitRename = () => {
44
+ if (!editing()) return;
45
+ setEditing(false);
46
+ const next = draft().trim();
47
+ if (next && next !== props.name) props.onRename?.(next);
48
+ };
49
+
50
+ return (
51
+ <div
52
+ class={props.class ? `zylem-track-header ${props.class}` : 'zylem-track-header'}
53
+ style={{ '--zylem-track-color': props.color ?? 'var(--zylem-color-primary)' }}
54
+ data-selected={props.selected ? '' : undefined}
55
+ data-muted={props.muted ? '' : undefined}
56
+ data-disabled={props.disabled ? '' : undefined}
57
+ onPointerDown={() => props.onSelect?.()}
58
+ >
59
+ <span class="zylem-track-header-swatch" aria-hidden="true" />
60
+ <div class="zylem-track-header-main">
61
+ <div class="zylem-track-header-title">
62
+ <Show when={props.icon}>
63
+ <span class="zylem-track-header-icon">{props.icon}</span>
64
+ </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
+ >
73
+ <input
74
+ class="zylem-track-header-input"
75
+ value={draft()}
76
+ aria-label="Track name"
77
+ ref={(element) => queueMicrotask(() => element.select())}
78
+ onInput={(event) => setDraft(event.currentTarget.value)}
79
+ onBlur={commitRename}
80
+ onKeyDown={(event) => {
81
+ if (event.key === 'Enter') commitRename();
82
+ if (event.key === 'Escape') setEditing(false);
83
+ }}
84
+ />
85
+ </Show>
86
+ </div>
87
+ <Show when={props.detail}>
88
+ <span class="zylem-track-header-detail">{props.detail}</span>
89
+ </Show>
90
+ </div>
91
+ <div class="zylem-track-header-toggles">
92
+ <Show when={props.onToggleMute}>
93
+ <button
94
+ type="button"
95
+ class="zylem-track-toggle"
96
+ data-kind="mute"
97
+ aria-pressed={props.muted ? 'true' : 'false'}
98
+ aria-label={props.muted ? 'Unmute' : 'Mute'}
99
+ title={props.muted ? 'Unmute' : 'Mute'}
100
+ disabled={props.disabled}
101
+ onPointerDown={(event) => event.stopPropagation()}
102
+ onClick={() => props.onToggleMute?.()}
103
+ >
104
+ M
105
+ </button>
106
+ </Show>
107
+ <Show when={props.onToggleSolo}>
108
+ <button
109
+ type="button"
110
+ class="zylem-track-toggle"
111
+ data-kind="solo"
112
+ aria-pressed={props.solo ? 'true' : 'false'}
113
+ aria-label={props.solo ? 'Unsolo' : 'Solo'}
114
+ title={props.solo ? 'Unsolo' : 'Solo'}
115
+ disabled={props.disabled}
116
+ onPointerDown={(event) => event.stopPropagation()}
117
+ onClick={() => props.onToggleSolo?.()}
118
+ >
119
+ S
120
+ </button>
121
+ </Show>
122
+ <Show when={props.onToggleLock}>
123
+ <button
124
+ type="button"
125
+ class="zylem-track-toggle"
126
+ data-kind="lock"
127
+ aria-pressed={props.locked ? 'true' : 'false'}
128
+ aria-label={props.locked ? 'Unlock' : 'Lock'}
129
+ title={props.locked ? 'Unlock' : 'Lock'}
130
+ disabled={props.disabled}
131
+ onPointerDown={(event) => event.stopPropagation()}
132
+ onClick={() => props.onToggleLock?.()}
133
+ >
134
+ {props.locked ? <Lock class="zylem-icon" /> : <LockOpen class="zylem-icon" />}
135
+ </button>
136
+ </Show>
137
+ <Show when={props.actions}>
138
+ <span class="zylem-track-header-actions" onPointerDown={(event) => event.stopPropagation()}>
139
+ {props.actions}
140
+ </span>
141
+ </Show>
142
+ </div>
143
+ </div>
144
+ );
145
+ }
@@ -0,0 +1,65 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ globalStyle('.zylem-transport', {
5
+ display: 'flex',
6
+ alignItems: 'center',
7
+ width: '100%',
8
+ minWidth: 0,
9
+ boxSizing: 'border-box',
10
+ fontFamily: vars.typography.fontFamily,
11
+ color: vars.colors.text,
12
+ });
13
+
14
+ globalStyle('.zylem-transport > .zylem-playback', {
15
+ width: '100%',
16
+ });
17
+
18
+ globalStyle('.zylem-transport .zylem-playback-extra', {
19
+ flex: '1 1 auto',
20
+ minWidth: 0,
21
+ gap: vars.spacing.sm,
22
+ });
23
+
24
+ globalStyle('.zylem-transport-track', {
25
+ flex: '1 1 auto',
26
+ minWidth: '120px',
27
+ });
28
+
29
+ globalStyle('.zylem-transport-time', {
30
+ flex: 'none',
31
+ minWidth: '9ch',
32
+ fontSize: `calc(${vars.typography.fontSize} - 1px)`,
33
+ fontWeight: 600,
34
+ fontVariantNumeric: 'tabular-nums',
35
+ whiteSpace: 'nowrap',
36
+ textShadow: '0 1px 1px rgba(0,0,0,0.6)',
37
+ });
38
+
39
+ globalStyle('.zylem-transport-time-total', {
40
+ fontWeight: 400,
41
+ color: vars.colors.textSecondary,
42
+ });
43
+
44
+ globalStyle('.zylem-transport-field', {
45
+ display: 'inline-flex',
46
+ alignItems: 'center',
47
+ gap: vars.spacing.xs,
48
+ flex: 'none',
49
+ });
50
+
51
+ globalStyle('.zylem-transport-field-label', {
52
+ fontSize: '10px',
53
+ letterSpacing: '0.08em',
54
+ textTransform: 'uppercase',
55
+ color: vars.colors.textSecondary,
56
+ });
57
+
58
+ globalStyle('.zylem-transport-bpm', {
59
+ width: '84px',
60
+ });
61
+
62
+ globalStyle('.zylem-transport-signature', {
63
+ width: '84px',
64
+ flex: 'none',
65
+ });
@@ -0,0 +1,154 @@
1
+ import Square from 'lucide-solid/icons/square';
2
+ import { type JSX, Show } from 'solid-js';
3
+ import { NumberField } from '../NumberField/NumberField';
4
+ import { PlaybackControls } from '../PlaybackControls/PlaybackControls';
5
+ import { Select } from '../Select/Select';
6
+ import { formatBeats, formatClock, type TimeUnit } from '../TimelineLanes/lanes-math';
7
+ import { TimelineTrack } from '../TimelineTrack/TimelineTrack';
8
+ import { ToolbarButton } from '../ToolbarButton/ToolbarButton';
9
+
10
+ export interface TransportTimeSignature {
11
+ numerator: number;
12
+ denominator: number;
13
+ }
14
+
15
+ export interface TransportBarProps {
16
+ playing: boolean;
17
+ onTogglePlay: () => void;
18
+ /** Adds a Stop button (pause and return to the start). */
19
+ onStop?: () => void;
20
+ onRewind?: () => void;
21
+ onFastForward?: () => void;
22
+ loop?: boolean;
23
+ onToggleLoop?: () => void;
24
+ /** Playhead, in `unit`s. */
25
+ time: number;
26
+ /** Timeline length, in `unit`s. */
27
+ length: number;
28
+ onSeek?: (time: number) => void;
29
+ unit?: TimeUnit;
30
+ /** Beats per bar, for the beat readout. */
31
+ beatsPerBar?: number;
32
+ /** Shows a BPM field when set. */
33
+ bpm?: number;
34
+ onBpmChange?: (bpm: number) => void;
35
+ /** Shows a time-signature select when set. */
36
+ timeSignature?: TransportTimeSignature;
37
+ onTimeSignatureChange?: (signature: TransportTimeSignature) => void;
38
+ /** Extra controls after the built-in ones. */
39
+ children?: JSX.Element;
40
+ disabled?: boolean;
41
+ class?: string;
42
+ }
43
+
44
+ const SIGNATURES: TransportTimeSignature[] = [
45
+ { numerator: 2, denominator: 4 },
46
+ { numerator: 3, denominator: 4 },
47
+ { numerator: 4, denominator: 4 },
48
+ { numerator: 5, denominator: 4 },
49
+ { numerator: 6, denominator: 8 },
50
+ { numerator: 7, denominator: 8 },
51
+ { numerator: 12, denominator: 8 },
52
+ ];
53
+
54
+ const signatureKey = (signature: TransportTimeSignature) =>
55
+ `${signature.numerator}/${signature.denominator}`;
56
+
57
+ /** Readout for the current position: `m:ss.ff` for seconds, `bar.beat` for beats. */
58
+ export function formatTransportTime(time: number, unit: TimeUnit, beatsPerBar = 4): string {
59
+ return unit === 'beats' ? formatBeats(time, beatsPerBar) : formatClock(time);
60
+ }
61
+
62
+ /**
63
+ * Transport strip for the Director and Music editors: playback controls, a
64
+ * scrub track with a position readout, and optional BPM / time signature.
65
+ */
66
+ export function TransportBar(props: TransportBarProps) {
67
+ const unit = () => props.unit ?? 'seconds';
68
+ const bpb = () => props.beatsPerBar ?? props.timeSignature?.numerator ?? 4;
69
+ const signatureOptions = () => {
70
+ const options = SIGNATURES.map((signature) => ({
71
+ value: signatureKey(signature),
72
+ label: signatureKey(signature),
73
+ }));
74
+ const current = props.timeSignature ? signatureKey(props.timeSignature) : null;
75
+ if (current && !options.some((option) => option.value === current)) {
76
+ options.push({ value: current, label: current });
77
+ }
78
+ return options;
79
+ };
80
+
81
+ return (
82
+ <div
83
+ class={props.class ? `zylem-transport ${props.class}` : 'zylem-transport'}
84
+ data-unit={unit()}
85
+ >
86
+ <PlaybackControls
87
+ playing={props.playing}
88
+ onTogglePlay={props.onTogglePlay}
89
+ onRewind={props.onRewind}
90
+ onFastForward={props.onFastForward}
91
+ loop={props.loop}
92
+ onToggleLoop={props.onToggleLoop}
93
+ disabled={props.disabled}
94
+ >
95
+ <Show when={props.onStop}>
96
+ <ToolbarButton
97
+ label="Stop"
98
+ disabled={props.disabled}
99
+ onClick={() => props.onStop?.()}
100
+ class="zylem-transport-stop"
101
+ >
102
+ <Square class="zylem-icon" />
103
+ </ToolbarButton>
104
+ </Show>
105
+ <TimelineTrack
106
+ value={props.time}
107
+ max={props.length}
108
+ onChange={(value) => props.onSeek?.(value)}
109
+ disabled={props.disabled || !props.onSeek}
110
+ class="zylem-transport-track"
111
+ />
112
+ <span class="zylem-transport-time" aria-live="off">
113
+ {formatTransportTime(props.time, unit(), bpb())}
114
+ <span class="zylem-transport-time-total">
115
+ {' / '}
116
+ {formatTransportTime(props.length, unit(), bpb())}
117
+ </span>
118
+ </span>
119
+ <Show when={props.bpm !== undefined}>
120
+ <label class="zylem-transport-field">
121
+ <span class="zylem-transport-field-label">BPM</span>
122
+ <NumberField
123
+ value={props.bpm}
124
+ minValue={20}
125
+ maxValue={300}
126
+ step={1}
127
+ disabled={props.disabled || !props.onBpmChange}
128
+ onChange={(value) => props.onBpmChange?.(value)}
129
+ class="zylem-transport-bpm"
130
+ />
131
+ </label>
132
+ </Show>
133
+ <Show when={props.timeSignature}>
134
+ {(signature) => (
135
+ <Select
136
+ options={signatureOptions()}
137
+ value={signatureKey(signature())}
138
+ disabled={props.disabled || !props.onTimeSignatureChange}
139
+ onChange={(value) => {
140
+ if (!value) return;
141
+ const [numerator, denominator] = value.split('/').map(Number);
142
+ if (numerator && denominator) {
143
+ props.onTimeSignatureChange?.({ numerator, denominator });
144
+ }
145
+ }}
146
+ class="zylem-transport-signature"
147
+ />
148
+ )}
149
+ </Show>
150
+ {props.children}
151
+ </PlaybackControls>
152
+ </div>
153
+ );
154
+ }
@@ -0,0 +1,77 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ globalStyle('.zylem-waveform', {
5
+ position: 'relative',
6
+ width: '100%',
7
+ minWidth: 0,
8
+ boxSizing: 'border-box',
9
+ overflow: 'hidden',
10
+ borderRadius: vars.radii.control,
11
+ border: '1px solid rgba(97, 166, 232, 0.22)',
12
+ background: vars.material.fieldGlass,
13
+ boxShadow: vars.effects.shadowInset,
14
+ userSelect: 'none',
15
+ });
16
+
17
+ globalStyle('.zylem-waveform[data-disabled]', {
18
+ opacity: 0.6,
19
+ });
20
+
21
+ globalStyle('.zylem-waveform-canvas', {
22
+ display: 'block',
23
+ width: '100%',
24
+ height: '100%',
25
+ });
26
+
27
+ globalStyle('.zylem-waveform-shade', {
28
+ position: 'absolute',
29
+ top: 0,
30
+ bottom: 0,
31
+ background: 'rgba(0, 0, 0, 0.45)',
32
+ pointerEvents: 'none',
33
+ });
34
+
35
+ globalStyle('.zylem-waveform-edge', {
36
+ position: 'absolute',
37
+ top: 0,
38
+ bottom: 0,
39
+ width: '10px',
40
+ marginLeft: '-5px',
41
+ cursor: 'ew-resize',
42
+ });
43
+
44
+ globalStyle('.zylem-waveform-edge::before', {
45
+ content: '""',
46
+ position: 'absolute',
47
+ top: 0,
48
+ bottom: 0,
49
+ left: '4px',
50
+ width: '2px',
51
+ background: 'rgba(255, 244, 200, 0.95)',
52
+ boxShadow: '0 0 6px rgba(240, 180, 60, 0.7)',
53
+ });
54
+
55
+ globalStyle('.zylem-waveform-edge::after', {
56
+ content: '""',
57
+ position: 'absolute',
58
+ top: '50%',
59
+ left: '1px',
60
+ width: '8px',
61
+ height: '16px',
62
+ marginTop: '-8px',
63
+ borderRadius: '3px',
64
+ background: 'linear-gradient(180deg, rgba(255, 244, 200, 0.98), rgba(240, 180, 60, 0.95))',
65
+ border: '1px solid rgba(255,255,255,0.7)',
66
+ boxSizing: 'border-box',
67
+ });
68
+
69
+ globalStyle('.zylem-waveform-playhead', {
70
+ position: 'absolute',
71
+ top: 0,
72
+ bottom: 0,
73
+ width: '1px',
74
+ background: 'rgba(255, 236, 170, 0.9)',
75
+ boxShadow: '0 0 6px rgba(240, 180, 60, 0.7)',
76
+ pointerEvents: 'none',
77
+ });