@ambientcss/components 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +35 -0
  2. package/dist/index.cjs +1540 -391
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +749 -52
  5. package/dist/index.d.ts +749 -52
  6. package/dist/index.js +1501 -391
  7. package/dist/index.js.map +1 -1
  8. package/dist/styles.css +998 -291
  9. package/package.json +2 -2
  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 +138 -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 +163 -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 +99 -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 +998 -291
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,1607 @@ 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",
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
+ key: option.value,
909
+ type: "button",
910
+ ref: (node) => {
911
+ keyRefs.current[index] = node;
912
+ },
913
+ role: multiple ? "checkbox" : "radio",
914
+ "aria-checked": on,
915
+ "aria-label": option.ariaLabel,
916
+ title: option.ariaLabel,
917
+ disabled: option.disabled || disabled,
918
+ tabIndex: multiple ? 0 : index === tabStop ? 0 : -1,
919
+ "data-on": on ? "" : void 0,
920
+ onClick: () => select(option),
921
+ onKeyDown: (event) => onKeyDown(event, index)
922
+ };
923
+ };
924
+ return { selected, select, rootProps, keyProps };
925
+ }
926
+
927
+ // src/controls/AmbientBank.tsx
928
+ import { jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
929
+ function keyState(on, disabled) {
930
+ return {
931
+ value: on ? 1 : 0,
932
+ min: 0,
933
+ max: 1,
934
+ percent: on ? 1 : 0,
935
+ angle: 0,
936
+ travelStart: 0,
937
+ travelSweep: 0,
938
+ detents: 2,
939
+ dragging: false,
940
+ disabled,
941
+ atMin: !on,
942
+ atMax: on
943
+ };
944
+ }
945
+ function AmbientBank({
946
+ options,
947
+ keyParts,
948
+ parts,
949
+ size,
950
+ color,
951
+ label,
952
+ renderKey,
953
+ className,
954
+ style,
955
+ value,
956
+ defaultValue,
957
+ onChange,
958
+ multiple,
959
+ orientation = "vertical",
960
+ disabled = false,
961
+ ...rest
962
+ }) {
963
+ const labelId = useId4();
964
+ const { selected, rootProps, keyProps } = useBank({
965
+ options,
966
+ value,
967
+ defaultValue,
968
+ onChange,
969
+ multiple,
970
+ orientation,
971
+ disabled
972
+ });
973
+ const sized = sizeProps("bank", size);
974
+ const bank2 = /* @__PURE__ */ jsxs7(
975
+ "div",
976
+ {
977
+ ...rest,
978
+ ...rootProps,
979
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
980
+ className: cn(
981
+ "ambx-control ambx-bank",
982
+ `ambx-bank-${orientation}`,
983
+ sized.className,
984
+ className
985
+ ),
986
+ style: {
987
+ ...color ? { "--amb-led-color": color } : null,
988
+ ...sized.style,
989
+ ...style
990
+ },
991
+ children: [
992
+ /* @__PURE__ */ jsx16(Frames, { parts }),
993
+ options.map((option, index) => {
994
+ const on = selected.includes(option.value);
995
+ const ks = keyState(on, option.disabled || disabled);
996
+ return /* @__PURE__ */ jsx16(
997
+ "button",
326
998
  {
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
- ]
999
+ ...keyProps(option, index),
1000
+ ...stateData(ks),
1001
+ className: "ambx-key",
1002
+ style: {
1003
+ ...stateStyle(ks),
1004
+ ...option.color ? { "--amb-led-color": option.color } : null
1005
+ },
1006
+ children: /* @__PURE__ */ jsx16(ControlStateProvider, { value: ks, children: /* @__PURE__ */ jsx16(BankKeyProvider, { value: { option, on, index }, children: renderKey ? renderKey(option, on) : /* @__PURE__ */ jsx16(Frames, { parts: keyParts }) }) })
340
1007
  }
341
- )
342
- ]
343
- }
344
- ),
345
- label ? /* @__PURE__ */ jsx6("span", { id, className: "ambx-label", children: label }) : null
1008
+ );
1009
+ })
1010
+ ]
1011
+ }
1012
+ );
1013
+ if (!label) return bank2;
1014
+ return /* @__PURE__ */ jsxs7("div", { className: "ambx-stack", children: [
1015
+ bank2,
1016
+ /* @__PURE__ */ jsx16("span", { id: labelId, className: "ambx-label", children: label })
346
1017
  ] });
