@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.
Files changed (45) hide show
  1. package/README.md +35 -0
  2. package/dist/index.cjs +1544 -392
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +750 -52
  5. package/dist/index.d.ts +750 -52
  6. package/dist/index.js +1505 -392
  7. package/dist/index.js.map +1 -1
  8. package/dist/styles.css +1045 -298
  9. package/package.json +10 -4
  10. package/src/components/AmbientButton.tsx +23 -27
  11. package/src/components/AmbientFader.tsx +29 -115
  12. package/src/components/AmbientKnob.tsx +58 -220
  13. package/src/components/AmbientPanel.tsx +8 -2
  14. package/src/components/AmbientProvider.tsx +3 -0
  15. package/src/components/AmbientSelect.tsx +40 -0
  16. package/src/components/AmbientSlider.tsx +28 -113
  17. package/src/components/AmbientSwitch.tsx +58 -58
  18. package/src/controls/AmbientBank.tsx +139 -0
  19. package/src/controls/AmbientLatch.tsx +78 -0
  20. package/src/controls/AmbientPress.tsx +74 -0
  21. package/src/controls/AmbientRotary.tsx +94 -0
  22. package/src/controls/AmbientTravel.tsx +83 -0
  23. package/src/core/context.tsx +46 -0
  24. package/src/core/controllable.ts +33 -0
  25. package/src/core/dev.ts +15 -0
  26. package/src/core/frames.tsx +63 -0
  27. package/src/core/kit.tsx +107 -0
  28. package/src/core/material.ts +27 -0
  29. package/src/core/numeric.ts +88 -0
  30. package/src/core/types.ts +116 -0
  31. package/src/core/useBank.ts +165 -0
  32. package/src/core/useLatch.ts +54 -0
  33. package/src/core/usePress.ts +120 -0
  34. package/src/core/useRotary.ts +253 -0
  35. package/src/core/useTravel.ts +141 -0
  36. package/src/index.ts +122 -2
  37. package/src/kits/console.tsx +80 -0
  38. package/src/kits/grounded.tsx +113 -0
  39. package/src/parts/bank.tsx +32 -0
  40. package/src/parts/console.tsx +101 -0
  41. package/src/parts/knob.tsx +203 -0
  42. package/src/parts/latch.tsx +33 -0
  43. package/src/parts/press.tsx +56 -0
  44. package/src/parts/travel.tsx +70 -0
  45. package/src/styles.css +1045 -298
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ function AmbientPanel({ className, material = "matte", ...props }) {
42
42
  {
43
43
  className: cn(
44
44
  "ambient amb-surface amb-chamfer amb-elevation-2 ambx-panel",
45
- material === "matte" ? "amb-mat-matte" : material === "shiny" ? "amb-mat-shiny" : "amb-mat-glass",
45
+ `amb-mat-${material}`,
46
46
  className
47
47
  ),
48
48
  ...props
@@ -73,497 +73,1610 @@ function AmbientRack({
73
73
  );
74
74
  }
75
75
 
76
- // src/components/AmbientButton.tsx
77
- import { jsx as jsx4 } from "react/jsx-runtime";
78
- function AmbientButton({
76
+ // src/controls/AmbientPress.tsx
77
+ import { useRef as useRef4 } from "react";
78
+
79
+ // src/core/context.tsx
80
+ import { createContext, useContext } from "react";
81
+ var ControlStateContext = createContext(null);
82
+ var ControlStateProvider = ControlStateContext.Provider;
83
+ function useControlState() {
84
+ const state = useContext(ControlStateContext);
85
+ if (!state) {
86
+ throw new Error(
87
+ "useControlState() must be called from inside a control's parts. Pass the component through `parts` on AmbientRotary, AmbientTravel, AmbientPress, AmbientLatch or AmbientBank."
88
+ );
89
+ }
90
+ return state;
91
+ }
92
+ var BankKeyContext = createContext(null);
93
+ var BankKeyProvider = BankKeyContext.Provider;
94
+ function useBankKey() {
95
+ const key = useContext(BankKeyContext);
96
+ if (!key) {
97
+ throw new Error(
98
+ "useBankKey() must be called from inside a bank key's parts. Pass the component through `keyParts` on AmbientBank."
99
+ );
100
+ }
101
+ return key;
102
+ }
103
+
104
+ // src/core/frames.tsx
105
+ import { useEffect, useRef } from "react";
106
+
107
+ // src/core/dev.ts
108
+ var isDev = (() => {
109
+ try {
110
+ return process.env.NODE_ENV !== "production";
111
+ } catch {
112
+ return false;
113
+ }
114
+ })();
115
+
116
+ // src/core/types.ts
117
+ var FRAME_ORDER = ["panel", "base", "actuator", "fixture"];
118
+ function stateStyle(state) {
119
+ return {
120
+ "--ambx-value": state.value,
121
+ "--ambx-percent": state.percent,
122
+ "--ambx-angle": `${state.angle}deg`,
123
+ "--ambx-travel-start": `${state.travelStart}deg`,
124
+ "--ambx-travel-sweep": `${state.travelSweep}deg`,
125
+ "--ambx-detents": state.detents
126
+ };
127
+ }
128
+ function stateData(state) {
129
+ return {
130
+ "data-dragging": state.dragging ? "" : void 0,
131
+ "data-disabled": state.disabled ? "" : void 0,
132
+ "data-at-min": state.atMin ? "" : void 0,
133
+ "data-at-max": state.atMax ? "" : void 0
134
+ };
135
+ }
136
+ function sizeProps(family, size) {
137
+ if (size === void 0) return {};
138
+ if (size === "sm" || size === "md" || size === "lg") {
139
+ return { className: `ambx-${family}-${size}` };
140
+ }
141
+ return { style: { "--ambx-size": size } };
142
+ }
143
+
144
+ // src/core/frames.tsx
145
+ import { Fragment, jsx as jsx4 } from "react/jsx-runtime";
146
+ function Frames({ parts }) {
147
+ if (!parts) return null;
148
+ return /* @__PURE__ */ jsx4(Fragment, { children: FRAME_ORDER.map(
149
+ (name) => parts[name] == null ? null : /* @__PURE__ */ jsx4("div", { "data-frame": name, className: `ambx-frame ambx-frame-${name}`, children: parts[name] }, name)
150
+ ) });
151
+ }
152
+ var FOCUSABLE = "a[href], button, input, select, textarea, [tabindex], [contenteditable=true],[role=button], [role=checkbox], [role=radio], [role=slider], [role=switch], [role=link]";
153
+ function useDevPartCheck(ref, control) {
154
+ const warned2 = useRef(false);
155
+ useEffect(() => {
156
+ if (!isDev || warned2.current) return;
157
+ const root = ref.current;
158
+ if (!root) return;
159
+ const offenders = root.querySelectorAll(`[data-frame] :is(${FOCUSABLE})`);
160
+ if (offenders.length === 0) return;
161
+ warned2.current = true;
162
+ const tags = Array.from(offenders, (node) => `<${node.tagName.toLowerCase()}>`).join(", ");
163
+ console.warn(
164
+ `[@ambientcss/components] ${control}: a part contains a focusable element (${tags}). Parts are presentational \u2014 the control root already owns the role, the tab stop and the keyboard handler, so this creates a second tab stop and usually a conflicting role.`
165
+ );
166
+ }, [ref, control]);
167
+ }
168
+
169
+ // src/core/usePress.ts
170
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useRef as useRef3, useState as useState2 } from "react";
171
+
172
+ // src/core/controllable.ts
173
+ import { useCallback, useRef as useRef2, useState } from "react";
174
+ function useControllableValue(controlled, defaultValue, onChange) {
175
+ const isControlled = controlled !== void 0;
176
+ const [internal, setInternal] = useState(defaultValue);
177
+ const onChangeRef = useRef2(onChange);
178
+ onChangeRef.current = onChange;
179
+ const controlledRef = useRef2(isControlled);
180
+ controlledRef.current = isControlled;
181
+ const set = useCallback((next) => {
182
+ if (!controlledRef.current) setInternal(next);
183
+ onChangeRef.current?.(next);
184
+ }, []);
185
+ return [isControlled ? controlled : internal, set];
186
+ }
187
+
188
+ // src/core/usePress.ts
189
+ function usePress(options) {
190
+ const {
191
+ mode = "momentary",
192
+ repeatDelay = 400,
193
+ repeatInterval = 60,
194
+ disabled = false
195
+ } = options;
196
+ const [on, setOn] = useControllableValue(
197
+ options.value,
198
+ options.defaultValue ?? false,
199
+ options.onChange
200
+ );
201
+ const [held, setHeld] = useState2(false);
202
+ const onPressRef = useRef3(options.onPress);
203
+ onPressRef.current = options.onPress;
204
+ const timers = useRef3(
205
+ {}
206
+ );
207
+ const stopRepeat = useCallback2(() => {
208
+ if (timers.current.delay) clearTimeout(timers.current.delay);
209
+ if (timers.current.tick) clearInterval(timers.current.tick);
210
+ timers.current = {};
211
+ }, []);
212
+ useEffect2(() => stopRepeat, [stopRepeat]);
213
+ const activate = useCallback2(() => {
214
+ if (mode === "toggle") setOn(!on);
215
+ onPressRef.current?.();
216
+ }, [mode, on, setOn]);
217
+ const pressed = mode === "toggle" ? on : held;
218
+ const state = useMemo(
219
+ () => ({
220
+ value: pressed ? 1 : 0,
221
+ min: 0,
222
+ max: 1,
223
+ percent: pressed ? 1 : 0,
224
+ angle: 0,
225
+ travelStart: 0,
226
+ travelSweep: 0,
227
+ detents: 2,
228
+ dragging: held,
229
+ disabled,
230
+ atMin: !pressed,
231
+ atMax: pressed
232
+ }),
233
+ [pressed, held, disabled]
234
+ );
235
+ const rootProps = {
236
+ type: "button",
237
+ disabled,
238
+ "aria-pressed": mode === "toggle" ? on : void 0,
239
+ style: stateStyle(state),
240
+ "data-pressed": pressed ? "" : void 0,
241
+ "data-mode": mode,
242
+ ...stateData(state),
243
+ onPointerDown: (event) => {
244
+ if (disabled || event.button !== 0) return;
245
+ setHeld(true);
246
+ if (mode !== "repeat") return;
247
+ onPressRef.current?.();
248
+ timers.current.delay = setTimeout(() => {
249
+ timers.current.tick = setInterval(() => onPressRef.current?.(), repeatInterval);
250
+ }, repeatDelay);
251
+ },
252
+ onPointerUp: () => {
253
+ setHeld(false);
254
+ stopRepeat();
255
+ },
256
+ onPointerCancel: () => {
257
+ setHeld(false);
258
+ stopRepeat();
259
+ },
260
+ onPointerLeave: () => {
261
+ setHeld(false);
262
+ stopRepeat();
263
+ },
264
+ onClick: () => {
265
+ if (mode !== "repeat") activate();
266
+ }
267
+ };
268
+ return { state, rootProps, pressed, on, setOn };
269
+ }
270
+
271
+ // src/controls/AmbientPress.tsx
272
+ import { jsx as jsx5, jsxs } from "react/jsx-runtime";
273
+ function AmbientPress({
274
+ parts,
275
+ size,
79
276
  className,
277
+ mode,
278
+ value,
279
+ defaultValue,
280
+ onChange,
281
+ onPress,
282
+ repeatDelay,
283
+ repeatInterval,
284
+ disabled,
80
285
  children,
81
- material = "matte",
82
- shape = "pill",
83
- size = "md",
84
- ...props
286
+ ...rest
85
287
  }) {
86
- return /* @__PURE__ */ jsx4(
288
+ const ref = useRef4(null);
289
+ const { state, rootProps } = usePress({
290
+ mode,
291
+ value,
292
+ defaultValue,
293
+ onChange,
294
+ onPress,
295
+ repeatDelay,
296
+ repeatInterval,
297
+ disabled
298
+ });
299
+ useDevPartCheck(ref, "AmbientPress");
300
+ const sized = sizeProps("press", size);
301
+ const { style: restStyle, onClick, ...restProps } = rest;
302
+ return /* @__PURE__ */ jsx5(
87
303
  "button",
88
304
  {
89
- type: "button",
305
+ ...restProps,
306
+ ...rootProps,
307
+ ref,
308
+ className: cn("ambx-control ambx-press", sized.className, className),
309
+ style: { ...rootProps.style, ...sized.style, ...restStyle },
310
+ onClick: (event) => {
311
+ rootProps.onClick();
312
+ onClick?.(event);
313
+ },
314
+ children: /* @__PURE__ */ jsxs(ControlStateProvider, { value: state, children: [
315
+ /* @__PURE__ */ jsx5(Frames, { parts }),
316
+ children
317
+ ] })
318
+ }
319
+ );
320
+ }
321
+
322
+ // src/core/kit.tsx
323
+ import { createContext as createContext2, useContext as useContext2 } from "react";
324
+ import { jsx as jsx6 } from "react/jsx-runtime";
325
+ var KitContext = createContext2(null);
326
+ function AmbientKitProvider({ kit, children }) {
327
+ return /* @__PURE__ */ jsx6(KitContext.Provider, { value: kit, children });
328
+ }
329
+ function useKit() {
330
+ return useContext2(KitContext);
331
+ }
332
+ var warned = /* @__PURE__ */ new Set();
333
+ function useDress(family, look, fallback) {
334
+ const kit = useKit();
335
+ const dressed = kit?.[family];
336
+ if (isDev && kit && dressed) {
337
+ const honoured = kit.looks?.[family];
338
+ if (honoured) {
339
+ for (const key of Object.keys(look)) {
340
+ if (look[key] === void 0 || honoured.includes(key)) continue;
341
+ const id = `${kit.name}:${family}:${key}`;
342
+ if (warned.has(id)) continue;
343
+ warned.add(id);
344
+ console.warn(
345
+ `[@ambientcss/components] the "${kit.name}" kit's ${family} does not use \`${key}\` \u2014 it is a look prop from another kit's vocabulary, so it will have no effect here.`
346
+ );
347
+ }
348
+ }
349
+ }
350
+ return {
351
+ dress: (dressed ?? fallback)(look),
352
+ /* Defaults come from the kit that actually dressed the control: a kit
353
+ that falls through to grounded for a family has no say in how that
354
+ family behaves either. */
355
+ defaults: dressed ? kit?.defaults?.[family] : void 0
356
+ };
357
+ }
358
+
359
+ // src/parts/knob.tsx
360
+ import { useId } from "react";
361
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
362
+ var KNURLS = {
363
+ /* 48 ribs — the referent lineup's fine knurl (referents.py knob_cap /
364
+ knob_wheel) rather than the 36 of its coarse one, because a band a tenth
365
+ of the radius wide reads as a machined grip only if the ribs are finer
366
+ than it is; at 36 the same band came out a bottle cap. */
367
+ standard: { teeth: 48, depth: 9e-3, sharpness: 1.6, band: 0.05 }
368
+ };
369
+ var KNURL_BAND = KNURLS.standard.band;
370
+ var SEAM = 5e-3;
371
+ var STEPS = 6;
372
+ function knurlPath({ teeth, depth, sharpness, band }) {
373
+ const outer = 0.5;
374
+ const inner = outer - band - SEAM;
375
+ const segs = teeth * STEPS;
376
+ const pts = [];
377
+ for (let i = 0; i < segs; i++) {
378
+ const t = i / segs * Math.PI * 2;
379
+ const r2 = outer - depth * (0.5 + 0.5 * Math.cos(teeth * t)) ** sharpness;
380
+ pts.push(
381
+ `${(0.5 + r2 * Math.cos(t)).toFixed(4)} ${(0.5 + r2 * Math.sin(t)).toFixed(4)}`
382
+ );
383
+ }
384
+ const l = (0.5 - inner).toFixed(4);
385
+ const r = (0.5 + inner).toFixed(4);
386
+ const hole = `M${l} 0.5 A${inner} ${inner} 0 0 0 ${r} 0.5 A${inner} ${inner} 0 0 0 ${l} 0.5 Z`;
387
+ return `M${pts.join(" L")} Z ${hole}`;
388
+ }
389
+ var KNURL_PATH = knurlPath(KNURLS.standard);
390
+ function KnobBody({
391
+ material,
392
+ flush = false,
393
+ className
394
+ }) {
395
+ return /* @__PURE__ */ jsx7(
396
+ "span",
397
+ {
90
398
  className: cn(
91
- "amb-button amb-groove ambx-button",
92
- `ambx-button-${size}`,
93
- shape === "round" && "amb-button-round",
94
- shape === "square" && "amb-button-square",
399
+ "amb-knob-body ambient amb-thickness-2 amb-surface",
400
+ material && `amb-mat-${material}`,
95
401
  className
96
402
  ),
97
- ...props,
98
- children: /* @__PURE__ */ jsx4(
99
- "span",
100
- {
101
- className: cn(
102
- "amb-button-cap ambient amb-chamfer amb-surface amb-heading-3",
103
- material === "matte" ? "amb-mat-matte" : material === "shiny" ? "amb-mat-shiny" : "amb-mat-glass"
104
- ),
105
- children
403
+ style: flush ? void 0 : { inset: `${KNURL_BAND * 100}%` }
404
+ }
405
+ );
406
+ }
407
+ function KnurledFace({
408
+ material,
409
+ color,
410
+ className
411
+ }) {
412
+ const id = `amb-knurl-${useId().replace(/:/g, "")}`;
413
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
414
+ /* @__PURE__ */ jsx7("svg", { width: 0, height: 0, style: { position: "absolute" }, "aria-hidden": true, focusable: false, children: /* @__PURE__ */ jsx7("defs", { children: /* @__PURE__ */ jsx7("clipPath", { id, clipPathUnits: "objectBoundingBox", children: /* @__PURE__ */ jsx7("path", { d: KNURL_PATH, clipRule: "evenodd" }) }) }) }),
415
+ /* @__PURE__ */ jsx7(
416
+ "span",
417
+ {
418
+ className: cn("amb-knob-face", material && `amb-mat-${material}`, className),
419
+ style: {
420
+ clipPath: `url(#${id})`,
421
+ /* Not a paint colour: --amb-albedo is the ribs' REFLECTANCE, so a
422
+ dark knurl still takes the scene's exposure, the lamp's cast and
423
+ the rim's own --amb-shade step, and still goes dark when the
424
+ lights do. Inline, so it beats the albedo a micro-relief material
425
+ would otherwise set on this element — an explicit colour wins
426
+ over the finish's own, and the finish keeps its grain. */
427
+ ...color ? { "--amb-albedo": color } : null
106
428
  }
429
+ }
430
+ )
431
+ ] });
432
+ }
433
+ function IndicatorDot({ className }) {
434
+ return /* @__PURE__ */ jsx7("span", { className: cn("amb-knob-indicator-circle", className) });
435
+ }
436
+ function IndicatorBar({ className }) {
437
+ return /* @__PURE__ */ jsx7("span", { className: cn("amb-knob-indicator-rectangle", className) });
438
+ }
439
+ function ScaleRing({ count = 13, className, children }) {
440
+ const { travelStart, travelSweep } = useControlState();
441
+ const divisions = Math.max(1, count - 1);
442
+ const angles = count <= 1 ? [travelStart] : Array.from({ length: count }, (_, i) => travelStart + i / divisions * travelSweep);
443
+ return /* @__PURE__ */ jsxs2("span", { className: cn("amb-knob-marker-ring", className), "aria-hidden": true, children: [
444
+ angles.map((angle) => /* @__PURE__ */ jsx7(
445
+ "span",
446
+ {
447
+ className: "amb-knob-marker",
448
+ style: { "--amb-marker-angle": `${angle}deg` }
449
+ },
450
+ angle
451
+ )),
452
+ children
453
+ ] });
454
+ }
455
+
456
+ // src/parts/travel.tsx
457
+ import { jsx as jsx8 } from "react/jsx-runtime";
458
+ function TravelTrack({
459
+ depth = "slot",
460
+ className
461
+ }) {
462
+ return /* @__PURE__ */ jsx8(
463
+ "span",
464
+ {
465
+ className: cn(
466
+ "amb-travel-track amb-groove",
467
+ depth === "channel" && "amb-travel-track-channel",
468
+ className
469
+ )
470
+ }
471
+ );
472
+ }
473
+ function FaderCap({
474
+ material,
475
+ className
476
+ }) {
477
+ return /* @__PURE__ */ jsx8(
478
+ "span",
479
+ {
480
+ className: cn(
481
+ "amb-fader-thumb ambient amb-fillet",
482
+ material !== "glass" && "amb-surface-concave",
483
+ material && `amb-mat-${material}`,
484
+ className
485
+ ),
486
+ children: /* @__PURE__ */ jsx8("span", { className: "amb-fader-gripline" })
487
+ }
488
+ );
489
+ }
490
+ function SliderThumb({
491
+ material,
492
+ className
493
+ }) {
494
+ return /* @__PURE__ */ jsx8(
495
+ "span",
496
+ {
497
+ className: cn(
498
+ "amb-slider-thumb ambient amb-fillet",
499
+ material !== "glass" && "amb-surface-convex",
500
+ material && `amb-mat-${material}`,
501
+ className
107
502
  )
108
503
  }
109
504
  );
110
505
  }
111
506
 
112
- // src/components/AmbientSwitch.tsx
113
- import { useId, useState } from "react";
114
- import { jsx as jsx5, jsxs } from "react/jsx-runtime";
115
- function AmbientSwitch({
507
+ // src/core/material.ts
508
+ function isRelief(material) {
509
+ return material === "brushed" || material === "brushed-round" || material === "blasted";
510
+ }
511
+
512
+ // src/parts/press.tsx
513
+ import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
514
+ function ButtonCap({
515
+ material = "matte",
116
516
  className,
117
- checked,
118
- defaultChecked,
119
- onCheckedChange,
120
- onClick,
121
- size = "md",
122
- label,
123
- led,
124
- children,
125
- ...props
517
+ children
126
518
  }) {
127
- const labelId = useId();
128
- const isControlled = checked !== void 0;
129
- const [internalChecked, setInternalChecked] = useState(defaultChecked ?? false);
130
- const active = isControlled ? checked ?? false : internalChecked;
131
- const button = /* @__PURE__ */ jsxs(
132
- "button",
519
+ const relief = isRelief(material);
520
+ return /* @__PURE__ */ jsxs3(
521
+ "span",
133
522
  {
134
- type: "button",
135
- role: "switch",
136
- "aria-checked": active,
137
- "aria-labelledby": label ? labelId : props["aria-labelledby"],
138
523
  className: cn(
139
- "amb-switch",
140
- active && "amb-switch-on",
141
- "ambx-switch",
142
- `ambx-switch-${size}`,
524
+ "amb-button-cap ambient amb-chamfer amb-surface amb-heading-3",
525
+ relief ? void 0 : `amb-mat-${material}`,
143
526
  className
144
527
  ),
145
- onClick: (event) => {
146
- const next = !active;
147
- if (!isControlled) setInternalChecked(next);
148
- onCheckedChange?.(next);
149
- onClick?.(event);
150
- },
151
- ...props,
152
528
  children: [
153
- led ? /* @__PURE__ */ jsx5(
529
+ relief ? /* @__PURE__ */ jsx9(
154
530
  "span",
155
531
  {
156
- className: cn("amb-led", !active && "amb-led-off"),
157
- style: typeof led === "string" ? { "--amb-led-color": led } : void 0
532
+ className: cn("ambx-cap-face ambient amb-chamfer amb-surface", `amb-mat-${material}`),
533
+ "aria-hidden": true
158
534
  }
159
535
  ) : null,
160
- /* @__PURE__ */ jsx5("span", { className: "amb-switch-track amb-groove", children: /* @__PURE__ */ jsx5("span", { className: "amb-switch-pill ambient amb-fillet amb-surface-convex" }) }),
161
536
  children
162
537
  ]
163
538
  }
164
539
  );
165
- if (!label) return button;
166
- return /* @__PURE__ */ jsxs("div", { className: "ambx-stack", children: [
167
- button,
168
- /* @__PURE__ */ jsx5("span", { id: labelId, className: "ambx-label", children: label })
169
- ] });
170
540
  }
171
541
 
172
- // src/components/AmbientKnob.tsx
173
- import { useId as useId2, useRef } from "react";
174
- import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
175
- var KNURLS = {
176
- /* 36 ribs — the grounded referent knob (dot and line variants) */
177
- standard: { teeth: 36, root: 0.468, rise: 0.12, fall: [0.5, 0.62] },
178
- /* 14 broad flutes with deeper roots (OP-Z-style flute variant) */
179
- flute: { teeth: 14, root: 0.44, rise: 0.08, fall: [0.72, 0.8] },
180
- /* 48-rib fine knurl, shallower (cap and wheel variants) */
181
- fine: { teeth: 48, root: 0.476, rise: 0.12, fall: [0.5, 0.62] }
182
- };
183
- function knurlPath({ teeth, root, rise, fall }) {
184
- const outer = 0.5;
185
- const pitch = Math.PI * 2 / teeth;
186
- const pts = [];
187
- for (let i = 0; i < teeth; i++) {
188
- const a = i * pitch;
189
- const tooth = [
190
- [0, root],
191
- [rise, outer],
192
- [fall[0], outer],
193
- [fall[1], root]
194
- ];
195
- for (const [frac, radius] of tooth) {
196
- const t = a + frac * pitch;
197
- pts.push(
198
- `${(0.5 + radius * Math.cos(t)).toFixed(4)} ${(0.5 + radius * Math.sin(t)).toFixed(4)}`
199
- );
542
+ // src/parts/latch.tsx
543
+ import { jsx as jsx10 } from "react/jsx-runtime";
544
+ function SwitchTrack({ className }) {
545
+ return /* @__PURE__ */ jsx10("span", { className: cn("amb-switch-track amb-groove", className) });
546
+ }
547
+ function SwitchPill({ className }) {
548
+ return /* @__PURE__ */ jsx10("span", { className: cn("amb-switch-pill ambient amb-fillet amb-surface-convex", className) });
549
+ }
550
+ function Led({
551
+ on = true,
552
+ color,
553
+ className
554
+ }) {
555
+ return /* @__PURE__ */ jsx10(
556
+ "span",
557
+ {
558
+ className: cn("amb-led", !on && "amb-led-off", className),
559
+ style: color ? { "--amb-led-color": color } : void 0
200
560
  }
201
- }
202
- return `M${pts.join(" L")} Z`;
561
+ );
203
562
  }
204
- var KNURL_PATHS = {
205
- standard: knurlPath(KNURLS.standard),
206
- flute: knurlPath(KNURLS.flute),
207
- fine: knurlPath(KNURLS.fine)
208
- };
209
- var VARIANT_FAMILY = {
210
- dot: "standard",
211
- line: "standard",
212
- flute: "flute",
213
- cap: "fine",
214
- wheel: "fine"
563
+
564
+ // src/parts/bank.tsx
565
+ import { jsx as jsx11 } from "react/jsx-runtime";
566
+ function KeyLens({ className }) {
567
+ return /* @__PURE__ */ jsx11("span", { className: cn("amb-select-lens", className) });
568
+ }
569
+ function KeyCap({
570
+ className,
571
+ children
572
+ }) {
573
+ const { option } = useBankKey();
574
+ return /* @__PURE__ */ jsx11("span", { className: cn("amb-select-cap ambient amb-chamfer amb-mat-glass", className), children: children ?? option.label ?? option.value });
575
+ }
576
+
577
+ // src/kits/grounded.tsx
578
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
579
+ var FULL_MARKERS = 13;
580
+ function rotary(look) {
581
+ const material = look.material;
582
+ const knurling = look.knurling !== false;
583
+ const knurlColor = look.knurlColor;
584
+ const markers = look.markers ?? "none";
585
+ const indicator = look.indicator ?? "circle";
586
+ return {
587
+ /* The full ring reaches past the knob's own box, so its layout clearance
588
+ has to be reserved — and only the kit knows it put a ring there. */
589
+ className: cn("amb-knob", markers === "full" && "amb-knob-markers-full"),
590
+ parts: {
591
+ panel: markers === "none" ? null : /* @__PURE__ */ jsx12(ScaleRing, { count: markers === "ends" ? 2 : FULL_MARKERS }),
592
+ /* `material` goes on every element that paints. The cap always does —
593
+ it is the knob's top face either way — and the knurl ring does too
594
+ when there is one, so a knurled knob's rim and cap are the same
595
+ material rather than the rim being the only thing wearing it.
596
+
597
+ `knurlColor` is the one thing the ring may hold on its own: a
598
+ two-tone knob — dark grip round a pale cap — is a real piece of
599
+ hardware, and the cap has no matching prop because the cap's colour
600
+ is the control's colour, set the ordinary way with --amb-albedo. */
601
+ base: /* @__PURE__ */ jsx12(KnobBody, { flush: !knurling, material }),
602
+ actuator: /* @__PURE__ */ jsxs4(Fragment3, { children: [
603
+ knurling ? /* @__PURE__ */ jsx12(KnurledFace, { material, color: knurlColor }) : null,
604
+ indicator === "circle" ? /* @__PURE__ */ jsx12(IndicatorDot, {}) : /* @__PURE__ */ jsx12(IndicatorBar, {})
605
+ ] })
606
+ }
607
+ };
608
+ }
609
+ function travel(look) {
610
+ const material = look.material;
611
+ const upright = look.orientation === "vertical";
612
+ return {
613
+ className: upright ? "amb-fader" : "amb-slider",
614
+ parts: {
615
+ base: /* @__PURE__ */ jsx12(TravelTrack, { depth: upright ? "slot" : "channel" }),
616
+ actuator: upright ? /* @__PURE__ */ jsx12(FaderCap, { material }) : /* @__PURE__ */ jsx12(SliderThumb, { material })
617
+ }
618
+ };
619
+ }
620
+ function press(look) {
621
+ const shape = look.shape ?? "pill";
622
+ return {
623
+ className: cn(
624
+ "amb-button amb-groove",
625
+ shape === "round" && "amb-button-round",
626
+ shape === "square" && "amb-button-square"
627
+ ),
628
+ parts: {
629
+ actuator: /* @__PURE__ */ jsx12(ButtonCap, { material: look.material ?? "matte", children: look.children })
630
+ }
631
+ };
632
+ }
633
+ function latch() {
634
+ return {
635
+ className: "amb-switch",
636
+ parts: { base: /* @__PURE__ */ jsx12(SwitchTrack, {}), actuator: /* @__PURE__ */ jsx12(SwitchPill, {}) }
637
+ };
638
+ }
639
+ function bank() {
640
+ return {
641
+ className: "amb-select amb-groove",
642
+ parts: { base: /* @__PURE__ */ jsx12(KeyLens, {}), actuator: /* @__PURE__ */ jsx12(KeyCap, {}) }
643
+ };
644
+ }
645
+ var groundedKit = {
646
+ name: "grounded",
647
+ rotary,
648
+ travel,
649
+ press,
650
+ latch,
651
+ bank,
652
+ looks: {
653
+ rotary: ["material", "knurling", "knurlColor", "markers", "indicator"],
654
+ travel: ["material", "orientation"],
655
+ press: ["material", "shape", "children"],
656
+ latch: [],
657
+ bank: []
658
+ }
215
659
  };
216
- function clamp(value, min, max) {
217
- return Math.min(max, Math.max(min, value));
660
+
661
+ // src/components/AmbientButton.tsx
662
+ import { jsx as jsx13 } from "react/jsx-runtime";
663
+ function AmbientButton({
664
+ className,
665
+ children,
666
+ look,
667
+ material = "matte",
668
+ shape = "pill",
669
+ size = "md",
670
+ ...rest
671
+ }) {
672
+ const { dress } = useDress("press", { material, shape, children, ...look }, groundedKit.press);
673
+ return /* @__PURE__ */ jsx13(
674
+ AmbientPress,
675
+ {
676
+ ...rest,
677
+ size,
678
+ className: cn(dress.className, className),
679
+ parts: dress.parts
680
+ }
681
+ );
218
682
  }
219
- function AmbientKnob({
220
- value,
221
- min = 0,
222
- max = 100,
223
- step = 1,
683
+
684
+ // src/components/AmbientSwitch.tsx
685
+ import { useId as useId3 } from "react";
686
+
687
+ // src/controls/AmbientLatch.tsx
688
+ import { useId as useId2, useRef as useRef5 } from "react";
689
+
690
+ // src/core/useLatch.ts
691
+ import { useMemo as useMemo2 } from "react";
692
+ function useLatch(options) {
693
+ const { disabled = false } = options;
694
+ const [on, setOn] = useControllableValue(
695
+ options.value,
696
+ options.defaultValue ?? false,
697
+ options.onChange
698
+ );
699
+ const state = useMemo2(
700
+ () => ({
701
+ value: on ? 1 : 0,
702
+ min: 0,
703
+ max: 1,
704
+ percent: on ? 1 : 0,
705
+ angle: 0,
706
+ travelStart: 0,
707
+ travelSweep: 0,
708
+ detents: 2,
709
+ dragging: false,
710
+ disabled,
711
+ atMin: !on,
712
+ atMax: on
713
+ }),
714
+ [on, disabled]
715
+ );
716
+ const rootProps = {
717
+ type: "button",
718
+ role: "switch",
719
+ "aria-checked": on,
720
+ disabled,
721
+ style: stateStyle(state),
722
+ ...stateData(state),
723
+ onClick: () => setOn(!on)
724
+ };
725
+ return { state, rootProps, on, setOn };
726
+ }
727
+
728
+ // src/controls/AmbientLatch.tsx
729
+ import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
730
+ function AmbientLatch({
731
+ parts,
732
+ size,
733
+ animate = "auto",
224
734
  label,
225
- material,
226
- variant = "dot",
735
+ className,
736
+ value,
737
+ defaultValue,
738
+ onChange,
739
+ disabled,
740
+ children,
741
+ ...rest
742
+ }) {
743
+ const labelId = useId2();
744
+ const ref = useRef5(null);
745
+ const { state, rootProps } = useLatch({ value, defaultValue, onChange, disabled });
746
+ useDevPartCheck(ref, "AmbientLatch");
747
+ const sized = sizeProps("latch", size);
748
+ const { style: restStyle, onClick, ...restProps } = rest;
749
+ const control = /* @__PURE__ */ jsx14(
750
+ "button",
751
+ {
752
+ ...restProps,
753
+ ...rootProps,
754
+ ref,
755
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
756
+ "data-animate": animate,
757
+ className: cn("ambx-control ambx-latch", sized.className, className),
758
+ style: { ...rootProps.style, ...sized.style, ...restStyle },
759
+ onClick: (event) => {
760
+ rootProps.onClick();
761
+ onClick?.(event);
762
+ },
763
+ children: /* @__PURE__ */ jsxs5(ControlStateProvider, { value: state, children: [
764
+ /* @__PURE__ */ jsx14(Frames, { parts }),
765
+ children
766
+ ] })
767
+ }
768
+ );
769
+ if (!label) return control;
770
+ return /* @__PURE__ */ jsxs5("div", { className: "ambx-stack", children: [
771
+ control,
772
+ /* @__PURE__ */ jsx14("span", { id: labelId, className: "ambx-label", children: label })
773
+ ] });
774
+ }
775
+
776
+ // src/components/AmbientSwitch.tsx
777
+ import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
778
+ function AmbientSwitch({
227
779
  size = "md",
780
+ led,
781
+ label,
782
+ look,
783
+ value,
784
+ defaultValue,
228
785
  onChange,
786
+ animate,
229
787
  className,
230
- ...props
788
+ ...rest
231
789
  }) {
232
- const id = useId2();
233
- const clipId = `amb-knurl-${id.replace(/:/g, "")}`;
234
- const draggingRef = useRef(false);
235
- const knobRef = useRef(null);
236
- const safeStep = step > 0 ? step : 1;
237
- const family = VARIANT_FAMILY[variant];
238
- const percent = (value - min) / (max - min || 1);
239
- const rotation = percent * 270 - 135;
240
- const pointerToValue = (event) => {
241
- const rect = knobRef.current.getBoundingClientRect();
242
- const cx = rect.left + rect.width / 2;
243
- const cy = rect.top + rect.height / 2;
244
- const atan2Deg = Math.atan2(event.clientY - cy, event.clientX - cx) * (180 / Math.PI);
245
- let angle = atan2Deg + 90;
246
- if (angle > 180) angle -= 360;
247
- if (angle < -135 || angle > 135) {
248
- angle = percent >= 0.5 ? 135 : -135;
249
- }
250
- const range = max - min;
251
- const raw = min + (angle + 135) / 270 * range;
252
- const snapped = Math.round(raw / safeStep) * safeStep;
253
- onChange?.(clamp(snapped, min, max));
790
+ const labelId = useId3();
791
+ const [on, setOn] = useControllableValue(value, defaultValue ?? false, onChange);
792
+ const { dress, defaults } = useDress("latch", { ...look }, groundedKit.latch);
793
+ const control = /* @__PURE__ */ jsx15(
794
+ AmbientLatch,
795
+ {
796
+ ...rest,
797
+ value: on,
798
+ onChange: setOn,
799
+ size,
800
+ animate: animate ?? defaults?.animate,
801
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
802
+ className: cn(dress.className, className),
803
+ parts: dress.parts
804
+ }
805
+ );
806
+ if (!led && !label) return control;
807
+ return /* @__PURE__ */ jsxs6("div", { className: "ambx-stack", children: [
808
+ led ? /* @__PURE__ */ jsxs6("span", { className: "ambx-switch-mount", children: [
809
+ /* @__PURE__ */ jsx15(Led, { on, ...typeof led === "string" ? { color: led } : null }),
810
+ control
811
+ ] }) : control,
812
+ label ? /* @__PURE__ */ jsx15("span", { id: labelId, className: "ambx-label", children: label }) : null
813
+ ] });
814
+ }
815
+
816
+ // src/controls/AmbientBank.tsx
817
+ import { useId as useId4 } from "react";
818
+
819
+ // src/core/useBank.ts
820
+ import { useCallback as useCallback3, useRef as useRef6 } from "react";
821
+ function toArray(value) {
822
+ if (value === void 0) return [];
823
+ return Array.isArray(value) ? value : [value];
824
+ }
825
+ function useBank(options) {
826
+ const { options: items, multiple = false, orientation = "vertical", disabled = false } = options;
827
+ const [raw, setRaw] = useControllableValue(
828
+ options.value,
829
+ options.defaultValue ?? (multiple ? [] : ""),
830
+ options.onChange
831
+ );
832
+ const selected = toArray(raw);
833
+ const keyRefs = useRef6([]);
834
+ const commit2 = useCallback3(
835
+ (next) => setRaw(multiple ? next : next[0] ?? ""),
836
+ [setRaw, multiple]
837
+ );
838
+ const select = useCallback3(
839
+ (option) => {
840
+ if (!option || option.disabled || disabled) return;
841
+ if (!multiple) return commit2([option.value]);
842
+ commit2(
843
+ selected.includes(option.value) ? selected.filter((v) => v !== option.value) : [...selected, option.value]
844
+ );
845
+ },
846
+ [commit2, multiple, selected, disabled]
847
+ );
848
+ const enabled = items.filter((o) => !o.disabled);
849
+ const litIndex = items.findIndex((o) => selected.includes(o.value) && !o.disabled);
850
+ const firstEnabled = items.findIndex((o) => !o.disabled);
851
+ const tabStop = multiple ? -1 : litIndex >= 0 ? litIndex : firstEnabled;
852
+ const focusAt = (index) => keyRefs.current[index]?.focus();
853
+ const step = (from, delta) => {
854
+ if (enabled.length === 0) return;
855
+ let i = from;
856
+ for (let guard = 0; guard < items.length; guard += 1) {
857
+ i = (i + delta + items.length) % items.length;
858
+ if (!items[i]?.disabled) break;
859
+ }
860
+ focusAt(i);
861
+ if (!multiple) select(items[i]);
254
862
  };
255
- const setValue = (nextValue) => {
256
- const snapped = Math.round(nextValue / safeStep) * safeStep;
257
- onChange?.(clamp(snapped, min, max));
863
+ const edge = (which) => {
864
+ const i = which === "first" ? firstEnabled : items.map((o) => !o.disabled).lastIndexOf(true);
865
+ if (i < 0) return;
866
+ focusAt(i);
867
+ if (!multiple) select(items[i]);
258
868
  };
259
- const onKeyDown = (event) => {
260
- const pageStep = safeStep * 10;
869
+ const onKeyDown = (event, index) => {
870
+ const back = orientation === "vertical" ? "ArrowUp" : "ArrowLeft";
871
+ const forward = orientation === "vertical" ? "ArrowDown" : "ArrowRight";
261
872
  switch (event.key) {
262
- case "ArrowUp":
263
- case "ArrowRight":
264
- event.preventDefault();
265
- setValue(value + safeStep);
266
- break;
267
- case "ArrowDown":
268
- case "ArrowLeft":
873
+ case back:
269
874
  event.preventDefault();
270
- setValue(value - safeStep);
875
+ step(index, -1);
271
876
  break;
272
- case "PageUp":
877
+ case forward:
273
878
  event.preventDefault();
274
- setValue(value + pageStep);
275
- break;
276
- case "PageDown":
277
- event.preventDefault();
278
- setValue(value - pageStep);
879
+ step(index, 1);
279
880
  break;
280
881
  case "Home":
281
882
  event.preventDefault();
282
- setValue(min);
883
+ edge("first");
283
884
  break;
284
885
  case "End":
285
886
  event.preventDefault();
286
- setValue(max);
887
+ edge("last");
888
+ break;
889
+ case " ":
890
+ case "Enter":
891
+ event.preventDefault();
892
+ select(items[index]);
287
893
  break;
288
894
  default:
289
895
  break;
290
896
  }
291
897
  };
292
- return /* @__PURE__ */ jsxs2("div", { className: cn("ambx-stack", className), ...props, children: [
293
- /* @__PURE__ */ jsxs2(
294
- "div",
295
- {
296
- ref: knobRef,
297
- className: cn("amb-knob ambx-knob", `ambx-knob-${size}`),
298
- role: "slider",
299
- "aria-label": label,
300
- "aria-valuemin": min,
301
- "aria-valuemax": max,
302
- "aria-valuenow": value,
303
- "aria-labelledby": label ? id : void 0,
304
- "aria-orientation": "vertical",
305
- tabIndex: 0,
306
- onPointerDown: (event) => {
307
- event.currentTarget.setPointerCapture(event.pointerId);
308
- draggingRef.current = true;
309
- pointerToValue(event);
310
- },
311
- onPointerMove: (event) => {
312
- if (draggingRef.current) pointerToValue(event);
313
- },
314
- onPointerUp: () => {
315
- draggingRef.current = false;
316
- },
317
- onPointerCancel: () => {
318
- draggingRef.current = false;
319
- },
320
- onKeyDown,
321
- children: [
322
- /* @__PURE__ */ jsx6("svg", { width: 0, height: 0, style: { position: "absolute" }, "aria-hidden": true, focusable: false, children: /* @__PURE__ */ jsx6("defs", { children: /* @__PURE__ */ jsx6("clipPath", { id: clipId, clipPathUnits: "objectBoundingBox", children: /* @__PURE__ */ jsx6("path", { d: KNURL_PATHS[family] }) }) }) }),
323
- /* @__PURE__ */ jsx6("span", { className: "amb-knob-body ambient amb-thickness-2 amb-surface" }),
324
- /* @__PURE__ */ jsxs2(
325
- "div",
326
- {
327
- className: cn(
328
- "ambx-knob-rotation amb-knob-face",
329
- family === "flute" && "amb-knob-face-flute",
330
- family === "fine" && "amb-knob-face-fine",
331
- variant === "cap" && "amb-knob-face-cap",
332
- material && `amb-mat-${material}`
333
- ),
334
- style: { transform: `rotate(${rotation}deg)`, clipPath: `url(#${clipId})` },
335
- children: [
336
- variant === "dot" ? /* @__PURE__ */ jsx6("span", { className: "amb-knob-indicator-dot" }) : null,
337
- variant === "line" ? /* @__PURE__ */ jsx6("span", { className: "amb-knob-indicator-line" }) : null,
338
- variant === "flute" ? /* @__PURE__ */ jsx6("span", { className: "amb-knob-indicator-dot amb-knob-indicator-dot-center" }) : null
339
- ]
340
- }
341
- )
342
- ]
343
- }
344
- ),
345
- label ? /* @__PURE__ */ jsx6("span", { id, className: "ambx-label", children: label }) : null
346
- ] });
898
+ const rootProps = {
899
+ role: multiple ? "group" : "radiogroup",
900
+ "aria-orientation": multiple ? void 0 : orientation,
901
+ "aria-disabled": disabled || void 0,
902
+ "data-orientation": orientation,
903
+ "data-disabled": disabled ? "" : void 0
904
+ };
905
+ const keyProps = (option, index) => {
906
+ const on = selected.includes(option.value);
907
+ return {
908
+ /* No `key` here on purpose: the caller maps the options, so it owns
909
+ the list key. Spreading one into JSX makes React warn and drops
910
+ the key on release builds. */
911
+ type: "button",
912
+ ref: (node) => {
913
+ keyRefs.current[index] = node;
914
+ },
915
+ role: multiple ? "checkbox" : "radio",
916
+ "aria-checked": on,
917
+ "aria-label": option.ariaLabel,
918
+ title: option.ariaLabel,
919
+ disabled: option.disabled || disabled,
920
+ tabIndex: multiple ? 0 : index === tabStop ? 0 : -1,
921
+ "data-on": on ? "" : void 0,
922
+ onClick: () => select(option),
923
+ onKeyDown: (event) => onKeyDown(event, index)
924
+ };
925
+ };
926
+ return { selected, select, rootProps, keyProps };
347
927
  }
348
928
 
349
- // src/components/AmbientFader.tsx
350
- import { useId as useId3, useRef as useRef2 } from "react";
351
- import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
352
- function clamp2(value, min, max) {
353
- return Math.min(max, Math.max(min, value));
929
+ // src/controls/AmbientBank.tsx
930
+ import { jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
931
+ function keyState(on, disabled) {
932
+ return {
933
+ value: on ? 1 : 0,
934
+ min: 0,
935
+ max: 1,
936
+ percent: on ? 1 : 0,
937
+ angle: 0,
938
+ travelStart: 0,
939
+ travelSweep: 0,
940
+ detents: 2,
941
+ dragging: false,
942
+ disabled,
943
+ atMin: !on,
944
+ atMax: on
945
+ };
354
946
  }
355
- function AmbientFader({
356
- value,
357
- min = 0,
358
- max = 100,
359
- step = 1,
947
+ function AmbientBank({
948
+ options,
949
+ keyParts,
950
+ parts,
951
+ size,
952
+ color,
360
953
  label,
361
- material,
362
- size = "md",
363
- onChange,
954
+ renderKey,
364
955
  className,
365
- ...props
956
+ style,
957
+ value,
958
+ defaultValue,
959
+ onChange,
960
+ multiple,
961
+ orientation = "vertical",
962
+ disabled = false,
963
+ ...rest
366
964
  }) {
367
- const id = useId3();
368
- const trackRef = useRef2(null);
369
- const safeStep = step > 0 ? step : 1;
370
- const percent = (value - min) / (max - min || 1) * 100;
371
- const updateFromClientY = (clientY) => {
372
- const track = trackRef.current;
373
- if (!track) return;
374
- const rect = track.getBoundingClientRect();
375
- const ratio = 1 - (clientY - rect.top) / rect.height;
376
- const nextValue = min + clamp2(ratio, 0, 1) * (max - min);
377
- const snapped = Math.round(nextValue / safeStep) * safeStep;
378
- onChange?.(clamp2(snapped, min, max));
379
- };
380
- const setValue = (nextValue) => {
381
- const snapped = Math.round(nextValue / safeStep) * safeStep;
382
- onChange?.(clamp2(snapped, min, max));
383
- };
384
- const onKeyDown = (event) => {
385
- const pageStep = safeStep * 10;
965
+ const labelId = useId4();
966
+ const { selected, rootProps, keyProps } = useBank({
967
+ options,
968
+ value,
969
+ defaultValue,
970
+ onChange,
971
+ multiple,
972
+ orientation,
973
+ disabled
974
+ });
975
+ const sized = sizeProps("bank", size);
976
+ const bank2 = /* @__PURE__ */ jsxs7(
977
+ "div",
978
+ {
979
+ ...rest,
980
+ ...rootProps,
981
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
982
+ className: cn(
983
+ "ambx-control ambx-bank",
984
+ `ambx-bank-${orientation}`,
985
+ sized.className,
986
+ className
987
+ ),
988
+ style: {
989
+ ...color ? { "--amb-led-color": color } : null,
990
+ ...sized.style,
991
+ ...style
992
+ },
993
+ children: [
994
+ /* @__PURE__ */ jsx16(Frames, { parts }),
995
+ options.map((option, index) => {
996
+ const on = selected.includes(option.value);
997
+ const ks = keyState(on, option.disabled || disabled);
998
+ return /* @__PURE__ */ jsx16(
999
+ "button",
1000
+ {
1001
+ ...keyProps(option, index),
1002
+ ...stateData(ks),
1003
+ className: "ambx-key",
1004
+ style: {
1005
+ ...stateStyle(ks),
1006
+ ...option.color ? { "--amb-led-color": option.color } : null
1007
+ },
1008
+ children: /* @__PURE__ */ jsx16(ControlStateProvider, { value: ks, children: /* @__PURE__ */ jsx16(BankKeyProvider, { value: { option, on, index }, children: renderKey ? renderKey(option, on) : /* @__PURE__ */ jsx16(Frames, { parts: keyParts }) }) })
1009
+ },
1010
+ option.value
1011
+ );
1012
+ })
1013
+ ]
1014
+ }
1015
+ );
1016
+ if (!label) return bank2;
1017
+ return /* @__PURE__ */ jsxs7("div", { className: "ambx-stack", children: [
1018
+ bank2,
1019
+ /* @__PURE__ */ jsx16("span", { id: labelId, className: "ambx-label", children: label })
1020
+ ] });
1021
+ }
1022
+
1023
+ // src/components/AmbientSelect.tsx
1024
+ import { jsx as jsx17 } from "react/jsx-runtime";
1025
+ function AmbientSelect({ size = "md", look, className, ...rest }) {
1026
+ const { dress } = useDress("bank", { ...look }, groundedKit.bank);
1027
+ return /* @__PURE__ */ jsx17(
1028
+ AmbientBank,
1029
+ {
1030
+ ...rest,
1031
+ size,
1032
+ className: cn(dress.className, className),
1033
+ keyParts: dress.parts
1034
+ }
1035
+ );
1036
+ }
1037
+
1038
+ // src/controls/AmbientRotary.tsx
1039
+ import { useId as useId5, useRef as useRef8 } from "react";
1040
+
1041
+ // src/core/useRotary.ts
1042
+ import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef7, useState as useState3 } from "react";
1043
+
1044
+ // src/core/numeric.ts
1045
+ function clamp(value, min, max) {
1046
+ return Math.min(max, Math.max(min, value));
1047
+ }
1048
+ function snap(value, min, step) {
1049
+ if (!(step > 0)) return value;
1050
+ return min + Math.round((value - min) / step) * step;
1051
+ }
1052
+ function normalise(value, min, max) {
1053
+ return (value - min) / (max - min || 1);
1054
+ }
1055
+ function denormalise(t, min, max) {
1056
+ return min + t * (max - min);
1057
+ }
1058
+ function commit(value, min, max, step) {
1059
+ return clamp(snap(value, min, max === min ? 1 : step), min, max);
1060
+ }
1061
+ function valueKeyHandler(options) {
1062
+ return (event) => {
1063
+ const { value, min, max, step, invert, onChange, disabled } = options;
1064
+ if (disabled) return false;
1065
+ const dir = invert ? -1 : 1;
1066
+ const set = (next) => {
1067
+ event.preventDefault();
1068
+ onChange(commit(next, min, max, step));
1069
+ };
386
1070
  switch (event.key) {
387
1071
  case "ArrowUp":
388
1072
  case "ArrowRight":
389
- event.preventDefault();
390
- setValue(value + safeStep);
391
- break;
1073
+ set(value + step * dir);
1074
+ return true;
392
1075
  case "ArrowDown":
393
1076
  case "ArrowLeft":
394
- event.preventDefault();
395
- setValue(value - safeStep);
396
- break;
1077
+ set(value - step * dir);
1078
+ return true;
397
1079
  case "PageUp":
398
- event.preventDefault();
399
- setValue(value + pageStep);
400
- break;
1080
+ set(value + step * 10 * dir);
1081
+ return true;
401
1082
  case "PageDown":
402
- event.preventDefault();
403
- setValue(value - pageStep);
404
- break;
1083
+ set(value - step * 10 * dir);
1084
+ return true;
405
1085
  case "Home":
406
- event.preventDefault();
407
- setValue(min);
408
- break;
1086
+ set(min);
1087
+ return true;
409
1088
  case "End":
410
- event.preventDefault();
411
- setValue(max);
412
- break;
1089
+ set(max);
1090
+ return true;
413
1091
  default:
414
- break;
1092
+ return false;
415
1093
  }
416
1094
  };
417
- const onPointerMove = (event) => {
418
- if (event.buttons !== 1) return;
419
- updateFromClientY(event.clientY);
1095
+ }
1096
+
1097
+ // src/core/useRotary.ts
1098
+ var DEFAULT_TRAVEL = { start: -135, sweep: 270 };
1099
+ function wrapDeg(deg) {
1100
+ return ((deg + 180) % 360 + 360) % 360 - 180;
1101
+ }
1102
+ function resolveTravel(travel2) {
1103
+ if (travel2 === void 0) return DEFAULT_TRAVEL;
1104
+ if (typeof travel2 === "number") return { start: -travel2 / 2, sweep: travel2 };
1105
+ return travel2;
1106
+ }
1107
+ var warnedFullTurn = false;
1108
+ function capturePointer(target, pointerId) {
1109
+ try {
1110
+ target.setPointerCapture?.(pointerId);
1111
+ } catch {
1112
+ }
1113
+ }
1114
+ function useRotary(options) {
1115
+ const {
1116
+ min = 0,
1117
+ max = 100,
1118
+ step = 1,
1119
+ dragDistance = 200,
1120
+ wrap = false,
1121
+ disabled = false
1122
+ } = options;
1123
+ const { start, sweep } = resolveTravel(options.travel);
1124
+ let input = options.input ?? "drag";
1125
+ if (input === "angle" && Math.abs(sweep) >= 360) {
1126
+ if (isDev && !warnedFullTurn) {
1127
+ warnedFullTurn = true;
1128
+ console.warn(
1129
+ `[@ambientcss/components] input="angle" needs a sweep under a full turn (got ${sweep}deg): an absolute angle is ambiguous at 360deg, where the first and last values share a screen position. Falling back to "drag".`
1130
+ );
1131
+ }
1132
+ input = "drag";
1133
+ }
1134
+ const [value, setValue] = useControllableValue(
1135
+ options.value,
1136
+ options.defaultValue ?? min,
1137
+ options.onChange
1138
+ );
1139
+ const [dragging, setDragging] = useState3(false);
1140
+ const draggingRef = useRef7(false);
1141
+ const rootRef = useRef7(null);
1142
+ const drag = useRef7({ value: 0, clientY: 0, angle: 0, accum: 0 });
1143
+ const range = max - min;
1144
+ const keyStep = step > 0 ? step : range / 100 || 1;
1145
+ const percent = clamp(normalise(value, min, max), 0, 1);
1146
+ const angle = start + percent * sweep;
1147
+ const set = useCallback4(
1148
+ (next) => setValue(commit(next, min, max, step)),
1149
+ [setValue, min, max, step]
1150
+ );
1151
+ const pointerAngle = (event) => {
1152
+ const rect = rootRef.current?.getBoundingClientRect();
1153
+ if (!rect) return null;
1154
+ const dx = event.clientX - (rect.left + rect.width / 2);
1155
+ const dy = event.clientY - (rect.top + rect.height / 2);
1156
+ return Math.atan2(dy, dx) * (180 / Math.PI) + 90;
420
1157
  };
421
- return /* @__PURE__ */ jsxs3("div", { className: cn("ambx-stack", className), ...props, children: [
422
- /* @__PURE__ */ jsx7(
1158
+ const fromAngle = (event) => {
1159
+ const raw = pointerAngle(event);
1160
+ if (raw === null) return;
1161
+ const mid = start + sweep / 2;
1162
+ let a = mid + wrapDeg(raw - mid);
1163
+ const end = start + sweep;
1164
+ if (a < Math.min(start, end) || a > Math.max(start, end)) {
1165
+ const toStart = Math.abs(wrapDeg(a - start));
1166
+ const toEnd = Math.abs(wrapDeg(a - end));
1167
+ a = toStart <= toEnd ? start : end;
1168
+ }
1169
+ set(denormalise((a - start) / (sweep || 1), min, max));
1170
+ };
1171
+ const fromDrag = (event) => {
1172
+ const dy = drag.current.clientY - event.clientY;
1173
+ drag.current.clientY = event.clientY;
1174
+ drag.current.accum += dy * (event.shiftKey ? 0.25 : 1);
1175
+ set(drag.current.value + drag.current.accum / dragDistance * range);
1176
+ };
1177
+ const fromDelta = (event) => {
1178
+ const raw = pointerAngle(event);
1179
+ if (raw === null) return;
1180
+ drag.current.accum += wrapDeg(raw - drag.current.angle);
1181
+ drag.current.angle = raw;
1182
+ const next = drag.current.value + drag.current.accum / (sweep || 360) * range;
1183
+ set(wrap && range > 0 ? min + ((next - min) % range + range) % range : next);
1184
+ };
1185
+ const track = (event) => {
1186
+ if (input === "angle") fromAngle(event);
1187
+ else if (input === "delta") fromDelta(event);
1188
+ else fromDrag(event);
1189
+ };
1190
+ const state = useMemo3(
1191
+ () => ({
1192
+ value,
1193
+ min,
1194
+ max,
1195
+ percent,
1196
+ angle,
1197
+ travelStart: start,
1198
+ travelSweep: sweep,
1199
+ detents: options.detents ?? (step > 0 && range > 0 ? Math.round(range / step) : 0),
1200
+ dragging,
1201
+ disabled,
1202
+ atMin: value <= min,
1203
+ atMax: value >= max
1204
+ }),
1205
+ [value, min, max, percent, angle, start, sweep, options.detents, step, range, dragging, disabled]
1206
+ );
1207
+ const onKeyDown = valueKeyHandler({ value, min, max, step: keyStep, disabled, onChange: setValue });
1208
+ const rootProps = {
1209
+ ref: rootRef,
1210
+ role: "slider",
1211
+ "aria-valuemin": min,
1212
+ "aria-valuemax": max,
1213
+ "aria-valuenow": value,
1214
+ "aria-orientation": "vertical",
1215
+ "aria-disabled": disabled || void 0,
1216
+ tabIndex: disabled ? -1 : 0,
1217
+ style: stateStyle(state),
1218
+ ...stateData(state),
1219
+ onPointerDown: (event) => {
1220
+ if (disabled || event.button !== 0) return;
1221
+ capturePointer(event.currentTarget, event.pointerId);
1222
+ draggingRef.current = true;
1223
+ setDragging(true);
1224
+ drag.current = {
1225
+ value,
1226
+ clientY: event.clientY,
1227
+ angle: pointerAngle(event) ?? 0,
1228
+ accum: 0
1229
+ };
1230
+ if (input === "angle") fromAngle(event);
1231
+ },
1232
+ onPointerMove: (event) => {
1233
+ if (!draggingRef.current || disabled) return;
1234
+ track(event);
1235
+ },
1236
+ onPointerUp: () => {
1237
+ draggingRef.current = false;
1238
+ setDragging(false);
1239
+ },
1240
+ onPointerCancel: () => {
1241
+ draggingRef.current = false;
1242
+ setDragging(false);
1243
+ },
1244
+ onKeyDown: (event) => {
1245
+ onKeyDown(event);
1246
+ }
1247
+ };
1248
+ return { state, rootProps, setValue: set };
1249
+ }
1250
+
1251
+ // src/controls/AmbientRotary.tsx
1252
+ import { jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
1253
+ function AmbientRotary({
1254
+ parts,
1255
+ size,
1256
+ animate = "auto",
1257
+ label,
1258
+ className,
1259
+ value,
1260
+ defaultValue,
1261
+ min,
1262
+ max,
1263
+ step,
1264
+ detents,
1265
+ travel: travel2,
1266
+ input,
1267
+ dragDistance,
1268
+ wrap,
1269
+ disabled,
1270
+ onChange,
1271
+ ...rest
1272
+ }) {
1273
+ const labelId = useId5();
1274
+ const stackRef = useRef8(null);
1275
+ const { state, rootProps } = useRotary({
1276
+ value,
1277
+ defaultValue,
1278
+ min,
1279
+ max,
1280
+ step,
1281
+ detents,
1282
+ travel: travel2,
1283
+ input,
1284
+ dragDistance,
1285
+ wrap,
1286
+ disabled,
1287
+ onChange
1288
+ });
1289
+ useDevPartCheck(stackRef, "AmbientRotary");
1290
+ const sized = sizeProps("rotary", size);
1291
+ const { style: restStyle, ...restProps } = rest;
1292
+ const control = /* @__PURE__ */ jsx18(
1293
+ "div",
1294
+ {
1295
+ ...restProps,
1296
+ ...rootProps,
1297
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
1298
+ "data-animate": animate,
1299
+ className: cn("ambx-control ambx-rotary", sized.className, className),
1300
+ style: { ...rootProps.style, ...sized.style, ...restStyle },
1301
+ children: /* @__PURE__ */ jsx18(ControlStateProvider, { value: state, children: /* @__PURE__ */ jsx18(Frames, { parts }) })
1302
+ }
1303
+ );
1304
+ return /* @__PURE__ */ jsxs8("div", { className: "ambx-stack", ref: stackRef, children: [
1305
+ control,
1306
+ label ? /* @__PURE__ */ jsx18("span", { id: labelId, className: "ambx-label", children: label }) : null
1307
+ ] });
1308
+ }
1309
+
1310
+ // src/components/AmbientKnob.tsx
1311
+ import { jsx as jsx19 } from "react/jsx-runtime";
1312
+ function AmbientKnob({
1313
+ material,
1314
+ knurling,
1315
+ knurlColor,
1316
+ markers,
1317
+ indicator,
1318
+ look,
1319
+ size = "md",
1320
+ className,
1321
+ travel: travel2,
1322
+ input,
1323
+ animate,
1324
+ ...rest
1325
+ }) {
1326
+ const { dress, defaults } = useDress(
1327
+ "rotary",
1328
+ { material, knurling, knurlColor, markers, indicator, ...look },
1329
+ groundedKit.rotary
1330
+ );
1331
+ return /* @__PURE__ */ jsx19(
1332
+ AmbientRotary,
1333
+ {
1334
+ ...rest,
1335
+ travel: travel2 ?? defaults?.travel,
1336
+ input: input ?? defaults?.input,
1337
+ animate: animate ?? defaults?.animate,
1338
+ size,
1339
+ className: cn(dress.className, className),
1340
+ parts: dress.parts
1341
+ }
1342
+ );
1343
+ }
1344
+
1345
+ // src/controls/AmbientTravel.tsx
1346
+ import { useId as useId6, useRef as useRef10 } from "react";
1347
+
1348
+ // src/core/useTravel.ts
1349
+ import { useCallback as useCallback5, useMemo as useMemo4, useRef as useRef9, useState as useState4 } from "react";
1350
+ function useTravel(options) {
1351
+ const {
1352
+ min = 0,
1353
+ max = 100,
1354
+ step = 1,
1355
+ orientation = "horizontal",
1356
+ invert = false,
1357
+ disabled = false
1358
+ } = options;
1359
+ const [value, setValue] = useControllableValue(
1360
+ options.value,
1361
+ options.defaultValue ?? min,
1362
+ options.onChange
1363
+ );
1364
+ const [dragging, setDragging] = useState4(false);
1365
+ const draggingRef = useRef9(false);
1366
+ const rootRef = useRef9(null);
1367
+ const vertical = orientation === "vertical";
1368
+ const range = max - min;
1369
+ const keyStep = step > 0 ? step : range / 100 || 1;
1370
+ const percent = clamp(normalise(value, min, max), 0, 1);
1371
+ const set = useCallback5(
1372
+ (next) => setValue(commit(next, min, max, step)),
1373
+ [setValue, min, max, step]
1374
+ );
1375
+ const track = (event) => {
1376
+ const rect = rootRef.current?.getBoundingClientRect();
1377
+ if (!rect) return;
1378
+ const raw = vertical ? 1 - (event.clientY - rect.top) / (rect.height || 1) : (event.clientX - rect.left) / (rect.width || 1);
1379
+ set(denormalise(clamp(invert ? 1 - raw : raw, 0, 1), min, max));
1380
+ };
1381
+ const state = useMemo4(
1382
+ () => ({
1383
+ value,
1384
+ min,
1385
+ max,
1386
+ percent,
1387
+ angle: 0,
1388
+ travelStart: 0,
1389
+ travelSweep: 0,
1390
+ detents: options.detents ?? (step > 0 && range > 0 ? Math.round(range / step) : 0),
1391
+ dragging,
1392
+ disabled,
1393
+ atMin: value <= min,
1394
+ atMax: value >= max
1395
+ }),
1396
+ [value, min, max, percent, options.detents, step, range, dragging, disabled]
1397
+ );
1398
+ const onKeyDown = valueKeyHandler({
1399
+ value,
1400
+ min,
1401
+ max,
1402
+ step: keyStep,
1403
+ disabled,
1404
+ invert,
1405
+ onChange: setValue
1406
+ });
1407
+ const rootProps = {
1408
+ ref: rootRef,
1409
+ role: "slider",
1410
+ "aria-valuemin": min,
1411
+ "aria-valuemax": max,
1412
+ "aria-valuenow": value,
1413
+ "aria-orientation": orientation,
1414
+ "aria-disabled": disabled || void 0,
1415
+ tabIndex: disabled ? -1 : 0,
1416
+ style: stateStyle(state),
1417
+ "data-orientation": orientation,
1418
+ ...stateData(state),
1419
+ onPointerDown: (event) => {
1420
+ if (disabled || event.button !== 0) return;
1421
+ capturePointer(event.currentTarget, event.pointerId);
1422
+ draggingRef.current = true;
1423
+ setDragging(true);
1424
+ track(event);
1425
+ },
1426
+ onPointerMove: (event) => {
1427
+ if (!draggingRef.current || disabled) return;
1428
+ track(event);
1429
+ },
1430
+ onPointerUp: () => {
1431
+ draggingRef.current = false;
1432
+ setDragging(false);
1433
+ },
1434
+ onPointerCancel: () => {
1435
+ draggingRef.current = false;
1436
+ setDragging(false);
1437
+ },
1438
+ onKeyDown: (event) => {
1439
+ onKeyDown(event);
1440
+ }
1441
+ };
1442
+ return { state, rootProps, setValue: set };
1443
+ }
1444
+
1445
+ // src/controls/AmbientTravel.tsx
1446
+ import { jsx as jsx20, jsxs as jsxs9 } from "react/jsx-runtime";
1447
+ function AmbientTravel({
1448
+ parts,
1449
+ size,
1450
+ animate = "auto",
1451
+ label,
1452
+ className,
1453
+ value,
1454
+ defaultValue,
1455
+ min,
1456
+ max,
1457
+ step,
1458
+ detents,
1459
+ orientation = "horizontal",
1460
+ invert,
1461
+ disabled,
1462
+ onChange,
1463
+ ...rest
1464
+ }) {
1465
+ const labelId = useId6();
1466
+ const stackRef = useRef10(null);
1467
+ const { state, rootProps } = useTravel({
1468
+ value,
1469
+ defaultValue,
1470
+ min,
1471
+ max,
1472
+ step,
1473
+ detents,
1474
+ orientation,
1475
+ invert,
1476
+ disabled,
1477
+ onChange
1478
+ });
1479
+ useDevPartCheck(stackRef, "AmbientTravel");
1480
+ const sized = sizeProps("travel", size);
1481
+ const { style: restStyle, ...restProps } = rest;
1482
+ return /* @__PURE__ */ jsxs9("div", { className: "ambx-stack", ref: stackRef, children: [
1483
+ /* @__PURE__ */ jsx20(
423
1484
  "div",
424
1485
  {
425
- className: cn("amb-fader amb-groove ambx-fader", `ambx-fader-${size}`),
426
- ref: trackRef,
427
- role: "slider",
428
- "aria-label": label,
429
- "aria-labelledby": label ? id : void 0,
430
- "aria-valuemin": min,
431
- "aria-valuemax": max,
432
- "aria-valuenow": value,
433
- "aria-orientation": "vertical",
434
- tabIndex: 0,
435
- onPointerDown: (event) => {
436
- event.currentTarget.setPointerCapture(event.pointerId);
437
- updateFromClientY(event.clientY);
438
- },
439
- onPointerMove,
440
- onKeyDown,
441
- children: /* @__PURE__ */ jsx7(
442
- "div",
443
- {
444
- className: cn("amb-fader-thumb ambient amb-fillet ambx-fader-thumb", material !== "glass" && "amb-surface-concave", material && `amb-mat-${material}`),
445
- style: { top: `${100 - percent}%` },
446
- children: /* @__PURE__ */ jsx7("span", { className: "amb-fader-gripline" })
447
- }
448
- )
1486
+ ...restProps,
1487
+ ...rootProps,
1488
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
1489
+ "data-animate": animate,
1490
+ className: cn(
1491
+ "ambx-control ambx-travel",
1492
+ `ambx-travel-${orientation}`,
1493
+ sized.className,
1494
+ className
1495
+ ),
1496
+ style: { ...rootProps.style, ...sized.style, ...restStyle },
1497
+ children: /* @__PURE__ */ jsx20(ControlStateProvider, { value: state, children: /* @__PURE__ */ jsx20(Frames, { parts }) })
449
1498
  }
450
1499
  ),
451
- label ? /* @__PURE__ */ jsx7("span", { id, className: "ambx-label", children: label }) : null
1500
+ label ? /* @__PURE__ */ jsx20("span", { id: labelId, className: "ambx-label", children: label }) : null
452
1501
  ] });
453
1502
  }
454
1503
 
455
- // src/components/AmbientSlider.tsx
456
- import { useId as useId4, useRef as useRef3 } from "react";
457
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
458
- function clamp3(value, min, max) {
459
- return Math.min(max, Math.max(min, value));
1504
+ // src/components/AmbientFader.tsx
1505
+ import { jsx as jsx21 } from "react/jsx-runtime";
1506
+ function AmbientFader({
1507
+ material,
1508
+ look,
1509
+ size = "md",
1510
+ animate,
1511
+ className,
1512
+ ...rest
1513
+ }) {
1514
+ const { dress, defaults } = useDress(
1515
+ "travel",
1516
+ { material, orientation: "vertical", ...look },
1517
+ groundedKit.travel
1518
+ );
1519
+ return /* @__PURE__ */ jsx21(
1520
+ AmbientTravel,
1521
+ {
1522
+ ...rest,
1523
+ orientation: "vertical",
1524
+ size,
1525
+ animate: animate ?? defaults?.animate,
1526
+ className: cn(dress.className, className),
1527
+ parts: dress.parts
1528
+ }
1529
+ );
460
1530
  }
1531
+
1532
+ // src/components/AmbientSlider.tsx
1533
+ import { jsx as jsx22 } from "react/jsx-runtime";
461
1534
  function AmbientSlider({
462
- value,
463
- min = 0,
464
- max = 100,
465
- step = 1,
466
- label,
467
1535
  material,
1536
+ look,
468
1537
  size = "md",
469
- onChange,
1538
+ animate,
470
1539
  className,
471
- ...props
1540
+ ...rest
472
1541
  }) {
473
- const id = useId4();
474
- const trackRef = useRef3(null);
475
- const safeStep = step > 0 ? step : 1;
476
- const percent = (value - min) / (max - min || 1) * 100;
477
- const updateFromClientX = (clientX) => {
478
- const track = trackRef.current;
479
- if (!track) return;
480
- const rect = track.getBoundingClientRect();
481
- const ratio = (clientX - rect.left) / rect.width;
482
- const nextValue = min + clamp3(ratio, 0, 1) * (max - min);
483
- const snapped = Math.round(nextValue / safeStep) * safeStep;
484
- onChange?.(clamp3(snapped, min, max));
485
- };
486
- const setValue = (nextValue) => {
487
- const snapped = Math.round(nextValue / safeStep) * safeStep;
488
- onChange?.(clamp3(snapped, min, max));
489
- };
490
- const onKeyDown = (event) => {
491
- const pageStep = safeStep * 10;
492
- switch (event.key) {
493
- case "ArrowRight":
494
- case "ArrowUp":
495
- event.preventDefault();
496
- setValue(value + safeStep);
497
- break;
498
- case "ArrowLeft":
499
- case "ArrowDown":
500
- event.preventDefault();
501
- setValue(value - safeStep);
502
- break;
503
- case "PageUp":
504
- event.preventDefault();
505
- setValue(value + pageStep);
506
- break;
507
- case "PageDown":
508
- event.preventDefault();
509
- setValue(value - pageStep);
510
- break;
511
- case "Home":
512
- event.preventDefault();
513
- setValue(min);
514
- break;
515
- case "End":
516
- event.preventDefault();
517
- setValue(max);
518
- break;
519
- default:
520
- break;
1542
+ const { dress, defaults } = useDress(
1543
+ "travel",
1544
+ { material, orientation: "horizontal", ...look },
1545
+ groundedKit.travel
1546
+ );
1547
+ return /* @__PURE__ */ jsx22(
1548
+ AmbientTravel,
1549
+ {
1550
+ ...rest,
1551
+ orientation: "horizontal",
1552
+ size,
1553
+ animate: animate ?? defaults?.animate,
1554
+ className: cn(dress.className, className),
1555
+ parts: dress.parts
1556
+ }
1557
+ );
1558
+ }
1559
+
1560
+ // src/parts/console.tsx
1561
+ import { Fragment as Fragment4, jsx as jsx23, jsxs as jsxs10 } from "react/jsx-runtime";
1562
+ function ConsoleWell({ className }) {
1563
+ return /* @__PURE__ */ jsx23("span", { className: cn("amb-console-housing amb-groove", className), children: /* @__PURE__ */ jsx23("span", { className: "amb-console-face amb-surface" }) });
1564
+ }
1565
+ function ConsoleBar({ className }) {
1566
+ return /* @__PURE__ */ jsx23("span", { className: cn("amb-console-bar", className), children: /* @__PURE__ */ jsx23("span", { className: "amb-console-bar-body ambient amb-surface amb-chamfer amb-thickness-2", children: /* @__PURE__ */ jsx23("span", { className: "amb-console-indicator" }) }) });
1567
+ }
1568
+ function ConsoleMarks({
1569
+ mark = true,
1570
+ legend = true,
1571
+ className
1572
+ }) {
1573
+ return /* @__PURE__ */ jsxs10("span", { className: cn("amb-console-marks", className), "aria-hidden": true, children: [
1574
+ mark ? /* @__PURE__ */ jsx23("span", { className: "amb-console-mark amb-surface" }) : null,
1575
+ legend ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
1576
+ /* @__PURE__ */ jsx23("span", { className: "amb-console-legend amb-console-legend-min", children: "\u2212" }),
1577
+ /* @__PURE__ */ jsx23("span", { className: "amb-console-legend amb-console-legend-max", children: "+" })
1578
+ ] }) : null
1579
+ ] });
1580
+ }
1581
+ function ToggleTrack({ className }) {
1582
+ return /* @__PURE__ */ jsx23("span", { className: cn("amb-console-track amb-groove", className) });
1583
+ }
1584
+ function ToggleThumb({ className }) {
1585
+ return /* @__PURE__ */ jsx23("span", { className: cn("amb-console-thumb ambient amb-surface amb-thickness-2", className) });
1586
+ }
1587
+
1588
+ // src/kits/console.tsx
1589
+ import { jsx as jsx24 } from "react/jsx-runtime";
1590
+ function rotary2(look) {
1591
+ const mark = look.mark !== false;
1592
+ const legend = look.legend !== false;
1593
+ return {
1594
+ className: cn(
1595
+ "amb-console-knob",
1596
+ mark && "amb-console-knob-marked",
1597
+ legend && "amb-console-knob-legended"
1598
+ ),
1599
+ parts: {
1600
+ panel: mark || legend ? /* @__PURE__ */ jsx24(ConsoleMarks, { mark, legend }) : null,
1601
+ base: /* @__PURE__ */ jsx24(ConsoleWell, {}),
1602
+ actuator: /* @__PURE__ */ jsx24(ConsoleBar, {})
521
1603
  }
522
1604
  };
523
- const onPointerMove = (event) => {
524
- if (event.buttons !== 1) return;
525
- updateFromClientX(event.clientX);
1605
+ }
1606
+ function latch2() {
1607
+ return {
1608
+ className: "amb-console-toggle",
1609
+ parts: { base: /* @__PURE__ */ jsx24(ToggleTrack, {}), actuator: /* @__PURE__ */ jsx24(ToggleThumb, {}) }
526
1610
  };
527
- return /* @__PURE__ */ jsxs4("div", { className: cn("ambx-stack", className), ...props, children: [
528
- /* @__PURE__ */ jsx8(
529
- "div",
530
- {
531
- className: cn("amb-slider amb-groove ambx-slider", `ambx-slider-${size}`),
532
- ref: trackRef,
533
- role: "slider",
534
- "aria-label": label,
535
- "aria-labelledby": label ? id : void 0,
536
- "aria-valuemin": min,
537
- "aria-valuemax": max,
538
- "aria-valuenow": value,
539
- "aria-orientation": "horizontal",
540
- tabIndex: 0,
541
- onPointerDown: (event) => {
542
- event.currentTarget.setPointerCapture(event.pointerId);
543
- updateFromClientX(event.clientX);
544
- },
545
- onPointerMove,
546
- onKeyDown,
547
- children: /* @__PURE__ */ jsx8(
548
- "div",
549
- {
550
- className: cn("amb-slider-thumb ambient amb-fillet ambx-slider-thumb", material !== "glass" && "amb-surface-convex", material && `amb-mat-${material}`),
551
- style: { left: `${percent}%` }
552
- }
553
- )
554
- }
555
- ),
556
- label ? /* @__PURE__ */ jsx8("span", { id, className: "ambx-label", children: label }) : null
557
- ] });
1611
+ }
1612
+ var consoleKit = {
1613
+ name: "console",
1614
+ rotary: rotary2,
1615
+ latch: latch2,
1616
+ /* A visual identity may say how its controls feel to turn. The desk knob
1617
+ this is drawn from is an absolute-position pot with a centre detent, so
1618
+ it grabs where you press rather than tracking a drag. */
1619
+ defaults: {
1620
+ rotary: { input: "angle", travel: 280 }
1621
+ },
1622
+ looks: {
1623
+ rotary: ["mark", "legend"],
1624
+ latch: []
1625
+ }
1626
+ };
1627
+ function ConsoleKnob({ mark, legend, ...rest }) {
1628
+ return /* @__PURE__ */ jsx24(AmbientKnob, { ...rest, look: { mark, legend } });
1629
+ }
1630
+ function ConsoleToggle(props) {
1631
+ return /* @__PURE__ */ jsx24(AmbientSwitch, { ...props });
558
1632
  }
559
1633
  export {
1634
+ AmbientBank,
560
1635
  AmbientButton,
561
1636
  AmbientFader,
1637
+ AmbientKitProvider,
562
1638
  AmbientKnob,
1639
+ AmbientLatch,
563
1640
  AmbientPanel,
1641
+ AmbientPress,
564
1642
  AmbientProvider,
565
1643
  AmbientRack,
1644
+ AmbientRotary,
1645
+ AmbientSelect,
566
1646
  AmbientSlider,
567
- AmbientSwitch
1647
+ AmbientSwitch,
1648
+ AmbientTravel,
1649
+ ButtonCap,
1650
+ ConsoleBar,
1651
+ ConsoleKnob,
1652
+ ConsoleMarks,
1653
+ ConsoleToggle,
1654
+ ConsoleWell,
1655
+ FaderCap,
1656
+ IndicatorBar,
1657
+ IndicatorDot,
1658
+ KeyCap,
1659
+ KeyLens,
1660
+ KnobBody,
1661
+ KnurledFace,
1662
+ Led,
1663
+ ScaleRing,
1664
+ SliderThumb,
1665
+ SwitchPill,
1666
+ SwitchTrack,
1667
+ ToggleThumb,
1668
+ ToggleTrack,
1669
+ TravelTrack,
1670
+ consoleKit,
1671
+ groundedKit,
1672
+ useBank,
1673
+ useBankKey,
1674
+ useControlState,
1675
+ useDress,
1676
+ useKit,
1677
+ useLatch,
1678
+ usePress,
1679
+ useRotary,
1680
+ useTravel
568
1681
  };
569
1682
  //# sourceMappingURL=index.js.map