@zylem/ui 0.8.9 → 0.8.12

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 (61) hide show
  1. package/README.md +3 -0
  2. package/dist/styles.css +1 -1
  3. package/package.json +5 -1
  4. package/src/components/Accordion/Accordion.tsx +10 -1
  5. package/src/components/AddTile/AddTile.css.ts +17 -0
  6. package/src/components/AddTile/AddTile.tsx +15 -7
  7. package/src/components/Callout/Callout.css.ts +45 -0
  8. package/src/components/Callout/Callout.tsx +32 -0
  9. package/src/components/Card/Card.css.ts +18 -0
  10. package/src/components/Card/Card.tsx +25 -2
  11. package/src/components/Dialog/Dialog.css.ts +69 -2
  12. package/src/components/Dialog/Dialog.tsx +35 -7
  13. package/src/components/DropdownMenu/DropdownMenu.css.ts +22 -0
  14. package/src/components/DropdownMenu/DropdownMenu.tsx +67 -20
  15. package/src/components/EmptyState/EmptyState.css.ts +9 -0
  16. package/src/components/EmptyState/EmptyState.tsx +3 -1
  17. package/src/components/FrameStrip/FrameStrip.css.ts +6 -0
  18. package/src/components/FrameStrip/FrameStrip.tsx +30 -2
  19. package/src/components/Menubar/Menubar.css.ts +52 -0
  20. package/src/components/Menubar/Menubar.tsx +77 -0
  21. package/src/components/Mixer/Knob.tsx +188 -0
  22. package/src/components/Mixer/Mixer.css.ts +246 -0
  23. package/src/components/Mixer/MixerStrip.tsx +150 -0
  24. package/src/components/NumberField/NumberField.tsx +5 -1
  25. package/src/components/Panel/Panel.tsx +6 -5
  26. package/src/components/PianoRoll/PianoRoll.css.ts +224 -0
  27. package/src/components/PianoRoll/PianoRoll.tsx +353 -0
  28. package/src/components/PianoRoll/piano-roll-math.ts +89 -0
  29. package/src/components/Property/Property.css.ts +1 -1
  30. package/src/components/Property/Property.tsx +3 -0
  31. package/src/components/Sidebar/Sidebar.css.ts +8 -0
  32. package/src/components/Sidebar/Sidebar.tsx +10 -2
  33. package/src/components/Slider/Slider.tsx +6 -0
  34. package/src/components/SpriteSizePicker/SpriteSizePicker.tsx +15 -4
  35. package/src/components/Tabs/Tabs.css.ts +48 -0
  36. package/src/components/Tabs/Tabs.tsx +41 -3
  37. package/src/components/TextField/TextField.tsx +49 -5
  38. package/src/components/TimelineLanes/TimelineLanes.css.ts +365 -0
  39. package/src/components/TimelineLanes/TimelineLanes.tsx +509 -0
  40. package/src/components/TimelineLanes/lanes-math.ts +182 -0
  41. package/src/components/Toast/Toast.css.ts +48 -0
  42. package/src/components/Toast/Toast.tsx +37 -0
  43. package/src/components/TrackHeader/TrackHeader.css.ts +162 -0
  44. package/src/components/TrackHeader/TrackHeader.tsx +145 -0
  45. package/src/components/TransportBar/TransportBar.css.ts +65 -0
  46. package/src/components/TransportBar/TransportBar.tsx +154 -0
  47. package/src/components/Waveform/Waveform.css.ts +77 -0
  48. package/src/components/Waveform/Waveform.tsx +195 -0
  49. package/src/components/Waveform/peaks.ts +49 -0
  50. package/src/components/index.ts +72 -1
  51. package/src/global/accordion.css.ts +23 -0
  52. package/src/icons/create-icon.tsx +71 -0
  53. package/src/icons/icon-nodes.ts +75 -0
  54. package/src/icons/index.ts +54 -0
  55. package/src/icons/svg/camera-isometric.svg +16 -0
  56. package/src/icons/svg/free-perspective.svg +8 -0
  57. package/src/icons/svg/game-camera.svg +6 -0
  58. package/src/icons/svg/perspective.svg +5 -0
  59. package/src/icons/svg/sideview-perspective.svg +5 -0
  60. package/src/icons/svg/top-down-perspective.svg +5 -0
  61. package/src/styles.ts +11 -0
