@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.
Files changed (46) hide show
  1. package/README.md +35 -0
  2. package/dist/index.cjs +1563 -384
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +763 -48
  5. package/dist/index.d.ts +763 -48
  6. package/dist/index.js +1524 -385
  7. package/dist/index.js.map +1 -1
  8. package/dist/styles.css +1102 -256
  9. package/package.json +2 -2
  10. package/src/components/AmbientButton.tsx +25 -25
  11. package/src/components/AmbientFader.tsx +33 -115
  12. package/src/components/AmbientKnob.tsx +64 -222
  13. package/src/components/AmbientPanel.tsx +8 -2
  14. package/src/components/AmbientProvider.tsx +3 -0
  15. package/src/components/AmbientRack.tsx +32 -0
  16. package/src/components/AmbientSelect.tsx +40 -0
  17. package/src/components/AmbientSlider.tsx +32 -113
  18. package/src/components/AmbientSwitch.tsx +58 -58
  19. package/src/controls/AmbientBank.tsx +138 -0
  20. package/src/controls/AmbientLatch.tsx +78 -0
  21. package/src/controls/AmbientPress.tsx +74 -0
  22. package/src/controls/AmbientRotary.tsx +94 -0
  23. package/src/controls/AmbientTravel.tsx +83 -0
  24. package/src/core/context.tsx +46 -0
  25. package/src/core/controllable.ts +33 -0
  26. package/src/core/dev.ts +15 -0
  27. package/src/core/frames.tsx +63 -0
  28. package/src/core/kit.tsx +107 -0
  29. package/src/core/material.ts +27 -0
  30. package/src/core/numeric.ts +88 -0
  31. package/src/core/types.ts +116 -0
  32. package/src/core/useBank.ts +163 -0
  33. package/src/core/useLatch.ts +54 -0
  34. package/src/core/usePress.ts +120 -0
  35. package/src/core/useRotary.ts +253 -0
  36. package/src/core/useTravel.ts +141 -0
  37. package/src/index.ts +127 -4
  38. package/src/kits/console.tsx +80 -0
  39. package/src/kits/grounded.tsx +113 -0
  40. package/src/parts/bank.tsx +32 -0
  41. package/src/parts/console.tsx +99 -0
  42. package/src/parts/knob.tsx +203 -0
  43. package/src/parts/latch.tsx +33 -0
  44. package/src/parts/press.tsx +56 -0
  45. package/src/parts/travel.tsx +70 -0
  46. package/src/styles.css +1102 -256
