@ambientcss/components 2.1.0 → 3.0.1
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 +1544 -392
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +750 -52
- package/dist/index.d.ts +750 -52
- package/dist/index.js +1505 -392
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1045 -298
- package/package.json +10 -4
- package/src/components/AmbientButton.tsx +23 -27
- package/src/components/AmbientFader.tsx +29 -115
- package/src/components/AmbientKnob.tsx +58 -220
- package/src/components/AmbientPanel.tsx +8 -2
- package/src/components/AmbientProvider.tsx +3 -0
- package/src/components/AmbientSelect.tsx +40 -0
- package/src/components/AmbientSlider.tsx +28 -113
- package/src/components/AmbientSwitch.tsx +58 -58
- package/src/controls/AmbientBank.tsx +139 -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 +165 -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 +122 -2
- 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 +101 -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 +1045 -298
|
@@ -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
|
@@ -7,17 +7,137 @@ export type { AmbientPanelProps } from "./components/AmbientPanel";
|
|
|
7
7
|
export { AmbientRack } from "./components/AmbientRack";
|
|
8
8
|
export type { AmbientRackProps, AmbientRackGap } from "./components/AmbientRack";
|
|
9
9
|
|
|
10
|
+
/* ------------------------------------------------------------------ *\
|
|
11
|
+
Presets — a mechanism with a set of parts already chosen. Start here.
|
|
12
|
+
\* ------------------------------------------------------------------ */
|
|
13
|
+
|
|
10
14
|
export { AmbientButton } from "./components/AmbientButton";
|
|
11
|
-
export type {
|
|
15
|
+
export type {
|
|
16
|
+
AmbientButtonProps,
|
|
17
|
+
AmbientButtonShape,
|
|
18
|
+
AmbientButtonSize
|
|
19
|
+
} from "./components/AmbientButton";
|
|
12
20
|
|
|
13
21
|
export { AmbientSwitch } from "./components/AmbientSwitch";
|
|
14
22
|
export type { AmbientSwitchProps, AmbientSwitchSize } from "./components/AmbientSwitch";
|
|
15
23
|
|
|
24
|
+
export { AmbientSelect } from "./components/AmbientSelect";
|
|
25
|
+
export type {
|
|
26
|
+
AmbientSelectProps,
|
|
27
|
+
AmbientSelectOption,
|
|
28
|
+
AmbientSelectSize,
|
|
29
|
+
AmbientSelectOrientation
|
|
30
|
+
} from "./components/AmbientSelect";
|
|
31
|
+
|
|
16
32
|
export { AmbientKnob } from "./components/AmbientKnob";
|
|
17
|
-
export type {
|
|
33
|
+
export type {
|
|
34
|
+
AmbientKnobProps,
|
|
35
|
+
AmbientKnobMarkers,
|
|
36
|
+
AmbientKnobIndicator,
|
|
37
|
+
AmbientKnobSize
|
|
38
|
+
} from "./components/AmbientKnob";
|
|
18
39
|
|
|
19
40
|
export { AmbientFader } from "./components/AmbientFader";
|
|
20
41
|
export type { AmbientFaderProps, AmbientFaderSize } from "./components/AmbientFader";
|
|
21
42
|
|
|
22
43
|
export { AmbientSlider } from "./components/AmbientSlider";
|
|
23
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,101 @@
|
|
|
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 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 amb-surface" /> : 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 albedo disc inside a light 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. It wears `amb-surface`, so its colour IS
|
|
97
|
+
* the scene's albedo under the current light rather than a fixed paint —
|
|
98
|
+
* it re-shades when the lamp moves or the intensities change. */
|
|
99
|
+
export function ToggleThumb({ className }: { className?: string | undefined }) {
|
|
100
|
+
return <span className={cn("amb-console-thumb ambient amb-surface amb-thickness-2", className)} />;
|
|
101
|
+
}
|