@ambientcss/components 1.2.0 → 2.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/dist/index.cjs +97 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +97 -28
- package/dist/index.js.map +1 -1
- package/dist/styles.css +311 -66
- package/package.json +2 -2
- package/src/components/AmbientButton.tsx +27 -4
- package/src/components/AmbientFader.tsx +3 -7
- package/src/components/AmbientKnob.tsx +95 -9
- package/src/components/AmbientSlider.tsx +3 -9
- package/src/components/AmbientSwitch.tsx +5 -2
- package/src/index.ts +2 -2
- package/src/styles.css +311 -66
|
@@ -2,6 +2,73 @@ import { useId, useRef } from "react";
|
|
|
2
2
|
import type { HTMLAttributes, KeyboardEvent, PointerEvent } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
|
|
5
|
+
/* Straight-knurled silhouettes for the rotating face, in
|
|
6
|
+
objectBoundingBox units so the clip scales with the component. Each
|
|
7
|
+
variant family mirrors its referent's rib section
|
|
8
|
+
(ambient3d/ground_components.py): trapezoid teeth between the outer
|
|
9
|
+
radius and the tooth-root radius. The circular body underneath keeps
|
|
10
|
+
the smooth drop shadow. */
|
|
11
|
+
type KnurlSpec = {
|
|
12
|
+
teeth: number; // rib count
|
|
13
|
+
root: number; // tooth-root radius (bounding-box units; outer is 0.5)
|
|
14
|
+
rise: number; // fraction of the pitch spent climbing to the crest
|
|
15
|
+
fall: [number, number]; // crest-end and root-return pitch fractions
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const KNURLS: Record<"standard" | "flute" | "fine", KnurlSpec> = {
|
|
19
|
+
/* 36 ribs — the grounded referent knob (dot and line variants) */
|
|
20
|
+
standard: { teeth: 36, root: 0.468, rise: 0.12, fall: [0.5, 0.62] },
|
|
21
|
+
/* 14 broad flutes with deeper roots (OP-Z-style flute variant) */
|
|
22
|
+
flute: { teeth: 14, root: 0.44, rise: 0.08, fall: [0.72, 0.8] },
|
|
23
|
+
/* 48-rib fine knurl, shallower (cap and wheel variants) */
|
|
24
|
+
fine: { teeth: 48, root: 0.476, rise: 0.12, fall: [0.5, 0.62] }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function knurlPath({ teeth, root, rise, fall }: KnurlSpec): string {
|
|
28
|
+
const outer = 0.5;
|
|
29
|
+
const pitch = (Math.PI * 2) / teeth;
|
|
30
|
+
const pts: string[] = [];
|
|
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
|
+
|
|
5
72
|
export type AmbientKnobProps = Omit<HTMLAttributes<HTMLDivElement>, "onChange"> & {
|
|
6
73
|
value: number;
|
|
7
74
|
min?: number;
|
|
@@ -9,6 +76,7 @@ export type AmbientKnobProps = Omit<HTMLAttributes<HTMLDivElement>, "onChange">
|
|
|
9
76
|
step?: number;
|
|
10
77
|
label?: string;
|
|
11
78
|
material?: "matte" | "shiny" | "glass";
|
|
79
|
+
variant?: AmbientKnobVariant;
|
|
12
80
|
onChange?: (nextValue: number) => void;
|
|
13
81
|
};
|
|
14
82
|
|
|
@@ -23,14 +91,17 @@ export function AmbientKnob({
|
|
|
23
91
|
step = 1,
|
|
24
92
|
label,
|
|
25
93
|
material,
|
|
94
|
+
variant = "dot",
|
|
26
95
|
onChange,
|
|
27
96
|
className,
|
|
28
97
|
...props
|
|
29
98
|
}: AmbientKnobProps) {
|
|
30
99
|
const id = useId();
|
|
100
|
+
const clipId = `amb-knurl-${id.replace(/:/g, "")}`;
|
|
31
101
|
const draggingRef = useRef(false);
|
|
32
102
|
const knobRef = useRef<HTMLDivElement>(null);
|
|
33
103
|
const safeStep = step > 0 ? step : 1;
|
|
104
|
+
const family = VARIANT_FAMILY[variant];
|
|
34
105
|
|
|
35
106
|
const percent = (value - min) / (max - min || 1);
|
|
36
107
|
const rotation = percent * 270 - 135;
|
|
@@ -102,7 +173,7 @@ export function AmbientKnob({
|
|
|
102
173
|
<div className={cn("ambx-stack", className)} {...props}>
|
|
103
174
|
<div
|
|
104
175
|
ref={knobRef}
|
|
105
|
-
className=
|
|
176
|
+
className="amb-knob ambx-knob"
|
|
106
177
|
role="slider"
|
|
107
178
|
aria-label={label}
|
|
108
179
|
aria-valuemin={min}
|
|
@@ -127,14 +198,29 @@ export function AmbientKnob({
|
|
|
127
198
|
}}
|
|
128
199
|
onKeyDown={onKeyDown}
|
|
129
200
|
>
|
|
130
|
-
<
|
|
131
|
-
<
|
|
132
|
-
<
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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}
|
|
138
224
|
</div>
|
|
139
225
|
</div>
|
|
140
226
|
{label ? (
|
|
@@ -91,7 +91,7 @@ export function AmbientSlider({
|
|
|
91
91
|
return (
|
|
92
92
|
<div className={cn("ambx-stack", className)} {...props}>
|
|
93
93
|
<div
|
|
94
|
-
className="amb-slider ambx-slider"
|
|
94
|
+
className="amb-slider amb-groove ambx-slider"
|
|
95
95
|
ref={trackRef}
|
|
96
96
|
role="slider"
|
|
97
97
|
aria-label={label}
|
|
@@ -109,15 +109,9 @@ export function AmbientSlider({
|
|
|
109
109
|
onKeyDown={onKeyDown}
|
|
110
110
|
>
|
|
111
111
|
<div
|
|
112
|
-
className={cn("amb-slider-thumb ambient amb-fillet
|
|
112
|
+
className={cn("amb-slider-thumb ambient amb-fillet ambx-slider-thumb", material !== "glass" && "amb-surface-convex", material && `amb-mat-${material}`)}
|
|
113
113
|
style={{ left: `${percent}%` }}
|
|
114
|
-
|
|
115
|
-
<div className="amb-slider-grip">
|
|
116
|
-
<span className="amb-fader-dot" />
|
|
117
|
-
<span className="amb-fader-dot" />
|
|
118
|
-
<span className="amb-fader-dot" />
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
114
|
+
/>
|
|
121
115
|
</div>
|
|
122
116
|
{label ? (
|
|
123
117
|
<span id={id} className="ambx-label">
|
|
@@ -38,8 +38,8 @@ export function AmbientSwitch({
|
|
|
38
38
|
aria-checked={active}
|
|
39
39
|
aria-labelledby={label ? labelId : props["aria-labelledby"]}
|
|
40
40
|
className={cn(
|
|
41
|
-
"
|
|
42
|
-
active
|
|
41
|
+
"amb-switch",
|
|
42
|
+
active && "amb-switch-on",
|
|
43
43
|
"ambx-switch",
|
|
44
44
|
`ambx-switch-${size}`,
|
|
45
45
|
className
|
|
@@ -58,6 +58,9 @@ export function AmbientSwitch({
|
|
|
58
58
|
style={typeof led === "string" ? { "--amb-led-color": led } as React.CSSProperties : undefined}
|
|
59
59
|
/>
|
|
60
60
|
) : null}
|
|
61
|
+
<span className="amb-switch-track amb-groove">
|
|
62
|
+
<span className="amb-switch-pill ambient amb-fillet amb-surface-convex" />
|
|
63
|
+
</span>
|
|
61
64
|
{children}
|
|
62
65
|
</button>
|
|
63
66
|
);
|
package/src/index.ts
CHANGED
|
@@ -5,13 +5,13 @@ export { AmbientPanel } from "./components/AmbientPanel";
|
|
|
5
5
|
export type { AmbientPanelProps } from "./components/AmbientPanel";
|
|
6
6
|
|
|
7
7
|
export { AmbientButton } from "./components/AmbientButton";
|
|
8
|
-
export type { AmbientButtonProps } from "./components/AmbientButton";
|
|
8
|
+
export type { AmbientButtonProps, AmbientButtonShape } from "./components/AmbientButton";
|
|
9
9
|
|
|
10
10
|
export { AmbientSwitch } from "./components/AmbientSwitch";
|
|
11
11
|
export type { AmbientSwitchProps, AmbientSwitchSize } from "./components/AmbientSwitch";
|
|
12
12
|
|
|
13
13
|
export { AmbientKnob } from "./components/AmbientKnob";
|
|
14
|
-
export type { AmbientKnobProps } from "./components/AmbientKnob";
|
|
14
|
+
export type { AmbientKnobProps, AmbientKnobVariant } from "./components/AmbientKnob";
|
|
15
15
|
|
|
16
16
|
export { AmbientFader } from "./components/AmbientFader";
|
|
17
17
|
export type { AmbientFaderProps } from "./components/AmbientFader";
|
package/src/styles.css
CHANGED
|
@@ -35,51 +35,264 @@
|
|
|
35
35
|
letter-spacing: var(--ambx-type-tracking);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/* Button: a chamfered key cap seated in a clearance well — the referent
|
|
39
|
+
is components/button.py's "flush" style (cap seated into a hole with a
|
|
40
|
+
shadow gap ring around it). The button element IS the well: a shallow
|
|
41
|
+
groove (seat depth ~1.2mm = thickness 0.27) whose lume interior shows
|
|
42
|
+
as the gap ring around the cap. Pressing sinks the cap by the
|
|
43
|
+
referent's 0.7mm travel (thickness 1 -> 0.84): the chamfer bands and
|
|
44
|
+
the swept shadow shrink with it for free. */
|
|
38
45
|
.amb-button {
|
|
39
46
|
cursor: pointer;
|
|
40
|
-
border:
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
border: none;
|
|
48
|
+
padding: var(--ambx-grid-half); /* the clearance gap ring */
|
|
49
|
+
--amb-thickness: 0.27; /* seat depth of the well */
|
|
50
|
+
background-color: var(--amb-lume);
|
|
51
|
+
border-radius: calc(var(--ambx-grid) * 3 + var(--ambx-grid-half));
|
|
52
|
+
min-width: calc(var(--ambx-grid) * 16);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.amb-button-cap {
|
|
56
|
+
display: grid;
|
|
57
|
+
place-items: center;
|
|
58
|
+
width: 100%;
|
|
59
|
+
padding: calc(var(--ambx-grid) * 1.5) calc(var(--ambx-grid) * 2);
|
|
43
60
|
border-radius: calc(var(--ambx-grid) * 3);
|
|
44
61
|
text-transform: uppercase;
|
|
45
|
-
min-width: calc(var(--ambx-grid) * 16);
|
|
46
62
|
font-weight: 600;
|
|
47
|
-
color: var(--amb-
|
|
63
|
+
color: var(--amb-label);
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
.amb-button:focus {
|
|
51
67
|
outline-offset: var(--ambx-grid);
|
|
52
68
|
}
|
|
53
69
|
|
|
54
|
-
.amb-button:hover
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
.amb-button:hover .amb-button-cap {
|
|
71
|
+
color: var(--amb-highlight-color);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.amb-button:active .amb-button-cap {
|
|
75
|
+
--amb-thickness: 0.84;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Button shapes, matching the referent lineup (ambient3d/generate.py):
|
|
79
|
+
the default stadium key above, a round key (superellipse exponent 2 —
|
|
80
|
+
pair with amb-mat-shiny for the machined metal-button look), and a
|
|
81
|
+
squarer, flatter EP-133-style pad (exponent 6: tighter corners and a
|
|
82
|
+
3.6mm cap instead of the key's 4.5mm — thickness 0.8 — with the same
|
|
83
|
+
0.7mm press travel). */
|
|
84
|
+
.amb-button.amb-button-round {
|
|
85
|
+
min-width: calc(var(--ambx-grid) * 12);
|
|
86
|
+
aspect-ratio: 1;
|
|
87
|
+
border-radius: 50%;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.amb-button-round .amb-button-cap {
|
|
91
|
+
height: 100%;
|
|
92
|
+
padding: var(--ambx-grid);
|
|
93
|
+
border-radius: 50%;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.amb-button.amb-button-square {
|
|
97
|
+
min-width: calc(var(--ambx-grid) * 14);
|
|
98
|
+
aspect-ratio: 1;
|
|
99
|
+
border-radius: calc(var(--ambx-grid) * 2);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.amb-button-square .amb-button-cap {
|
|
103
|
+
height: 100%;
|
|
104
|
+
padding: var(--ambx-grid);
|
|
105
|
+
border-radius: calc(var(--ambx-grid) * 1.5);
|
|
106
|
+
--amb-thickness: 0.8;
|
|
57
107
|
}
|
|
58
108
|
|
|
59
|
-
.amb-button:active {
|
|
60
|
-
--amb-
|
|
109
|
+
.amb-button.amb-button-square:active .amb-button-cap {
|
|
110
|
+
--amb-thickness: 0.64;
|
|
61
111
|
}
|
|
62
112
|
|
|
113
|
+
/* Knob: a knob-scale body (thickness 2 = the referent knob.py's 9mm
|
|
114
|
+
height) resting on the panel. The rotating face carries the knurl — a
|
|
115
|
+
repeating-conic rib pattern, physically the referent's ribs catching
|
|
116
|
+
key light on one flank and shading on the other — under a smooth
|
|
117
|
+
opaque top disc (the OP-1-style cap), with an accent indicator dot.
|
|
118
|
+
The whole face rotates with the value, so the ribs move under the
|
|
119
|
+
fingers like the real thing. */
|
|
63
120
|
.amb-knob {
|
|
64
121
|
border-radius: 50%;
|
|
65
122
|
height: calc(var(--ambx-grid) * 16);
|
|
66
123
|
width: calc(var(--ambx-grid) * 16);
|
|
67
|
-
border: medium solid var(--amb-lume);
|
|
68
124
|
}
|
|
69
125
|
|
|
70
|
-
|
|
126
|
+
/* The body is the smooth circle at the tooth-root radius: it carries
|
|
127
|
+
the ambient drop shadow (kept circular — the teeth are too fine to
|
|
128
|
+
read in a penumbra) while the clipped face above it supplies the
|
|
129
|
+
knurled silhouette. */
|
|
130
|
+
.amb-knob-body {
|
|
71
131
|
position: absolute;
|
|
72
|
-
|
|
132
|
+
inset: calc(var(--ambx-grid) / 2);
|
|
133
|
+
border-radius: 50%;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* The rotating face: clipped to the straight-knurl silhouette (the
|
|
137
|
+
inline SVG clipPath in AmbientKnob), opaque at the knob's surface
|
|
138
|
+
color, with per-tooth flank shading — a lit leading flank and a
|
|
139
|
+
shaded trailing flank every 10deg, phase-aligned with the clip's
|
|
140
|
+
teeth (conic 0 is rotated to the +x axis where the tooth path
|
|
141
|
+
starts) — under the smooth top disc. */
|
|
142
|
+
.amb-knob-face {
|
|
143
|
+
position: absolute;
|
|
144
|
+
inset: 0;
|
|
145
|
+
background-color: hsl(
|
|
146
|
+
var(--amb-light-hue)
|
|
147
|
+
var(--amb-light-saturation)
|
|
148
|
+
calc(
|
|
149
|
+
var(--amb-key-light-intensity) * 31.5% +
|
|
150
|
+
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
background-image:
|
|
154
|
+
radial-gradient(
|
|
155
|
+
circle,
|
|
156
|
+
hsl(
|
|
157
|
+
var(--amb-light-hue)
|
|
158
|
+
var(--amb-light-saturation)
|
|
159
|
+
calc(
|
|
160
|
+
var(--amb-key-light-intensity) * 31.5% +
|
|
161
|
+
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
162
|
+
)
|
|
163
|
+
) 0 60%,
|
|
164
|
+
transparent 61%
|
|
165
|
+
),
|
|
166
|
+
repeating-conic-gradient(
|
|
167
|
+
from 90deg,
|
|
168
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
169
|
+
calc(var(--amb-key-light-intensity) * 0.35)) 0deg 1.4deg,
|
|
170
|
+
transparent 2.2deg 4.6deg,
|
|
171
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
172
|
+
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
173
|
+
* 0.3 + 0.16)) 5deg 6.4deg,
|
|
174
|
+
transparent 7.2deg 10deg
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Fluted face (flute variant — the OP-Z-style referent: 14 broad
|
|
179
|
+
ridges split by narrow grooves, pitch 25.714deg). Flank bands sit on
|
|
180
|
+
the clip's rising (0–2.1deg) and falling (18.5–20.6deg) flanks;
|
|
181
|
+
deeper flutes catch more key light, so both bands run stronger than
|
|
182
|
+
the 36-rib knurl's. */
|
|
183
|
+
.amb-knob-face-flute {
|
|
184
|
+
background-image:
|
|
185
|
+
radial-gradient(
|
|
186
|
+
circle,
|
|
187
|
+
hsl(
|
|
188
|
+
var(--amb-light-hue)
|
|
189
|
+
var(--amb-light-saturation)
|
|
190
|
+
calc(
|
|
191
|
+
var(--amb-key-light-intensity) * 31.5% +
|
|
192
|
+
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
193
|
+
)
|
|
194
|
+
) 0 60%,
|
|
195
|
+
transparent 61%
|
|
196
|
+
),
|
|
197
|
+
repeating-conic-gradient(
|
|
198
|
+
from 90deg,
|
|
199
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
200
|
+
calc(var(--amb-key-light-intensity) * 0.45)) 0deg 2.1deg,
|
|
201
|
+
transparent 3.2deg 17.4deg,
|
|
202
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
203
|
+
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
204
|
+
* 0.35 + 0.2)) 18.5deg 20.6deg,
|
|
205
|
+
transparent 21.6deg 25.7143deg
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Fine knurl (cap and wheel variants: 48 ribs, pitch 7.5deg) — the
|
|
210
|
+
36-rib pattern's angles scaled by 3/4, same flank alphas. */
|
|
211
|
+
.amb-knob-face-fine {
|
|
212
|
+
background-image:
|
|
213
|
+
radial-gradient(
|
|
214
|
+
circle,
|
|
215
|
+
hsl(
|
|
216
|
+
var(--amb-light-hue)
|
|
217
|
+
var(--amb-light-saturation)
|
|
218
|
+
calc(
|
|
219
|
+
var(--amb-key-light-intensity) * 31.5% +
|
|
220
|
+
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
221
|
+
)
|
|
222
|
+
) 0 60%,
|
|
223
|
+
transparent 61%
|
|
224
|
+
),
|
|
225
|
+
repeating-conic-gradient(
|
|
226
|
+
from 90deg,
|
|
227
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
228
|
+
calc(var(--amb-key-light-intensity) * 0.35)) 0deg 1.05deg,
|
|
229
|
+
transparent 1.65deg 3.45deg,
|
|
230
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
231
|
+
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
232
|
+
* 0.3 + 0.16)) 3.75deg 4.8deg,
|
|
233
|
+
transparent 5.4deg 7.5deg
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Encoder cap (cap variant — the OP-1-style referent's smooth
|
|
238
|
+
contrasting top disc, 0.35mm proud of the knurl): the accent disc
|
|
239
|
+
replaces the surface-colored center disc over the fine knurl. */
|
|
240
|
+
.amb-knob-face-cap {
|
|
241
|
+
background-image:
|
|
242
|
+
radial-gradient(circle, var(--amb-highlight-color) 0 65%, transparent 66%),
|
|
243
|
+
repeating-conic-gradient(
|
|
244
|
+
from 90deg,
|
|
245
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 100% /
|
|
246
|
+
calc(var(--amb-key-light-intensity) * 0.35)) 0deg 1.05deg,
|
|
247
|
+
transparent 1.65deg 3.45deg,
|
|
248
|
+
hsl(var(--amb-light-hue) var(--amb-light-saturation) 0% /
|
|
249
|
+
calc((var(--amb-key-light-intensity) - var(--amb-fill-light-intensity))
|
|
250
|
+
* 0.3 + 0.16)) 3.75deg 4.8deg,
|
|
251
|
+
transparent 5.4deg 7.5deg
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.amb-knob-indicator-dot {
|
|
256
|
+
position: absolute;
|
|
257
|
+
top: 16%;
|
|
73
258
|
left: 50%;
|
|
74
259
|
transform: translateX(-50%);
|
|
260
|
+
width: calc(var(--ambx-grid) * 2);
|
|
261
|
+
height: calc(var(--ambx-grid) * 2);
|
|
262
|
+
border-radius: 50%;
|
|
263
|
+
background: var(--amb-highlight-color);
|
|
75
264
|
}
|
|
76
265
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
266
|
+
/* Centered dot (flute variant): the OP-Z referent's dot sits in the
|
|
267
|
+
middle of the cap (dot_frac 0.32), not at the rim. */
|
|
268
|
+
.amb-knob-indicator-dot-center {
|
|
269
|
+
top: 50%;
|
|
270
|
+
transform: translate(-50%, -50%);
|
|
271
|
+
width: calc(var(--ambx-grid) * 4.5);
|
|
272
|
+
height: calc(var(--ambx-grid) * 4.5);
|
|
81
273
|
}
|
|
82
274
|
|
|
275
|
+
/* Radial indicator line (line variant): the referent's accent bar,
|
|
276
|
+
center to ~85% of the cap radius, pointing at 12 o'clock. */
|
|
277
|
+
.amb-knob-indicator-line {
|
|
278
|
+
position: absolute;
|
|
279
|
+
top: 12%;
|
|
280
|
+
left: 50%;
|
|
281
|
+
transform: translateX(-50%);
|
|
282
|
+
width: calc(var(--ambx-grid) * 0.75);
|
|
283
|
+
height: 34%;
|
|
284
|
+
border-radius: calc(var(--ambx-grid) * 0.375);
|
|
285
|
+
background: var(--amb-highlight-color);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.amb-knob:hover .amb-knob-indicator-dot,
|
|
289
|
+
.amb-knob:hover .amb-knob-indicator-line {
|
|
290
|
+
background: color-mix(in oklab, var(--amb-highlight-color), white 25%);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* Tracks are grounded grooves (.amb-groove supplies the recess: lit-wall
|
|
294
|
+
shadow band + far-wall bounce) with a lume interior — dark in bright
|
|
295
|
+
light, glowing in low light — overriding the groove's neutral floor. */
|
|
83
296
|
.amb-fader {
|
|
84
297
|
position: relative;
|
|
85
298
|
width: var(--ambx-grid);
|
|
@@ -88,6 +301,9 @@
|
|
|
88
301
|
background-color: var(--amb-lume);
|
|
89
302
|
}
|
|
90
303
|
|
|
304
|
+
/* Fader thumb: the referent (fader.py) is a pill cap on a stem — 7mm
|
|
305
|
+
tall (thickness 1.5) riding 2.2mm above the plate (elevation 0.28) —
|
|
306
|
+
with a single grip line across the top. */
|
|
91
307
|
.amb-fader-thumb {
|
|
92
308
|
position: absolute;
|
|
93
309
|
width: calc(var(--ambx-grid) * 6);
|
|
@@ -95,55 +311,41 @@
|
|
|
95
311
|
left: 50%;
|
|
96
312
|
top: 50%;
|
|
97
313
|
transform: translate(-50%, -50%);
|
|
98
|
-
border-radius: calc(var(--ambx-grid) *
|
|
314
|
+
border-radius: calc(var(--ambx-grid) * 2);
|
|
99
315
|
display: grid;
|
|
100
316
|
place-items: center;
|
|
317
|
+
--amb-thickness: 1.5;
|
|
318
|
+
--amb-elevation: 0.28;
|
|
101
319
|
}
|
|
102
320
|
|
|
103
|
-
.amb-fader-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.amb-fader-dot {
|
|
109
|
-
width: 3px;
|
|
110
|
-
height: 3px;
|
|
111
|
-
border-radius: 50%;
|
|
112
|
-
background: radial-gradient(
|
|
113
|
-
circle at 30% 30%,
|
|
114
|
-
color-mix(in oklab, var(--amb-lume), white 30%) 0%,
|
|
115
|
-
var(--amb-lume) 55%,
|
|
116
|
-
color-mix(in oklab, var(--amb-lume), black 35%) 100%
|
|
117
|
-
);
|
|
321
|
+
.amb-fader-gripline {
|
|
322
|
+
width: 70%;
|
|
323
|
+
height: 2px;
|
|
324
|
+
border-radius: 1px;
|
|
325
|
+
background: var(--amb-lume);
|
|
118
326
|
}
|
|
119
327
|
|
|
328
|
+
/* Slider: the referent (slider.py) is a shallow concave channel (1mm
|
|
329
|
+
deep = thickness 0.22), not a through-slot, with a domed disc thumb
|
|
330
|
+
gliding over it. */
|
|
120
331
|
.amb-slider {
|
|
121
332
|
position: relative;
|
|
122
|
-
height: var(--ambx-grid);
|
|
333
|
+
height: calc(var(--ambx-grid) * 1.5);
|
|
123
334
|
width: calc(var(--ambx-grid) * 30);
|
|
124
335
|
border-radius: var(--ambx-grid);
|
|
125
336
|
background-color: var(--amb-lume);
|
|
337
|
+
--amb-thickness: 0.22;
|
|
126
338
|
}
|
|
127
339
|
|
|
128
340
|
.amb-slider-thumb {
|
|
129
341
|
position: absolute;
|
|
130
|
-
width: calc(var(--ambx-grid) *
|
|
342
|
+
width: calc(var(--ambx-grid) * 6);
|
|
131
343
|
height: calc(var(--ambx-grid) * 6);
|
|
132
344
|
top: 50%;
|
|
133
345
|
left: 50%;
|
|
134
346
|
transform: translate(-50%, -50%);
|
|
135
|
-
border-radius:
|
|
136
|
-
|
|
137
|
-
place-items: center;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.amb-slider-grip {
|
|
141
|
-
display: flex;
|
|
142
|
-
flex-direction: column;
|
|
143
|
-
gap: 2px;
|
|
144
|
-
box-shadow:
|
|
145
|
-
inset 0 var(--ambx-grid-quarter) var(--ambx-grid-quarter) rgb(255 255 255 / 0.28),
|
|
146
|
-
inset 0 calc(var(--ambx-grid-quarter) * -1) var(--ambx-grid-quarter) rgb(0 0 0 / 0.22);
|
|
347
|
+
border-radius: 50%;
|
|
348
|
+
--amb-thickness: 0.7;
|
|
147
349
|
}
|
|
148
350
|
|
|
149
351
|
.amb-led {
|
|
@@ -164,16 +366,65 @@
|
|
|
164
366
|
box-shadow: none;
|
|
165
367
|
}
|
|
166
368
|
|
|
369
|
+
/* Switch: a slide switch like the referent (switch.py) — a pill riding
|
|
370
|
+
in a dark stadium recess (well 1.5mm deep = thickness 0.33; the pill
|
|
371
|
+
stands 2.6mm above the recess floor = thickness 0.58), with an
|
|
372
|
+
optional LED above. The pill slides, nothing presses. */
|
|
167
373
|
.amb-switch {
|
|
168
|
-
position: relative;
|
|
169
|
-
width: calc(var(--ambx-grid) * 6);
|
|
170
|
-
height: calc(var(--ambx-grid) * 6);
|
|
171
|
-
border-radius: 50%;
|
|
172
374
|
cursor: pointer;
|
|
173
|
-
border:
|
|
375
|
+
border: none;
|
|
376
|
+
padding: 0;
|
|
377
|
+
background: none;
|
|
378
|
+
display: inline-grid;
|
|
379
|
+
justify-items: center;
|
|
380
|
+
gap: var(--ambx-grid);
|
|
174
381
|
margin: 0;
|
|
175
382
|
}
|
|
176
383
|
|
|
384
|
+
.amb-switch-track {
|
|
385
|
+
position: relative;
|
|
386
|
+
display: block;
|
|
387
|
+
width: var(--ambx-switch-w);
|
|
388
|
+
height: var(--ambx-switch-h);
|
|
389
|
+
border-radius: calc(var(--ambx-switch-h) / 2);
|
|
390
|
+
background-color: var(--amb-lume);
|
|
391
|
+
--amb-thickness: 0.33;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.amb-switch-pill {
|
|
395
|
+
position: absolute;
|
|
396
|
+
top: 50%;
|
|
397
|
+
left: 0;
|
|
398
|
+
width: calc(var(--ambx-switch-w) * 0.52);
|
|
399
|
+
height: calc(var(--ambx-switch-h) * 0.78);
|
|
400
|
+
border-radius: calc(var(--ambx-switch-h) * 0.39);
|
|
401
|
+
transform: translate(calc(var(--ambx-switch-h) * 0.11), -50%);
|
|
402
|
+
transition: transform 140ms ease;
|
|
403
|
+
--amb-thickness: 0.58;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.amb-switch-on .amb-switch-pill {
|
|
407
|
+
transform: translate(
|
|
408
|
+
calc(var(--ambx-switch-w) - 100% - var(--ambx-switch-h) * 0.11),
|
|
409
|
+
-50%
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.amb-switch:hover .amb-switch-pill {
|
|
414
|
+
background-color: color-mix(
|
|
415
|
+
in oklab,
|
|
416
|
+
hsl(
|
|
417
|
+
var(--amb-light-hue)
|
|
418
|
+
var(--amb-light-saturation)
|
|
419
|
+
calc(
|
|
420
|
+
var(--amb-key-light-intensity) * 31.5% +
|
|
421
|
+
var(--amb-fill-light-intensity) * 26.4% + 43.6%
|
|
422
|
+
)
|
|
423
|
+
),
|
|
424
|
+
var(--amb-highlight-color) 12%
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
177
428
|
.ambx-panel {
|
|
178
429
|
border-radius: calc(var(--ambx-grid) * 4);
|
|
179
430
|
padding: calc(var(--ambx-grid) * 4);
|
|
@@ -185,29 +436,23 @@
|
|
|
185
436
|
}
|
|
186
437
|
|
|
187
438
|
.ambx-switch {
|
|
188
|
-
--ambx-switch-
|
|
189
|
-
|
|
190
|
-
height: var(--ambx-switch-size);
|
|
191
|
-
display: inline-grid;
|
|
192
|
-
place-items: center;
|
|
193
|
-
padding: 0;
|
|
439
|
+
--ambx-switch-w: calc(var(--ambx-grid) * 12);
|
|
440
|
+
--ambx-switch-h: calc(var(--ambx-grid) * 6);
|
|
194
441
|
}
|
|
195
442
|
|
|
196
443
|
.ambx-switch-sm {
|
|
197
|
-
--ambx-switch-
|
|
444
|
+
--ambx-switch-w: calc(var(--ambx-grid) * 10);
|
|
445
|
+
--ambx-switch-h: calc(var(--ambx-grid) * 5);
|
|
198
446
|
}
|
|
199
447
|
|
|
200
448
|
.ambx-switch-md {
|
|
201
|
-
--ambx-switch-
|
|
449
|
+
--ambx-switch-w: calc(var(--ambx-grid) * 12);
|
|
450
|
+
--ambx-switch-h: calc(var(--ambx-grid) * 6);
|
|
202
451
|
}
|
|
203
452
|
|
|
204
453
|
.ambx-switch-lg {
|
|
205
|
-
--ambx-switch-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
.amb-switch-on {
|
|
209
|
-
box-shadow:
|
|
210
|
-
inset 0 0 0 var(--ambx-grid-quarter) rgb(255 255 255 / 0.25);
|
|
454
|
+
--ambx-switch-w: calc(var(--ambx-grid) * 16);
|
|
455
|
+
--ambx-switch-h: calc(var(--ambx-grid) * 8);
|
|
211
456
|
}
|
|
212
457
|
|
|
213
458
|
.ambx-stack {
|