@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,146 @@
1
+ import ArrowLeftToLine from 'lucide-solid/icons/arrow-left-to-line';
2
+ import ArrowRightToLine from 'lucide-solid/icons/arrow-right-to-line';
3
+ import ChevronLeft from 'lucide-solid/icons/chevron-left';
4
+ import ChevronRight from 'lucide-solid/icons/chevron-right';
5
+ import Copy from 'lucide-solid/icons/copy';
6
+ import Trash2 from 'lucide-solid/icons/trash-2';
7
+ import { type JSX, Show, splitProps } from 'solid-js';
8
+ import { Button } from '../Button/Button';
9
+ import { ToolbarButton } from '../ToolbarButton/ToolbarButton';
10
+
11
+ export interface FrameStripProps extends JSX.HTMLAttributes<HTMLDivElement> {
12
+ /** Caption, e.g. `Timeline`. */
13
+ label?: string;
14
+ /** Name of the animation the frames belong to, shown after the caption. */
15
+ title?: string;
16
+ /** `FrameThumb`s followed by an `AddTile variant="tall"`. */
17
+ children: JSX.Element;
18
+ }
19
+
20
+ /** Captioned, horizontally scrolling row of animation frames. */
21
+ export function FrameStrip(props: FrameStripProps) {
22
+ const [local, rest] = splitProps(props, ['label', 'title', 'children', 'class']);
23
+ return (
24
+ <div class={local.class ? `zylem-frame-strip ${local.class}` : 'zylem-frame-strip'} {...rest}>
25
+ <div class="zylem-frame-strip-header">
26
+ <span class="zylem-frame-strip-label">{local.label ?? 'Timeline'}</span>
27
+ <Show when={local.title}>
28
+ <span class="zylem-frame-strip-title">{local.title}</span>
29
+ </Show>
30
+ </div>
31
+ <div class="zylem-frame-strip-row zylem-scroll" role="listbox" aria-label={local.label ?? 'Frames'}>
32
+ {local.children}
33
+ </div>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ export interface FrameThumbProps {
39
+ /** One-based frame number shown in the corner badge. */
40
+ index: number;
41
+ selected?: boolean;
42
+ onClick?: (event: MouseEvent) => void;
43
+ /** Thumbnail content (a canvas or img); empty frames show the checker. */
44
+ children?: JSX.Element;
45
+ label?: string;
46
+ class?: string;
47
+ }
48
+
49
+ /** Square frame thumbnail on the checker with a frame-number badge. */
50
+ export function FrameThumb(props: FrameThumbProps) {
51
+ return (
52
+ <button
53
+ type="button"
54
+ role="option"
55
+ class={props.class ? `zylem-frame-thumb ${props.class}` : 'zylem-frame-thumb'}
56
+ data-selected={props.selected ? '' : undefined}
57
+ aria-selected={props.selected ? 'true' : 'false'}
58
+ aria-label={props.label ?? `Frame ${props.index}`}
59
+ onClick={props.onClick}
60
+ >
61
+ <span class="zylem-frame-thumb-surface">{props.children}</span>
62
+ <span class="zylem-frame-thumb-index" aria-hidden="true">
63
+ {props.index}
64
+ </span>
65
+ </button>
66
+ );
67
+ }
68
+
69
+ export interface FrameActionsProps {
70
+ onDuplicate?: () => void;
71
+ onDelete?: () => void;
72
+ /** Reorder: swap the selected frame with its left neighbour. Shown when set. */
73
+ onMoveLeft?: () => void;
74
+ /** Reorder: swap the selected frame with its right neighbour. Shown when set. */
75
+ onMoveRight?: () => void;
76
+ onPrev?: () => void;
77
+ onNext?: () => void;
78
+ canDuplicate?: boolean;
79
+ canDelete?: boolean;
80
+ canMoveLeft?: boolean;
81
+ canMoveRight?: boolean;
82
+ canPrev?: boolean;
83
+ canNext?: boolean;
84
+ class?: string;
85
+ }
86
+
87
+ /**
88
+ * Frame commands under the strip: Duplicate and Delete, the optional reorder
89
+ * pair, then step arrows whose chevrons are centered in square toolbar buttons.
90
+ */
91
+ export function FrameActions(props: FrameActionsProps) {
92
+ return (
93
+ <div class={props.class ? `zylem-frame-actions ${props.class}` : 'zylem-frame-actions'}>
94
+ <Button
95
+ variant="primary"
96
+ size="sm"
97
+ disabled={props.canDuplicate === false}
98
+ onClick={() => props.onDuplicate?.()}
99
+ >
100
+ <Copy class="zylem-icon" aria-hidden="true" />
101
+ Duplicate
102
+ </Button>
103
+ <Button size="sm" disabled={props.canDelete === false} onClick={() => props.onDelete?.()}>
104
+ <Trash2 class="zylem-icon" aria-hidden="true" />
105
+ Delete
106
+ </Button>
107
+ <Show when={props.onMoveLeft || props.onMoveRight}>
108
+ <span class="zylem-frame-actions-group" role="group" aria-label="Reorder frame">
109
+ <ToolbarButton
110
+ label="Move frame left"
111
+ disabled={props.canMoveLeft === false}
112
+ onClick={() => props.onMoveLeft?.()}
113
+ class="zylem-frame-actions-step"
114
+ >
115
+ <ArrowLeftToLine class="zylem-icon" />
116
+ </ToolbarButton>
117
+ <ToolbarButton
118
+ label="Move frame right"
119
+ disabled={props.canMoveRight === false}
120
+ onClick={() => props.onMoveRight?.()}
121
+ class="zylem-frame-actions-step"
122
+ >
123
+ <ArrowRightToLine class="zylem-icon" />
124
+ </ToolbarButton>
125
+ </span>
126
+ </Show>
127
+ <span class="zylem-frame-actions-spacer" />
128
+ <ToolbarButton
129
+ label="Previous frame"
130
+ disabled={props.canPrev === false}
131
+ onClick={() => props.onPrev?.()}
132
+ class="zylem-frame-actions-step"
133
+ >
134
+ <ChevronLeft class="zylem-icon" />
135
+ </ToolbarButton>
136
+ <ToolbarButton
137
+ label="Next frame"
138
+ disabled={props.canNext === false}
139
+ onClick={() => props.onNext?.()}
140
+ class="zylem-frame-actions-step"
141
+ >
142
+ <ChevronRight class="zylem-icon" />
143
+ </ToolbarButton>
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,52 @@
1
+ import { globalStyle } from '@vanilla-extract/css';
2
+ import { vars } from '../../theme.css';
3
+
4
+ globalStyle('.zylem-menubar', {
5
+ display: 'flex',
6
+ alignItems: 'center',
7
+ gap: '2px',
8
+ minWidth: 0,
9
+ });
10
+
11
+ // Flat chrome (see DESIGN.md): transparent at rest, a tint on hover and a
12
+ // stronger tint while the menu is open. No gloss, so the bar's glossy title
13
+ // surfaces and primary buttons stay the focal points.
14
+ globalStyle('.zylem-menubar-trigger', {
15
+ display: 'inline-flex',
16
+ alignItems: 'center',
17
+ gap: vars.spacing.xs,
18
+ padding: `${vars.spacing.xs} ${vars.spacing.sm}`,
19
+ border: '1px solid transparent',
20
+ borderRadius: vars.radii.button,
21
+ background: 'transparent',
22
+ color: vars.colors.textSecondary,
23
+ fontFamily: vars.typography.fontFamily,
24
+ fontSize: `calc(${vars.typography.fontSize} - 1px)`,
25
+ fontWeight: 600,
26
+ letterSpacing: '0.02em',
27
+ lineHeight: 1.2,
28
+ cursor: 'pointer',
29
+ userSelect: 'none',
30
+ outline: 'none',
31
+ transition: `background ${vars.motion.fast} ${vars.motion.easeOut}, color ${vars.motion.fast} ${vars.motion.easeOut}`,
32
+ });
33
+
34
+ globalStyle('.zylem-menubar-trigger:hover', {
35
+ background: 'rgba(97, 166, 232, 0.12)',
36
+ color: vars.colors.text,
37
+ });
38
+
39
+ globalStyle('.zylem-menubar-trigger[data-expanded]', {
40
+ background: 'rgba(97, 166, 232, 0.22)',
41
+ borderColor: 'rgba(97, 166, 232, 0.35)',
42
+ color: '#F4FAFF',
43
+ });
44
+
45
+ globalStyle('.zylem-menubar-trigger:focus-visible', {
46
+ boxShadow: vars.effects.focusRing,
47
+ });
48
+
49
+ globalStyle('.zylem-menubar-trigger[data-disabled]', {
50
+ opacity: 0.45,
51
+ cursor: 'not-allowed',
52
+ });
@@ -0,0 +1,77 @@
1
+ import { Menubar as KMenubar } from '@kobalte/core';
2
+ import { For, Show } from 'solid-js';
3
+ import type { JSX } from 'solid-js';
4
+ import { createDismissGuard, useLayer } from '../../layers';
5
+ import { MenuItems, type DropdownMenuItemConfig } from '../DropdownMenu/DropdownMenu';
6
+
7
+ export interface MenubarMenuConfig {
8
+ id: string;
9
+ label: JSX.Element;
10
+ /**
11
+ * Rows of the menu. A function is read each time the menu renders, so a
12
+ * menu whose rows depend on app state (the open stages, the dirty flag) can
13
+ * hand over its accessor instead of rebuilding the whole bar on every change.
14
+ */
15
+ items: DropdownMenuItemConfig[] | (() => DropdownMenuItemConfig[]);
16
+ disabled?: boolean;
17
+ }
18
+
19
+ export interface MenubarProps {
20
+ menus: MenubarMenuConfig[];
21
+ class?: string;
22
+ 'aria-label'?: string;
23
+ /** Class applied to every trigger button. */
24
+ triggerClass?: string;
25
+ }
26
+
27
+ /**
28
+ * Desktop-style menu bar: a row of flat text triggers, one open at a time.
29
+ * Moving the pointer onto a sibling trigger while a menu is open swaps menus,
30
+ * and the arrow keys walk the bar — behaviour that comes from Kobalte's
31
+ * Menubar. Each menu is a `DropdownMenu` sheet, portaled at the `menu` tier.
32
+ */
33
+ export function Menubar(props: MenubarProps) {
34
+ const layer = useLayer('menu');
35
+
36
+ return (
37
+ <KMenubar.Root
38
+ ref={layer.anchorRef}
39
+ class={props.class ? `zylem-menubar ${props.class}` : 'zylem-menubar'}
40
+ aria-label={props['aria-label']}
41
+ >
42
+ <For each={props.menus}>
43
+ {(menu) => {
44
+ const dismiss = createDismissGuard();
45
+ const items = () => (typeof menu.items === 'function' ? menu.items() : menu.items);
46
+ return (
47
+ <KMenubar.Menu>
48
+ <KMenubar.Trigger
49
+ class={
50
+ props.triggerClass
51
+ ? `zylem-menubar-trigger ${props.triggerClass}`
52
+ : 'zylem-menubar-trigger'
53
+ }
54
+ disabled={menu.disabled}
55
+ >
56
+ {menu.label}
57
+ </KMenubar.Trigger>
58
+ <Show when={layer.mount()} keyed>
59
+ {(mount) => (
60
+ <KMenubar.Portal mount={mount}>
61
+ <KMenubar.Content
62
+ ref={dismiss.ref}
63
+ class="zylem-dropdown-content zylem-menubar-content"
64
+ onInteractOutside={dismiss.onInteractOutside}
65
+ >
66
+ <MenuItems items={items()} />
67
+ </KMenubar.Content>
68
+ </KMenubar.Portal>
69
+ )}
70
+ </Show>
71
+ </KMenubar.Menu>
72
+ );
73
+ }}
74
+ </For>
75
+ </KMenubar.Root>
76
+ );
77
+ }
@@ -0,0 +1,188 @@
1
+ import { createSignal, Show } from 'solid-js';
2
+ import { clamp } from '../TimelineLanes/lanes-math';
3
+
4
+ export interface KnobProps {
5
+ value: number;
6
+ onChange?: (value: number) => void;
7
+ /** Fires once when a drag ends or a key edit lands. */
8
+ onCommit?: (value: number) => void;
9
+ min?: number;
10
+ max?: number;
11
+ step?: number;
12
+ /** Double-click resets to this. */
13
+ defaultValue?: number;
14
+ label?: string;
15
+ /** Readout under the knob; defaults to the value with `step` precision. */
16
+ format?: (value: number) => string;
17
+ /** Draw the arc from the centre (for pan and other ±ranges). */
18
+ bipolar?: boolean;
19
+ size?: number;
20
+ disabled?: boolean;
21
+ class?: string;
22
+ }
23
+
24
+ const SWEEP = 270;
25
+ const START_ANGLE = -135;
26
+
27
+ function polar(cx: number, cy: number, radius: number, degrees: number) {
28
+ const radians = ((degrees - 90) * Math.PI) / 180;
29
+ return { x: cx + radius * Math.cos(radians), y: cy + radius * Math.sin(radians) };
30
+ }
31
+
32
+ /** SVG arc path from `from` to `to` degrees (clockwise, 12 o'clock = 0). */
33
+ export function arcPath(cx: number, cy: number, radius: number, from: number, to: number): string {
34
+ const [a, b] = from <= to ? [from, to] : [to, from];
35
+ const start = polar(cx, cy, radius, a);
36
+ const end = polar(cx, cy, radius, b);
37
+ const large = b - a > 180 ? 1 : 0;
38
+ return `M ${start.x.toFixed(3)} ${start.y.toFixed(3)} A ${radius} ${radius} 0 ${large} 1 ${end.x.toFixed(3)} ${end.y.toFixed(3)}`;
39
+ }
40
+
41
+ /**
42
+ * Rotary control: drag vertically (Shift for fine steps), arrow keys to
43
+ * step, double-click to reset. The arc fills from the minimum, or from the
44
+ * centre when `bipolar`.
45
+ */
46
+ export function Knob(props: KnobProps) {
47
+ const min = () => props.min ?? 0;
48
+ const max = () => props.max ?? 1;
49
+ const step = () => props.step ?? (max() - min()) / 100;
50
+ const size = () => props.size ?? 36;
51
+ const ratio = () => (max() > min() ? clamp((props.value - min()) / (max() - min()), 0, 1) : 0);
52
+ const angle = () => START_ANGLE + ratio() * SWEEP;
53
+ const zeroAngle = () =>
54
+ START_ANGLE + clamp((0 - min()) / (max() - min() || 1), 0, 1) * SWEEP;
55
+ const [dragging, setDragging] = createSignal(false);
56
+
57
+ const quantize = (value: number) => {
58
+ const s = step();
59
+ const snapped = s > 0 ? Math.round((value - min()) / s) * s + min() : value;
60
+ return clamp(Number(snapped.toFixed(6)), min(), max());
61
+ };
62
+ const readout = () => {
63
+ if (props.format) return props.format(props.value);
64
+ const decimals = Math.max(0, Math.min(3, -Math.floor(Math.log10(step() || 1))));
65
+ return props.value.toFixed(decimals);
66
+ };
67
+
68
+ const beginDrag = (event: PointerEvent) => {
69
+ if (props.disabled || event.button !== 0) return;
70
+ event.preventDefault();
71
+ const target = event.currentTarget as HTMLElement;
72
+ target.focus({ preventScroll: true });
73
+ target.setPointerCapture?.(event.pointerId);
74
+ const originY = event.clientY;
75
+ const originValue = props.value;
76
+ let last = originValue;
77
+ setDragging(true);
78
+ const move = (moveEvent: PointerEvent) => {
79
+ const pixels = moveEvent.shiftKey ? 600 : 150;
80
+ const delta = ((originY - moveEvent.clientY) / pixels) * (max() - min());
81
+ last = quantize(originValue + delta);
82
+ props.onChange?.(last);
83
+ };
84
+ const stop = () => {
85
+ target.removeEventListener('pointermove', move);
86
+ target.removeEventListener('pointerup', stop);
87
+ target.removeEventListener('pointercancel', stop);
88
+ target.releasePointerCapture?.(event.pointerId);
89
+ setDragging(false);
90
+ props.onCommit?.(last);
91
+ };
92
+ target.addEventListener('pointermove', move);
93
+ target.addEventListener('pointerup', stop);
94
+ target.addEventListener('pointercancel', stop);
95
+ };
96
+
97
+ const nudge = (direction: number, big: boolean) => {
98
+ const next = quantize(props.value + direction * step() * (big ? 10 : 1));
99
+ props.onChange?.(next);
100
+ props.onCommit?.(next);
101
+ };
102
+
103
+ const handleKeyDown = (event: KeyboardEvent) => {
104
+ if (props.disabled) return;
105
+ switch (event.key) {
106
+ case 'ArrowUp':
107
+ case 'ArrowRight':
108
+ event.preventDefault();
109
+ nudge(1, event.shiftKey);
110
+ break;
111
+ case 'ArrowDown':
112
+ case 'ArrowLeft':
113
+ event.preventDefault();
114
+ nudge(-1, event.shiftKey);
115
+ break;
116
+ case 'Home':
117
+ event.preventDefault();
118
+ props.onChange?.(min());
119
+ props.onCommit?.(min());
120
+ break;
121
+ case 'End':
122
+ event.preventDefault();
123
+ props.onChange?.(max());
124
+ props.onCommit?.(max());
125
+ break;
126
+ default:
127
+ }
128
+ };
129
+
130
+ const reset = () => {
131
+ if (props.disabled || props.defaultValue === undefined) return;
132
+ const next = quantize(props.defaultValue);
133
+ props.onChange?.(next);
134
+ props.onCommit?.(next);
135
+ };
136
+
137
+ const radius = () => size() / 2 - 3;
138
+ const center = () => size() / 2;
139
+
140
+ return (
141
+ <div
142
+ class={props.class ? `zylem-knob ${props.class}` : 'zylem-knob'}
143
+ data-disabled={props.disabled ? '' : undefined}
144
+ data-dragging={dragging() ? '' : undefined}
145
+ >
146
+ <div
147
+ class="zylem-knob-dial"
148
+ role="slider"
149
+ tabIndex={props.disabled ? -1 : 0}
150
+ aria-label={props.label}
151
+ aria-valuemin={min()}
152
+ aria-valuemax={max()}
153
+ aria-valuenow={props.value}
154
+ aria-valuetext={readout()}
155
+ aria-disabled={props.disabled ? 'true' : undefined}
156
+ style={{ width: `${size()}px`, height: `${size()}px` }}
157
+ onPointerDown={beginDrag}
158
+ onKeyDown={handleKeyDown}
159
+ onDblClick={reset}
160
+ >
161
+ <svg viewBox={`0 0 ${size()} ${size()}`} width={size()} height={size()} aria-hidden="true">
162
+ <path
163
+ class="zylem-knob-track"
164
+ d={arcPath(center(), center(), radius(), START_ANGLE, START_ANGLE + SWEEP)}
165
+ />
166
+ <path
167
+ class="zylem-knob-fill"
168
+ d={arcPath(center(), center(), radius(), props.bipolar ? zeroAngle() : START_ANGLE, angle())}
169
+ />
170
+ <g transform={`rotate(${angle()} ${center()} ${center()})`}>
171
+ <circle class="zylem-knob-cap" cx={center()} cy={center()} r={radius() - 4} />
172
+ <line
173
+ class="zylem-knob-pointer"
174
+ x1={center()}
175
+ y1={center() - radius() + 6}
176
+ x2={center()}
177
+ y2={center() - radius() / 2}
178
+ />
179
+ </g>
180
+ </svg>
181
+ </div>
182
+ <Show when={props.label}>
183
+ <span class="zylem-knob-label">{props.label}</span>
184
+ </Show>
185
+ <span class="zylem-knob-value">{readout()}</span>
186
+ </div>
187
+ );
188
+ }