@ambientcss/components 1.2.1 → 2.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.
@@ -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={cn("ambient amb-knob amb-chamfer amb-elevation-2 amb-surface ambx-knob", material && `amb-mat-${material}`)}
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
- <div className="ambx-knob-rotation" style={{ transform: `rotate(${rotation}deg)` }}>
131
- <div className="amb-knob-indicator">
132
- <div className="amb-knob-grip">
133
- <span className="amb-fader-dot" />
134
- <span className="amb-fader-dot" />
135
- <span className="amb-fader-dot" />
136
- </div>
137
- </div>
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 amb-elevation-1 ambx-slider-thumb", material !== "glass" && "amb-surface-concave-h", material && `amb-mat-${material}`)}
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
- "ambient amb-switch amb-chamfer amb-elevation-1 amb-heading-3",
42
- active ? "amb-surface-convex amb-switch-on" : "amb-surface-concave",
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";