@@ -0,0 +1,141 @@
1
+ import { useCallback, useMemo, useRef, useState } from "react";
2
+ import type { CSSProperties, KeyboardEvent, PointerEvent } from "react";
3
+ import { useControllableValue } from "./controllable";
4
+ import { clamp, commit, denormalise, normalise, valueKeyHandler } from "./numeric";
5
+ import { capturePointer } from "./useRotary";
6
+ import { stateData, stateStyle } from "./types";
7
+ import type { ControlState } from "./types";
8
+
9
+ export type TravelOrientation = "horizontal" | "vertical";
10
+
11
+ export type UseTravelOptions = {
12
+ value?: number | undefined;
13
+ defaultValue?: number | undefined;
14
+ min?: number | undefined;
15
+ max?: number | undefined;
16
+ step?: number | undefined;
17
+ detents?: number | undefined;
18
+ orientation?: TravelOrientation | undefined;
19
+ /** Flip which end of the axis is `min`. */
20
+ invert?: boolean | undefined;
21
+ disabled?: boolean | undefined;
22
+ onChange?: ((next: number) => void) | undefined;
23
+ };
24
+
25
+ /** A value riding a straight track.
26
+ *
27
+ * One mechanism behind both the slider and the fader, which before v3 were
28
+ * the same file twice with X and Y swapped. The only real difference is
29
+ * which axis the pointer reads — and which end is `min`, where the two
30
+ * disagree for a physical reason rather than a stylistic one: a horizontal
31
+ * track runs min-at-the-left, and an upright one runs min-at-the-bottom,
32
+ * because that is how a fader is built. Both are the default here, so
33
+ * neither preset has to ask. */
34
+ export function useTravel(options: UseTravelOptions) {
35
+ const {
36
+ min = 0,
37
+ max = 100,
38
+ step = 1,
39
+ orientation = "horizontal",
40
+ invert = false,
41
+ disabled = false
42
+ } = options;
43
+
44
+ const [value, setValue] = useControllableValue(
45
+ options.value,
46
+ options.defaultValue ?? min,
47
+ options.onChange
48
+ );
49
+
50
+ const [dragging, setDragging] = useState(false);
51
+ const draggingRef = useRef(false);
52
+ const rootRef = useRef<HTMLDivElement | null>(null);
53
+
54
+ const vertical = orientation === "vertical";
55
+ const range = max - min;
56
+ const keyStep = step > 0 ? step : range / 100 || 1;
57
+ const percent = clamp(normalise(value, min, max), 0, 1);
58
+
59
+ const set = useCallback(
60
+ (next: number) => setValue(commit(next, min, max, step)),
61
+ [setValue, min, max, step]
62
+ );
63
+
64
+ const track = (event: PointerEvent<HTMLElement>) => {
65
+ const rect = rootRef.current?.getBoundingClientRect();
66
+ if (!rect) return;
67
+ // An upright track reads bottom-up: 1 at the top of the box.
68
+ const raw = vertical
69
+ ? 1 - (event.clientY - rect.top) / (rect.height || 1)
70
+ : (event.clientX - rect.left) / (rect.width || 1);
71
+ set(denormalise(clamp(invert ? 1 - raw : raw, 0, 1), min, max));
72
+ };
73
+
74
+ const state: ControlState = useMemo(
75
+ () => ({
76
+ value,
77
+ min,
78
+ max,
79
+ percent,
80
+ angle: 0,
81
+ travelStart: 0,
82
+ travelSweep: 0,
83
+ detents: options.detents ?? (step > 0 && range > 0 ? Math.round(range / step) : 0),
84
+ dragging,
85
+ disabled,
86
+ atMin: value <= min,
87
+ atMax: value >= max
88
+ }),
89
+ [value, min, max, percent, options.detents, step, range, dragging, disabled]
90
+ );
91
+
92
+ const onKeyDown = valueKeyHandler({
93
+ value,
94
+ min,
95
+ max,
96
+ step: keyStep,
97
+ disabled,
98
+ invert,
99
+ onChange: setValue
100
+ });
101
+
102
+ const rootProps = {
103
+ ref: rootRef,
104
+ role: "slider" as const,
105
+ "aria-valuemin": min,
106
+ "aria-valuemax": max,
107
+ "aria-valuenow": value,
108
+ "aria-orientation": orientation,
109
+ "aria-disabled": disabled || undefined,
110
+ tabIndex: disabled ? -1 : 0,
111
+ style: stateStyle(state) as CSSProperties,
112
+ "data-orientation": orientation,
113
+ ...stateData(state),
114
+ onPointerDown: (event: PointerEvent<HTMLDivElement>) => {
115
+ if (disabled || event.button !== 0) return;
116
+ capturePointer(event.currentTarget, event.pointerId);
117
+ draggingRef.current = true;
118
+ setDragging(true);
119
+ /* A track is absolute by nature: pressing anywhere on it means "put
120
+ the thumb here", which is what a fader does under a finger. */
121
+ track(event);
122
+ },
123
+ onPointerMove: (event: PointerEvent<HTMLDivElement>) => {
124
+ if (!draggingRef.current || disabled) return;
125
+ track(event);
126
+ },
127
+ onPointerUp: () => {
128
+ draggingRef.current = false;
129
+ setDragging(false);
130
+ },
131
+ onPointerCancel: () => {
132
+ draggingRef.current = false;
133
+ setDragging(false);
134
+ },
135
+ onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => {
136
+ onKeyDown(event);
137
+ }
138
+ };
139
+
140
+ return { state, rootProps, setValue: set };
141
+ }
package/src/index.ts CHANGED
@@ -4,17 +4,140 @@ export type { AmbientProviderProps, AmbientTheme } from "./components/AmbientPro
4
4
  export { AmbientPanel } from "./components/AmbientPanel";
5
5
  export type { AmbientPanelProps } from "./components/AmbientPanel";
6
6
 
