@zylem/ui 0.9.6 → 0.9.8
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-UQ3MDDFI.js +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.js +1 -1
- package/etc/ui.api.md +1938 -0
- package/package.json +13 -12
- package/src/components/Button/Button.css.ts +14 -0
- package/src/components/Button/Button.tsx +11 -3
- package/src/components/Console/Console.tsx +5 -0
- package/src/components/MenuButton/MenuButton.css.ts +25 -0
- package/src/components/MenuButton/MenuButton.tsx +9 -0
- package/src/components/NumberField/NumberField.tsx +116 -3
- package/src/components/Property/Property.tsx +12 -3
- package/src/components/TimelineTrack/timeline-math.ts +2 -1
- package/src/components/TitleBar/TitleBar.tsx +2 -0
- package/src/components/ToolButton/ToolButton.tsx +2 -1
- package/src/components/ToolbarButton/ToolbarButton.css.ts +27 -1
- package/src/components/ToolbarButton/ToolbarButton.tsx +22 -5
- package/src/components/ToolbarDropdownMenu/ToolbarDropdownMenu.tsx +10 -1
- package/src/components/ToolboxButton/ToolboxButton.css.ts +13 -0
- package/src/components/ToolboxButton/ToolboxButton.tsx +8 -2
- package/src/components/WindowControls/WindowControls.tsx +18 -5
- package/src/components/index.ts +14 -0
- package/src/icons/icon-nodes.ts +36 -0
- package/src/icons/index.ts +48 -0
- package/src/layers/LayerContext.tsx +1 -1
- package/src/theme.css.ts +8 -0
- package/dist/chunk-YJVLOHJL.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zylem/ui",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "Design tokens, global styles, and Solid components for Zylem, built with vanilla-extract",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -50,18 +50,10 @@
|
|
|
50
50
|
},
|
|
51
51
|
"files": [
|
|
52
52
|
"dist",
|
|
53
|
+
"etc",
|
|
53
54
|
"src",
|
|
54
55
|
"vite"
|
|
55
56
|
],
|
|
56
|
-
"scripts": {
|
|
57
|
-
"build": "tsup",
|
|
58
|
-
"prepublishOnly": "NODE_ENV=production tsup",
|
|
59
|
-
"dev": "tsup --watch",
|
|
60
|
-
"showcase": "vite --host",
|
|
61
|
-
"test": "vitest run",
|
|
62
|
-
"test:watch": "vitest",
|
|
63
|
-
"lint": "biome check ."
|
|
64
|
-
},
|
|
65
57
|
"peerDependencies": {
|
|
66
58
|
"solid-js": "^1.9.9"
|
|
67
59
|
},
|
|
@@ -72,6 +64,7 @@
|
|
|
72
64
|
},
|
|
73
65
|
"devDependencies": {
|
|
74
66
|
"@biomejs/biome": "^2.3.7",
|
|
67
|
+
"@microsoft/api-extractor": "^7.55.2",
|
|
75
68
|
"@clack/prompts": "^1.6.0",
|
|
76
69
|
"@solidjs/testing-library": "^0.8.10",
|
|
77
70
|
"@vanilla-extract/esbuild-plugin": "^2.3.22",
|
|
@@ -95,5 +88,13 @@
|
|
|
95
88
|
"node": ">=22.12.0",
|
|
96
89
|
"pnpm": ">=10.32.1"
|
|
97
90
|
},
|
|
98
|
-
"
|
|
99
|
-
|
|
91
|
+
"scripts": {
|
|
92
|
+
"build": "tsup",
|
|
93
|
+
"dev": "tsup --watch",
|
|
94
|
+
"showcase": "vite --host",
|
|
95
|
+
"test": "vitest run",
|
|
96
|
+
"test:watch": "vitest",
|
|
97
|
+
"lint": "biome check .",
|
|
98
|
+
"api:extract": "tsc -p tsconfig.api.json && api-extractor run"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -67,6 +67,20 @@ globalStyle('.zylem-button[data-variant="primary"]:hover:not([disabled])', {
|
|
|
67
67
|
filter: 'brightness(1.08)',
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
globalStyle('.zylem-button[data-on]', {
|
|
71
|
+
borderColor: 'rgba(46, 231, 255, 0.9)',
|
|
72
|
+
background: vars.material.buttonGlassOn,
|
|
73
|
+
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowOn}`,
|
|
74
|
+
color: '#F3FDFF',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
globalStyle('.zylem-button[data-on]:hover:not([disabled])', {
|
|
78
|
+
background: vars.material.buttonGlassOn,
|
|
79
|
+
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowOn}`,
|
|
80
|
+
filter: 'brightness(1.08)',
|
|
81
|
+
color: '#F3FDFF',
|
|
82
|
+
});
|
|
83
|
+
|
|
70
84
|
globalStyle('.zylem-button[data-variant="active"]', {
|
|
71
85
|
borderColor: 'rgba(20, 255, 60, 0.78)',
|
|
72
86
|
background: vars.material.buttonGlassActive,
|
|
@@ -9,18 +9,24 @@ export interface ButtonProps
|
|
|
9
9
|
size?: ButtonSize;
|
|
10
10
|
/** Icon drawn before the label. Omitted, the button is label only. */
|
|
11
11
|
icon?: JSX.Element | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Checkbox-like toggle. Electric blue while on, and exposed as `aria-pressed`.
|
|
14
|
+
* Pass `false` while off so the button still announces itself.
|
|
15
|
+
*/
|
|
16
|
+
on?: boolean | undefined;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* HyperGlass button: squared Aqua-like bevel with glass depth. Variants map
|
|
16
|
-
* to the Zylem palette — `active` for live/run actions, `
|
|
17
|
-
* destructive actions, `ghost` for
|
|
21
|
+
* to the Zylem palette — `active` for live/run actions, `on` for a toggle
|
|
22
|
+
* that is switched on, `danger` for destructive actions, `ghost` for
|
|
23
|
+
* low-emphasis chrome.
|
|
18
24
|
*
|
|
19
25
|
* Styling lives on the global `.zylem-button` class (bundled in
|
|
20
26
|
* `@zylem/ui/styles.css`), selected via `data-variant` / `data-size`.
|
|
21
27
|
*/
|
|
22
28
|
export function Button(props: ButtonProps) {
|
|
23
|
-
const [local, rest] = splitProps(props, ['variant', 'size', 'class', 'children', 'icon']);
|
|
29
|
+
const [local, rest] = splitProps(props, ['variant', 'size', 'class', 'children', 'icon', 'on']);
|
|
24
30
|
return (
|
|
25
31
|
<button
|
|
26
32
|
type="button"
|
|
@@ -28,6 +34,8 @@ export function Button(props: ButtonProps) {
|
|
|
28
34
|
data-variant={local.variant ?? 'default'}
|
|
29
35
|
data-size={local.size ?? 'md'}
|
|
30
36
|
{...rest}
|
|
37
|
+
data-on={local.on ? '' : undefined}
|
|
38
|
+
aria-pressed={local.on === undefined ? undefined : local.on}
|
|
31
39
|
>
|
|
32
40
|
<Show when={local.icon}>
|
|
33
41
|
<span class="zylem-button-icon" aria-hidden="true">
|
|
@@ -19,6 +19,11 @@ export interface ConsoleProps {
|
|
|
19
19
|
/**
|
|
20
20
|
* HyperGlass console: dark low-gloss terminal field with a floating Clear
|
|
21
21
|
* action.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <Console value={text()} onInput={setText} onClear={() => setText('')} />
|
|
26
|
+
* ```
|
|
22
27
|
*/
|
|
23
28
|
export function Console(props: ConsoleProps) {
|
|
24
29
|
return (
|
|
@@ -104,3 +104,28 @@ globalStyle('.zylem-menu-button__stack', {
|
|
|
104
104
|
gap: vars.spacing.xs,
|
|
105
105
|
padding: vars.spacing.sm,
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* View cube. A short row (game camera, perspective) sits above a 3×3 square:
|
|
110
|
+
* isometric corners, orthographic edges, free camera in the middle.
|
|
111
|
+
*/
|
|
112
|
+
globalStyle('.zylem-menu-button__compass', {
|
|
113
|
+
display: 'flex',
|
|
114
|
+
flexDirection: 'column',
|
|
115
|
+
alignItems: 'flex-start',
|
|
116
|
+
gap: vars.spacing.xs,
|
|
117
|
+
padding: vars.spacing.sm,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
globalStyle('.zylem-menu-button__compass-row, .zylem-menu-button__compass-square', {
|
|
121
|
+
display: 'grid',
|
|
122
|
+
gap: vars.spacing.xs,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
globalStyle('.zylem-menu-button__compass-row', {
|
|
126
|
+
gridTemplateColumns: 'repeat(2, auto)',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
globalStyle('.zylem-menu-button__compass-square', {
|
|
130
|
+
gridTemplateColumns: 'repeat(3, auto)',
|
|
131
|
+
});
|
|
@@ -22,6 +22,12 @@ export interface MenuButtonProps {
|
|
|
22
22
|
onAction?: (() => void) | undefined;
|
|
23
23
|
/** Renders the face in its selected state. */
|
|
24
24
|
selected?: boolean | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Checkbox-like toggle for the face. Electric blue while on. A split face
|
|
27
|
+
* exposes it as `aria-pressed`, so pass `false` for a toggle that is off and
|
|
28
|
+
* the face still announces itself.
|
|
29
|
+
*/
|
|
30
|
+
on?: boolean | undefined;
|
|
25
31
|
disabled?: boolean | undefined;
|
|
26
32
|
/** Tooltip text; defaults to `label`. Pass `null` for no tooltip. */
|
|
27
33
|
tooltip?: string | null | undefined;
|
|
@@ -108,6 +114,7 @@ export function MenuButton(props: MenuButtonProps) {
|
|
|
108
114
|
aria-label={props.label}
|
|
109
115
|
disabled={props.disabled}
|
|
110
116
|
data-selected={props.selected ? '' : undefined}
|
|
117
|
+
data-on={props.on ? '' : undefined}
|
|
111
118
|
>
|
|
112
119
|
{props.children}
|
|
113
120
|
{/*
|
|
@@ -129,8 +136,10 @@ export function MenuButton(props: MenuButtonProps) {
|
|
|
129
136
|
ref={layer.anchorRef}
|
|
130
137
|
class="zylem-toolbar-btn zylem-menu-button__face"
|
|
131
138
|
aria-label={props.label}
|
|
139
|
+
aria-pressed={props.on}
|
|
132
140
|
disabled={props.disabled}
|
|
133
141
|
data-selected={props.selected ? '' : undefined}
|
|
142
|
+
data-on={props.on ? '' : undefined}
|
|
134
143
|
onClick={() => props.onAction?.()}
|
|
135
144
|
>
|
|
136
145
|
{props.children}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NumberField as KNumberField } from '@kobalte/core';
|
|
2
2
|
import ChevronDown from 'lucide-solid/icons/chevron-down';
|
|
3
3
|
import ChevronUp from 'lucide-solid/icons/chevron-up';
|
|
4
|
-
import { Show } from 'solid-js';
|
|
4
|
+
import { createEffect, createSignal, on, onCleanup, Show } from 'solid-js';
|
|
5
5
|
|
|
6
6
|
export interface NumberFieldProps {
|
|
7
7
|
label?: string;
|
|
@@ -9,7 +9,15 @@ export interface NumberFieldProps {
|
|
|
9
9
|
'aria-label'?: string;
|
|
10
10
|
error?: string;
|
|
11
11
|
value?: number;
|
|
12
|
+
/** Fires on every edit. */
|
|
12
13
|
onChange?: (value: number) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Fires on blur, Enter, and a stepper press. While set, typing stays in the
|
|
16
|
+
* field and only this reports it, so a parent that snaps or rounds the value
|
|
17
|
+
* does not fight the typing. A draft also outlasts changes to `value`, and
|
|
18
|
+
* Escape drops it; after each commit the field shows `value` again.
|
|
19
|
+
*/
|
|
20
|
+
onCommit?: (value: number) => void;
|
|
13
21
|
minValue?: number;
|
|
14
22
|
maxValue?: number;
|
|
15
23
|
step?: number;
|
|
@@ -23,15 +31,91 @@ export interface NumberFieldProps {
|
|
|
23
31
|
* HyperGlass number field: inset glass input with square jewel steppers.
|
|
24
32
|
*/
|
|
25
33
|
export function NumberField(props: NumberFieldProps) {
|
|
34
|
+
// A stepper press blurs the input before the click lands. Armed on pointer
|
|
35
|
+
// down, so that blur does not commit the pre-step value, and the click commits
|
|
36
|
+
// the stepped one.
|
|
37
|
+
let stepper = false;
|
|
38
|
+
// Enter commits, and the blur that follows it must not commit again.
|
|
39
|
+
let committed = false;
|
|
40
|
+
|
|
41
|
+
// Kobalte rewrites the input whenever `rawValue` changes, so a value that
|
|
42
|
+
// moves every frame would overwrite the typing. A draft hands it `held`.
|
|
43
|
+
const [drafting, setDrafting] = createSignal(false);
|
|
44
|
+
let held: number | undefined;
|
|
45
|
+
// A committed draft stays up until `value` changes, so the field does not
|
|
46
|
+
// flash the old value while the owner's answer is on its way. One frame is
|
|
47
|
+
// the limit, for an owner that rounds the commit back to the same value.
|
|
48
|
+
let settling = false;
|
|
49
|
+
let settleFrame = 0;
|
|
50
|
+
|
|
51
|
+
const hold = (value: number | undefined) => {
|
|
52
|
+
if (drafting() || value === undefined) return;
|
|
53
|
+
held = value;
|
|
54
|
+
setDrafting(true);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const stopSettling = () => {
|
|
58
|
+
settling = false;
|
|
59
|
+
if (settleFrame) cancelAnimationFrame(settleFrame);
|
|
60
|
+
settleFrame = 0;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const release = () => {
|
|
64
|
+
stopSettling();
|
|
65
|
+
setDrafting(false);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
createEffect(
|
|
69
|
+
on(
|
|
70
|
+
() => props.value,
|
|
71
|
+
() => {
|
|
72
|
+
if (settling) release();
|
|
73
|
+
},
|
|
74
|
+
{ defer: true },
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
onCleanup(stopSettling);
|
|
78
|
+
|
|
79
|
+
const commit = (value: number) => {
|
|
80
|
+
if (!props.onCommit) return;
|
|
81
|
+
if (Number.isNaN(value) || value === props.value) {
|
|
82
|
+
release();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// A stepper commit has no draft yet. Held at the stepped value, the field
|
|
86
|
+
// keeps showing it rather than the value it stepped from.
|
|
87
|
+
hold(value);
|
|
88
|
+
settling = true;
|
|
89
|
+
props.onCommit(value);
|
|
90
|
+
committed = true;
|
|
91
|
+
if (settling) settleFrame = requestAnimationFrame(release);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const commitInput = (input: HTMLInputElement) => {
|
|
95
|
+
const parsed = Number.parseFloat(input.value);
|
|
96
|
+
commit(Number.isFinite(parsed) ? parsed : Number.NaN);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const armStepper = () => {
|
|
100
|
+
stepper = true;
|
|
101
|
+
};
|
|
102
|
+
|
|
26
103
|
return (
|
|
27
104
|
<KNumberField.Root
|
|
28
105
|
class={props.class ? `zylem-field-root ${props.class}` : 'zylem-field-root'}
|
|
29
|
-
rawValue={props.value}
|
|
106
|
+
rawValue={drafting() ? held : props.value}
|
|
30
107
|
// Kobalte reports the parsed value on mount and after each format pass;
|
|
31
108
|
// only real edits reach the owner, so a controlled form is not marked
|
|
32
109
|
// dirty just by rendering.
|
|
33
110
|
onRawValueChange={(value) => {
|
|
34
|
-
if (
|
|
111
|
+
if (Number.isNaN(value)) return;
|
|
112
|
+
if (props.onCommit) {
|
|
113
|
+
if (!stepper) return;
|
|
114
|
+
stepper = false;
|
|
115
|
+
commit(value);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!props.onChange || value === props.value) return;
|
|
35
119
|
props.onChange(value);
|
|
36
120
|
}}
|
|
37
121
|
minValue={props.minValue}
|
|
@@ -51,16 +135,45 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
51
135
|
class="zylem-field zylem-numberfield-input"
|
|
52
136
|
aria-label={props['aria-label']}
|
|
53
137
|
placeholder={props.placeholder}
|
|
138
|
+
onInput={() => {
|
|
139
|
+
committed = false;
|
|
140
|
+
if (!props.onCommit) return;
|
|
141
|
+
// Typing again while a commit settles carries on with the draft, so
|
|
142
|
+
// the value that lands does not overwrite the new keystrokes.
|
|
143
|
+
stopSettling();
|
|
144
|
+
hold(props.value);
|
|
145
|
+
}}
|
|
146
|
+
onBlur={(event) => {
|
|
147
|
+
if (!props.onCommit || stepper || committed) {
|
|
148
|
+
committed = false;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
commitInput(event.currentTarget);
|
|
152
|
+
}}
|
|
153
|
+
onKeyDown={(event) => {
|
|
154
|
+
if (!props.onCommit || event.defaultPrevented) return;
|
|
155
|
+
if (event.key === 'Enter') commitInput(event.currentTarget);
|
|
156
|
+
else if (event.key === 'Escape' && drafting()) {
|
|
157
|
+
// Claimed, so an accordion or dialog around the field leaves this
|
|
158
|
+
// Escape alone rather than collapsing or closing as well.
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
release();
|
|
161
|
+
}
|
|
162
|
+
}}
|
|
54
163
|
/>
|
|
55
164
|
<KNumberField.IncrementTrigger
|
|
56
165
|
class="zylem-numberfield-stepper zylem-numberfield-stepper--up"
|
|
57
166
|
aria-label="Increment"
|
|
167
|
+
onPointerDown={armStepper}
|
|
168
|
+
onClick={armStepper}
|
|
58
169
|
>
|
|
59
170
|
<ChevronUp aria-hidden="true" />
|
|
60
171
|
</KNumberField.IncrementTrigger>
|
|
61
172
|
<KNumberField.DecrementTrigger
|
|
62
173
|
class="zylem-numberfield-stepper zylem-numberfield-stepper--down"
|
|
63
174
|
aria-label="Decrement"
|
|
175
|
+
onPointerDown={armStepper}
|
|
176
|
+
onClick={armStepper}
|
|
64
177
|
>
|
|
65
178
|
<ChevronDown aria-hidden="true" />
|
|
66
179
|
</KNumberField.DecrementTrigger>
|
|
@@ -102,9 +102,18 @@ export function PropertyRow(props: PropertyRowProps) {
|
|
|
102
102
|
<Show
|
|
103
103
|
when={display().isClickable}
|
|
104
104
|
fallback={
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// A div when the value is elements. A NumberField root is a div, and a
|
|
106
|
+
// span would close before it, dropping the fields out of this cell.
|
|
107
|
+
<Show
|
|
108
|
+
when={props.children !== undefined}
|
|
109
|
+
fallback={
|
|
110
|
+
<span class={`zylem-property-value ${props.valueClass ?? ''}`}>
|
|
111
|
+
{display().element}
|
|
112
|
+
</span>
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
<div class={`zylem-property-value ${props.valueClass ?? ''}`}>{props.children}</div>
|
|
116
|
+
</Show>
|
|
108
117
|
}
|
|
109
118
|
>
|
|
110
119
|
<button
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Position of `t` along a track, as a 0–100 percentage clamped to the
|
|
3
|
-
* track's range. A degenerate range
|
|
3
|
+
* track's range. A degenerate range, where max is less than or equal to min,
|
|
4
|
+
* pins everything to 0.
|
|
4
5
|
*/
|
|
5
6
|
export function markerPercent(t: number, min: number, max: number): number {
|
|
6
7
|
if (!Number.isFinite(t) || max <= min) {
|
|
@@ -34,6 +34,7 @@ export function TitleBar(props: TitleBarProps) {
|
|
|
34
34
|
'closeLabel',
|
|
35
35
|
'closeTestId',
|
|
36
36
|
'dock',
|
|
37
|
+
'dockOptions',
|
|
37
38
|
'onDock',
|
|
38
39
|
'class',
|
|
39
40
|
]);
|
|
@@ -63,6 +64,7 @@ export function TitleBar(props: TitleBarProps) {
|
|
|
63
64
|
closeLabel={local.closeLabel}
|
|
64
65
|
closeTestId={local.closeTestId}
|
|
65
66
|
dock={local.dock}
|
|
67
|
+
dockOptions={local.dockOptions}
|
|
66
68
|
onDock={local.onDock}
|
|
67
69
|
/>
|
|
68
70
|
</div>
|
|
@@ -21,7 +21,7 @@ export type ArtTool =
|
|
|
21
21
|
| 'eyedropper'
|
|
22
22
|
| 'move';
|
|
23
23
|
|
|
24
|
-
type IconComponent = Component<JSX.SvgSVGAttributes<SVGSVGElement>>;
|
|
24
|
+
export type IconComponent = Component<JSX.SvgSVGAttributes<SVGSVGElement>>;
|
|
25
25
|
|
|
26
26
|
export const ART_TOOL_ICONS: Record<ArtTool, IconComponent> = {
|
|
27
27
|
pencil: Pencil,
|
|
@@ -81,6 +81,7 @@ export function ToolButton(props: ToolButtonProps) {
|
|
|
81
81
|
return (
|
|
82
82
|
<ToolbarButton
|
|
83
83
|
label={label()}
|
|
84
|
+
selected={props.selected}
|
|
84
85
|
pressed={props.selected ?? false}
|
|
85
86
|
disabled={props.disabled}
|
|
86
87
|
onClick={props.onClick}
|
|
@@ -4,7 +4,8 @@ import { vars } from '../../theme.css';
|
|
|
4
4
|
/**
|
|
5
5
|
* Square glass icon button for toolbars — owns the `.zylem-toolbar-btn`
|
|
6
6
|
* class the editor already uses. Selected state uses the live-green jewel
|
|
7
|
-
* treatment.
|
|
7
|
+
* treatment. `data-danger` is that same selected state in the danger jewel,
|
|
8
|
+
* used by the Delete tool.
|
|
8
9
|
*/
|
|
9
10
|
globalStyle('.zylem-toolbar-btn', {
|
|
10
11
|
boxSizing: 'border-box',
|
|
@@ -48,12 +49,37 @@ globalStyle('.zylem-toolbar-btn:focus-visible', {
|
|
|
48
49
|
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.focusRing}`,
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
globalStyle('.zylem-toolbar-btn[data-on]', {
|
|
53
|
+
borderColor: 'rgba(46, 231, 255, 0.9)',
|
|
54
|
+
background: vars.material.buttonGlassOn,
|
|
55
|
+
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowOn}`,
|
|
56
|
+
color: '#F3FDFF',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
globalStyle('.zylem-toolbar-btn[data-on]:hover', {
|
|
60
|
+
background: vars.material.buttonGlassOn,
|
|
61
|
+
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowOn}`,
|
|
62
|
+
filter: 'brightness(1.08)',
|
|
63
|
+
});
|
|
64
|
+
|
|
51
65
|
globalStyle('.zylem-toolbar-btn[data-selected], .zylem-toolbar-btn.selected', {
|
|
52
66
|
borderColor: 'rgba(20, 255, 60, 0.78)',
|
|
53
67
|
background: vars.material.buttonGlassActive,
|
|
54
68
|
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowActive}`,
|
|
55
69
|
});
|
|
56
70
|
|
|
71
|
+
globalStyle('.zylem-toolbar-btn[data-danger]', {
|
|
72
|
+
borderColor: 'rgba(255, 132, 114, 0.78)',
|
|
73
|
+
background: vars.material.buttonGlassDanger,
|
|
74
|
+
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowDanger}`,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
globalStyle('.zylem-toolbar-btn[data-danger]:hover', {
|
|
78
|
+
background: vars.material.buttonGlassDanger,
|
|
79
|
+
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowDanger}`,
|
|
80
|
+
filter: 'brightness(1.1)',
|
|
81
|
+
});
|
|
82
|
+
|
|
57
83
|
globalStyle('.zylem-toolbar-btn[disabled]', {
|
|
58
84
|
opacity: 0.45,
|
|
59
85
|
cursor: 'not-allowed',
|
|
@@ -7,10 +7,24 @@ export interface ToolbarButtonProps {
|
|
|
7
7
|
onClick?: (event: MouseEvent) => void;
|
|
8
8
|
/**
|
|
9
9
|
* The selected look alone, for a button whose label already says its state
|
|
10
|
-
* (Play / Pause). For a toggle with a constant label, use `
|
|
10
|
+
* (Play / Pause). Live green. For a toggle with a constant label, use `on`.
|
|
11
|
+
* With `danger`, the same state is the danger jewel instead.
|
|
11
12
|
*/
|
|
12
13
|
selected?: boolean;
|
|
13
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Draw `selected` as the danger jewel instead of live green. The button stays
|
|
16
|
+
* ordinary glass until it is selected. Only Delete uses this.
|
|
17
|
+
*/
|
|
18
|
+
danger?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Checkbox-like toggle. Electric blue while on, and exposed as `aria-pressed`.
|
|
21
|
+
* Pass `false` for a toggle that is off so the button still announces itself.
|
|
22
|
+
*/
|
|
23
|
+
on?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Toggle drawn as `on`, unless `selected` is also set, which keeps the
|
|
26
|
+
* live-green jewel. Prefer `on` for new toggles.
|
|
27
|
+
*/
|
|
14
28
|
pressed?: boolean;
|
|
15
29
|
disabled?: boolean;
|
|
16
30
|
children: JSX.Element;
|
|
@@ -28,7 +42,8 @@ function insidePopup(element: Element | undefined): boolean {
|
|
|
28
42
|
|
|
29
43
|
/**
|
|
30
44
|
* HyperGlass toolbar button: square glass icon button with a tooltip.
|
|
31
|
-
*
|
|
45
|
+
* `selected` is the live-green jewel. `danger` swaps that jewel for the
|
|
46
|
+
* danger one while selected. `on` is the electric-blue toggle.
|
|
32
47
|
*
|
|
33
48
|
* The tooltip portals into the layer container for its own tree, so it stays
|
|
34
49
|
* styled when the toolbar lives inside a web component's shadow root.
|
|
@@ -58,7 +73,7 @@ export function ToolbarButton(props: ToolbarButtonProps) {
|
|
|
58
73
|
>
|
|
59
74
|
<KButton.Root
|
|
60
75
|
aria-label={props.label}
|
|
61
|
-
aria-pressed={props.pressed}
|
|
76
|
+
aria-pressed={props.on !== undefined ? props.on : props.pressed}
|
|
62
77
|
onClick={props.onClick}
|
|
63
78
|
disabled={props.disabled}
|
|
64
79
|
class={
|
|
@@ -66,7 +81,9 @@ export function ToolbarButton(props: ToolbarButtonProps) {
|
|
|
66
81
|
? `zylem-toolbar-btn ${props.class}`
|
|
67
82
|
: 'zylem-toolbar-btn'
|
|
68
83
|
}
|
|
69
|
-
data-selected={props.selected
|
|
84
|
+
data-selected={props.selected && !props.danger ? '' : undefined}
|
|
85
|
+
data-danger={props.selected && props.danger ? '' : undefined}
|
|
86
|
+
data-on={props.on || (props.pressed && !props.selected) ? '' : undefined}
|
|
70
87
|
>
|
|
71
88
|
{props.children}
|
|
72
89
|
</KButton.Root>
|
|
@@ -15,6 +15,13 @@ export interface ToolbarDropdownMenuProps {
|
|
|
15
15
|
items: readonly ToolbarDropdownItem[];
|
|
16
16
|
value: string;
|
|
17
17
|
onChange: (id: string) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Draws the face as an on/off toggle instead of always selected, for a picker
|
|
20
|
+
* where one choice means off, such as the game camera among editor cameras.
|
|
21
|
+
*/
|
|
22
|
+
on?: boolean | undefined;
|
|
23
|
+
/** Splits the button: the face fires this and the chevron opens the menu. */
|
|
24
|
+
onAction?: (() => void) | undefined;
|
|
18
25
|
/** Where the menu opens. Defaults to dropping down from the face. */
|
|
19
26
|
placement?: MenuButtonProps['placement'];
|
|
20
27
|
}
|
|
@@ -33,7 +40,9 @@ export function ToolbarDropdownMenu(props: ToolbarDropdownMenuProps) {
|
|
|
33
40
|
{(item) => (
|
|
34
41
|
<MenuButton
|
|
35
42
|
label={item.label}
|
|
36
|
-
selected
|
|
43
|
+
selected={props.on === undefined}
|
|
44
|
+
on={props.on}
|
|
45
|
+
onAction={props.onAction}
|
|
37
46
|
placement={props.placement ?? 'bottom-start'}
|
|
38
47
|
menu={(close) => (
|
|
39
48
|
<div class="zylem-menu-button__stack">
|
|
@@ -50,6 +50,19 @@ globalStyle('.zylem-toolbox-btn:focus-visible', {
|
|
|
50
50
|
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.focusRing}`,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
globalStyle('.zylem-toolbox-btn[data-on]', {
|
|
54
|
+
borderColor: 'rgba(46, 231, 255, 0.9)',
|
|
55
|
+
background: vars.material.buttonGlassOn,
|
|
56
|
+
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowOn}`,
|
|
57
|
+
color: '#F3FDFF',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
globalStyle('.zylem-toolbox-btn[data-on]:hover:not([disabled])', {
|
|
61
|
+
background: vars.material.buttonGlassOn,
|
|
62
|
+
boxShadow: `${vars.effects.shadowButton}, ${vars.effects.glowOn}`,
|
|
63
|
+
filter: 'brightness(1.08)',
|
|
64
|
+
});
|
|
65
|
+
|
|
53
66
|
globalStyle('.zylem-toolbox-btn[data-selected]', {
|
|
54
67
|
borderColor: 'rgba(20, 255, 60, 0.78)',
|
|
55
68
|
background: vars.material.buttonGlassActive,
|
|
@@ -11,8 +11,13 @@ export interface ToolboxButtonProps {
|
|
|
11
11
|
label: string;
|
|
12
12
|
/** Icon rendered above the label. */
|
|
13
13
|
children: JSX.Element;
|
|
14
|
-
/** Given at all, the tile
|
|
14
|
+
/** Live-green jewel. Given at all, the tile exposes this as `aria-pressed`. */
|
|
15
15
|
selected?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Checkbox-like toggle. Electric blue while on. Given at all, the tile
|
|
18
|
+
* exposes this as `aria-pressed` instead of `selected`.
|
|
19
|
+
*/
|
|
20
|
+
on?: boolean;
|
|
16
21
|
disabled?: boolean;
|
|
17
22
|
/** Overrides the size inherited from the enclosing `ToolboxGrid`. */
|
|
18
23
|
size?: ToolboxSize;
|
|
@@ -30,7 +35,8 @@ export function ToolboxButton(props: ToolboxButtonProps) {
|
|
|
30
35
|
class={props.class ? `zylem-toolbox-btn ${props.class}` : 'zylem-toolbox-btn'}
|
|
31
36
|
data-size={props.size}
|
|
32
37
|
data-selected={props.selected ? '' : undefined}
|
|
33
|
-
|
|
38
|
+
data-on={props.on ? '' : undefined}
|
|
39
|
+
aria-pressed={props.on !== undefined ? props.on : props.selected}
|
|
34
40
|
disabled={props.disabled}
|
|
35
41
|
onClick={props.onClick}
|
|
36
42
|
>
|
|
@@ -5,10 +5,17 @@ import X from 'lucide-solid/icons/x';
|
|
|
5
5
|
import { Dock } from '../../icons';
|
|
6
6
|
import { DropdownMenu } from '../DropdownMenu/DropdownMenu';
|
|
7
7
|
|
|
8
|
-
/** Where a panel sits. `float` leaves it free of
|
|
9
|
-
export type DockPosition = 'top' | 'float' | 'bottom';
|
|
8
|
+
/** Where a panel sits. `float` leaves it free of a dock edge. */
|
|
9
|
+
export type DockPosition = 'left' | 'right' | 'top' | 'float' | 'bottom';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/** One row in the dock menu. */
|
|
12
|
+
export interface DockOption {
|
|
13
|
+
id: DockPosition;
|
|
14
|
+
label: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Top, Float, and Bottom: the menu a strip uses when it docks to those edges. */
|
|
18
|
+
const DEFAULT_DOCK_OPTIONS: readonly DockOption[] = [
|
|
12
19
|
{ id: 'top', label: 'Top' },
|
|
13
20
|
{ id: 'float', label: 'Float' },
|
|
14
21
|
{ id: 'bottom', label: 'Bottom' },
|
|
@@ -27,6 +34,11 @@ export interface WindowControlsProps {
|
|
|
27
34
|
closeTestId?: string | undefined;
|
|
28
35
|
/** Current dock, checked in the menu. Omit for none checked. */
|
|
29
36
|
dock?: DockPosition | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Rows in the dock menu. Defaults to Top, Float, and Bottom. Pass this when
|
|
39
|
+
* the window docks to other edges, such as Left and Right.
|
|
40
|
+
*/
|
|
41
|
+
dockOptions?: readonly DockOption[] | undefined;
|
|
30
42
|
/** Renders the dock control. Called with the position chosen from the menu. */
|
|
31
43
|
onDock?: ((position: DockPosition) => void) | undefined;
|
|
32
44
|
}
|
|
@@ -34,7 +46,8 @@ export interface WindowControlsProps {
|
|
|
34
46
|
/**
|
|
35
47
|
* Shared ghost window controls, used by floating and detached panels. Close
|
|
36
48
|
* takes the danger color only while hovered or focused. Dock opens a menu of
|
|
37
|
-
* where the panel sits
|
|
49
|
+
* where the panel sits: Top, Float, and Bottom, or `dockOptions` when the
|
|
50
|
+
* window uses other edges. Only controls with a handler are rendered.
|
|
38
51
|
*/
|
|
39
52
|
export function WindowControls(props: WindowControlsProps) {
|
|
40
53
|
return (
|
|
@@ -46,7 +59,7 @@ export function WindowControls(props: WindowControlsProps) {
|
|
|
46
59
|
triggerClass="zylem-window-control zylem-window-control--dock"
|
|
47
60
|
placement="bottom-end"
|
|
48
61
|
trigger={<Dock size={11} aria-hidden="true" />}
|
|
49
|
-
items={
|
|
62
|
+
items={(props.dockOptions ?? DEFAULT_DOCK_OPTIONS).map((option) => ({
|
|
50
63
|
id: option.id,
|
|
51
64
|
label: option.label,
|
|
52
65
|
checked: props.dock === option.id,
|