@ambientcss/components 2.0.1 → 3.0.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.
- package/README.md +35 -0
- package/dist/index.cjs +1563 -384
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +763 -48
- package/dist/index.d.ts +763 -48
- package/dist/index.js +1524 -385
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1102 -256
- package/package.json +2 -2
- package/src/components/AmbientButton.tsx +25 -25
- package/src/components/AmbientFader.tsx +33 -115
- package/src/components/AmbientKnob.tsx +64 -222
- package/src/components/AmbientPanel.tsx +8 -2
- package/src/components/AmbientProvider.tsx +3 -0
- package/src/components/AmbientRack.tsx +32 -0
- package/src/components/AmbientSelect.tsx +40 -0
- package/src/components/AmbientSlider.tsx +32 -113
- package/src/components/AmbientSwitch.tsx +58 -58
- package/src/controls/AmbientBank.tsx +138 -0
- package/src/controls/AmbientLatch.tsx +78 -0
- package/src/controls/AmbientPress.tsx +74 -0
- package/src/controls/AmbientRotary.tsx +94 -0
- package/src/controls/AmbientTravel.tsx +83 -0
- package/src/core/context.tsx +46 -0
- package/src/core/controllable.ts +33 -0
- package/src/core/dev.ts +15 -0
- package/src/core/frames.tsx +63 -0
- package/src/core/kit.tsx +107 -0
- package/src/core/material.ts +27 -0
- package/src/core/numeric.ts +88 -0
- package/src/core/types.ts +116 -0
- package/src/core/useBank.ts +163 -0
- package/src/core/useLatch.ts +54 -0
- package/src/core/usePress.ts +120 -0
- package/src/core/useRotary.ts +253 -0
- package/src/core/useTravel.ts +141 -0
- package/src/index.ts +127 -4
- package/src/kits/console.tsx +80 -0
- package/src/kits/grounded.tsx +113 -0
- package/src/parts/bank.tsx +32 -0
- package/src/parts/console.tsx +99 -0
- package/src/parts/knob.tsx +203 -0
- package/src/parts/latch.tsx +33 -0
- package/src/parts/press.tsx +56 -0
- package/src/parts/travel.tsx +70 -0
- package/src/styles.css +1102 -256
|
@@ -1,123 +1,42 @@
|
|
|
1
|
-
import { useId, useRef } from "react";
|
|
2
|
-
import type { HTMLAttributes, KeyboardEvent, PointerEvent } from "react";
|
|
3
1
|
import { cn } from "../lib/cn";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
import { AmbientTravel } from "../controls/AmbientTravel";
|
|
3
|
+
import type { AmbientTravelProps } from "../controls/AmbientTravel";
|
|
4
|
+
import { useDress } from "../core/kit";
|
|
5
|
+
import type { KitLook } from "../core/kit";
|
|
6
|
+
import { groundedKit } from "../kits/grounded";
|
|
7
|
+
import type { AmbientMaterial } from "../core/material";
|
|
8
|
+
|
|
9
|
+
export type AmbientSliderSize = "sm" | "md" | "lg";
|
|
10
|
+
|
|
11
|
+
export type AmbientSliderProps = Omit<AmbientTravelProps, "parts" | "size" | "orientation"> & {
|
|
12
|
+
material?: AmbientMaterial | undefined;
|
|
13
|
+
size?: AmbientSliderSize | undefined;
|
|
14
|
+
/** Look options in the active kit's vocabulary. */
|
|
15
|
+
look?: KitLook | undefined;
|
|
13
16
|
};
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
return Math.min(max, Math.max(min, value));
|
|
17
|
-
}
|
|
18
|
-
|
|
18
|
+
/** A domed disc gliding over a shallow concave channel. */
|
|
19
19
|
export function AmbientSlider({
|
|
20
|
-
value,
|
|
21
|
-
min = 0,
|
|
22
|
-
max = 100,
|
|
23
|
-
step = 1,
|
|
24
|
-
label,
|
|
25
20
|
material,
|
|
26
|
-
|
|
21
|
+
look,
|
|
22
|
+
size = "md",
|
|
23
|
+
animate,
|
|
27
24
|
className,
|
|
28
|
-
...
|
|
25
|
+
...rest
|
|
29
26
|
}: AmbientSliderProps) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const updateFromClientX = (clientX: number) => {
|
|
37
|
-
const track = trackRef.current;
|
|
38
|
-
if (!track) return;
|
|
39
|
-
|
|
40
|
-
const rect = track.getBoundingClientRect();
|
|
41
|
-
const ratio = (clientX - rect.left) / rect.width;
|
|
42
|
-
const nextValue = min + clamp(ratio, 0, 1) * (max - min);
|
|
43
|
-
const snapped = Math.round(nextValue / safeStep) * safeStep;
|
|
44
|
-
onChange?.(clamp(snapped, min, max));
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const setValue = (nextValue: number) => {
|
|
48
|
-
const snapped = Math.round(nextValue / safeStep) * safeStep;
|
|
49
|
-
onChange?.(clamp(snapped, min, max));
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
53
|
-
const pageStep = safeStep * 10;
|
|
54
|
-
switch (event.key) {
|
|
55
|
-
case "ArrowRight":
|
|
56
|
-
case "ArrowUp":
|
|
57
|
-
event.preventDefault();
|
|
58
|
-
setValue(value + safeStep);
|
|
59
|
-
break;
|
|
60
|
-
case "ArrowLeft":
|
|
61
|
-
case "ArrowDown":
|
|
62
|
-
event.preventDefault();
|
|
63
|
-
setValue(value - safeStep);
|
|
64
|
-
break;
|
|
65
|
-
case "PageUp":
|
|
66
|
-
event.preventDefault();
|
|
67
|
-
setValue(value + pageStep);
|
|
68
|
-
break;
|
|
69
|
-
case "PageDown":
|
|
70
|
-
event.preventDefault();
|
|
71
|
-
setValue(value - pageStep);
|
|
72
|
-
break;
|
|
73
|
-
case "Home":
|
|
74
|
-
event.preventDefault();
|
|
75
|
-
setValue(min);
|
|
76
|
-
break;
|
|
77
|
-
case "End":
|
|
78
|
-
event.preventDefault();
|
|
79
|
-
setValue(max);
|
|
80
|
-
break;
|
|
81
|
-
default:
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const onPointerMove = (event: PointerEvent<HTMLDivElement>) => {
|
|
87
|
-
if (event.buttons !== 1) return;
|
|
88
|
-
updateFromClientX(event.clientX);
|
|
89
|
-
};
|
|
90
|
-
|
|
27
|
+
const { dress, defaults } = useDress(
|
|
28
|
+
"travel",
|
|
29
|
+
{ material, orientation: "horizontal", ...look },
|
|
30
|
+
groundedKit.travel!
|
|
31
|
+
);
|
|
91
32
|
return (
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
aria-valuemax={max}
|
|
101
|
-
aria-valuenow={value}
|
|
102
|
-
aria-orientation="horizontal"
|
|
103
|
-
tabIndex={0}
|
|
104
|
-
onPointerDown={(event) => {
|
|
105
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
106
|
-
updateFromClientX(event.clientX);
|
|
107
|
-
}}
|
|
108
|
-
onPointerMove={onPointerMove}
|
|
109
|
-
onKeyDown={onKeyDown}
|
|
110
|
-
>
|
|
111
|
-
<div
|
|
112
|
-
className={cn("amb-slider-thumb ambient amb-fillet ambx-slider-thumb", material !== "glass" && "amb-surface-convex", material && `amb-mat-${material}`)}
|
|
113
|
-
style={{ left: `${percent}%` }}
|
|
114
|
-
/>
|
|
115
|
-
</div>
|
|
116
|
-
{label ? (
|
|
117
|
-
<span id={id} className="ambx-label">
|
|
118
|
-
{label}
|
|
119
|
-
</span>
|
|
120
|
-
) : null}
|
|
121
|
-
</div>
|
|
33
|
+
<AmbientTravel
|
|
34
|
+
{...rest}
|
|
35
|
+
orientation="horizontal"
|
|
36
|
+
size={size}
|
|
37
|
+
animate={animate ?? defaults?.animate}
|
|
38
|
+
className={cn(dress.className, className)}
|
|
39
|
+
parts={dress.parts}
|
|
40
|
+
/>
|
|
122
41
|
);
|
|
123
42
|
}
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import { useId
|
|
2
|
-
import type { ButtonHTMLAttributes } from "react";
|
|
1
|
+
import { useId } from "react";
|
|
3
2
|
import { cn } from "../lib/cn";
|
|
3
|
+
import { AmbientLatch } from "../controls/AmbientLatch";
|
|
4
|
+
import type { AmbientLatchProps } from "../controls/AmbientLatch";
|
|
5
|
+
import { useControllableValue } from "../core/controllable";
|
|
6
|
+
import { useDress } from "../core/kit";
|
|
7
|
+
import type { KitLook } from "../core/kit";
|
|
8
|
+
import { groundedKit } from "../kits/grounded";
|
|
9
|
+
import { Led } from "../parts/latch";
|
|
4
10
|
|
|
5
11
|
export type AmbientSwitchSize = "sm" | "md" | "lg";
|
|
6
12
|
|
|
7
|
-
export type AmbientSwitchProps = Omit<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
led?: boolean | string;
|
|
13
|
+
export type AmbientSwitchProps = Omit<AmbientLatchProps, "parts" | "size"> & {
|
|
14
|
+
size?: AmbientSwitchSize | undefined;
|
|
15
|
+
/** A lamp above the switch. `true` for the scene's own colour, or any
|
|
16
|
+
* CSS colour string. */
|
|
17
|
+
led?: boolean | string | undefined;
|
|
18
|
+
/** Look options in the active kit's vocabulary. */
|
|
19
|
+
look?: KitLook | undefined;
|
|
15
20
|
};
|
|
16
21
|
|
|
22
|
+
/** A pill sliding in a dark stadium recess, with an optional lamp above.
|
|
23
|
+
*
|
|
24
|
+
* The lamp is mounted beside the switch rather than inside it: the latch
|
|
25
|
+
* control IS the track, so anything that is neither track nor pill belongs
|
|
26
|
+
* to whatever composes them. That means the preset has to hold the state —
|
|
27
|
+
* the lamp and the pill read the same boolean, and only their common
|
|
28
|
+
* parent can see both.
|
|
29
|
+
*/
|
|
17
30
|
export function AmbientSwitch({
|
|
18
|
-
className,
|
|
19
|
-
checked,
|
|
20
|
-
defaultChecked,
|
|
21
|
-
onCheckedChange,
|
|
22
|
-
onClick,
|
|
23
31
|
size = "md",
|
|
24
|
-
label,
|
|
25
32
|
led,
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
label,
|
|
34
|
+
look,
|
|
35
|
+
value,
|
|
36
|
+
defaultValue,
|
|
37
|
+
onChange,
|
|
38
|
+
animate,
|
|
39
|
+
className,
|
|
40
|
+
...rest
|
|
28
41
|
}: AmbientSwitchProps) {
|
|
29
42
|
const labelId = useId();
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const active = isControlled ? (checked ?? false) : internalChecked;
|
|
43
|
+
const [on, setOn] = useControllableValue(value, defaultValue ?? false, onChange);
|
|
44
|
+
const { dress, defaults } = useDress("latch", { ...look }, groundedKit.latch!);
|
|
33
45
|
|
|
34
|
-
const
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
className
|
|
46
|
-
)}
|
|
47
|
-
onClick={(event) => {
|
|
48
|
-
const next = !active;
|
|
49
|
-
if (!isControlled) setInternalChecked(next);
|
|
50
|
-
onCheckedChange?.(next);
|
|
51
|
-
onClick?.(event);
|
|
52
|
-
}}
|
|
53
|
-
{...props}
|
|
54
|
-
>
|
|
55
|
-
{led ? (
|
|
56
|
-
<span
|
|
57
|
-
className={cn("amb-led", !active && "amb-led-off")}
|
|
58
|
-
style={typeof led === "string" ? { "--amb-led-color": led } as React.CSSProperties : undefined}
|
|
59
|
-
/>
|
|
60
|
-
) : null}
|
|
61
|
-
<span className="amb-switch-track amb-groove">
|
|
62
|
-
<span className="amb-switch-pill ambient amb-fillet amb-surface-convex" />
|
|
63
|
-
</span>
|
|
64
|
-
{children}
|
|
65
|
-
</button>
|
|
46
|
+
const control = (
|
|
47
|
+
<AmbientLatch
|
|
48
|
+
{...rest}
|
|
49
|
+
value={on}
|
|
50
|
+
onChange={setOn}
|
|
51
|
+
size={size}
|
|
52
|
+
animate={animate ?? defaults?.animate}
|
|
53
|
+
aria-labelledby={label ? labelId : rest["aria-labelledby"]}
|
|
54
|
+
className={cn(dress.className, className)}
|
|
55
|
+
parts={dress.parts}
|
|
56
|
+
/>
|
|
66
57
|
);
|
|
67
58
|
|
|
68
|
-
if (!label) return
|
|
59
|
+
if (!led && !label) return control;
|
|
69
60
|
|
|
70
61
|
return (
|
|
71
62
|
<div className="ambx-stack">
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
{led ? (
|
|
64
|
+
<span className="ambx-switch-mount">
|
|
65
|
+
<Led on={on} {...(typeof led === "string" ? { color: led } : null)} />
|
|
66
|
+
{control}
|
|
67
|
+
</span>
|
|
68
|
+
) : (
|
|
69
|
+
control
|
|
70
|
+
)}
|
|
71
|
+
{label ? (
|
|
72
|
+
<span id={labelId} className="ambx-label">
|
|
73
|
+
{label}
|
|
74
|
+
</span>
|
|
75
|
+
) : null}
|
|
76
76
|
</div>
|
|
77
77
|
);
|
|
78
78
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { BankKeyProvider, ControlStateProvider } from "../core/context";
|
|
5
|
+
import { Frames } from "../core/frames";
|
|
6
|
+
import { sizeProps, stateData, stateStyle } from "../core/types";
|
|
7
|
+
import type { ControlParts, ControlSize, ControlState } from "../core/types";
|
|
8
|
+
import { useBank } from "../core/useBank";
|
|
9
|
+
import type { BankOption, UseBankOptions } from "../core/useBank";
|
|
10
|
+
|
|
11
|
+
export type AmbientBankProps = Omit<
|
|
12
|
+
HTMLAttributes<HTMLDivElement>,
|
|
13
|
+
"onChange" | "defaultValue"
|
|
14
|
+
> &
|
|
15
|
+
UseBankOptions & {
|
|
16
|
+
/** Parts applied to every key. The bank's own frames go in `parts`. */
|
|
17
|
+
keyParts?: ControlParts | undefined;
|
|
18
|
+
parts?: ControlParts | undefined;
|
|
19
|
+
size?: ControlSize | undefined;
|
|
20
|
+
/** Lamp colour. Defaults to the scene's `--amb-highlight-color`. */
|
|
21
|
+
color?: string | undefined;
|
|
22
|
+
label?: ReactNode | undefined;
|
|
23
|
+
/** Per-key override, for a bank whose keys are not interchangeable. */
|
|
24
|
+
renderKey?: ((option: BankOption, on: boolean) => ReactNode) | undefined;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function keyState(on: boolean, disabled: boolean): ControlState {
|
|
28
|
+
return {
|
|
29
|
+
value: on ? 1 : 0,
|
|
30
|
+
min: 0,
|
|
31
|
+
max: 1,
|
|
32
|
+
percent: on ? 1 : 0,
|
|
33
|
+
angle: 0,
|
|
34
|
+
travelStart: 0,
|
|
35
|
+
travelSweep: 0,
|
|
36
|
+
detents: 2,
|
|
37
|
+
dragging: false,
|
|
38
|
+
disabled,
|
|
39
|
+
atMin: !on,
|
|
40
|
+
atMax: on
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** N keys sharing a selection model, with no appearance of its own.
|
|
45
|
+
*
|
|
46
|
+
* Parts apply per key rather than once, because that is what a bank is.
|
|
47
|
+
* The focusable element is the key `<button>`, which this mechanism
|
|
48
|
+
* renders — your parts go inside it, which is why the presentational-parts
|
|
49
|
+
* rule still holds here. */
|
|
50
|
+
export function AmbientBank({
|
|
51
|
+
options,
|
|
52
|
+
keyParts,
|
|
53
|
+
parts,
|
|
54
|
+
size,
|
|
55
|
+
color,
|
|
56
|
+
label,
|
|
57
|
+
renderKey,
|
|
58
|
+
className,
|
|
59
|
+
style,
|
|
60
|
+
value,
|
|
61
|
+
defaultValue,
|
|
62
|
+
onChange,
|
|
63
|
+
multiple,
|
|
64
|
+
orientation = "vertical",
|
|
65
|
+
disabled = false,
|
|
66
|
+
...rest
|
|
67
|
+
}: AmbientBankProps) {
|
|
68
|
+
const labelId = useId();
|
|
69
|
+
const { selected, rootProps, keyProps } = useBank({
|
|
70
|
+
options,
|
|
71
|
+
value,
|
|
72
|
+
defaultValue,
|
|
73
|
+
onChange,
|
|
74
|
+
multiple,
|
|
75
|
+
orientation,
|
|
76
|
+
disabled
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const sized = sizeProps("bank", size);
|
|
80
|
+
|
|
81
|
+
const bank = (
|
|
82
|
+
<div
|
|
83
|
+
{...rest}
|
|
84
|
+
{...rootProps}
|
|
85
|
+
aria-labelledby={label ? labelId : rest["aria-labelledby"]}
|
|
86
|
+
className={cn(
|
|
87
|
+
"ambx-control ambx-bank",
|
|
88
|
+
`ambx-bank-${orientation}`,
|
|
89
|
+
sized.className,
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
style={{
|
|
93
|
+
...(color ? ({ "--amb-led-color": color } as CSSProperties) : null),
|
|
94
|
+
...sized.style,
|
|
95
|
+
...style
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<Frames parts={parts} />
|
|
99
|
+
{options.map((option, index) => {
|
|
100
|
+
const on = selected.includes(option.value);
|
|
101
|
+
/* Each key publishes its own state channel, exactly as a standalone
|
|
102
|
+
control does. Without this a key's parts would inherit the bank
|
|
103
|
+
root's neutral --ambx-percent and a pure-CSS part could never see
|
|
104
|
+
that its lamp is lit — the one family where "the properties are
|
|
105
|
+
canonical" would quietly not be true. */
|
|
106
|
+
const ks = keyState(on, option.disabled || disabled);
|
|
107
|
+
return (
|
|
108
|
+
<button
|
|
109
|
+
{...keyProps(option, index)}
|
|
110
|
+
{...stateData(ks)}
|
|
111
|
+
className="ambx-key"
|
|
112
|
+
style={{
|
|
113
|
+
...(stateStyle(ks) as CSSProperties),
|
|
114
|
+
...(option.color ? ({ "--amb-led-color": option.color } as CSSProperties) : null)
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
<ControlStateProvider value={ks}>
|
|
118
|
+
<BankKeyProvider value={{ option, on, index }}>
|
|
119
|
+
{renderKey ? renderKey(option, on) : <Frames parts={keyParts} />}
|
|
120
|
+
</BankKeyProvider>
|
|
121
|
+
</ControlStateProvider>
|
|
122
|
+
</button>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (!label) return bank;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="ambx-stack">
|
|
132
|
+
{bank}
|
|
133
|
+
<span id={labelId} className="ambx-label">
|
|
134
|
+
{label}
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useId, useRef } from "react";
|
|
2
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { ControlStateProvider } from "../core/context";
|
|
5
|
+
import { Frames, useDevPartCheck } from "../core/frames";
|
|
6
|
+
import { sizeProps } from "../core/types";
|
|
7
|
+
import type { ControlAnimate, ControlParts, ControlSize } from "../core/types";
|
|
8
|
+
import { useLatch } from "../core/useLatch";
|
|
9
|
+
import type { UseLatchOptions } from "../core/useLatch";
|
|
10
|
+
|
|
11
|
+
export type AmbientLatchProps = Omit<
|
|
12
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
13
|
+
"onChange" | "value" | "defaultValue" | "type"
|
|
14
|
+
> &
|
|
15
|
+
UseLatchOptions & {
|
|
16
|
+
parts?: ControlParts | undefined;
|
|
17
|
+
size?: ControlSize | undefined;
|
|
18
|
+
animate?: ControlAnimate | undefined;
|
|
19
|
+
label?: ReactNode | undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** A two-position slide, with no appearance of its own. The control IS the
|
|
23
|
+
* track: its actuator is a pill-sized frame that travels across it, which
|
|
24
|
+
* is why anything sitting beside a switch — a lamp, a legend — belongs to
|
|
25
|
+
* whatever composes it rather than to the switch. */
|
|
26
|
+
export function AmbientLatch({
|
|
27
|
+
parts,
|
|
28
|
+
size,
|
|
29
|
+
animate = "auto",
|
|
30
|
+
label,
|
|
31
|
+
className,
|
|
32
|
+
value,
|
|
33
|
+
defaultValue,
|
|
34
|
+
onChange,
|
|
35
|
+
disabled,
|
|
36
|
+
children,
|
|
37
|
+
...rest
|
|
38
|
+
}: AmbientLatchProps) {
|
|
39
|
+
const labelId = useId();
|
|
40
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
41
|
+
const { state, rootProps } = useLatch({ value, defaultValue, onChange, disabled });
|
|
42
|
+
useDevPartCheck(ref, "AmbientLatch");
|
|
43
|
+
|
|
44
|
+
const sized = sizeProps("latch", size);
|
|
45
|
+
const { style: restStyle, onClick, ...restProps } = rest;
|
|
46
|
+
|
|
47
|
+
const control = (
|
|
48
|
+
<button
|
|
49
|
+
{...restProps}
|
|
50
|
+
{...rootProps}
|
|
51
|
+
ref={ref}
|
|
52
|
+
aria-labelledby={label ? labelId : rest["aria-labelledby"]}
|
|
53
|
+
data-animate={animate}
|
|
54
|
+
className={cn("ambx-control ambx-latch", sized.className, className)}
|
|
55
|
+
style={{ ...rootProps.style, ...sized.style, ...restStyle }}
|
|
56
|
+
onClick={(event) => {
|
|
57
|
+
rootProps.onClick();
|
|
58
|
+
onClick?.(event);
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<ControlStateProvider value={state}>
|
|
62
|
+
<Frames parts={parts} />
|
|
63
|
+
{children}
|
|
64
|
+
</ControlStateProvider>
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!label) return control;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="ambx-stack">
|
|
72
|
+
{control}
|
|
73
|
+
<span id={labelId} className="ambx-label">
|
|
74
|
+
{label}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { ControlStateProvider } from "../core/context";
|
|
5
|
+
import { Frames, useDevPartCheck } from "../core/frames";
|
|
6
|
+
import { sizeProps } from "../core/types";
|
|
7
|
+
import type { ControlParts, ControlSize } from "../core/types";
|
|
8
|
+
import { usePress } from "../core/usePress";
|
|
9
|
+
import type { UsePressOptions } from "../core/usePress";
|
|
10
|
+
|
|
11
|
+
export type AmbientPressProps = Omit<
|
|
12
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
13
|
+
"onChange" | "value" | "defaultValue" | "type"
|
|
14
|
+
> &
|
|
15
|
+
UsePressOptions & {
|
|
16
|
+
parts?: ControlParts | undefined;
|
|
17
|
+
size?: ControlSize | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** A key that sinks under a finger, with no appearance of its own.
|
|
21
|
+
*
|
|
22
|
+
* Its frames are `display: contents` markers rather than boxes, because a
|
|
23
|
+
* button is sized by its cap: the width is `min-width` plus the legend.
|
|
24
|
+
* Wrapping the cap in a positioned frame would collapse the control. */
|
|
25
|
+
export function AmbientPress({
|
|
26
|
+
parts,
|
|
27
|
+
size,
|
|
28
|
+
className,
|
|
29
|
+
mode,
|
|
30
|
+
value,
|
|
31
|
+
defaultValue,
|
|
32
|
+
onChange,
|
|
33
|
+
onPress,
|
|
34
|
+
repeatDelay,
|
|
35
|
+
repeatInterval,
|
|
36
|
+
disabled,
|
|
37
|
+
children,
|
|
38
|
+
...rest
|
|
39
|
+
}: AmbientPressProps) {
|
|
40
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
41
|
+
const { state, rootProps } = usePress({
|
|
42
|
+
mode,
|
|
43
|
+
value,
|
|
44
|
+
defaultValue,
|
|
45
|
+
onChange,
|
|
46
|
+
onPress,
|
|
47
|
+
repeatDelay,
|
|
48
|
+
repeatInterval,
|
|
49
|
+
disabled
|
|
50
|
+
});
|
|
51
|
+
useDevPartCheck(ref, "AmbientPress");
|
|
52
|
+
|
|
53
|
+
const sized = sizeProps("press", size);
|
|
54
|
+
const { style: restStyle, onClick, ...restProps } = rest;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<button
|
|
58
|
+
{...restProps}
|
|
59
|
+
{...rootProps}
|
|
60
|
+
ref={ref}
|
|
61
|
+
className={cn("ambx-control ambx-press", sized.className, className)}
|
|
62
|
+
style={{ ...rootProps.style, ...sized.style, ...restStyle }}
|
|
63
|
+
onClick={(event) => {
|
|
64
|
+
rootProps.onClick();
|
|
65
|
+
onClick?.(event);
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<ControlStateProvider value={state}>
|
|
69
|
+
<Frames parts={parts} />
|
|
70
|
+
{children}
|
|
71
|
+
</ControlStateProvider>
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useId, useRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { ControlStateProvider } from "../core/context";
|
|
5
|
+
import { Frames, useDevPartCheck } from "../core/frames";
|
|
6
|
+
import { sizeProps } from "../core/types";
|
|
7
|
+
import type { ControlAnimate, ControlParts, ControlSize } from "../core/types";
|
|
8
|
+
import { useRotary } from "../core/useRotary";
|
|
9
|
+
import type { UseRotaryOptions } from "../core/useRotary";
|
|
10
|
+
|
|
11
|
+
export type AmbientRotaryProps = Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> &
|
|
12
|
+
UseRotaryOptions & {
|
|
13
|
+
parts?: ControlParts | undefined;
|
|
14
|
+
size?: ControlSize | undefined;
|
|
15
|
+
animate?: ControlAnimate | undefined;
|
|
16
|
+
label?: ReactNode | undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** A rotary mechanism with no appearance of its own.
|
|
20
|
+
*
|
|
21
|
+
* It owns the value, the three pointer mappings, the sweep, the keyboard
|
|
22
|
+
* contract and the ARIA; what it looks like is entirely the `parts` you
|
|
23
|
+
* give it. There is deliberately no `material` prop: which element a
|
|
24
|
+
* material belongs on is a fact about a particular knob's construction —
|
|
25
|
+
* on the clipped face when it is knurled, on the body when it is not —
|
|
26
|
+
* and a mechanism cannot know that once the body is yours. Presets carry
|
|
27
|
+
* `material`, because a preset knows its own parts. */
|
|
28
|
+
export function AmbientRotary({
|
|
29
|
+
parts,
|
|
30
|
+
size,
|
|
31
|
+
animate = "auto",
|
|
32
|
+
label,
|
|
33
|
+
className,
|
|
34
|
+
value,
|
|
35
|
+
defaultValue,
|
|
36
|
+
min,
|
|
37
|
+
max,
|
|
38
|
+
step,
|
|
39
|
+
detents,
|
|
40
|
+
travel,
|
|
41
|
+
input,
|
|
42
|
+
dragDistance,
|
|
43
|
+
wrap,
|
|
44
|
+
disabled,
|
|
45
|
+
onChange,
|
|
46
|
+
...rest
|
|
47
|
+
}: AmbientRotaryProps) {
|
|
48
|
+
const labelId = useId();
|
|
49
|
+
const stackRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
const { state, rootProps } = useRotary({
|
|
51
|
+
value,
|
|
52
|
+
defaultValue,
|
|
53
|
+
min,
|
|
54
|
+
max,
|
|
55
|
+
step,
|
|
56
|
+
detents,
|
|
57
|
+
travel,
|
|
58
|
+
input,
|
|
59
|
+
dragDistance,
|
|
60
|
+
wrap,
|
|
61
|
+
disabled,
|
|
62
|
+
onChange
|
|
63
|
+
});
|
|
64
|
+
useDevPartCheck(stackRef, "AmbientRotary");
|
|
65
|
+
|
|
66
|
+
const sized = sizeProps("rotary", size);
|
|
67
|
+
const { style: restStyle, ...restProps } = rest;
|
|
68
|
+
|
|
69
|
+
const control = (
|
|
70
|
+
<div
|
|
71
|
+
{...restProps}
|
|
72
|
+
{...rootProps}
|
|
73
|
+
aria-labelledby={label ? labelId : rest["aria-labelledby"]}
|
|
74
|
+
data-animate={animate}
|
|
75
|
+
className={cn("ambx-control ambx-rotary", sized.className, className)}
|
|
76
|
+
style={{ ...rootProps.style, ...sized.style, ...restStyle }}
|
|
77
|
+
>
|
|
78
|
+
<ControlStateProvider value={state}>
|
|
79
|
+
<Frames parts={parts} />
|
|
80
|
+
</ControlStateProvider>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="ambx-stack" ref={stackRef}>
|
|
86
|
+
{control}
|
|
87
|
+
{label ? (
|
|
88
|
+
<span id={labelId} className="ambx-label">
|
|
89
|
+
{label}
|
|
90
|
+
</span>
|
|
91
|
+
) : null}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|