@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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Show } from 'solid-js';
|
|
1
|
+
import { createUniqueId, onCleanup, onMount, Show } from 'solid-js';
|
|
3
2
|
import type { JSX } from 'solid-js';
|
|
4
|
-
import {
|
|
3
|
+
import { Portal } from 'solid-js/web';
|
|
4
|
+
import { LAYER_PORTAL_CLASS, LayerScope, useLayer } from '../../layers';
|
|
5
5
|
import { Button } from '../Button/Button';
|
|
6
6
|
import { WindowControls } from '../WindowControls/WindowControls';
|
|
7
7
|
|
|
@@ -16,7 +16,7 @@ export interface DialogProps {
|
|
|
16
16
|
open?: boolean;
|
|
17
17
|
onOpenChange?: (open: boolean) => void;
|
|
18
18
|
title?: JSX.Element;
|
|
19
|
-
/** Secondary line under the title. */
|
|
19
|
+
/** Secondary line under the title. Describes the dialog unless `aria-describedby` says otherwise. */
|
|
20
20
|
subtitle?: JSX.Element;
|
|
21
21
|
/** Content between the titles and the close control, e.g. a search field. */
|
|
22
22
|
headerExtra?: JSX.Element;
|
|
@@ -28,72 +28,147 @@ export interface DialogProps {
|
|
|
28
28
|
class?: string;
|
|
29
29
|
/** Extra class on the body, e.g. to hand scrolling to the content's columns. */
|
|
30
30
|
bodyClass?: string;
|
|
31
|
+
/** `alertdialog` for an urgent question the user has to answer, such as a confirmation. */
|
|
32
|
+
role?: 'dialog' | 'alertdialog';
|
|
33
|
+
/** Names the dialog when it has no `title`. */
|
|
34
|
+
'aria-label'?: string;
|
|
35
|
+
/** Id of the element that describes the dialog. */
|
|
36
|
+
'aria-describedby'?: string;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
40
|
* HyperGlass dialog: glass window with a glossy title bar and jewel close
|
|
35
41
|
* control, centered over a dimmed backdrop.
|
|
36
42
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
43
|
+
* A native `<dialog>` opened with `showModal()`: it renders in the top layer,
|
|
44
|
+
* above everything else on the page, which is inert while it is open. Escape
|
|
45
|
+
* and a click outside the card ask to close it, and closing hands focus back
|
|
46
|
+
* to whatever had it before.
|
|
47
|
+
*
|
|
48
|
+
* The card shares a {@link LayerScope} with the scope's portal container, so
|
|
49
|
+
* overlays opened from the dialog render inside the `<dialog>` (outside it they
|
|
50
|
+
* would be inert) but beside the card rather than in it, where the card's
|
|
51
|
+
* clipping would cut them off.
|
|
42
52
|
*/
|
|
43
53
|
export function Dialog(props: DialogProps) {
|
|
44
54
|
const layer = useLayer('modal');
|
|
45
|
-
const dismiss = createDismissGuard();
|
|
46
55
|
|
|
47
56
|
return (
|
|
48
|
-
|
|
57
|
+
<>
|
|
49
58
|
{/*
|
|
50
59
|
A dialog is controlled and renders no trigger, so it has nothing in the
|
|
51
60
|
page to resolve its tree from. This marker sits where the dialog was
|
|
52
61
|
declared, which is enough to find an enclosing shadow root.
|
|
53
62
|
*/}
|
|
54
63
|
<span ref={layer.anchorRef} style={{ display: 'none' }} aria-hidden="true" />
|
|
55
|
-
<Show when={layer.mount()} keyed>
|
|
64
|
+
<Show when={props.open && layer.mount()} keyed>
|
|
56
65
|
{(mount) => (
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
<KDialog.Content
|
|
61
|
-
ref={dismiss.ref}
|
|
62
|
-
class={props.class ? `zylem-dialog-content ${props.class}` : 'zylem-dialog-content'}
|
|
63
|
-
data-size={props.size}
|
|
64
|
-
onInteractOutside={dismiss.onInteractOutside}
|
|
65
|
-
>
|
|
66
|
-
<div class="zylem-dialog-header">
|
|
67
|
-
<Show when={props.title || props.subtitle}>
|
|
68
|
-
<div class="zylem-dialog-titles">
|
|
69
|
-
<Show when={props.title}>
|
|
70
|
-
<KDialog.Title class="zylem-dialog-title">{props.title}</KDialog.Title>
|
|
71
|
-
</Show>
|
|
72
|
-
<Show when={props.subtitle}>
|
|
73
|
-
<p class="zylem-dialog-subtitle">{props.subtitle}</p>
|
|
74
|
-
</Show>
|
|
75
|
-
</div>
|
|
76
|
-
</Show>
|
|
77
|
-
<Show when={props.headerExtra}>
|
|
78
|
-
<div class="zylem-dialog-header-extra">{props.headerExtra}</div>
|
|
79
|
-
</Show>
|
|
80
|
-
<WindowControls onClose={() => props.onOpenChange?.(false)} />
|
|
81
|
-
</div>
|
|
82
|
-
<KDialog.Description
|
|
83
|
-
as="div"
|
|
84
|
-
class={props.bodyClass ? `zylem-dialog-body ${props.bodyClass}` : 'zylem-dialog-body'}
|
|
85
|
-
>
|
|
86
|
-
<LayerScope class="zylem-dialog-scope">{props.children}</LayerScope>
|
|
87
|
-
</KDialog.Description>
|
|
88
|
-
<Show when={props.footer}>
|
|
89
|
-
<div class="zylem-dialog-footer">{props.footer}</div>
|
|
90
|
-
</Show>
|
|
91
|
-
</KDialog.Content>
|
|
92
|
-
</div>
|
|
93
|
-
</KDialog.Portal>
|
|
66
|
+
<Portal mount={mount}>
|
|
67
|
+
<DialogWindow {...props} />
|
|
68
|
+
</Portal>
|
|
94
69
|
)}
|
|
95
70
|
</Show>
|
|
96
|
-
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Overlays a click outside the card dismisses on its own, without closing the dialog too. */
|
|
76
|
+
const DISMISSABLE_OVERLAY = '[role="menu"], [role="listbox"], [role="dialog"]';
|
|
77
|
+
|
|
78
|
+
/** The `<dialog>` itself, mounted for as long as the dialog is open. */
|
|
79
|
+
function DialogWindow(props: DialogProps) {
|
|
80
|
+
const titleId = createUniqueId();
|
|
81
|
+
const subtitleId = createUniqueId();
|
|
82
|
+
let dialog!: HTMLDialogElement;
|
|
83
|
+
let disposed = false;
|
|
84
|
+
let pressedBackdrop = false;
|
|
85
|
+
|
|
86
|
+
const close = () => props.onOpenChange?.(false);
|
|
87
|
+
// Outside the card means on the dialog itself or on the positioner filling it.
|
|
88
|
+
const isBackdrop = (target: EventTarget | null | undefined) =>
|
|
89
|
+
target === dialog || target === dialog.firstElementChild;
|
|
90
|
+
const hasOpenOverlay = () =>
|
|
91
|
+
dialog
|
|
92
|
+
.querySelector(`.zylem-dialog-positioner > .${LAYER_PORTAL_CLASS}`)
|
|
93
|
+
?.querySelector(DISMISSABLE_OVERLAY) != null;
|
|
94
|
+
|
|
95
|
+
onMount(() => {
|
|
96
|
+
// jsdom has no `showModal()`.
|
|
97
|
+
if (typeof dialog.showModal === 'function') dialog.showModal();
|
|
98
|
+
else dialog.setAttribute('open', '');
|
|
99
|
+
|
|
100
|
+
// Kobalte dismisses an open overlay from a capture listener on `document`,
|
|
101
|
+
// so this has to run first, on `window`, to still see it open.
|
|
102
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
103
|
+
pressedBackdrop = isBackdrop(event.composedPath()[0]) && !hasOpenOverlay();
|
|
104
|
+
};
|
|
105
|
+
window.addEventListener('pointerdown', onPointerDown, true);
|
|
106
|
+
onCleanup(() => window.removeEventListener('pointerdown', onPointerDown, true));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
onCleanup(() => {
|
|
110
|
+
disposed = true;
|
|
111
|
+
// Runs before the portal detaches the element: only a dialog closed while
|
|
112
|
+
// still in the document returns focus to whatever opened it.
|
|
113
|
+
if (dialog.open) dialog.close();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<dialog
|
|
118
|
+
ref={dialog}
|
|
119
|
+
class="zylem-dialog"
|
|
120
|
+
role={props.role}
|
|
121
|
+
aria-labelledby={props.title ? titleId : undefined}
|
|
122
|
+
aria-label={props['aria-label']}
|
|
123
|
+
aria-describedby={props['aria-describedby'] ?? (props.subtitle ? subtitleId : undefined)}
|
|
124
|
+
onCancel={(event) => {
|
|
125
|
+
// `open` stays the source of truth: the host closes the dialog.
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
close();
|
|
128
|
+
}}
|
|
129
|
+
onClose={() => {
|
|
130
|
+
// The browser closed it anyway, e.g. a second Escape it would not let be prevented.
|
|
131
|
+
if (!disposed) close();
|
|
132
|
+
}}
|
|
133
|
+
onClick={(event) => {
|
|
134
|
+
if (pressedBackdrop && isBackdrop(event.target)) close();
|
|
135
|
+
pressedBackdrop = false;
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<LayerScope class="zylem-dialog-positioner">
|
|
139
|
+
<div
|
|
140
|
+
class={props.class ? `zylem-dialog-content ${props.class}` : 'zylem-dialog-content'}
|
|
141
|
+
data-size={props.size}
|
|
142
|
+
>
|
|
143
|
+
<div class="zylem-dialog-header">
|
|
144
|
+
<Show when={props.title || props.subtitle}>
|
|
145
|
+
<div class="zylem-dialog-titles">
|
|
146
|
+
<Show when={props.title}>
|
|
147
|
+
<h2 id={titleId} class="zylem-dialog-title">
|
|
148
|
+
{props.title}
|
|
149
|
+
</h2>
|
|
150
|
+
</Show>
|
|
151
|
+
<Show when={props.subtitle}>
|
|
152
|
+
<p id={subtitleId} class="zylem-dialog-subtitle">
|
|
153
|
+
{props.subtitle}
|
|
154
|
+
</p>
|
|
155
|
+
</Show>
|
|
156
|
+
</div>
|
|
157
|
+
</Show>
|
|
158
|
+
<Show when={props.headerExtra}>
|
|
159
|
+
<div class="zylem-dialog-header-extra">{props.headerExtra}</div>
|
|
160
|
+
</Show>
|
|
161
|
+
<WindowControls onClose={close} closeLabel="Close dialog" />
|
|
162
|
+
</div>
|
|
163
|
+
<div class={props.bodyClass ? `zylem-dialog-body ${props.bodyClass}` : 'zylem-dialog-body'}>
|
|
164
|
+
{props.children}
|
|
165
|
+
</div>
|
|
166
|
+
<Show when={props.footer}>
|
|
167
|
+
<div class="zylem-dialog-footer">{props.footer}</div>
|
|
168
|
+
</Show>
|
|
169
|
+
</div>
|
|
170
|
+
</LayerScope>
|
|
171
|
+
</dialog>
|
|
97
172
|
);
|
|
98
173
|
}
|
|
99
174
|
|
|
@@ -110,21 +185,27 @@ export interface ConfirmDialogProps {
|
|
|
110
185
|
|
|
111
186
|
/**
|
|
112
187
|
* Confirmation dialog built on Dialog — a drop-in replacement for
|
|
113
|
-
* `window.confirm()` flows.
|
|
188
|
+
* `window.confirm()` flows. An `alertdialog` described by its message; it
|
|
189
|
+
* opens with focus on Confirm, or on Cancel when the action is destructive.
|
|
114
190
|
*/
|
|
115
191
|
export function ConfirmDialog(props: ConfirmDialogProps) {
|
|
192
|
+
const messageId = createUniqueId();
|
|
193
|
+
|
|
116
194
|
return (
|
|
117
195
|
<Dialog
|
|
118
196
|
open={props.open}
|
|
119
197
|
onOpenChange={props.onOpenChange}
|
|
198
|
+
role="alertdialog"
|
|
199
|
+
aria-describedby={messageId}
|
|
120
200
|
title={props.title ?? 'Confirm'}
|
|
121
201
|
footer={
|
|
122
202
|
<>
|
|
123
|
-
<Button variant="ghost" onClick={() => props.onOpenChange(false)}>
|
|
203
|
+
<Button variant="ghost" autofocus={props.danger} onClick={() => props.onOpenChange(false)}>
|
|
124
204
|
{props.cancelLabel ?? 'Cancel'}
|
|
125
205
|
</Button>
|
|
126
206
|
<Button
|
|
127
207
|
variant={props.danger ? 'danger' : 'primary'}
|
|
208
|
+
autofocus={!props.danger}
|
|
128
209
|
onClick={() => {
|
|
129
210
|
props.onConfirm();
|
|
130
211
|
props.onOpenChange(false);
|
|
@@ -135,7 +216,7 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
|
|
|
135
216
|
</>
|
|
136
217
|
}
|
|
137
218
|
>
|
|
138
|
-
{props.message}
|
|
219
|
+
<div id={messageId}>{props.message}</div>
|
|
139
220
|
</Dialog>
|
|
140
221
|
);
|
|
141
222
|
}
|
|
@@ -28,7 +28,7 @@ export function FrameStrip(props: FrameStripProps) {
|
|
|
28
28
|
<span class="zylem-frame-strip-title">{local.title}</span>
|
|
29
29
|
</Show>
|
|
30
30
|
</div>
|
|
31
|
-
<div class="zylem-frame-strip-row zylem-scroll" role="
|
|
31
|
+
<div class="zylem-frame-strip-row zylem-scroll" role="group" aria-label={local.label ?? 'Frames'}>
|
|
32
32
|
{local.children}
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
@@ -51,10 +51,9 @@ export function FrameThumb(props: FrameThumbProps) {
|
|
|
51
51
|
return (
|
|
52
52
|
<button
|
|
53
53
|
type="button"
|
|
54
|
-
role="option"
|
|
55
54
|
class={props.class ? `zylem-frame-thumb ${props.class}` : 'zylem-frame-thumb'}
|
|
56
55
|
data-selected={props.selected ? '' : undefined}
|
|
57
|
-
aria-
|
|
56
|
+
aria-pressed={props.selected ? 'true' : 'false'}
|
|
58
57
|
aria-label={props.label ?? `Frame ${props.index}`}
|
|
59
58
|
onClick={props.onClick}
|
|
60
59
|
>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { For, Show, createMemo, createSignal, type JSX } from 'solid-js';
|
|
1
|
+
import { For, Show, createMemo, createSignal, createUniqueId, type JSX } from 'solid-js';
|
|
2
2
|
import { SearchInput } from '../SearchInput/SearchInput';
|
|
3
3
|
import { filterItems, groupItems, type PickerItem } from './filter-items';
|
|
4
4
|
|
|
5
5
|
export interface ItemPickerProps<T extends PickerItem = PickerItem> {
|
|
6
6
|
items: T[];
|
|
7
7
|
onSelect: (item: T) => void;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Marked as the current choice. Given at all (`null` for none), the items
|
|
10
|
+
* become toggles that expose it as `aria-pressed`.
|
|
11
|
+
*/
|
|
9
12
|
selectedId?: string | null | undefined;
|
|
10
13
|
/** Hide the search field for a list short enough not to need one. */
|
|
11
14
|
searchable?: boolean | undefined;
|
|
@@ -34,6 +37,9 @@ export function ItemPicker<T extends PickerItem>(props: ItemPickerProps<T>) {
|
|
|
34
37
|
|
|
35
38
|
const searchable = () => props.searchable !== false;
|
|
36
39
|
const groups = createMemo(() => groupItems(filterItems(props.items, query())));
|
|
40
|
+
/** Items are toggles only when the caller tracks a choice (`null` is "none yet"). */
|
|
41
|
+
const pressed = (item: T) =>
|
|
42
|
+
props.selectedId === undefined ? undefined : props.selectedId === item.id;
|
|
37
43
|
|
|
38
44
|
return (
|
|
39
45
|
<div class={props.class ? `zylem-picker ${props.class}` : 'zylem-picker'}>
|
|
@@ -68,37 +74,44 @@ export function ItemPicker<T extends PickerItem>(props: ItemPickerProps<T>) {
|
|
|
68
74
|
>
|
|
69
75
|
<div class="zylem-picker__groups zylem-scroll">
|
|
70
76
|
<For each={groups()}>
|
|
71
|
-
{(group) =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
77
|
+
{(group) => {
|
|
78
|
+
const titleId = createUniqueId();
|
|
79
|
+
return (
|
|
80
|
+
<div class="zylem-picker__group" role="group" aria-labelledby={titleId}>
|
|
81
|
+
<div id={titleId} class="zylem-picker__group-title">
|
|
82
|
+
{group.group}
|
|
83
|
+
</div>
|
|
84
|
+
<For each={group.items}>
|
|
85
|
+
{(item) => (
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
class="zylem-picker__item"
|
|
89
|
+
disabled={item.disabled}
|
|
90
|
+
data-selected={
|
|
91
|
+
props.selectedId === item.id ? '' : undefined
|
|
92
|
+
}
|
|
93
|
+
aria-pressed={pressed(item)}
|
|
94
|
+
title={item.description}
|
|
95
|
+
onClick={() => props.onSelect(item)}
|
|
96
|
+
>
|
|
97
|
+
<Show when={item.icon}>
|
|
98
|
+
{(icon) => (
|
|
99
|
+
<span
|
|
100
|
+
class="zylem-picker__item-glyph"
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
// Markup from the caller's own registry, in the
|
|
103
|
+
// caller's own realm; not a new trust boundary.
|
|
104
|
+
innerHTML={icon()}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
</Show>
|
|
108
|
+
<span class="zylem-picker__item-label">{item.label}</span>
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
</For>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}}
|
|
102
115
|
</For>
|
|
103
116
|
</div>
|
|
104
117
|
</Show>
|
|
@@ -7,15 +7,15 @@ import { vars } from '../../theme.css';
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
globalStyle('.zylem-menu-button', {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
position: 'relative',
|
|
11
|
+
display: 'inline-flex',
|
|
12
|
+
alignItems: 'stretch',
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
globalStyle('.zylem-menu-button__face', {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
display: 'inline-flex',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
justifyContent: 'center',
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
/*
|
|
@@ -24,23 +24,27 @@ globalStyle('.zylem-menu-button__face', {
|
|
|
24
24
|
* rhythm.
|
|
25
25
|
*/
|
|
26
26
|
globalStyle('.zylem-menu-button__chevron', {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
position: 'absolute',
|
|
28
|
+
right: '-5px',
|
|
29
|
+
bottom: '-5px',
|
|
30
|
+
display: 'flex',
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
justifyContent: 'center',
|
|
33
|
+
width: '18px',
|
|
34
|
+
height: '18px',
|
|
35
|
+
padding: 0,
|
|
36
|
+
color: vars.colors.text,
|
|
37
|
+
background: vars.colors.border,
|
|
38
|
+
border: `0.5px solid ${vars.colors.surface}`,
|
|
39
|
+
borderRadius: '50%',
|
|
40
|
+
cursor: 'pointer',
|
|
41
|
+
transition: `color ${vars.motion.fast} ${vars.motion.easeOut}`,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
globalStyle('.zylem-menu-button__chevron svg', {
|
|
45
|
+
width: '18px',
|
|
46
|
+
height: '18px',
|
|
47
|
+
stroke: 'currentColor',
|
|
44
48
|
});
|
|
45
49
|
|
|
46
50
|
/*
|
|
@@ -49,43 +53,42 @@ globalStyle('.zylem-menu-button__chevron', {
|
|
|
49
53
|
* control, and lets clicks through to the face beneath.
|
|
50
54
|
*/
|
|
51
55
|
globalStyle('.zylem-menu-button__chevron--static', {
|
|
52
|
-
|
|
56
|
+
pointerEvents: 'none',
|
|
53
57
|
});
|
|
54
58
|
|
|
55
59
|
globalStyle('.zylem-menu-button__chevron:hover:not([disabled])', {
|
|
56
|
-
|
|
57
|
-
borderColor: 'rgba(255, 255, 255, 0.4)',
|
|
60
|
+
color: '#fff',
|
|
58
61
|
});
|
|
59
62
|
|
|
60
63
|
globalStyle('.zylem-menu-button__chevron:focus-visible', {
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
outline: 'none',
|
|
65
|
+
boxShadow: vars.effects.focusRing,
|
|
63
66
|
});
|
|
64
67
|
|
|
65
68
|
globalStyle('.zylem-menu-button__chevron[disabled]', {
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
opacity: 0.45,
|
|
70
|
+
cursor: 'not-allowed',
|
|
68
71
|
});
|
|
69
72
|
|
|
70
73
|
globalStyle('.zylem-menu-button__panel', {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
zIndex: vars.layers.menu,
|
|
75
|
+
color: vars.colors.text,
|
|
76
|
+
fontFamily: vars.typography.fontFamily,
|
|
77
|
+
borderRadius: vars.radii.panel,
|
|
78
|
+
border: '1px solid rgba(97, 166, 232, 0.45)',
|
|
79
|
+
background: vars.material.glassPanelDark,
|
|
80
|
+
boxShadow: vars.effects.shadowPanel,
|
|
81
|
+
backdropFilter: `blur(${vars.effects.blurMd}) saturate(1.22)`,
|
|
82
|
+
outline: 'none',
|
|
83
|
+
'@supports': {
|
|
84
|
+
'not (backdrop-filter: blur(1px))': {
|
|
85
|
+
background: vars.colors.surface,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
85
88
|
});
|
|
86
89
|
|
|
87
90
|
/** Icon glyphs supplied as inline SVG markup rather than components. */
|
|
88
91
|
globalStyle('.zylem-menu-button__face svg', {
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
width: '18px',
|
|
93
|
+
height: '18px',
|
|
91
94
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Popover as KPopover } from '@kobalte/core';
|
|
2
|
+
import CircleChevronDown from 'lucide-solid/icons/circle-chevron-down';
|
|
2
3
|
import { Show, createSignal, type JSX } from 'solid-js';
|
|
3
4
|
import { createDismissGuard, useLayer } from '../../layers';
|
|
4
5
|
import { Tooltip } from '../Tooltip/Tooltip';
|
|
@@ -40,9 +41,12 @@ export interface MenuButtonProps {
|
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Shown in both modes, because a chevron means "this has a menu" and that is
|
|
43
|
-
* true whether or not the face carries its own action.
|
|
44
|
+
* true whether or not the face carries its own action. The circle is the icon's
|
|
45
|
+
* own stroke, so the badge does not draw a second plate behind it.
|
|
44
46
|
*/
|
|
45
|
-
|
|
47
|
+
function MenuChevron() {
|
|
48
|
+
return <CircleChevronDown aria-hidden="true" />;
|
|
49
|
+
}
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* A toolbar button that owns a menu, optionally split.
|
|
@@ -70,11 +74,18 @@ export function MenuButton(props: MenuButtonProps) {
|
|
|
70
74
|
const isSplit = () => props.onAction !== undefined;
|
|
71
75
|
const tooltipText = () =>
|
|
72
76
|
props.tooltip === null ? null : props.tooltip ?? props.label;
|
|
77
|
+
/** The menu takes one side of the button, so the label takes the other. */
|
|
78
|
+
const tooltipPlacement = () =>
|
|
79
|
+
(props.placement ?? 'bottom-start').startsWith('top') ? 'bottom' : 'top';
|
|
73
80
|
|
|
74
81
|
/** Wrap in a tooltip only when there is text for one. */
|
|
75
82
|
const described = (content: JSX.Element) => (
|
|
76
83
|
<Show when={tooltipText()} fallback={content} keyed>
|
|
77
|
-
{(text) =>
|
|
84
|
+
{(text) => (
|
|
85
|
+
<Tooltip content={text} placement={tooltipPlacement()}>
|
|
86
|
+
{content}
|
|
87
|
+
</Tooltip>
|
|
88
|
+
)}
|
|
78
89
|
</Show>
|
|
79
90
|
);
|
|
80
91
|
|
|
@@ -107,7 +118,7 @@ export function MenuButton(props: MenuButtonProps) {
|
|
|
107
118
|
class="zylem-menu-button__chevron zylem-menu-button__chevron--static"
|
|
108
119
|
aria-hidden="true"
|
|
109
120
|
>
|
|
110
|
-
|
|
121
|
+
<MenuChevron />
|
|
111
122
|
</span>
|
|
112
123
|
</KPopover.Trigger>,
|
|
113
124
|
)}
|
|
@@ -130,7 +141,7 @@ export function MenuButton(props: MenuButtonProps) {
|
|
|
130
141
|
aria-label={`${props.label} options`}
|
|
131
142
|
disabled={props.disabled}
|
|
132
143
|
>
|
|
133
|
-
<
|
|
144
|
+
<MenuChevron />
|
|
134
145
|
</KPopover.Trigger>
|
|
135
146
|
</Show>
|
|
136
147
|
|
|
@@ -140,6 +151,7 @@ export function MenuButton(props: MenuButtonProps) {
|
|
|
140
151
|
<KPopover.Content
|
|
141
152
|
ref={dismiss.ref}
|
|
142
153
|
class="zylem-menu-button__panel"
|
|
154
|
+
aria-label={props.label}
|
|
143
155
|
onInteractOutside={dismiss.onInteractOutside}
|
|
144
156
|
>
|
|
145
157
|
{props.menu(() => setOpen(false))}
|
|
@@ -33,7 +33,7 @@ globalStyle('.zylem-menubar-trigger', {
|
|
|
33
33
|
|
|
34
34
|
globalStyle('.zylem-menubar-trigger:hover', {
|
|
35
35
|
background: 'rgba(97, 166, 232, 0.12)',
|
|
36
|
-
color: vars.colors.
|
|
36
|
+
color: vars.colors.text,
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
globalStyle('.zylem-menubar-trigger[data-expanded]', {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal, Show } from 'solid-js';
|
|
1
|
+
import { createSignal, createUniqueId, Show } from 'solid-js';
|
|
2
2
|
import { clamp } from '../TimelineLanes/lanes-math';
|
|
3
3
|
|
|
4
4
|
export interface KnobProps {
|
|
@@ -11,7 +11,10 @@ export interface KnobProps {
|
|
|
11
11
|
step?: number;
|
|
12
12
|
/** Double-click resets to this. */
|
|
13
13
|
defaultValue?: number;
|
|
14
|
+
/** Caption under the knob, which also names it. */
|
|
14
15
|
label?: string;
|
|
16
|
+
/** Accessible name when there is no visible `label`. */
|
|
17
|
+
'aria-label'?: string;
|
|
15
18
|
/** Readout under the knob; defaults to the value with `step` precision. */
|
|
16
19
|
format?: (value: number) => string;
|
|
17
20
|
/** Draw the arc from the centre (for pan and other ±ranges). */
|
|
@@ -48,6 +51,7 @@ export function Knob(props: KnobProps) {
|
|
|
48
51
|
const max = () => props.max ?? 1;
|
|
49
52
|
const step = () => props.step ?? (max() - min()) / 100;
|
|
50
53
|
const size = () => props.size ?? 36;
|
|
54
|
+
const labelId = createUniqueId();
|
|
51
55
|
const ratio = () => (max() > min() ? clamp((props.value - min()) / (max() - min()), 0, 1) : 0);
|
|
52
56
|
const angle = () => START_ANGLE + ratio() * SWEEP;
|
|
53
57
|
const zeroAngle = () =>
|
|
@@ -147,7 +151,8 @@ export function Knob(props: KnobProps) {
|
|
|
147
151
|
class="zylem-knob-dial"
|
|
148
152
|
role="slider"
|
|
149
153
|
tabIndex={props.disabled ? -1 : 0}
|
|
150
|
-
aria-
|
|
154
|
+
aria-labelledby={props.label ? labelId : undefined}
|
|
155
|
+
aria-label={props.label ? undefined : props['aria-label']}
|
|
151
156
|
aria-valuemin={min()}
|
|
152
157
|
aria-valuemax={max()}
|
|
153
158
|
aria-valuenow={props.value}
|
|
@@ -180,7 +185,9 @@ export function Knob(props: KnobProps) {
|
|
|
180
185
|
</svg>
|
|
181
186
|
</div>
|
|
182
187
|
<Show when={props.label}>
|
|
183
|
-
<span class="zylem-knob-label">
|
|
188
|
+
<span id={labelId} class="zylem-knob-label">
|
|
189
|
+
{props.label}
|
|
190
|
+
</span>
|
|
184
191
|
</Show>
|
|
185
192
|
<span class="zylem-knob-value">{readout()}</span>
|
|
186
193
|
</div>
|