@@ -0,0 +1,150 @@
1
+ import { Slider as KSlider } from '@kobalte/core';
2
+ import { Show } from 'solid-js';
3
+ import { clamp } from '../TimelineLanes/lanes-math';
4
+ import { Knob } from './Knob';
5
+
6
+ export interface MixerStripProps {
7
+ name: string;
8
+ color?: string;
9
+ /** Fader position in dB. */
10
+ volume: number;
11
+ onVolumeChange?: (volume: number) => void;
12
+ onVolumeCommit?: (volume: number) => void;
13
+ minVolume?: number;
14
+ maxVolume?: number;
15
+ /** -1 (left) … 1 (right). Omit to hide the pan knob. */
16
+ pan?: number;
17
+ onPanChange?: (pan: number) => void;
18
+ onPanCommit?: (pan: number) => void;
19
+ /** Mute / solo render only when their handler is given. */
20
+ muted?: boolean;
21
+ onToggleMute?: () => void;
22
+ solo?: boolean;
23
+ onToggleSolo?: () => void;
24
+ /** Live output level, 0–1, drawn as a meter beside the fader. */
25
+ level?: number;
26
+ /** Wider strip with a stronger label; for the master bus. */
27
+ master?: boolean;
28
+ selected?: boolean;
29
+ onSelect?: () => void;
30
+ disabled?: boolean;
31
+ class?: string;
32
+ }
33
+
34
+ /** `-60` (the floor) reads as `-inf`; otherwise one decimal with a sign. */
35
+ export function formatDb(value: number, floor = -60): string {
36
+ if (!Number.isFinite(value) || value <= floor) return '-inf';
37
+ const rounded = Math.round(value * 10) / 10;
38
+ return `${rounded > 0 ? '+' : ''}${rounded.toFixed(1)} dB`;
39
+ }
40
+
41
+ /** `-1` → `L100`, `0` → `C`, `0.5` → `R50`. */
42
+ export function formatPan(value: number): string {
43
+ const percent = Math.round(Math.abs(value) * 100);
44
+ if (percent === 0) return 'C';
45
+ return `${value < 0 ? 'L' : 'R'}${percent}`;
46
+ }
47
+
48
+ /**
49
+ * Channel strip: pan knob, vertical fader with a level meter, mute/solo, and
50
+ * the track name with its color. Compose several into a mixer row.
51
+ */
52
+ export function MixerStrip(props: MixerStripProps) {
53
+ const min = () => props.minVolume ?? -60;
54
+ const max = () => props.maxVolume ?? 6;
55
+
56
+ return (
57
+ <div
58
+ class={props.class ? `zylem-mixer-strip ${props.class}` : 'zylem-mixer-strip'}
59
+ style={{ '--zylem-track-color': props.color ?? 'var(--zylem-color-primary)' }}
60
+ data-master={props.master ? '' : undefined}
61
+ data-selected={props.selected ? '' : undefined}
62
+ data-disabled={props.disabled ? '' : undefined}
63
+ data-muted={props.muted ? '' : undefined}
64
+ onPointerDown={() => props.onSelect?.()}
65
+ >
66
+ <Show when={props.pan !== undefined}>
67
+ <Knob
68
+ value={props.pan ?? 0}
69
+ min={-1}
70
+ max={1}
71
+ step={0.01}
72
+ defaultValue={0}
73
+ bipolar
74
+ size={30}
75
+ label="Pan"
76
+ format={formatPan}
77
+ disabled={props.disabled}
78
+ onChange={(value) => props.onPanChange?.(value)}
79
+ onCommit={(value) => props.onPanCommit?.(value)}
80
+ class="zylem-mixer-pan"
81
+ />
82
+ </Show>
83
+ <div class="zylem-mixer-fader-row">
84
+ <KSlider.Root
85
+ class="zylem-fader"
86
+ orientation="vertical"
87
+ value={[clamp(props.volume, min(), max())]}
88
+ minValue={min()}
89
+ maxValue={max()}
90
+ step={0.1}
91
+ disabled={props.disabled}
92
+ onChange={(values) => props.onVolumeChange?.(values[0] ?? min())}
93
+ onChangeEnd={(values) => props.onVolumeCommit?.(values[0] ?? min())}
94
+ getValueLabel={(params) => formatDb(params.values[0] ?? min(), min())}
95
+ >
96
+ <KSlider.Label class="zylem-fader-sr">{`${props.name} volume`}</KSlider.Label>
97
+ <KSlider.Track class="zylem-fader-track">
98
+ <KSlider.Fill class="zylem-fader-fill" />
99
+ <span class="zylem-fader-zero" aria-hidden="true" />
100
+ <KSlider.Thumb class="zylem-fader-thumb">
101
+ <KSlider.Input class="zylem-fader-sr" />
102
+ </KSlider.Thumb>
103
+ </KSlider.Track>
104
+ </KSlider.Root>
105
+ <div class="zylem-mixer-meter" aria-hidden="true">
106
+ <div
107
+ class="zylem-mixer-meter-fill"
108
+ style={{ height: `${Math.round(clamp(props.level ?? 0, 0, 1) * 100)}%` }}
109
+ />
110
+ </div>
111
+ </div>
112
+ <span class="zylem-mixer-readout">{formatDb(props.volume, min())}</span>
113
+ <div class="zylem-mixer-toggles">
114
+ <Show when={props.onToggleMute}>
115
+ <button
116
+ type="button"
117
+ class="zylem-track-toggle"
118
+ data-kind="mute"
119
+ aria-pressed={props.muted ? 'true' : 'false'}
120
+ aria-label={props.muted ? 'Unmute' : 'Mute'}
121
+ title={props.muted ? 'Unmute' : 'Mute'}
122
+ disabled={props.disabled}
123
+ onPointerDown={(event) => event.stopPropagation()}
124
+ onClick={() => props.onToggleMute?.()}
125
+ >
126
+ M
127
+ </button>
128
+ </Show>
129
+ <Show when={props.onToggleSolo}>
130
+ <button
131
+ type="button"
132
+ class="zylem-track-toggle"
133
+ data-kind="solo"
134
+ aria-pressed={props.solo ? 'true' : 'false'}
135
+ aria-label={props.solo ? 'Unsolo' : 'Solo'}
136
+ title={props.solo ? 'Unsolo' : 'Solo'}
137
+ disabled={props.disabled}
138
+ onPointerDown={(event) => event.stopPropagation()}
139
+ onClick={() => props.onToggleSolo?.()}
140
+ >
141
+ S
142
+ </button>
143
+ </Show>
144
+ </div>
145
+ <span class="zylem-mixer-name" title={props.name}>
146
+ {props.name}
147
+ </span>
148
+ </div>
149
+ );
150
+ }
@@ -23,8 +23,12 @@ export function NumberField(props: NumberFieldProps) {
23
23
  <KNumberField.Root
24
24
  class={props.class ? `zylem-field-root ${props.class}` : 'zylem-field-root'}
25
25
  rawValue={props.value}
26
+ // Kobalte reports the parsed value on mount and after each format pass;
27
+ // only real edits reach the owner, so a controlled form is not marked
28
+ // dirty just by rendering.
26
29
  onRawValueChange={(value) => {
27
- if (props.onChange && !Number.isNaN(value)) props.onChange(value);
30
+ if (!props.onChange || Number.isNaN(value) || value === props.value) return;
31
+ props.onChange(value);
28
32
  }}
29
33
  minValue={props.minValue}
30
34
  maxValue={props.maxValue}
@@ -16,6 +16,8 @@ export interface PanelProps
16
16
  * inside an already-outlined region.
17
17
  */
18
18
  borderless?: boolean;
19
+ /** Class for the body element, for layouts the body's children need (a form's column, a grid). */
20
+ bodyClass?: string;
19
21
  children?: JSX.Element;
20
22
  }