347
1018
  }
348
1019
 
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) {
1020
+ // src/components/AmbientSelect.tsx
1021
+ import { jsx as jsx17 } from "react/jsx-runtime";
1022
+ function AmbientSelect({ size = "md", look, className, ...rest }) {
1023
+ const { dress } = useDress("bank", { ...look }, groundedKit.bank);
1024
+ return /* @__PURE__ */ jsx17(
1025
+ AmbientBank,
1026
+ {
1027
+ ...rest,
1028
+ size,
1029
+ className: cn(dress.className, className),
1030
+ keyParts: dress.parts
1031
+ }
1032
+ );
1033
+ }
1034
+
1035
+ // src/controls/AmbientRotary.tsx
1036
+ import { useId as useId5, useRef as useRef8 } from "react";
1037
+
1038
+ // src/core/useRotary.ts
1039
+ import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef7, useState as useState3 } from "react";
1040
+
1041
+ // src/core/numeric.ts
1042
+ function clamp(value, min, max) {
353
1043
  return Math.min(max, Math.max(min, value));
354
1044
  }
355
- function AmbientFader({
356
- value,
357
- min = 0,
358
- max = 100,
359
- step = 1,
360
- label,
361
- material,
362
- size = "md",
363
- onChange,
364
- className,
365
- ...props
366
- }) {
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;
1045
+ function snap(value, min, step) {
1046
+ if (!(step > 0)) return value;
1047
+ return min + Math.round((value - min) / step) * step;
1048
+ }
1049
+ function normalise(value, min, max) {
1050
+ return (value - min) / (max - min || 1);
1051
+ }
1052
+ function denormalise(t, min, max) {
1053
+ return min + t * (max - min);
1054
+ }
1055
+ function commit(value, min, max, step) {
1056
+ return clamp(snap(value, min, max === min ? 1 : step), min, max);
1057
+ }
1058
+ function valueKeyHandler(options) {
1059
+ return (event) => {
1060
+ const { value, min, max, step, invert, onChange, disabled } = options;
1061
+ if (disabled) return false;
1062
+ const dir = invert ? -1 : 1;
1063
+ const set = (next) => {
1064
+ event.preventDefault();
1065
+ onChange(commit(next, min, max, step));
1066
+ };
386
1067
  switch (event.key) {
387
1068
  case "ArrowUp":
388
1069
  case "ArrowRight":
389
- event.preventDefault();
390
- setValue(value + safeStep);
391
- break;
1070
+ set(value + step * dir);
1071
+ return true;
392
1072
  case "ArrowDown":
393
1073
  case "ArrowLeft":
394
- event.preventDefault();
395
- setValue(value - safeStep);
396
- break;
1074
+ set(value - step * dir);
1075
+ return true;
397
1076
  case "PageUp":
398
- event.preventDefault();
399
- setValue(value + pageStep);
400
- break;
1077
+ set(value + step * 10 * dir);
1078
+ return true;
401
1079
  case "PageDown":
402
- event.preventDefault();
403
- setValue(value - pageStep);
404
- break;
1080
+ set(value - step * 10 * dir);
1081
+ return true;
405
1082
  case "Home":
406
- event.preventDefault();
407
- setValue(min);
408
- break;
1083
+ set(min);
1084
+ return true;
409
1085
  case "End":
410
- event.preventDefault();
411
- setValue(max);
412
- break;
1086
+ set(max);
1087
+ return true;
413
1088
  default:
414
- break;
1089
+ return false;
415
1090
  }
416
1091
  };
417
- const onPointerMove = (event) => {
418
- if (event.buttons !== 1) return;
419
- updateFromClientY(event.clientY);
1092
+ }
1093
+
1094
+ // src/core/useRotary.ts
1095
+ var DEFAULT_TRAVEL = { start: -135, sweep: 270 };
1096
+ function wrapDeg(deg) {
1097
+ return ((deg + 180) % 360 + 360) % 360 - 180;
1098
+ }
1099
+ function resolveTravel(travel2) {
1100
+ if (travel2 === void 0) return DEFAULT_TRAVEL;
1101
+ if (typeof travel2 === "number") return { start: -travel2 / 2, sweep: travel2 };
1102
+ return travel2;
1103
+ }
1104
+ var warnedFullTurn = false;
1105
+ function capturePointer(target, pointerId) {
1106
+ try {
1107
+ target.setPointerCapture?.(pointerId);
1108
+ } catch {
1109
+ }
1110
+ }
1111
+ function useRotary(options) {
1112
+ const {
1113
+ min = 0,
1114
+ max = 100,
1115
+ step = 1,
1116
+ dragDistance = 200,
1117
+ wrap = false,
1118
+ disabled = false
1119
+ } = options;
1120
+ const { start, sweep } = resolveTravel(options.travel);
1121
+ let input = options.input ?? "drag";
1122
+ if (input === "angle" && Math.abs(sweep) >= 360) {
1123
+ if (isDev && !warnedFullTurn) {
1124
+ warnedFullTurn = true;
1125
+ console.warn(
1126
+ `[@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".`
1127
+ );
1128
+ }
1129
+ input = "drag";
1130
+ }
1131
+ const [value, setValue] = useControllableValue(
1132
+ options.value,
1133
+ options.defaultValue ?? min,
1134
+ options.onChange
1135
+ );
1136
+ const [dragging, setDragging] = useState3(false);
1137
+ const draggingRef = useRef7(false);
1138
+ const rootRef = useRef7(null);
1139
+ const drag = useRef7({ value: 0, clientY: 0, angle: 0, accum: 0 });
1140
+ const range = max - min;
1141
+ const keyStep = step > 0 ? step : range / 100 || 1;
1142
+ const percent = clamp(normalise(value, min, max), 0, 1);
1143
+ const angle = start + percent * sweep;
1144
+ const set = useCallback4(
1145
+ (next) => setValue(commit(next, min, max, step)),
1146
+ [setValue, min, max, step]
1147
+ );
1148
+ const pointerAngle = (event) => {
1149
+ const rect = rootRef.current?.getBoundingClientRect();
1150
+ if (!rect) return null;
1151
+ const dx = event.clientX - (rect.left + rect.width / 2);
1152
+ const dy = event.clientY - (rect.top + rect.height / 2);
1153
+ return Math.atan2(dy, dx) * (180 / Math.PI) + 90;
420
1154
  };
421
- return /* @__PURE__ */ jsxs3("div", { className: cn("ambx-stack", className), ...props, children: [
422
- /* @__PURE__ */ jsx7(
1155
+ const fromAngle = (event) => {
1156
+ const raw = pointerAngle(event);
1157
+ if (raw === null) return;
1158
+ const mid = start + sweep / 2;
1159
+ let a = mid + wrapDeg(raw - mid);
1160
+ const end = start + sweep;
1161
+ if (a < Math.min(start, end) || a > Math.max(start, end)) {
1162
+ const toStart = Math.abs(wrapDeg(a - start));
1163
+ const toEnd = Math.abs(wrapDeg(a - end));
1164
+ a = toStart <= toEnd ? start : end;
1165
+ }
1166
+ set(denormalise((a - start) / (sweep || 1), min, max));
1167
+ };
1168
+ const fromDrag = (event) => {
1169
+ const dy = drag.current.clientY - event.clientY;
1170
+ drag.current.clientY = event.clientY;
1171
+ drag.current.accum += dy * (event.shiftKey ? 0.25 : 1);
1172
+ set(drag.current.value + drag.current.accum / dragDistance * range);
1173
+ };
1174
+ const fromDelta = (event) => {
1175
+ const raw = pointerAngle(event);
1176
+ if (raw === null) return;
1177
+ drag.current.accum += wrapDeg(raw - drag.current.angle);
1178
+ drag.current.angle = raw;
1179
+ const next = drag.current.value + drag.current.accum / (sweep || 360) * range;
1180
+ set(wrap && range > 0 ? min + ((next - min) % range + range) % range : next);
1181
+ };
1182
+ const track = (event) => {
1183
+ if (input === "angle") fromAngle(event);
1184
+ else if (input === "delta") fromDelta(event);
1185
+ else fromDrag(event);
1186
+ };
1187
+ const state = useMemo3(
1188
+ () => ({
1189
+ value,
1190
+ min,
1191
+ max,
1192
+ percent,
1193
+ angle,
1194
+ travelStart: start,
1195
+ travelSweep: sweep,
1196
+ detents: options.detents ?? (step > 0 && range > 0 ? Math.round(range / step) : 0),
1197
+ dragging,
1198
+ disabled,
1199
+ atMin: value <= min,
1200
+ atMax: value >= max
1201
+ }),
1202
+ [value, min, max, percent, angle, start, sweep, options.detents, step, range, dragging, disabled]
1203
+ );
1204
+ const onKeyDown = valueKeyHandler({ value, min, max, step: keyStep, disabled, onChange: setValue });
1205
+ const rootProps = {
1206
+ ref: rootRef,
1207
+ role: "slider",
1208
+ "aria-valuemin": min,
1209
+ "aria-valuemax": max,
1210
+ "aria-valuenow": value,
1211
+ "aria-orientation": "vertical",
1212
+ "aria-disabled": disabled || void 0,
1213
+ tabIndex: disabled ? -1 : 0,
1214
+ style: stateStyle(state),
1215
+ ...stateData(state),
1216
+ onPointerDown: (event) => {
1217
+ if (disabled || event.button !== 0) return;
1218
+ capturePointer(event.currentTarget, event.pointerId);
1219
+ draggingRef.current = true;
1220
+ setDragging(true);
1221
+ drag.current = {
1222
+ value,
1223
+ clientY: event.clientY,
1224
+ angle: pointerAngle(event) ?? 0,
1225
+ accum: 0
1226
+ };
1227
+ if (input === "angle") fromAngle(event);
1228
+ },
1229
+ onPointerMove: (event) => {
1230
+ if (!draggingRef.current || disabled) return;
1231
+ track(event);
1232
+ },
1233
+ onPointerUp: () => {
1234
+ draggingRef.current = false;
1235
+ setDragging(false);
1236
+ },
1237
+ onPointerCancel: () => {
1238
+ draggingRef.current = false;
1239
+ setDragging(false);
1240
+ },
1241
+ onKeyDown: (event) => {
1242
+ onKeyDown(event);
1243
+ }
1244
+ };
1245
+ return { state, rootProps, setValue: set };
1246
+ }
1247
+
1248
+ // src/controls/AmbientRotary.tsx
1249
+ import { jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
1250
+ function AmbientRotary({
1251
+ parts,
1252
+ size,
1253
+ animate = "auto",
1254
+ label,
1255
+ className,
1256
+ value,
1257
+ defaultValue,
1258
+ min,
1259
+ max,
1260
+ step,
1261
+ detents,
1262
+ travel: travel2,
1263
+ input,
1264
+ dragDistance,
1265
+ wrap,
1266
+ disabled,
1267
+ onChange,
1268
+ ...rest
1269
+ }) {
1270
+ const labelId = useId5();
1271
+ const stackRef = useRef8(null);
1272
+ const { state, rootProps } = useRotary({
1273
+ value,
1274
+ defaultValue,
1275
+ min,
1276
+ max,
1277
+ step,
1278
+ detents,
1279
+ travel: travel2,
1280
+ input,
1281
+ dragDistance,
1282
+ wrap,
1283
+ disabled,
1284
+ onChange
1285
+ });
1286
+ useDevPartCheck(stackRef, "AmbientRotary");
1287
+ const sized = sizeProps("rotary", size);
1288
+ const { style: restStyle, ...restProps } = rest;
1289
+ const control = /* @__PURE__ */ jsx18(
1290
+ "div",
1291
+ {
1292
+ ...restProps,
1293
+ ...rootProps,
1294
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
1295
+ "data-animate": animate,
1296
+ className: cn("ambx-control ambx-rotary", sized.className, className),
1297
+ style: { ...rootProps.style, ...sized.style, ...restStyle },
1298
+ children: /* @__PURE__ */ jsx18(ControlStateProvider, { value: state, children: /* @__PURE__ */ jsx18(Frames, { parts }) })
1299
+ }
1300
+ );
1301
+ return /* @__PURE__ */ jsxs8("div", { className: "ambx-stack", ref: stackRef, children: [
1302
+ control,
1303
+ label ? /* @__PURE__ */ jsx18("span", { id: labelId, className: "ambx-label", children: label }) : null
1304
+ ] });
1305
+ }
1306
+
1307
+ // src/components/AmbientKnob.tsx
1308
+ import { jsx as jsx19 } from "react/jsx-runtime";
1309
+ function AmbientKnob({
1310
+ material,
1311
+ knurling,
1312
+ knurlColor,
1313
+ markers,
1314
+ indicator,
1315
+ look,
1316
+ size = "md",
1317
+ className,
1318
+ travel: travel2,
1319
+ input,
1320
+ animate,
1321
+ ...rest
1322
+ }) {
1323
+ const { dress, defaults } = useDress(
1324
+ "rotary",
1325
+ { material, knurling, knurlColor, markers, indicator, ...look },
1326
+ groundedKit.rotary
1327
+ );
1328
+ return /* @__PURE__ */ jsx19(
1329
+ AmbientRotary,
1330
+ {
1331
+ ...rest,
1332
+ travel: travel2 ?? defaults?.travel,
1333
+ input: input ?? defaults?.input,
1334
+ animate: animate ?? defaults?.animate,
1335
+ size,
1336
+ className: cn(dress.className, className),
1337
+ parts: dress.parts
1338
+ }
1339
+ );
1340
+ }
1341
+
1342
+ // src/controls/AmbientTravel.tsx
1343
+ import { useId as useId6, useRef as useRef10 } from "react";
1344
+
1345
+ // src/core/useTravel.ts
1346
+ import { useCallback as useCallback5, useMemo as useMemo4, useRef as useRef9, useState as useState4 } from "react";
1347
+ function useTravel(options) {
1348
+ const {
1349
+ min = 0,
1350
+ max = 100,
1351
+ step = 1,
1352
+ orientation = "horizontal",
1353
+ invert = false,
1354
+ disabled = false
1355
+ } = options;
1356
+ const [value, setValue] = useControllableValue(
1357
+ options.value,
1358
+ options.defaultValue ?? min,
1359
+ options.onChange
1360
+ );
1361
+ const [dragging, setDragging] = useState4(false);
1362
+ const draggingRef = useRef9(false);
1363
+ const rootRef = useRef9(null);
1364
+ const vertical = orientation === "vertical";
1365
+ const range = max - min;
1366
+ const keyStep = step > 0 ? step : range / 100 || 1;
1367
+ const percent = clamp(normalise(value, min, max), 0, 1);
1368
+ const set = useCallback5(
1369
+ (next) => setValue(commit(next, min, max, step)),
1370
+ [setValue, min, max, step]
1371
+ );
1372
+ const track = (event) => {
1373
+ const rect = rootRef.current?.getBoundingClientRect();
1374
+ if (!rect) return;
1375
+ const raw = vertical ? 1 - (event.clientY - rect.top) / (rect.height || 1) : (event.clientX - rect.left) / (rect.width || 1);
1376
+ set(denormalise(clamp(invert ? 1 - raw : raw, 0, 1), min, max));
1377
+ };
1378
+ const state = useMemo4(
1379
+ () => ({
1380
+ value,
1381
+ min,
1382
+ max,
1383
+ percent,
1384
+ angle: 0,
1385
+ travelStart: 0,
1386
+ travelSweep: 0,
1387
+ detents: options.detents ?? (step > 0 && range > 0 ? Math.round(range / step) : 0),
1388
+ dragging,
1389
+ disabled,
1390
+ atMin: value <= min,
1391
+ atMax: value >= max
1392
+ }),
1393
+ [value, min, max, percent, options.detents, step, range, dragging, disabled]
1394
+ );
1395
+ const onKeyDown = valueKeyHandler({
1396
+ value,
1397
+ min,
1398
+ max,
1399
+ step: keyStep,
1400
+ disabled,
1401
+ invert,
1402
+ onChange: setValue
1403
+ });
1404
+ const rootProps = {
1405
+ ref: rootRef,
1406
+ role: "slider",
1407
+ "aria-valuemin": min,
1408
+ "aria-valuemax": max,
1409
+ "aria-valuenow": value,
1410
+ "aria-orientation": orientation,
1411
+ "aria-disabled": disabled || void 0,
1412
+ tabIndex: disabled ? -1 : 0,
1413
+ style: stateStyle(state),
1414
+ "data-orientation": orientation,
1415
+ ...stateData(state),
1416
+ onPointerDown: (event) => {
1417
+ if (disabled || event.button !== 0) return;
1418
+ capturePointer(event.currentTarget, event.pointerId);
1419
+ draggingRef.current = true;
1420
+ setDragging(true);
1421
+ track(event);
1422
+ },
1423
+ onPointerMove: (event) => {
1424
+ if (!draggingRef.current || disabled) return;
1425
+ track(event);
1426
+ },
1427
+ onPointerUp: () => {
1428
+ draggingRef.current = false;
1429
+ setDragging(false);
1430
+ },
1431
+ onPointerCancel: () => {
1432
+ draggingRef.current = false;
1433
+ setDragging(false);
1434
+ },
1435
+ onKeyDown: (event) => {
1436
+ onKeyDown(event);
1437
+ }
1438
+ };
1439
+ return { state, rootProps, setValue: set };
1440
+ }
1441
+
1442
+ // src/controls/AmbientTravel.tsx
1443
+ import { jsx as jsx20, jsxs as jsxs9 } from "react/jsx-runtime";
1444
+ function AmbientTravel({
1445
+ parts,
1446
+ size,
1447
+ animate = "auto",
1448
+ label,
1449
+ className,
1450
+ value,
1451
+ defaultValue,
1452
+ min,
1453
+ max,
1454
+ step,
1455
+ detents,
1456
+ orientation = "horizontal",
1457
+ invert,
1458
+ disabled,
1459
+ onChange,
1460
+ ...rest
1461
+ }) {
1462
+ const labelId = useId6();
1463
+ const stackRef = useRef10(null);
1464
+ const { state, rootProps } = useTravel({
1465
+ value,
1466
+ defaultValue,
1467
+ min,
1468
+ max,
1469
+ step,
1470
+ detents,
1471
+ orientation,
1472
+ invert,
1473
+ disabled,
1474
+ onChange
1475
+ });
1476
+ useDevPartCheck(stackRef, "AmbientTravel");
1477
+ const sized = sizeProps("travel", size);
1478
+ const { style: restStyle, ...restProps } = rest;
1479
+ return /* @__PURE__ */ jsxs9("div", { className: "ambx-stack", ref: stackRef, children: [
1480
+ /* @__PURE__ */ jsx20(
423
1481
  "div",
424
1482
  {
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
- )
1483
+ ...restProps,
1484
+ ...rootProps,
1485
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
1486
+ "data-animate": animate,
1487
+ className: cn(
1488
+ "ambx-control ambx-travel",
1489
+ `ambx-travel-${orientation}`,
1490
+ sized.className,
1491
+ className
1492
+ ),
1493
+ style: { ...rootProps.style, ...sized.style, ...restStyle },
1494
+ children: /* @__PURE__ */ jsx20(ControlStateProvider, { value: state, children: /* @__PURE__ */ jsx20(Frames, { parts }) })
449
1495
  }
450
1496
  ),
451
- label ? /* @__PURE__ */ jsx7("span", { id, className: "ambx-label", children: label }) : null
1497
+ label ? /* @__PURE__ */ jsx20("span", { id: labelId, className: "ambx-label", children: label }) : null
452
1498
  ] });
453
1499
  }
454
1500
 
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));
1501
+ // src/components/AmbientFader.tsx
1502
+ import { jsx as jsx21 } from "react/jsx-runtime";
1503
+ function AmbientFader({
1504
+ material,
1505
+ look,
1506
+ size = "md",
1507
+ animate,
1508
+ className,
1509
+ ...rest
1510
+ }) {
1511
+ const { dress, defaults } = useDress(
1512
+ "travel",
1513
+ { material, orientation: "vertical", ...look },
1514
+ groundedKit.travel
1515
+ );
1516
+ return /* @__PURE__ */ jsx21(
1517
+ AmbientTravel,
1518
+ {
1519
+ ...rest,
1520
+ orientation: "vertical",
1521
+ size,
1522
+ animate: animate ?? defaults?.animate,
1523
+ className: cn(dress.className, className),
1524
+ parts: dress.parts
1525
+ }
1526
+ );
460
1527
  }
1528
+
1529
+ // src/components/AmbientSlider.tsx
1530
+ import { jsx as jsx22 } from "react/jsx-runtime";
461
1531
  function AmbientSlider({
462
- value,
463
- min = 0,
464
- max = 100,
465
- step = 1,
466
- label,
467
1532
  material,
1533
+ look,
468
1534
  size = "md",
469
- onChange,
1535
+ animate,
470
1536
  className,
471
- ...props
1537
+ ...rest
472
1538
  }) {
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;
1539
+ const { dress, defaults } = useDress(
1540
+ "travel",
1541
+ { material, orientation: "horizontal", ...look },
1542
+ groundedKit.travel
1543
+ );
1544
+ return /* @__PURE__ */ jsx22(
1545
+ AmbientTravel,
1546
+ {
1547
+ ...rest,
1548
+ orientation: "horizontal",
1549
+ size,
1550
+ animate: animate ?? defaults?.animate,
1551
+ className: cn(dress.className, className),
1552
+ parts: dress.parts
1553
+ }
1554
+ );
1555
+ }
1556
+
1557
+ // src/parts/console.tsx
1558
+ import { Fragment as Fragment4, jsx as jsx23, jsxs as jsxs10 } from "react/jsx-runtime";
1559
+ function ConsoleWell({ className }) {
1560
+ return /* @__PURE__ */ jsx23("span", { className: cn("amb-console-housing amb-groove", className), children: /* @__PURE__ */ jsx23("span", { className: "amb-console-face amb-surface" }) });
1561
+ }
1562
+ function ConsoleBar({ className }) {
1563
+ 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" }) }) });
1564
+ }
1565
+ function ConsoleMarks({
1566
+ mark = true,
1567
+ legend = true,
1568
+ className
1569
+ }) {
1570
+ return /* @__PURE__ */ jsxs10("span", { className: cn("amb-console-marks", className), "aria-hidden": true, children: [
1571
+ mark ? /* @__PURE__ */ jsx23("span", { className: "amb-console-mark" }) : null,
1572
+ legend ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
1573
+ /* @__PURE__ */ jsx23("span", { className: "amb-console-legend amb-console-legend-min", children: "\u2212" }),
1574
+ /* @__PURE__ */ jsx23("span", { className: "amb-console-legend amb-console-legend-max", children: "+" })
1575
+ ] }) : null
1576
+ ] });
1577
+ }
1578
+ function ToggleTrack({ className }) {
1579
+ return /* @__PURE__ */ jsx23("span", { className: cn("amb-console-track amb-groove", className) });
1580
+ }
1581
+ function ToggleThumb({ className }) {
1582
+ return /* @__PURE__ */ jsx23("span", { className: cn("amb-console-thumb ambient amb-thickness-2", className) });
1583
+ }
1584
+
1585
+ // src/kits/console.tsx
1586
+ import { jsx as jsx24 } from "react/jsx-runtime";
1587
+ function rotary2(look) {
1588
+ const mark = look.mark !== false;
1589
+ const legend = look.legend !== false;
1590
+ return {
1591
+ className: cn(
1592
+ "amb-console-knob",
1593
+ mark && "amb-console-knob-marked",
1594
+ legend && "amb-console-knob-legended"
1595
+ ),
1596
+ parts: {
1597
+ panel: mark || legend ? /* @__PURE__ */ jsx24(ConsoleMarks, { mark, legend }) : null,
1598
+ base: /* @__PURE__ */ jsx24(ConsoleWell, {}),
1599
+ actuator: /* @__PURE__ */ jsx24(ConsoleBar, {})
521
1600
  }
522
1601
  };
523
- const onPointerMove = (event) => {
524
- if (event.buttons !== 1) return;
525
- updateFromClientX(event.clientX);
1602
+ }
1603
+ function latch2() {
1604
+ return {
1605
+ className: "amb-console-toggle",
1606
+ parts: { base: /* @__PURE__ */ jsx24(ToggleTrack, {}), actuator: /* @__PURE__ */ jsx24(ToggleThumb, {}) }
526
1607
  };
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
- ] });
1608
+ }
1609
+ var consoleKit = {
1610
+ name: "console",
1611
+ rotary: rotary2,
1612
+ latch: latch2,
1613
+ /* A visual identity may say how its controls feel to turn. The desk knob
1614
+ this is drawn from is an absolute-position pot with a centre detent, so
1615
+ it grabs where you press rather than tracking a drag. */
1616
+ defaults: {
1617
+ rotary: { input: "angle", travel: 280 }
1618
+ },
1619
+ looks: {
1620
+ rotary: ["mark", "legend"],
1621
+ latch: []
1622
+ }
1623
+ };
1624
+ function ConsoleKnob({ mark, legend, ...rest }) {
1625
+ return /* @__PURE__ */ jsx24(AmbientKnob, { ...rest, look: { mark, legend } });
1626
+ }
1627
+ function ConsoleToggle(props) {
1628
+ return /* @__PURE__ */ jsx24(AmbientSwitch, { ...props });
558
1629
  }
