@zylem/ui 0.8.13 → 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.
Files changed (58) hide show
  1. package/dist/styles.css +1 -1
  2. package/package.json +1 -1
  3. package/src/components/Accordion/Accordion.tsx +4 -1
  4. package/src/components/BuildLabel/BuildLabel.tsx +0 -1
  5. package/src/components/BuildLabel/mount-build-label.ts +0 -1
  6. package/src/components/Callout/Callout.tsx +1 -1
  7. package/src/components/Card/Card.css.ts +1 -0
  8. package/src/components/Card/Card.tsx +11 -2
  9. package/src/components/Checkbox/Checkbox.tsx +4 -2
  10. package/src/components/ColorPalette/ColorPalette.tsx +1 -1
  11. package/src/components/Console/Console.tsx +3 -0
  12. package/src/components/Dialog/Dialog.css.ts +41 -17
  13. package/src/components/Dialog/Dialog.tsx +134 -53
  14. package/src/components/FrameStrip/FrameStrip.tsx +2 -3
  15. package/src/components/ItemPicker/ItemPicker.tsx +46 -33
  16. package/src/components/MenuButton/MenuButton.tsx +1 -0
  17. package/src/components/Menubar/Menubar.css.ts +29 -29
  18. package/src/components/Mixer/Knob.tsx +10 -3
  19. package/src/components/Mixer/Mixer.css.ts +19 -0
  20. package/src/components/Mixer/MixerStrip.tsx +29 -8
  21. package/src/components/NumberField/NumberField.tsx +5 -2
  22. package/src/components/Panel/Panel.css.ts +1 -0
  23. package/src/components/Panel/Panel.tsx +9 -1
  24. package/src/components/PianoRoll/PianoRoll.tsx +1 -0
  25. package/src/components/Progress/Progress.tsx +3 -0
  26. package/src/components/Property/Property.css.ts +13 -0
  27. package/src/components/Property/Property.tsx +3 -2
  28. package/src/components/SearchInput/SearchInput.tsx +1 -1
  29. package/src/components/Select/Select.tsx +3 -1
  30. package/src/components/Sidebar/Sidebar.css.ts +1 -0
  31. package/src/components/Sidebar/Sidebar.tsx +23 -5
  32. package/src/components/Slider/Slider.tsx +4 -2
  33. package/src/components/Switch/Switch.tsx +3 -1
  34. package/src/components/Tabs/Tabs.css.ts +0 -5
  35. package/src/components/Tabs/Tabs.tsx +23 -12
  36. package/src/components/TextField/TextField.tsx +4 -0
  37. package/src/components/TimelineLanes/TimelineLanes.css.ts +6 -0
  38. package/src/components/TimelineLanes/TimelineLanes.tsx +52 -4
  39. package/src/components/TimelineLanes/lanes-math.ts +51 -0
  40. package/src/components/TimelineTrack/TimelineTrack.tsx +16 -3
  41. package/src/components/TitleBar/TitleBar.css.ts +1 -0
  42. package/src/components/TitleBar/TitleBar.tsx +7 -1
  43. package/src/components/Toast/Toast.tsx +1 -2
  44. package/src/components/ToolButton/ToolButton.tsx +1 -1
  45. package/src/components/Toolbar/Toolbar.tsx +6 -1
  46. package/src/components/ToolbarButton/ToolbarButton.tsx +9 -2
  47. package/src/components/ToolboxButton/ToolboxButton.tsx +2 -1
  48. package/src/components/TrackHeader/TrackHeader.css.ts +21 -0
  49. package/src/components/TrackHeader/TrackHeader.tsx +37 -12
  50. package/src/components/TransportBar/TransportBar.tsx +4 -1
  51. package/src/components/Waveform/Waveform.css.ts +10 -0
  52. package/src/components/Waveform/Waveform.tsx +59 -27
  53. package/src/components/WindowControls/WindowControls.tsx +4 -4
  54. package/src/components/index.ts +1 -0
  55. package/src/components/shared/Heading.tsx +30 -0
  56. package/src/global/accordion.css.ts +6 -1
  57. package/src/global/common.css.ts +13 -0
  58. package/src/layers/LayerContext.tsx +2 -2