21
23
 
@@ -30,6 +32,7 @@ export function Panel(props: PanelProps) {
30
32
  'window',
31
33
  'flush',
32
34
  'borderless',
35
+ 'bodyClass',
33
36
  'children',
34
37
  'class',
35
38
  ]);
@@ -53,11 +56,9 @@ export function Panel(props: PanelProps) {
53
56
  </div>
54
57
  </Show>
55
58
  <div
56
- class={
57
- local.flush
58
- ? 'zylem-panel-body zylem-panel-body--flush'
59
- : 'zylem-panel-body'
60
- }
59
+ class={['zylem-panel-body', local.flush ? 'zylem-panel-body--flush' : '', local.bodyClass ?? '']
60
+ .filter(Boolean)
61
+ .join(' ')}
61
62
  >
62
63
  {local.children}
63
64
  </div>
@@ -0,0 +1,224 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ globalStyle('.zylem-piano-roll', {
5
+ position: 'relative',
6
+ display: 'flex',
7
+ flexDirection: 'column',
8
+ minHeight: 0,
9
+ minWidth: 0,
10
+ fontFamily: vars.typography.fontFamily,
11
+ color: vars.colors.text,
12
+ background: vars.material.fieldGlass,
13
+ border: '1px solid rgba(97, 166, 232, 0.22)',
14
+ borderRadius: vars.radii.control,
15
+ boxShadow: vars.effects.shadowInset,
16
+ outline: 'none',
17
+ userSelect: 'none',
18
+ });
19
+
20
+ globalStyle('.zylem-piano-roll:focus-visible', {
21
+ boxShadow: `${vars.effects.shadowInset}, ${vars.effects.focusRing}`,
22
+ });
23
+
24
+ globalStyle('.zylem-piano-roll[data-disabled]', {
25
+ opacity: 0.6,
26
+ });
27
+
28
+ globalStyle('.zylem-piano-roll-scroll', {
29
+ flex: '1 1 auto',
30
+ minHeight: 0,
31
+ overflow: 'auto',
32
+ });
33
+
34
+ globalStyle('.zylem-piano-roll-content', {
35
+ position: 'relative',
36
+ display: 'flex',
37
+ });
38
+
39
+ // Keys: sticky column of white/black rows, labelled at each C.
40
+ globalStyle('.zylem-piano-roll-keys', {
41
+ position: 'sticky',
42
+ left: 0,
43
+ zIndex: 2,
44
+ flex: 'none',
45
+ display: 'flex',
46
+ flexDirection: 'column',
47
+ borderRight: '1px solid rgba(97, 166, 232, 0.3)',
48
+ boxShadow: '2px 0 6px rgba(0,0,0,0.35)',
49
+ });
50
+
51
+ globalStyle('.zylem-piano-roll-key', {
52
+ boxSizing: 'border-box',
53
+ display: 'flex',
54
+ alignItems: 'center',
55
+ justifyContent: 'flex-end',
56
+ padding: '0 4px 0 0',
57
+ margin: 0,
58
+ border: 0,
59
+ borderBottom: '1px solid rgba(0,0,0,0.35)',
60
+ fontFamily: vars.typography.fontFamily,
61
+ fontSize: '9px',
62
+ lineHeight: 1,
63
+ color: '#1c2633',
64
+ background: 'linear-gradient(90deg, #dbe6f2, #f4f8fc)',
65
+ cursor: 'pointer',
66
+ outline: 'none',
67
+ });
68
+
69
+ globalStyle('.zylem-piano-roll-key[data-black]', {
70
+ color: '#cdd8e6',
71
+ background: 'linear-gradient(90deg, #0d151f, #263241)',
72
+ boxShadow: 'inset -6px 0 0 rgba(255,255,255,0.05)',
73
+ });
74
+
75
+ globalStyle('.zylem-piano-roll-key[data-c]', {
76
+ fontWeight: 700,
77
+ });
78
+
79
+ globalStyle('.zylem-piano-roll-key:hover:not(:disabled)', {
80
+ filter: 'brightness(1.1)',
81
+ });
82
+
83
+ globalStyle('.zylem-piano-roll-key:active:not(:disabled)', {
84
+ filter: 'brightness(0.92)',
85
+ });
86
+
87
+ // Grid: row stripes plus beat/bar/snap columns from gradients.
88
+ globalStyle('.zylem-piano-roll-grid', {
89
+ position: 'relative',
90
+ flex: 'none',
91
+ backgroundImage: [
92
+ 'repeating-linear-gradient(90deg, rgba(205, 232, 255, 0.28) 0 1px, transparent 1px var(--zylem-pr-bar))',
93
+ 'repeating-linear-gradient(90deg, rgba(205, 232, 255, 0.12) 0 1px, transparent 1px var(--zylem-pr-ppb))',
94
+ 'repeating-linear-gradient(90deg, rgba(205, 232, 255, 0.05) 0 1px, transparent 1px var(--zylem-pr-snap))',
95
+ ].join(', '),
96
+ });
97
+
98
+ globalStyle('.zylem-piano-roll-row', {
99
+ position: 'absolute',
100
+ left: 0,
101
+ right: 0,
102
+ boxSizing: 'border-box',
103
+ borderBottom: '1px solid rgba(255,255,255,0.04)',
104
+ pointerEvents: 'none',
105
+ });
106
+
107
+ globalStyle('.zylem-piano-roll-row[data-black]', {
108
+ background: 'rgba(0, 0, 0, 0.22)',
109
+ });
110
+
111
+ globalStyle('.zylem-piano-roll-row[data-c]', {
112
+ borderBottom: '1px solid rgba(205, 232, 255, 0.25)',
113
+ });
114
+
115
+ globalStyle('.zylem-piano-roll-end', {
116
+ position: 'absolute',
117
+ top: 0,
118
+ bottom: 0,
119
+ right: 0,
120
+ background: 'rgba(0, 0, 0, 0.35)',
121
+ pointerEvents: 'none',
122
+ });
123
+
124
+ globalStyle('.zylem-piano-roll-note', {
125
+ position: 'absolute',
126
+ boxSizing: 'border-box',
127
+ display: 'flex',
128
+ alignItems: 'center',
129
+ borderRadius: '3px',
130
+ border: '1px solid rgba(255, 255, 255, 0.55)',
131
+ background:
132
+ 'linear-gradient(180deg, rgba(178, 219, 255, 0.98), rgba(48, 137, 220, 0.98))',
133
+ boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.45), 0 1px 2px rgba(0,0,0,0.5)',
134
+ opacity: 'calc(0.45 + 0.55 * var(--zylem-pr-velocity, 0.8))',
135
+ color: '#08131f',
136
+ cursor: 'grab',
137
+ outline: 'none',
138
+ overflow: 'hidden',
139
+ });
140
+
141
+ globalStyle('.zylem-piano-roll-note[data-selected]', {
142
+ background:
143
+ 'linear-gradient(180deg, rgba(255, 244, 200, 0.98), rgba(240, 180, 60, 0.98))',
144
+ borderColor: 'white',
145
+ boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6), 0 0 8px rgba(240, 180, 60, 0.7)',
146
+ opacity: 1,
147
+ });
148
+
149
+ globalStyle('.zylem-piano-roll[data-dragging] .zylem-piano-roll-note', {
150
+ cursor: 'grabbing',
151
+ });
152
+
153
+ globalStyle('.zylem-piano-roll-note-label', {
154
+ padding: '0 4px',
155
+ fontSize: '9px',
156
+ fontWeight: 700,
157
+ lineHeight: 1,
158
+ whiteSpace: 'nowrap',
159
+ pointerEvents: 'none',
160
+ });
161
+
162
+ globalStyle('.zylem-piano-roll-note-handle', {
163
+ position: 'absolute',
164
+ top: 0,
165
+ bottom: 0,
166
+ right: 0,
167
+ width: '6px',
168
+ cursor: 'ew-resize',
169
+ });
170
+
171
+ globalStyle('.zylem-piano-roll-playhead', {
172
+ position: 'absolute',
173
+ top: 0,
174
+ bottom: 0,
175
+ width: '1px',
176
+ background: 'rgba(255, 236, 170, 0.9)',
177
+ boxShadow: '0 0 6px rgba(240, 180, 60, 0.7)',
178
+ pointerEvents: 'none',
179
+ });
180
+
181
+ // Velocity lane under the grid: one bar per note, drag vertically.
182
+ globalStyle('.zylem-piano-roll-velocity', {
183
+ display: 'flex',
184
+ flex: 'none',
185
+ borderTop: '1px solid rgba(97, 166, 232, 0.3)',
186
+ background: 'rgba(8, 19, 31, 0.6)',
187
+ });
188
+
189
+ globalStyle('.zylem-piano-roll-velocity-label', {
190
+ position: 'sticky',
191
+ left: 0,
192
+ zIndex: 2,
193
+ flex: 'none',
194
+ display: 'flex',
195
+ alignItems: 'flex-end',
196
+ justifyContent: 'flex-end',
197
+ padding: '0 4px 4px 0',
198
+ fontSize: '9px',
199
+ letterSpacing: '0.08em',
200
+ textTransform: 'uppercase',
201
+ color: vars.colors.textSecondary,
202
+ background: 'rgba(12, 22, 34, 0.98)',
203
+ borderRight: '1px solid rgba(97, 166, 232, 0.3)',
204
+ });
205
+
206
+ globalStyle('.zylem-piano-roll-velocity-lane', {
207
+ position: 'relative',
208
+ flex: 'none',
209
+ height: '100%',
210
+ });
211
+
212
+ globalStyle('.zylem-piano-roll-velocity-bar', {
213
+ position: 'absolute',
214
+ bottom: 0,
215
+ width: '5px',
216
+ marginLeft: '1px',
217
+ borderRadius: '2px 2px 0 0',
218
+ background: 'linear-gradient(180deg, rgba(178, 219, 255, 0.95), rgba(48, 137, 220, 0.85))',
219
+ cursor: 'ns-resize',
220
+ });
221
+
222
+ globalStyle('.zylem-piano-roll-velocity-bar[data-selected]', {
223
+ background: 'linear-gradient(180deg, rgba(255, 244, 200, 0.98), rgba(240, 180, 60, 0.95))',
224
+ });