7
+ export { AmbientRack } from "./components/AmbientRack";
8
+ export type { AmbientRackProps, AmbientRackGap } from "./components/AmbientRack";
9
+
10
+ /* ------------------------------------------------------------------ *\
11
+ Presets — a mechanism with a set of parts already chosen. Start here.
12
+ \* ------------------------------------------------------------------ */
13
+
7
14
  export { AmbientButton } from "./components/AmbientButton";
8
- export type { AmbientButtonProps, AmbientButtonShape } from "./components/AmbientButton";
15
+ export type {
16
+ AmbientButtonProps,
17
+ AmbientButtonShape,
18
+ AmbientButtonSize
19
+ } from "./components/AmbientButton";
9
20
 
10
21
  export { AmbientSwitch } from "./components/AmbientSwitch";
11
22
  export type { AmbientSwitchProps, AmbientSwitchSize } from "./components/AmbientSwitch";
12
23
 
24
+ export { AmbientSelect } from "./components/AmbientSelect";
25
+ export type {
26
+ AmbientSelectProps,
27
+ AmbientSelectOption,
28
+ AmbientSelectSize,
29
+ AmbientSelectOrientation
30
+ } from "./components/AmbientSelect";
31
+
13
32
  export { AmbientKnob } from "./components/AmbientKnob";
14
- export type { AmbientKnobProps, AmbientKnobVariant } from "./components/AmbientKnob";
33
+ export type {
34
+ AmbientKnobProps,
35
+ AmbientKnobMarkers,
36
+ AmbientKnobIndicator,
37
+ AmbientKnobSize
38
+ } from "./components/AmbientKnob";
15
39
 
16
40
  export { AmbientFader } from "./components/AmbientFader";
17
- export type { AmbientFaderProps } from "./components/AmbientFader";
41
+ export type { AmbientFaderProps, AmbientFaderSize } from "./components/AmbientFader";
18
42
 
19
43
  export { AmbientSlider } from "./components/AmbientSlider";