559
1630
  export {
1631
+ AmbientBank,
560
1632
  AmbientButton,
561
1633
  AmbientFader,
1634
+ AmbientKitProvider,
562
1635
  AmbientKnob,
1636
+ AmbientLatch,
563
1637
  AmbientPanel,
1638
+ AmbientPress,
564
1639
  AmbientProvider,
565
1640
  AmbientRack,
1641
+ AmbientRotary,
1642
+ AmbientSelect,
566
1643
  AmbientSlider,
567
- AmbientSwitch
1644
+ AmbientSwitch,
1645
+ AmbientTravel,
1646
+ ButtonCap,
1647
+ ConsoleBar,
1648
+ ConsoleKnob,
1649
+ ConsoleMarks,
1650
+ ConsoleToggle,
1651
+ ConsoleWell,
1652
+ FaderCap,
1653
+ IndicatorBar,
1654
+ IndicatorDot,
1655
+ KeyCap,
1656
+ KeyLens,
1657
+ KnobBody,
1658
+ KnurledFace,
1659
+ Led,
1660
+ ScaleRing,
1661
+ SliderThumb,
1662
+ SwitchPill,
1663
+ SwitchTrack,
1664
+ ToggleThumb,
1665
+ ToggleTrack,
1666
+ TravelTrack,
1667
+ consoleKit,
1668
+ groundedKit,
1669
+ useBank,
1670
+ useBankKey,
1671
+ useControlState,
1672
+ useDress,
1673
+ useKit,
1674
+ useLatch,
1675
+ usePress,
1676
+ useRotary,
1677
+ useTravel
568
1678
  };
569
1679
  //# sourceMappingURL=index.js.map