@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ambientcss/components",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "React skeuomorphic components powered by Ambient CSS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@ambientcss/css": "
|
|
47
|
+
"@ambientcss/css": "3.0.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/react": "^19.0.8",
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import type { ButtonHTMLAttributes } from "react";
|
|
2
1
|
import { cn } from "../lib/cn";
|
|
2
|
+
import { AmbientPress } from "../controls/AmbientPress";
|
|
3
|
+
import type { AmbientPressProps } from "../controls/AmbientPress";
|
|
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";
|
|
3
8
|
|
|
4
9
|
/* Cap silhouettes from the referent lineup (ambient3d/generate.py):
|
|
5
10
|
- "pill": the default wide stadium key (transport-key style)
|
|
@@ -8,38 +13,33 @@ import { cn } from "../lib/cn";
|
|
|
8
13
|
- "square": a squarer, flatter pad (EP-133-style, tighter corners,
|
|
9
14
|
lower cap) */
|
|
10
15
|
export type AmbientButtonShape = "pill" | "round" | "square";
|
|
16
|
+
export type AmbientButtonSize = "sm" | "md" | "lg";
|
|
11
17
|
|
|
12
|
-
export
|
|
13
|
-
material?:
|
|
14
|
-
shape?: AmbientButtonShape;
|
|
15
|
-
|
|
18
|
+
export type AmbientButtonProps = Omit<AmbientPressProps, "parts" | "size"> & {
|
|
19
|
+
material?: AmbientMaterial | undefined;
|
|
20
|
+
shape?: AmbientButtonShape | undefined;
|
|
21
|
+
size?: AmbientButtonSize | undefined;
|
|
22
|
+
/** Look options in the active kit's vocabulary. */
|
|
23
|
+
look?: KitLook | undefined;
|
|
24
|
+
};
|
|
16
25
|
|
|
26
|
+
/** A key cap seated in a clearance well. */
|
|
17
27
|
export function AmbientButton({
|
|
18
28
|
className,
|
|
19
29
|
children,
|
|
30
|
+
look,
|
|
20
31
|
material = "matte",
|
|
21
32
|
shape = "pill",
|
|
22
|
-
|
|
33
|
+
size = "md",
|
|
34
|
+
...rest
|
|
23
35
|
}: AmbientButtonProps) {
|
|
36
|
+
const { dress } = useDress("press", { material, shape, children, ...look }, groundedKit.press!);
|
|
24
37
|
return (
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
className
|
|
32
|
-
)}
|
|
33
|
-
{...props}
|
|
34
|
-
>
|
|
35
|
-
<span
|
|
36
|
-
className={cn(
|
|
37
|
-
"amb-button-cap ambient amb-chamfer amb-surface amb-heading-3",
|
|
38
|
-
material === "matte" ? "amb-mat-matte" : material === "shiny" ? "amb-mat-shiny" : "amb-mat-glass"
|
|
39
|
-
)}
|
|
40
|
-
>
|
|
41
|
-
{children}
|
|
42
|
-
</span>
|
|
43
|
-
</button>
|
|
38
|
+
<AmbientPress
|
|
39
|
+
{...rest}
|
|
40
|
+
size={size}
|
|
41
|
+
className={cn(dress.className, className)}
|
|
42
|
+
parts={dress.parts}
|
|
43
|
+
/>
|
|
44
44
|
);
|
|
45
45
|
}
|
|
@@ -1,125 +1,43 @@
|
|
|
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 AmbientFaderSize = "sm" | "md" | "lg";
|
|
10
|
+
|
|
11
|
+
export type AmbientFaderProps = Omit<AmbientTravelProps, "parts" | "size" | "orientation"> & {
|
|
12
|
+
material?: AmbientMaterial | undefined;
|
|
13
|
+
size?: AmbientFaderSize | undefined;
|
|
14
|
+
/** Look options in the active kit's vocabulary. */
|
|
15
|
+
look?: KitLook | undefined;
|
|
13
16
|
};
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
18
|
+
/** A pill cap on a stem, riding an upright slot. Min is at the bottom,
|
|
19
|
+
* which `AmbientTravel` already does for a vertical track. */
|
|
19
20
|
export function AmbientFader({
|
|
20
|
-
value,
|
|
21
|
-
min = 0,
|
|
22
|
-
max = 100,
|
|
23
|
-
step = 1,
|
|
24
|
-
label,
|
|
25
21
|
material,
|
|
26
|
-
|
|
22
|
+
look,
|
|
23
|
+
size = "md",
|
|
24
|
+
animate,
|
|
27
25
|
className,
|
|
28
|
-
...
|
|
26
|
+
...rest
|
|
29
27
|
}: AmbientFaderProps) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const updateFromClientY = (clientY: number) => {
|
|
37
|
-
const track = trackRef.current;
|
|
38
|
-
if (!track) return;
|
|
39
|
-
|
|
40
|
-
const rect = track.getBoundingClientRect();
|
|
41
|
-
const ratio = 1 - (clientY - rect.top) / rect.height;
|
|
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 "ArrowUp":
|
|
56
|
-
case "ArrowRight":
|
|
57
|
-
event.preventDefault();
|
|
58
|
-
setValue(value + safeStep);
|
|
59
|
-
break;
|
|
60
|
-
case "ArrowDown":
|
|
61
|
-
case "ArrowLeft":
|
|
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
|
-
updateFromClientY(event.clientY);
|
|
89
|
-
};
|
|
90
|
-
|
|
28
|
+
const { dress, defaults } = useDress(
|
|
29
|
+
"travel",
|
|
30
|
+
{ material, orientation: "vertical", ...look },
|
|
31
|
+
groundedKit.travel!
|
|
32
|
+
);
|
|
91
33
|
return (
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
aria-valuemax={max}
|
|
101
|
-
aria-valuenow={value}
|
|
102
|
-
aria-orientation="vertical"
|
|
103
|
-
tabIndex={0}
|
|
104
|
-
onPointerDown={(event) => {
|
|
105
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
106
|
-
updateFromClientY(event.clientY);
|
|
107
|
-
}}
|
|
108
|
-
onPointerMove={onPointerMove}
|
|
109
|
-
onKeyDown={onKeyDown}
|
|
110
|
-
>
|
|
111
|
-
<div
|
|
112
|
-
className={cn("amb-fader-thumb ambient amb-fillet ambx-fader-thumb", material !== "glass" && "amb-surface-concave", material && `amb-mat-${material}`)}
|
|
113
|
-
style={{ top: `${100 - percent}%` }}
|
|
114
|
-
>
|
|
115
|
-
<span className="amb-fader-gripline" />
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
{label ? (
|
|
119
|
-
<span id={id} className="ambx-label">
|
|
120
|
-
{label}
|
|
121
|
-
</span>
|
|
122
|
-
) : null}
|
|
123
|
-
</div>
|
|
34
|
+
<AmbientTravel
|
|
35
|
+
{...rest}
|
|
36
|
+
orientation="vertical"
|
|
37
|
+
size={size}
|
|
38
|
+
animate={animate ?? defaults?.animate}
|
|
39
|
+
className={cn(dress.className, className)}
|
|
40
|
+
parts={dress.parts}
|
|
41
|
+
/>
|
|
124
42
|
);
|
|
125
43
|
}
|
|
@@ -1,233 +1,75 @@
|
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
2
|
+
import { AmbientRotary } from "../controls/AmbientRotary";
|
|
3
|
+
import type { AmbientRotaryProps } from "../controls/AmbientRotary";
|
|
4
|
+
import { useDress } from "../core/kit";
|
|
5
|
+
import type { KitLook } from "../core/kit";
|
|
6
|
+
import type { AmbientMaterial } from "../core/material";
|
|
7
|
+
import { groundedKit } from "../kits/grounded";
|
|
8
|
+
|
|
9
|
+
/** Printed scale dots on the panel around the knob: the arc's two ends, the
|
|
10
|
+
* full graduated ring, or nothing. */
|
|
11
|
+
export type AmbientKnobMarkers = "none" | "ends" | "full";
|
|
12
|
+
|
|
13
|
+
/** The pointer riding the rotating face: a radial bar or an offset dot. */
|
|
14
|
+
export type AmbientKnobIndicator = "rectangle" | "circle";
|
|
15
|
+
|
|
16
|
+
export type AmbientKnobSize = "sm" | "md" | "lg";
|
|
17
|
+
|
|
18
|
+
export type AmbientKnobProps = Omit<AmbientRotaryProps, "parts" | "size"> & {
|
|
19
|
+
material?: AmbientMaterial | undefined;
|
|
20
|
+
/** Ribbed grip around the rotating body. Off gives a smooth turned body. */
|
|
21
|
+
knurling?: boolean | undefined;
|
|
22
|
+
/** The grip ring's own colour, for a two-tone knob — any CSS colour, read
|
|
23
|
+
* as an albedo, so it still takes the scene's light rather than being
|
|
24
|
+
* painted flat. Unset, the ring is the same material as the cap. */
|
|
25
|
+
knurlColor?: string | undefined;
|
|
26
|
+
markers?: AmbientKnobMarkers | undefined;
|
|
27
|
+
indicator?: AmbientKnobIndicator | undefined;
|
|
28
|
+
size?: AmbientKnobSize | undefined;
|
|
29
|
+
/** Look options in another kit's vocabulary.
|
|
30
|
+
*
|
|
31
|
+
* The five props above are the *grounded* kit's words. A kit that dresses
|
|
32
|
+
* knobs differently has its own, and they arrive here rather than as
|
|
33
|
+
* loose props so the common path stays typed and a misspelt `knurling`
|
|
34
|
+
* is still a compile error. Kits usually ship a preset of their own that
|
|
35
|
+
* types this properly — see `ConsoleKnob`. */
|
|
36
|
+
look?: KitLook | undefined;
|
|
25
37
|
};
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for (let i = 0; i < teeth; i++) {
|
|
32
|
-
const a = i * pitch;
|
|
33
|
-
const tooth: Array<[number, number]> = [
|
|
34
|
-
[0, root],
|
|
35
|
-
[rise, outer],
|
|
36
|
-
[fall[0], outer],
|
|
37
|
-
[fall[1], root]
|
|
38
|
-
];
|
|
39
|
-
for (const [frac, radius] of tooth) {
|
|
40
|
-
const t = a + frac * pitch;
|
|
41
|
-
pts.push(
|
|
42
|
-
`${(0.5 + radius * Math.cos(t)).toFixed(4)} ${(0.5 + radius * Math.sin(t)).toFixed(4)}`
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return `M${pts.join(" L")} Z`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const KNURL_PATHS = {
|
|
50
|
-
standard: knurlPath(KNURLS.standard),
|
|
51
|
-
flute: knurlPath(KNURLS.flute),
|
|
52
|
-
fine: knurlPath(KNURLS.fine)
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/* Knob types from the referent lineup (ambient3d/generate.py):
|
|
56
|
-
- "dot": the grounded referent — 36-rib knurl, offset indicator dot
|
|
57
|
-
- "line": same body with a radial indicator line (classic pot)
|
|
58
|
-
- "flute": 14 broad flutes, centered dot (OP-Z-style encoder)
|
|
59
|
-
- "cap": fine knurl under a smooth accent top disc (OP-1-style)
|
|
60
|
-
- "wheel": bare fine knurl, no indicator (machined wheel — pair with
|
|
61
|
-
material="shiny") */
|
|
62
|
-
export type AmbientKnobVariant = "dot" | "line" | "flute" | "cap" | "wheel";
|
|
63
|
-
|
|
64
|
-
const VARIANT_FAMILY: Record<AmbientKnobVariant, keyof typeof KNURL_PATHS> = {
|
|
65
|
-
dot: "standard",
|
|
66
|
-
line: "standard",
|
|
67
|
-
flute: "flute",
|
|
68
|
-
cap: "fine",
|
|
69
|
-
wheel: "fine"
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export type AmbientKnobProps = Omit<HTMLAttributes<HTMLDivElement>, "onChange"> & {
|
|
73
|
-
value: number;
|
|
74
|
-
min?: number;
|
|
75
|
-
max?: number;
|
|
76
|
-
step?: number;
|
|
77
|
-
label?: string;
|
|
78
|
-
material?: "matte" | "shiny" | "glass";
|
|
79
|
-
variant?: AmbientKnobVariant;
|
|
80
|
-
onChange?: (nextValue: number) => void;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
function clamp(value: number, min: number, max: number): number {
|
|
84
|
-
return Math.min(max, Math.max(min, value));
|
|
85
|
-
}
|
|
86
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The rotary preset: `AmbientRotary` wearing whatever the active kit says a
|
|
41
|
+
* knob looks like, and the grounded hardware knob when no kit is set.
|
|
42
|
+
*/
|
|
87
43
|
export function AmbientKnob({
|
|
88
|
-
value,
|
|
89
|
-
min = 0,
|
|
90
|
-
max = 100,
|
|
91
|
-
step = 1,
|
|
92
|
-
label,
|
|
93
44
|
material,
|
|
94
|
-
|
|
95
|
-
|
|
45
|
+
knurling,
|
|
46
|
+
knurlColor,
|
|
47
|
+
markers,
|
|
48
|
+
indicator,
|
|
49
|
+
look,
|
|
50
|
+
size = "md",
|
|
96
51
|
className,
|
|
97
|
-
|
|
52
|
+
travel,
|
|
53
|
+
input,
|
|
54
|
+
animate,
|
|
55
|
+
...rest
|
|
98
56
|
}: AmbientKnobProps) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const family = VARIANT_FAMILY[variant];
|
|
105
|
-
|
|
106
|
-
const percent = (value - min) / (max - min || 1);
|
|
107
|
-
const rotation = percent * 270 - 135;
|
|
108
|
-
|
|
109
|
-
// Convert pointer position to a knob angle in [-180, 180] where 0 = up,
|
|
110
|
-
// positive = clockwise. The knob's active range is [-135, 135] (270°),
|
|
111
|
-
// with the dead zone at the bottom (~135° to ~225° i.e. past ±135°).
|
|
112
|
-
const pointerToValue = (event: PointerEvent<HTMLDivElement>) => {
|
|
113
|
-
const rect = knobRef.current!.getBoundingClientRect();
|
|
114
|
-
const cx = rect.left + rect.width / 2;
|
|
115
|
-
const cy = rect.top + rect.height / 2;
|
|
116
|
-
// atan2 with screen coords: 0=right, 90=down; add 90 to rotate so 0=up
|
|
117
|
-
const atan2Deg = Math.atan2(event.clientY - cy, event.clientX - cx) * (180 / Math.PI);
|
|
118
|
-
let angle = atan2Deg + 90;
|
|
119
|
-
// Normalize to [-180, 180]
|
|
120
|
-
if (angle > 180) angle -= 360;
|
|
121
|
-
|
|
122
|
-
// Clamp dead zone based on current value position
|
|
123
|
-
if (angle < -135 || angle > 135) {
|
|
124
|
-
angle = percent >= 0.5 ? 135 : -135;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const range = max - min;
|
|
128
|
-
const raw = min + ((angle + 135) / 270) * range;
|
|
129
|
-
const snapped = Math.round(raw / safeStep) * safeStep;
|
|
130
|
-
onChange?.(clamp(snapped, min, max));
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const setValue = (nextValue: number) => {
|
|
134
|
-
const snapped = Math.round(nextValue / safeStep) * safeStep;
|
|
135
|
-
onChange?.(clamp(snapped, min, max));
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
139
|
-
const pageStep = safeStep * 10;
|
|
140
|
-
switch (event.key) {
|
|
141
|
-
case "ArrowUp":
|
|
142
|
-
case "ArrowRight":
|
|
143
|
-
event.preventDefault();
|
|
144
|
-
setValue(value + safeStep);
|
|
145
|
-
break;
|
|
146
|
-
case "ArrowDown":
|
|
147
|
-
case "ArrowLeft":
|
|
148
|
-
event.preventDefault();
|
|
149
|
-
setValue(value - safeStep);
|
|
150
|
-
break;
|
|
151
|
-
case "PageUp":
|
|
152
|
-
event.preventDefault();
|
|
153
|
-
setValue(value + pageStep);
|
|
154
|
-
break;
|
|
155
|
-
case "PageDown":
|
|
156
|
-
event.preventDefault();
|
|
157
|
-
setValue(value - pageStep);
|
|
158
|
-
break;
|
|
159
|
-
case "Home":
|
|
160
|
-
event.preventDefault();
|
|
161
|
-
setValue(min);
|
|
162
|
-
break;
|
|
163
|
-
case "End":
|
|
164
|
-
event.preventDefault();
|
|
165
|
-
setValue(max);
|
|
166
|
-
break;
|
|
167
|
-
default:
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
};
|
|
57
|
+
const { dress, defaults } = useDress(
|
|
58
|
+
"rotary",
|
|
59
|
+
{ material, knurling, knurlColor, markers, indicator, ...look },
|
|
60
|
+
groundedKit.rotary!
|
|
61
|
+
);
|
|
171
62
|
|
|
172
63
|
return (
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
aria-orientation="vertical"
|
|
184
|
-
tabIndex={0}
|
|
185
|
-
onPointerDown={(event) => {
|
|
186
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
187
|
-
draggingRef.current = true;
|
|
188
|
-
pointerToValue(event);
|
|
189
|
-
}}
|
|
190
|
-
onPointerMove={(event) => {
|
|
191
|
-
if (draggingRef.current) pointerToValue(event);
|
|
192
|
-
}}
|
|
193
|
-
onPointerUp={() => {
|
|
194
|
-
draggingRef.current = false;
|
|
195
|
-
}}
|
|
196
|
-
onPointerCancel={() => {
|
|
197
|
-
draggingRef.current = false;
|
|
198
|
-
}}
|
|
199
|
-
onKeyDown={onKeyDown}
|
|
200
|
-
>
|
|
201
|
-
<svg width={0} height={0} style={{ position: "absolute" }} aria-hidden focusable={false}>
|
|
202
|
-
<defs>
|
|
203
|
-
<clipPath id={clipId} clipPathUnits="objectBoundingBox">
|
|
204
|
-
<path d={KNURL_PATHS[family]} />
|
|
205
|
-
</clipPath>
|
|
206
|
-
</defs>
|
|
207
|
-
</svg>
|
|
208
|
-
<span className="amb-knob-body ambient amb-thickness-2 amb-surface" />
|
|
209
|
-
<div
|
|
210
|
-
className={cn(
|
|
211
|
-
"ambx-knob-rotation amb-knob-face",
|
|
212
|
-
family === "flute" && "amb-knob-face-flute",
|
|
213
|
-
family === "fine" && "amb-knob-face-fine",
|
|
214
|
-
variant === "cap" && "amb-knob-face-cap",
|
|
215
|
-
material && `amb-mat-${material}`
|
|
216
|
-
)}
|
|
217
|
-
style={{ transform: `rotate(${rotation}deg)`, clipPath: `url(#${clipId})` }}
|
|
218
|
-
>
|
|
219
|
-
{variant === "dot" ? <span className="amb-knob-indicator-dot" /> : null}
|
|
220
|
-
{variant === "line" ? <span className="amb-knob-indicator-line" /> : null}
|
|
221
|
-
{variant === "flute" ? (
|
|
222
|
-
<span className="amb-knob-indicator-dot amb-knob-indicator-dot-center" />
|
|
223
|
-
) : null}
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
{label ? (
|
|
227
|
-
<span id={id} className="ambx-label">
|
|
228
|
-
{label}
|
|
229
|
-
</span>
|
|
230
|
-
) : null}
|
|
231
|
-
</div>
|
|
64
|
+
<AmbientRotary
|
|
65
|
+
{...rest}
|
|
66
|
+
/* Caller wins over kit, kit wins over the mechanism's own default. */
|
|
67
|
+
travel={travel ?? defaults?.travel}
|
|
68
|
+
input={input ?? defaults?.input}
|
|
69
|
+
animate={animate ?? defaults?.animate}
|
|
70
|
+
size={size}
|
|
71
|
+
className={cn(dress.className, className)}
|
|
72
|
+
parts={dress.parts}
|
|
73
|
+
/>
|
|
232
74
|
);
|
|
233
75
|
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import type { HTMLAttributes } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
|
+
import type { AmbientMaterial } from "../core/material";
|
|
3
4
|
|
|
4
5
|
export interface AmbientPanelProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
-
material?:
|
|
6
|
+
material?: AmbientMaterial;
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
/** A panel is a plain surface, so it can wear any finish directly: it spends
|
|
10
|
+
* neither pseudo-element of its own, which is what the two micro-relief
|
|
11
|
+
* materials need. Their grain paints below the panel's children, and their
|
|
12
|
+
* `overflow: hidden` clips whatever hangs outside it — a control's drop
|
|
13
|
+
* shadow at the very edge included. */
|
|
8
14
|
export function AmbientPanel({ className, material = "matte", ...props }: AmbientPanelProps) {
|
|
9
15
|
return (
|
|
10
16
|
<div
|
|
11
17
|
className={cn(
|
|
12
18
|
"ambient amb-surface amb-chamfer amb-elevation-2 ambx-panel",
|
|
13
|
-
|
|
19
|
+
`amb-mat-${material}`,
|
|
14
20
|
className
|
|
15
21
|
)}
|
|
16
22
|
{...props}
|
|
@@ -47,6 +47,9 @@ export function AmbientProvider({ children, className, style, theme }: AmbientPr
|
|
|
47
47
|
].join(" ");
|
|
48
48
|
themeVars["--amb-label"] =
|
|
49
49
|
"hsl(var(--amb-light-hue) var(--amb-light-saturation) calc((1 - var(--amb-key-light-intensity)) * 60% + 20%))";
|
|
50
|
+
// --amb-curve-delta deliberately absent: unlike these two it is declared
|
|
51
|
+
// per element in ambient.css, so it already re-resolves against the light
|
|
52
|
+
// overridden here. Re-declaring it would only re-copy the fit.
|
|
50
53
|
const mergedStyle = { ...themeVars, ...style } as CSSProperties;
|
|
51
54
|
|
|
52
55
|
return (
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type AmbientRackGap = "tight" | "normal" | "loose";
|
|
5
|
+
|
|
6
|
+
export interface AmbientRackProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Clearance between controls: tight for one functional unit/repeated
|
|
8
|
+
* row, normal (default) for a few related controls, loose for
|
|
9
|
+
* separating distinct zones. */
|
|
10
|
+
gap?: AmbientRackGap;
|
|
11
|
+
direction?: "row" | "column";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function AmbientRack({
|
|
15
|
+
className,
|
|
16
|
+
gap = "normal",
|
|
17
|
+
direction = "row",
|
|
18
|
+
...props
|
|
19
|
+
}: AmbientRackProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={cn(
|
|
23
|
+
"ambx-rack",
|
|
24
|
+
gap === "tight" && "ambx-rack-tight",
|
|
25
|
+
gap === "loose" && "ambx-rack-loose",
|
|
26
|
+
direction === "column" && "ambx-rack-column",
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cn } from "../lib/cn";
|
|
2
|
+
import { AmbientBank } from "../controls/AmbientBank";
|
|
3
|
+
import type { AmbientBankProps } from "../controls/AmbientBank";
|
|
4
|
+
import type { BankOption, BankOrientation } from "../core/useBank";
|
|
5
|
+
import { useDress } from "../core/kit";
|
|
6
|
+
import type { KitLook } from "../core/kit";
|
|
7
|
+
import { groundedKit } from "../kits/grounded";
|
|
8
|
+
|
|
9
|
+
export type AmbientSelectOption = BankOption;
|
|
10
|
+
export type AmbientSelectOrientation = BankOrientation;
|
|
11
|
+
export type AmbientSelectSize = "sm" | "md" | "lg";
|
|
12
|
+
|
|
13
|
+
export type AmbientSelectProps = Omit<AmbientBankProps, "keyParts" | "parts" | "size"> & {
|
|
14
|
+
size?: AmbientSelectSize | undefined;
|
|
15
|
+
/** Look options in the active kit's vocabulary. */
|
|
16
|
+
look?: KitLook | undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A bank of lamp-lit keys in a shared rail — the hardware idiom for a
|
|
21
|
+
* mode selector, where the state is a lamp rather than a mark.
|
|
22
|
+
*
|
|
23
|
+
* Three physical layers per key, and the ORDER is the whole trick: the key
|
|
24
|
+
* itself is the pocket floor, the lens is a disc lying on it, and the cap
|
|
25
|
+
* is a translucent diffuser over both. The cap is `.amb-mat-glass`, so its
|
|
26
|
+
* backdrop-filter blurs the lens behind it — which is why the lens goes in
|
|
27
|
+
* the `base` frame and the cap in `actuator`, and why swapping them would
|
|
28
|
+
* put out every lamp.
|
|
29
|
+
*/
|
|
30
|
+
export function AmbientSelect({ size = "md", look, className, ...rest }: AmbientSelectProps) {
|
|
31
|
+
const { dress } = useDress("bank", { ...look }, groundedKit.bank!);
|
|
32
|
+
return (
|
|
33
|
+
<AmbientBank
|
|
34
|
+
{...rest}
|
|
35
|
+
size={size}
|
|
36
|
+
className={cn(dress.className, className)}
|
|
37
|
+
keyParts={dress.parts}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|