@@ -1,7 +1,15 @@
1
- import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js';
2
- import { clamp } from '../TimelineLanes/lanes-math';
1
+ import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js';
2
+ import { clamp, describeSeconds, sliderKeyValue } from '../TimelineLanes/lanes-math';
3
3
  import { computePeaks, peaksFromAudioBuffer } from './peaks';
4
4
 
5
+ /** Seconds the trim edges must stay apart. */
6
+ const MIN_REGION = 0.01;
7
+
8
+ const EDGES = [
9
+ { edge: 'start', label: 'Trim start' },
10
+ { edge: 'end', label: 'Trim end' },
11
+ ] as const;
12
+
5
13
  export type { PeakSource } from './peaks';
6
14
 
7
15
  export interface WaveformRegion {
@@ -17,7 +25,10 @@ export interface WaveformProps {
17
25
  buffer?: { duration: number; numberOfChannels: number; getChannelData(channel: number): Float32Array };
18
26
  /** Seconds the peaks span; defaults to `buffer.duration`. Needed for regions and the playhead. */
19
27
  duration?: number;
20
- /** Highlighted span with draggable edges. */
28
+ /**
29
+ * Highlighted span with draggable edges. Focused, an edge also takes the
30
+ * arrow keys (1% of the duration, 10% with Shift) and Home / End.
31
+ */
21
32
  region?: WaveformRegion;
22
33
  onRegionChange?: (region: WaveformRegion) => void;
23
34
  onRegionCommit?: (region: WaveformRegion) => void;
@@ -106,14 +117,15 @@ export function Waveform(props: WaveformProps) {
106
117
  event.preventDefault();
107
118
  event.stopPropagation();
108
119
  const target = event.currentTarget as HTMLElement;
120
+ target.focus({ preventScroll: true });
109
121
  target.setPointerCapture?.(event.pointerId);
110
122
  let last = region;
111
123
  const move = (moveEvent: PointerEvent) => {
112
124
  const seconds = secondsAt(moveEvent.clientX);
113
125
  last =
114
126
  edge === 'start'
115
- ? { start: Math.min(seconds, region.end - 0.01), end: region.end }
116
- : { start: region.start, end: Math.max(seconds, region.start + 0.01) };
127
+ ? { start: Math.min(seconds, region.end - MIN_REGION), end: region.end }
128
+ : { start: region.start, end: Math.max(seconds, region.start + MIN_REGION) };
117
129
  props.onRegionChange?.(last);
118
130
  };
119
131
  const stop = () => {
@@ -128,6 +140,29 @@ export function Waveform(props: WaveformProps) {
128
140
  target.addEventListener('pointercancel', stop);
129
141
  };
130
142
 
143
+ /** The span each edge may move in, short of crossing the other edge. */
144
+ const edgeBounds = (edge: 'start' | 'end', region: WaveformRegion) =>
145
+ edge === 'start'
146
+ ? { min: 0, max: Math.max(0, region.end - MIN_REGION) }
147
+ : { min: Math.min(duration(), region.start + MIN_REGION), max: duration() };
148
+
149
+ const nudgeEdge = (edge: 'start' | 'end') => (event: KeyboardEvent) => {
150
+ const region = props.region;
151
+ if (props.disabled || !region) return;
152
+ const next = sliderKeyValue(event, {
153
+ value: region[edge],
154
+ ...edgeBounds(edge, region),
155
+ step: duration() / 100,
156
+ });
157
+ if (next === null) return;
158
+ event.preventDefault();
159
+ // Page-level shortcuts (Home to rewind, arrows to seek) must not also act on it.
160
+ event.stopPropagation();
161
+ const changed = { ...region, [edge]: next };
162
+ props.onRegionChange?.(changed);
163
+ props.onRegionCommit?.(changed);
164
+ };
165
+
131
166
  return (
132
167
  <div
133
168
  ref={host}
@@ -153,28 +188,25 @@ export function Waveform(props: WaveformProps) {
153
188
  class="zylem-waveform-shade"
154
189
  style={{ left: `${percent(region().end)}%`, right: 0 }}
155
190
  />
156
- <div
157
- class="zylem-waveform-edge"
158
- data-edge="start"
159
- style={{ left: `${percent(region().start)}%` }}
160
- onPointerDown={beginRegionDrag('start')}
161
- role="slider"
162
- aria-label="Trim start"
163
- aria-valuenow={region().start}
164
- aria-valuemin={0}
165
- aria-valuemax={duration()}
166
- />
167
- <div
168
- class="zylem-waveform-edge"
169
- data-edge="end"
170
- style={{ left: `${percent(region().end)}%` }}
171
- onPointerDown={beginRegionDrag('end')}
172
- role="slider"
173
- aria-label="Trim end"
174
- aria-valuenow={region().end}
175
- aria-valuemin={0}
176
- aria-valuemax={duration()}
177
- />
191
+ <For each={EDGES}>
192
+ {(edge) => (
193
+ <div
194
+ class="zylem-waveform-edge"
195
+ data-edge={edge.edge}
196
+ style={{ left: `${percent(region()[edge.edge])}%` }}
197
+ onPointerDown={beginRegionDrag(edge.edge)}
198
+ onKeyDown={nudgeEdge(edge.edge)}
199
+ role="slider"
200
+ tabIndex={props.disabled ? -1 : 0}
201
+ aria-label={edge.label}
202
+ aria-valuenow={region()[edge.edge]}
203
+ aria-valuemin={edgeBounds(edge.edge, region()).min}
204
+ aria-valuemax={edgeBounds(edge.edge, region()).max}
205
+ aria-valuetext={describeSeconds(region()[edge.edge])}
206
+ aria-disabled={props.disabled ? 'true' : undefined}
207
+ />
208
+ )}
209
+ </For>
178
210
  </>
179
211
  );
180
212
  }}
@@ -23,7 +23,7 @@ export interface WindowControlsProps {
23
23
  export function WindowControls(props: WindowControlsProps) {
24
24
  return (
25
25
  <Show when={props.onClose || props.onCollapse}>
26
- <div class="zylem-window-controls" aria-label="Window controls">
26
+ <div class="zylem-window-controls" role="group" aria-label="Window controls">
27
27
  <Show when={props.onCollapse}>
28
28
  <button
29
29
  type="button"
@@ -33,9 +33,9 @@ export function WindowControls(props: WindowControlsProps) {
33
33
  onClick={() => props.onCollapse?.()}
34
34
  >
35
35
  {props.collapsed ? (
36
- <ChevronUp size={11} strokeWidth={2.5} />
36
+ <ChevronUp size={11} strokeWidth={2.5} aria-hidden="true" />
37
37
  ) : (
38
- <ChevronDown size={11} strokeWidth={2.5} />
38
+ <ChevronDown size={11} strokeWidth={2.5} aria-hidden="true" />
39
39
  )}
40
40
  </button>
41
41
  </Show>
@@ -48,7 +48,7 @@ export function WindowControls(props: WindowControlsProps) {
48
48
  data-testid={props.closeTestId}
49
49
  onClick={() => props.onClose?.()}
50
50
  >
51
- <X size={11} strokeWidth={2.5} />
51
+ <X size={11} strokeWidth={2.5} aria-hidden="true" />
52
52
  </button>
53
53
  </Show>
54
54
  </div>
@@ -24,6 +24,7 @@ export {
24
24
  } from '../layers';
25
25
 
26
26
  // Primitives
27
+ export { type HeadingLevel } from './shared/Heading';
27
28
  export { Button, type ButtonProps, type ButtonVariant, type ButtonSize } from './Button/Button';
28
29
  export { ToolbarButton, type ToolbarButtonProps } from './ToolbarButton/ToolbarButton';
29
30
  export { TextField, type TextFieldProps } from './TextField/TextField';
@@ -0,0 +1,30 @@
1
+ import type { JSX } from 'solid-js';
2
+ import { Dynamic } from 'solid-js/web';
3
+
4
+ /** Outline level of a component's title: `2` renders an `<h2>`. */
5
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
6
+
7
+ /** The element name for a level, for primitives that take an `as` tag. */
8
+ export function headingTag(level: HeadingLevel) {
9
+ return `h${level}` as const;
10
+ }
11
+
12
+ export interface HeadingProps {
13
+ level: HeadingLevel;
14
+ id?: string | undefined;
15
+ class?: string | undefined;
16
+ title?: string | undefined;
17
+ children: JSX.Element;
18
+ }
19
+
20
+ /**
21
+ * An `<h1>`…`<h6>` chosen at runtime, for titles whose place in the page
22
+ * outline is the host's call.
23
+ */
24
+ export function Heading(props: HeadingProps) {
25
+ return (
26
+ <Dynamic component={headingTag(props.level)} id={props.id} class={props.class} title={props.title}>
27
+ {props.children}
28
+ </Dynamic>
29
+ );
30
+ }
@@ -14,11 +14,14 @@ globalStyle('.zylem-accordion', {
14
14
  minHeight: 0,
15
15
  });
16
16
 
17
+ // The header is a heading element; its UA margins would open gaps around the
18
+ // trigger, and differ by heading level.
17
19
  globalStyle('.accordion-header', {
18
20
  position: 'sticky',
19
21
  top: 0,
20
22
  zIndex: 1,
21
23
  display: 'flex',
24
+ margin: 0,
22
25
  });
23
26
 
24
27
  globalStyle('.accordion-item', {
@@ -105,7 +108,9 @@ globalStyle('.zylem-accordion--stack .accordion-content', {
105
108
  });
106
109
 
107
110
  globalStyle('.accordion-trigger::after', {
108
- content: "'\\25BC'",
111
+ // The empty alternative text keeps the chevron out of the trigger's
112
+ // accessible name; the first value is for browsers without that syntax.
113
+ content: ["'\\25BC'", "'\\25BC' / ''"],
109
114
  position: 'relative',
110
115
  color: vars.colors.textSecondary,
111
116
  fontSize: '10px',
@@ -13,6 +13,19 @@ globalStyle('.overlay', {
13
13
  pointerEvents: 'none',
14
14
  });
15
15
 
16
+ // Text for assistive tech only: off screen, still in the accessibility tree.
17
+ globalStyle('.zylem-visually-hidden', {
18
+ position: 'absolute',
19
+ width: '1px',
20
+ height: '1px',
21
+ padding: 0,
22
+ margin: '-1px',
23
+ overflow: 'hidden',
24
+ clip: 'rect(0, 0, 0, 0)',
25
+ whiteSpace: 'nowrap',
26
+ border: 0,
27
+ });
28
+
16
29
  // Default scrollbars: slim glass rail (use `.zylem-scroll` for the full
17
30
  // 13px HyperGlass treatment defined in hyperglass-base.css.ts).
18
31
  globalStyle('::-webkit-scrollbar', {
@@ -60,8 +60,8 @@ export interface LayerScopeProps {
60
60
  * them can paint above anything outside; and the scope owns the portal container
61
61
  * its descendants mount into, so overlays opened inside it stay inside it.
62
62
  *
63
- * A dialog wraps its content in one, so a tooltip opened within the dialog floats
64
- * over the dialog body without also floating over a menu belonging to the page.
63
+ * A dialog puts its card in one, so a menu opened within the dialog mounts
64
+ * inside the `<dialog>`, floats over the card, and is not clipped by it.
65
65
  */
66
66
  export function LayerScope(props: LayerScopeProps) {
67
67
  const [container, setContainer] = createSignal<HTMLElement>();