20
- export type { AmbientSliderProps } from "./components/AmbientSlider";
44
+ export type { AmbientSliderProps, AmbientSliderSize } from "./components/AmbientSlider";
45
+
46
+ /* ------------------------------------------------------------------ *\
47
+ Mechanisms — kinematics, state and ARIA with no appearance at all.
48
+ Give one a set of parts and it becomes whatever you drew.
49
+ \* ------------------------------------------------------------------ */
50
+
51
+ export { AmbientRotary } from "./controls/AmbientRotary";
52
+ export type { AmbientRotaryProps } from "./controls/AmbientRotary";
53
+
54
+ export { AmbientTravel } from "./controls/AmbientTravel";
55
+ export type { AmbientTravelProps } from "./controls/AmbientTravel";
56
+
57
+ export { AmbientPress } from "./controls/AmbientPress";
58
+ export type { AmbientPressProps } from "./controls/AmbientPress";
59
+
60
+ export { AmbientLatch } from "./controls/AmbientLatch";
61
+ export type { AmbientLatchProps } from "./controls/AmbientLatch";
62
+
63
+ export { AmbientBank } from "./controls/AmbientBank";
64
+ export type { AmbientBankProps } from "./controls/AmbientBank";
65
+
66
+ /* ------------------------------------------------------------------ *\
67
+ Hooks — the mechanism without the markup, for a control you render
68
+ yourself from the ground up.
69
+ \* ------------------------------------------------------------------ */
70
+
71
+ export { useRotary } from "./core/useRotary";
72
+ export type { UseRotaryOptions, RotaryInput, RotaryTravel } from "./core/useRotary";
73
+
74
+ export { useTravel } from "./core/useTravel";
75
+ export type { UseTravelOptions, TravelOrientation } from "./core/useTravel";
76
+
77
+ export { usePress } from "./core/usePress";
78
+ export type { UsePressOptions, PressMode } from "./core/usePress";
79
+
80
+ export { useLatch } from "./core/useLatch";
81
+ export type { UseLatchOptions } from "./core/useLatch";
82
+
83
+ export { useBank } from "./core/useBank";
84
+ export type { UseBankOptions, BankOption, BankOrientation } from "./core/useBank";
85
+
86
+ /* ------------------------------------------------------------------ *\
87
+ Kits — a named bundle of parts, tokens and presentation defaults that
88
+ dresses every control below it. A kit is a plain object, so publishing
89
+ one is publishing a module. A kit that leaves a family undefined falls
90
+ through to `grounded`.
91
+ \* ------------------------------------------------------------------ */
92
+
93
+ export { AmbientKitProvider, useKit, useDress } from "./core/kit";
94
+ export type {
95
+ ControlKit,
96
+ ControlFamily,
97
+ KitDress,
98
+ KitLook,
99
+ KitDefaults
100
+ } from "./core/kit";
101
+
102
+ export { groundedKit } from "./kits/grounded";
103
+ export { consoleKit, ConsoleKnob, ConsoleToggle } from "./kits/console";
104
+ export type { ConsoleKnobProps, ConsoleToggleProps } from "./kits/console";
105
+
106
+ export {
107
+ ConsoleBar,
108
+ ConsoleMarks,
109
+ ConsoleWell,
110
+ ToggleTrack,
111
+ ToggleThumb
112
+ } from "./parts/console";
113
+
114
+ /* ------------------------------------------------------------------ *\
115
+ The state channel — read the enclosing control from inside a part.
116
+ The custom properties on the control root are canonical; these are a
117
+ typed view of the same values, for parts that need JS.
118
+ \* ------------------------------------------------------------------ */
119
+
120
+ export { useControlState, useBankKey } from "./core/context";
121
+ export type { BankKeyState } from "./core/context";
122
+ export type {
123
+ ControlParts,
124
+ ControlState,
125
+ ControlSize,
126
+ ControlAnimate,
127
+ FrameName
128
+ } from "./core/types";
129
+ export type { AmbientMaterial } from "./core/material";
130
+
131
+ /* ------------------------------------------------------------------ *\
132
+ Default parts. Nothing privileged about them — each is markup with
133
+ some ambient classes, and a replacement of yours stands on exactly the
134
+ same footing. They live in their own modules so an app that ships only
135
+ its own parts does not pay for these.
136
+ \* ------------------------------------------------------------------ */
137
+
138
+ export { KnobBody, KnurledFace, IndicatorDot, IndicatorBar, ScaleRing } from "./parts/knob";
139
+ export type { ScaleRingProps } from "./parts/knob";
140
+ export { TravelTrack, FaderCap, SliderThumb } from "./parts/travel";
141
+ export { ButtonCap } from "./parts/press";
142
+ export { SwitchTrack, SwitchPill, Led } from "./parts/latch";
143
+ export { KeyLens, KeyCap } from "./parts/bank";
@@ -0,0 +1,80 @@
1
+ import { cn } from "../lib/cn";
2
+ import { AmbientKnob } from "../components/AmbientKnob";
3
+ import type { AmbientKnobProps } from "../components/AmbientKnob";
4
+ import { AmbientSwitch } from "../components/AmbientSwitch";
5
+ import type { AmbientSwitchProps } from "../components/AmbientSwitch";
6
+ import type { ControlKit, KitDress, KitLook } from "../core/kit";
7
+ import { ConsoleBar, ConsoleMarks, ConsoleWell, ToggleThumb, ToggleTrack } from "../parts/console";
8
+
9
+ /**
10
+ * A mixer-desk visual identity: cuboid bar knobs seated in a circular
11
+ * groove, and pill toggles that light up with the panel accent.
12
+ *
13
+ * It dresses two families and leaves the other three to fall through to
14
+ * `grounded` — which is what a real third-party kit looks like. A kit is not
15
+ * obliged to have an opinion about everything.
16
+ */
17
+
18
+ function rotary(look: KitLook): KitDress {
19
+ const mark = look.mark !== false;
20
+ const legend = look.legend !== false;
21
+ return {
22
+ className: cn(
23
+ "amb-console-knob",
24
+ mark && "amb-console-knob-marked",
25
+ legend && "amb-console-knob-legended"
26
+ ),
27
+ parts: {
28
+ panel: mark || legend ? <ConsoleMarks mark={mark} legend={legend} /> : null,
29
+ base: <ConsoleWell />,
30
+ actuator: <ConsoleBar />
31
+ }
32
+ };
33
+ }
34
+
35
+ function latch(): KitDress {
36
+ return {
37
+ className: "amb-console-toggle",
38
+ parts: { base: <ToggleTrack />, actuator: <ToggleThumb /> }
39
+ };
40
+ }
41
+
42
+ export const consoleKit: ControlKit = {
43
+ name: "console",
44
+ rotary,
45
+ latch,
46
+ /* A visual identity may say how its controls feel to turn. The desk knob
47
+ this is drawn from is an absolute-position pot with a centre detent, so
48
+ it grabs where you press rather than tracking a drag. */
49
+ defaults: {
50
+ rotary: { input: "angle", travel: 280 }
51
+ },
52
+ looks: {
53
+ rotary: ["mark", "legend"],
54
+ latch: []
55
+ }
56
+ };
57
+
58
+ /* A kit can also ship presets of its own. They are three lines each — the
59
+ shared preset with this kit's look vocabulary typed — and they are how a
60
+ kit gives its own words the same compile-time checking `knurling` gets. */
61
+
62
+ export type ConsoleKnobProps = Omit<
63
+ AmbientKnobProps,
64
+ "look" | "material" | "knurling" | "knurlColor" | "markers" | "indicator"
65
+ > & {
66
+ /** The accent centre mark printed above the knob. */
67
+ mark?: boolean | undefined;
68
+ /** The −/+ legends at the ends of the travel. */
69
+ legend?: boolean | undefined;
70
+ };
71
+
72
+ export function ConsoleKnob({ mark, legend, ...rest }: ConsoleKnobProps) {
73
+ return <AmbientKnob {...rest} look={{ mark, legend }} />;
74
+ }
75
+
76
+ export type ConsoleToggleProps = Omit<AmbientSwitchProps, "look">;
77
+
78
+ export function ConsoleToggle(props: ConsoleToggleProps) {
79
+ return <AmbientSwitch {...props} />;
80
+ }
@@ -0,0 +1,113 @@
1
+ import { cn } from "../lib/cn";
2
+ import type { ControlKit, KitDress, KitLook } from "../core/kit";
3
+ import type { AmbientMaterial } from "../core/material";
4
+ import { IndicatorBar, IndicatorDot, KnobBody, KnurledFace, ScaleRing } from "../parts/knob";
5
+ import { FaderCap, SliderThumb, TravelTrack } from "../parts/travel";
6
+ import { ButtonCap } from "../parts/press";
7
+ import { SwitchPill, SwitchTrack } from "../parts/latch";
8
+ import { KeyCap, KeyLens } from "../parts/bank";
9
+
10
+ /* The built-in look, packaged as a kit rather than hardwired into the
11
+ presets. That is the integrity test for the whole idea: if the grounded
12
+ parts needed a private back door the presets could reach and a third party
13
+ could not, the abstraction would be a fiction. They do not — everything
14
+ below is the same `(look) => { parts, className }` a published kit writes. */
15
+
16
+ /* 12 divisions — 13 dots over the sweep, the pitch measured off the
17
+ reference panel. */
18
+ const FULL_MARKERS = 13;
19
+
20
+ function rotary(look: KitLook): KitDress {
21
+ const material = look.material as AmbientMaterial | undefined;
22
+ const knurling = look.knurling !== false;
23
+ const knurlColor = look.knurlColor as string | undefined;
24
+ const markers = (look.markers as "none" | "ends" | "full" | undefined) ?? "none";
25
+ const indicator = (look.indicator as "circle" | "rectangle" | undefined) ?? "circle";
26
+
27
+ return {
28
+ /* The full ring reaches past the knob's own box, so its layout clearance
29
+ has to be reserved — and only the kit knows it put a ring there. */
30
+ className: cn("amb-knob", markers === "full" && "amb-knob-markers-full"),
31
+ parts: {
32
+ panel:
33
+ markers === "none" ? null : <ScaleRing count={markers === "ends" ? 2 : FULL_MARKERS} />,
34
+ /* `material` goes on every element that paints. The cap always does —
35
+ it is the knob's top face either way — and the knurl ring does too
36
+ when there is one, so a knurled knob's rim and cap are the same
37
+ material rather than the rim being the only thing wearing it.
38
+
39
+ `knurlColor` is the one thing the ring may hold on its own: a
40
+ two-tone knob — dark grip round a pale cap — is a real piece of
41
+ hardware, and the cap has no matching prop because the cap's colour
42
+ is the control's colour, set the ordinary way with --amb-albedo. */
43
+ base: <KnobBody flush={!knurling} material={material} />,
44
+ actuator: (
45
+ <>
46
+ {knurling ? <KnurledFace material={material} color={knurlColor} /> : null}
47
+ {indicator === "circle" ? <IndicatorDot /> : <IndicatorBar />}
48
+ </>
49
+ )
50
+ }
51
+ };
52
+ }
53
+
54
+ function travel(look: KitLook): KitDress {
55
+ const material = look.material as AmbientMaterial | undefined;
56
+ const upright = look.orientation === "vertical";
57
+ return {
58
+ className: upright ? "amb-fader" : "amb-slider",
59
+ parts: {
60
+ base: <TravelTrack depth={upright ? "slot" : "channel"} />,
61
+ actuator: upright ? <FaderCap material={material} /> : <SliderThumb material={material} />
62
+ }
63
+ };
64
+ }
65
+
66
+ function press(look: KitLook): KitDress {
67
+ const shape = (look.shape as "pill" | "round" | "square" | undefined) ?? "pill";
68
+ return {
69
+ className: cn(
70
+ "amb-button amb-groove",
71
+ shape === "round" && "amb-button-round",
72
+ shape === "square" && "amb-button-square"
73
+ ),
74
+ parts: {
75
+ actuator: (
76
+ <ButtonCap material={(look.material as AmbientMaterial | undefined) ?? "matte"}>
77
+ {look.children as React.ReactNode}
78
+ </ButtonCap>
79
+ )
80
+ }
81
+ };
82
+ }
83
+
84
+ function latch(): KitDress {
85
+ return {
86
+ className: "amb-switch",
87
+ parts: { base: <SwitchTrack />, actuator: <SwitchPill /> }
88
+ };
89
+ }
90
+
91
+ function bank(): KitDress {
92
+ return {
93
+ className: "amb-select amb-groove",
94
+ parts: { base: <KeyLens />, actuator: <KeyCap /> }
95
+ };
96
+ }
97
+
98
+ /** The Blender-grounded hardware look: the default every preset falls back to. */
99
+ export const groundedKit: ControlKit = {
100
+ name: "grounded",
101
+ rotary,
102
+ travel,
103
+ press,
104
+ latch,
105
+ bank,
106
+ looks: {
107
+ rotary: ["material", "knurling", "knurlColor", "markers", "indicator"],
108
+ travel: ["material", "orientation"],
109
+ press: ["material", "shape", "children"],
110
+ latch: [],
111
+ bank: []
112
+ }
113
+ };
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+ import { useBankKey } from "../core/context";
4
+
5
+ /** The lamp under a key: a big disc lying on the pocket floor.
6
+ *
7
+ * It has to paint BEFORE the cap, because the cap's `backdrop-filter` is
8
+ * what diffuses it — which is the whole trick, and the reason the lens
9
+ * belongs in the `base` frame and the cap in `actuator`. */
10
+ export function KeyLens({ className }: { className?: string | undefined }) {
11
+ return <span className={cn("amb-select-lens", className)} />;
12
+ }
13
+
14
+ /** The translucent diffuser over the lamp, carrying the key's legend.
15
+ *
16
+ * With no children it prints the option's own label, which is why a bank
17
+ * of numerals needs no `renderKey`: the part reads the option it belongs
18
+ * to out of the key context. */
19
+ export function KeyCap({
20
+ className,
21
+ children
22
+ }: {
23
+ className?: string | undefined;
24
+ children?: ReactNode | undefined;
25
+ }) {
26
+ const { option } = useBankKey();
27
+ return (
28
+ <span className={cn("amb-select-cap ambient amb-chamfer amb-mat-glass", className)}>
29
+ {children ?? option.label ?? option.value}
30
+ </span>
31
+ );
32
+ }
@@ -0,0 +1,99 @@
1
+ import { cn } from "../lib/cn";
2
+
3
+ /* Parts for the `console` kit — a mixer-desk visual language, measured off
4
+ photographs of the real controls. Nothing here is a variant of a grounded
5
+ part: a bar knob and a lit pill track are a different vocabulary, which is
6
+ the reason kits exist rather than another round of boolean props. */
7
+
8
+ /** The knob's base: a flat face housed in a circular groove.
9
+ *
10
+ * Two elements, because they are two pieces of the panel: the groove is the
11
+ * cut, and the face is the flat disc sitting in it, showing the ring of the
12
+ * cut around itself. Neither carries `.ambient` — the face has no body, so
13
+ * there is no edge to cut and nothing to cast, and every cue that reads as
14
+ * depth here belongs either to the walls of the housing or to the bar
15
+ * standing on the face.
16
+ *
17
+ * It sits in the `base` frame, so it does not turn. A plain disc looks the
18
+ * same at every angle, and leaving it still keeps the scene's light on it
19
+ * untouched — only the bar has to do the work below. */
20
+ export function ConsoleWell({ className }: { className?: string | undefined }) {
21
+ return (
22
+ <span className={cn("amb-console-housing amb-groove", className)}>
23
+ <span className="amb-console-face amb-surface" />
24
+ </span>
25
+ );
26
+ }
27
+
28
+ /** The actuator: a cuboid bar lying across the disc on its diameter.
29
+ *
30
+ * Two spans, and the nesting is load-bearing. The bar rides the rotating
31
+ * actuator frame, so a chamfer highlight painted from the inherited light
32
+ * would turn with it and put the lit edge on the wrong side of the screen
33
+ * at half the angles. The fix is to rotate the LIGHT the other way: the
34
+ * outer span captures the scene's light vector, the inner one re-states it
35
+ * in the frame's own turned coordinates, so the bright edge and the drop
36
+ * shadow both stay put on screen while the bar sweeps under them.
37
+ *
38
+ * It has to be two elements because a custom property cannot read itself —
39
+ * `--amb-light-x: calc(var(--amb-light-x) ...)` is a cycle, which resolves
40
+ * to invalid at computed-value time and takes the whole `box-shadow`
41
+ * composite down with it, silently. */
42
+ export function ConsoleBar({ className }: { className?: string | undefined }) {
43
+ return (
44
+ <span className={cn("amb-console-bar", className)}>
45
+ <span className="amb-console-bar-body ambient amb-surface amb-chamfer amb-thickness-2">
46
+ {/* Printed on the bar near one end, the way the reference prints a
47
+ short black mark at the outer edge of its pointer: with a bar
48
+ that crosses the whole face, this is what says which end reads. */}
49
+ <span className="amb-console-indicator" />
50
+ </span>
51
+ </span>
52
+ );
53
+ }
54
+
55
+ /** Panel graphics around the knob: the accent centre mark above it, and the
56
+ * −/+ legends at the ends of the travel. Both sit outside the knob's own
57
+ * box, which is what the `panel` frame is for. */
58
+ export function ConsoleMarks({
59
+ mark = true,
60
+ legend = true,
61
+ className
62
+ }: {
63
+ mark?: boolean | undefined;
64
+ legend?: boolean | undefined;
65
+ className?: string | undefined;
66
+ }) {
67
+ return (
68
+ <span className={cn("amb-console-marks", className)} aria-hidden>
69
+ {mark ? <span className="amb-console-mark" /> : null}
70
+ {legend ? (
71
+ <>
72
+ <span className="amb-console-legend amb-console-legend-min">−</span>
73
+ <span className="amb-console-legend amb-console-legend-max">+</span>
74
+ </>
75
+ ) : null}
76
+ </span>
77
+ );
78
+ }
79
+
80
+ /** The toggle's track: a pill groove that fills with the accent as the
81
+ * switch travels.
82
+ *
83
+ * A pure-CSS part — it reads `--ambx-percent` off the control root and
84
+ * mixes its own colour from it, so the mechanism does not know this element
85
+ * exists and no React state reaches it. */
86
+ export function ToggleTrack({ className }: { className?: string | undefined }) {
87
+ return <span className={cn("amb-console-track amb-groove", className)} />;
88
+ }
89
+
90
+ /** The travelling thumb: an accent disc inside a white ring.
91
+ *
92
+ * Flat on top and deliberately so — no chamfer, no fillet — but still a
93
+ * knob-scale body, so it casts. That pairing is why the classes are spelt
94
+ * out rather than reached through `.amb-fillet-2`: the edge treatments set
95
+ * a thickness of their own, and here the thickness is wanted without the
96
+ * cut that usually comes with it. */
97
+ export function ToggleThumb({ className }: { className?: string | undefined }) {
98
+ return <span className={cn("amb-console-thumb ambient amb-thickness-2", className)} />;
99
+ }