@5voltfx/design-system 0.2.0 → 0.3.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.
@@ -0,0 +1,1208 @@
1
+ import {
2
+ useDetentTravel,
3
+ usePointerDrag,
4
+ useReducedMotion,
5
+ validateStationCount
6
+ } from "./chunk-UZW4E2UH.js";
7
+
8
+ // src/components/DialNav/DialNav.jsx
9
+ import { useCallback, useEffect, useMemo as useMemo4, useRef as useRef4, useState } from "react";
10
+ import { NavLink } from "react-router-dom";
11
+
12
+ // src/components/DialNav/geometry.js
13
+ var DIAL = Object.freeze({
14
+ /** C-I — a disc that swings out of the edge and spins to an index. */
15
+ volvelle: Object.freeze({
16
+ viewBox: Object.freeze([440, 420]),
17
+ cx: 210,
18
+ cy: 210,
19
+ /** Disc face, and the hairline circle just inside it. */
20
+ r: 200,
21
+ rInner: 193,
22
+ /** Decorative rim ticks run inward from rInner. */
23
+ rRing: 179,
24
+ /** Station band: quarter-detent ticks run inward from rBand. */
25
+ rBand: 177,
26
+ /** Radial centre of a station label — the pivot its flip turns about. */
27
+ labelR: 129,
28
+ /** Logarithmic ring at the hub, for the same reason the original had one. */
29
+ rLogInner: 62,
30
+ rLogOuter: 76,
31
+ rLogPlate: 78,
32
+ rLogNumeral: 52,
33
+ rHub: 34,
34
+ /** Total sweep the stations are spread across. */
35
+ arc: 216,
36
+ /** Fixed cursor arm, due east. */
37
+ arm: Object.freeze({ x: 38, w: 166, h: 38, hairline: 208, index: 218 }),
38
+ /**
39
+ * Half the width of the widest station label, including its padding.
40
+ * Labels are HTML, so this cannot be derived from the SVG — it is
41
+ * measured, and it is what the extraction distance has to clear.
42
+ */
43
+ labelClearance: 60,
44
+ rest: -376,
45
+ /*
46
+ * Far enough out that the hub sits at labelR + labelClearance from the
47
+ * stage edge, so a label swung to due west still fits. The artboards used
48
+ * -110, but their labels were SVG text with no padding; HTML labels run
49
+ * off the edge at that distance.
50
+ */
51
+ out: -10,
52
+ settleMs: 520
53
+ }),
54
+ /** C-II — two concentric rings and a swept arm, after the E6B. */
55
+ concentric: Object.freeze({
56
+ /* Wider than tall: the fixed index chevron reaches cx + 232. */
57
+ viewBox: Object.freeze([460, 440]),
58
+ cx: 220,
59
+ cy: 220,
60
+ r: 208,
61
+ /** Outer ring: sections, spread over a full turn. */
62
+ rOuter: 200,
63
+ rOuterInner: 146,
64
+ labelR: 164,
65
+ /* Measured: the widest section label renders 85px across at this ring's
66
+ reduced size, so half of it is 43. */
67
+ labelClearance: 44,
68
+ /** Inner ring: the pages of whichever section is indexed. */
69
+ rInnerPlate: 132,
70
+ rHubPlate: 58,
71
+ innerLabelR: 122,
72
+ innerArc: 135,
73
+ innerCount: 4,
74
+ arm: Object.freeze({ x: 50, w: 106, h: 30, tip: 156, point: 172 }),
75
+ index: Object.freeze({ from: 198, to: 226, chevron: 232, back: 214 }),
76
+ rest: -396,
77
+ /* cx - labelR - labelClearance leaves 12px; -6 keeps 6px of margin for a
78
+ section label swung due west by the ring's full turn. */
79
+ out: -6,
80
+ settleMs: 480,
81
+ innerSettleMs: 380
82
+ }),
83
+ /** C-III — a quadrant that blooms out of the edge on a ratchet. */
84
+ fan: Object.freeze({
85
+ viewBox: Object.freeze([400, 420]),
86
+ /** The pivot sits hard against the left edge. */
87
+ cx: 46,
88
+ cy: 210,
89
+ /** Spoke length. With halfAngle 80°, ro·sin(80°) ≈ 193 — the most a
90
+ 420-tall viewBox will take. Do not raise without re-measuring. */
91
+ r: 192,
92
+ labelR: 130,
93
+ labelClearance: 60,
94
+ arc: 150,
95
+ wedge: Object.freeze({ halfAngle: 80, ri: 64, ro: 196 }),
96
+ hub: Object.freeze({ outer: 64, inner: 52 }),
97
+ /** Ratchet teeth ride just outside the spoke ends. */
98
+ tooth: Object.freeze({ from: 3, to: 10, lead: 3.4 }),
99
+ /** How far the clip is held closed at rest, hiding all but the hub. */
100
+ restClip: 328,
101
+ bloomStaggerMs: 34,
102
+ settleMs: 440
103
+ })
104
+ });
105
+ var polar = (cx, cy, r, deg) => [
106
+ cx + r * Math.cos(deg * Math.PI / 180),
107
+ cy + r * Math.sin(deg * Math.PI / 180)
108
+ ];
109
+ function uprightFlip(absDeg) {
110
+ const a = (absDeg % 360 + 360) % 360;
111
+ return a > 90 && a < 270;
112
+ }
113
+ var pitchFor = (spec, count) => spec.arc / Math.max(1, count - 1);
114
+ var startAngle = (spec) => -spec.arc / 2;
115
+ var stationAngle = (spec, count, pos) => startAngle(spec) + pos * pitchFor(spec, count);
116
+ var rotationFor = (spec, count, pos) => -stationAngle(spec, count, pos);
117
+ function angleDelta(to, from) {
118
+ let d = to - from;
119
+ while (d > 180) d -= 360;
120
+ while (d <= -180) d += 360;
121
+ return d;
122
+ }
123
+ function logRingTicks() {
124
+ const bands = [
125
+ [1, 2, 0.05],
126
+ [2, 5, 0.1],
127
+ [5, 10, 0.2]
128
+ ];
129
+ const out = [];
130
+ for (const [from, to, step] of bands) {
131
+ const steps = Math.round((to - from) / step);
132
+ for (let k = 0; k < steps; k++) {
133
+ const v = Math.round((from + k * step) * 100) / 100;
134
+ out.push({
135
+ v,
136
+ angle: Math.log10(v) * 360 - 90,
137
+ major: Number.isInteger(v)
138
+ });
139
+ }
140
+ }
141
+ return out;
142
+ }
143
+ var innerPitchFor = (spec, count) => spec.innerArc / Math.max(1, count - 1);
144
+ var innerStartAngle = (spec) => -spec.innerArc / 2;
145
+ var innerAngle = (spec, count, pos) => innerStartAngle(spec) + pos * innerPitchFor(spec, count);
146
+ var toothLead = (spec, detent) => Math.min(spec.tooth.lead, detent * 0.45);
147
+ function wedgePath(spec) {
148
+ const { cx, cy } = spec;
149
+ const { halfAngle, ri, ro } = spec.wedge;
150
+ const [ax, ay] = polar(cx, cy, ri, -halfAngle);
151
+ const [bx, by] = polar(cx, cy, ro, -halfAngle);
152
+ const [dx, dy] = polar(cx, cy, ro, halfAngle);
153
+ const [ex, ey] = polar(cx, cy, ri, halfAngle);
154
+ return `M${ax},${ay} L${bx},${by} A${ro},${ro} 0 0 1 ${dx},${dy} L${ex},${ey} A${ri},${ri} 0 0 0 ${ax},${ay} Z`;
155
+ }
156
+
157
+ // src/components/DialNav/variants/Volvelle.jsx
158
+ import { forwardRef, useMemo, useRef } from "react";
159
+ import { jsx, jsxs } from "react/jsx-runtime";
160
+ var Volvelle = forwardRef(function Volvelle2({ spec, count, travel, theta, pitch, onDragStateChange }, ref) {
161
+ const [vbW, vbH] = spec.viewBox;
162
+ const { cx, cy, arm } = spec;
163
+ const a0 = startAngle(spec);
164
+ const discRef = useRef(null);
165
+ const grab = useRef({ angle: 0, pos: 0 });
166
+ const angleAt = (event) => {
167
+ const box = ref.current?.getBoundingClientRect();
168
+ if (!box?.width) return null;
169
+ const scale = vbW / box.width;
170
+ const x = (event.clientX - box.left) * scale - cx;
171
+ const y = (event.clientY - box.top) * scale - cy;
172
+ return Math.atan2(y, x) * 180 / Math.PI;
173
+ };
174
+ usePointerDrag(discRef, {
175
+ onBegin: (event) => {
176
+ const a = angleAt(event);
177
+ if (a == null) return;
178
+ grab.current = { angle: a, pos: travel.pos };
179
+ travel.begin();
180
+ onDragStateChange?.(true);
181
+ },
182
+ onMove: (event) => {
183
+ const a = angleAt(event);
184
+ if (a == null) return;
185
+ travel.track(grab.current.pos - angleDelta(a, grab.current.angle) / pitch);
186
+ },
187
+ onEnd: (_event, wasTap) => {
188
+ if (!wasTap) travel.release();
189
+ onDragStateChange?.(false);
190
+ }
191
+ });
192
+ const rim = useMemo(() => {
193
+ const out = [];
194
+ for (let a = 0; a < 360; a += 2) {
195
+ const len = a % 10 === 0 ? 14 : a % 5 === 0 ? 9 : 5;
196
+ out.push({
197
+ a,
198
+ len,
199
+ faint: a % 10 !== 0
200
+ });
201
+ }
202
+ return out;
203
+ }, []);
204
+ const band = useMemo(() => {
205
+ const out = [];
206
+ for (let q = 0; q <= (count - 1) * 4; q++) {
207
+ const major = q % 4 === 0;
208
+ const half = q % 2 === 0;
209
+ out.push({
210
+ a: a0 + q / 4 * pitch,
211
+ len: major ? 22 : half ? 12 : 7,
212
+ faint: !major,
213
+ station: major
214
+ });
215
+ }
216
+ return out;
217
+ }, [a0, count, pitch]);
218
+ const logTicks = useMemo(logRingTicks, []);
219
+ return /* @__PURE__ */ jsxs(
220
+ "svg",
221
+ {
222
+ ref,
223
+ className: "ds-dial__chrome",
224
+ viewBox: `0 0 ${vbW} ${vbH}`,
225
+ "aria-hidden": "true",
226
+ focusable: "false",
227
+ children: [
228
+ /* @__PURE__ */ jsx("circle", { className: "ds-dial__face", cx, cy, r: spec.r }),
229
+ /* @__PURE__ */ jsx(
230
+ "circle",
231
+ {
232
+ className: "ds-dial__hairline-ring",
233
+ cx,
234
+ cy,
235
+ r: spec.rInner
236
+ }
237
+ ),
238
+ /* @__PURE__ */ jsxs("g", { ref: discRef, className: "ds-dial__disc", children: [
239
+ /* @__PURE__ */ jsx(
240
+ "circle",
241
+ {
242
+ className: "ds-dial__grab",
243
+ cx,
244
+ cy,
245
+ r: spec.rInner
246
+ }
247
+ ),
248
+ /* @__PURE__ */ jsxs("g", { transform: `rotate(${theta} ${cx} ${cy})`, children: [
249
+ rim.map((tick, i) => {
250
+ const [x1, y1] = polar(cx, cy, spec.rInner, tick.a);
251
+ const [x2, y2] = polar(cx, cy, spec.rInner - tick.len, tick.a);
252
+ return /* @__PURE__ */ jsx(
253
+ "line",
254
+ {
255
+ className: "ds-dial__tick" + (tick.faint ? " ds-dial__tick--faint" : ""),
256
+ x1,
257
+ y1,
258
+ x2,
259
+ y2
260
+ },
261
+ i
262
+ );
263
+ }),
264
+ /* @__PURE__ */ jsx(
265
+ "circle",
266
+ {
267
+ className: "ds-dial__hairline-ring",
268
+ cx,
269
+ cy,
270
+ r: spec.rRing
271
+ }
272
+ ),
273
+ band.map((tick, i) => {
274
+ const [x1, y1] = polar(cx, cy, spec.rBand, tick.a);
275
+ const [x2, y2] = polar(cx, cy, spec.rBand - tick.len, tick.a);
276
+ return /* @__PURE__ */ jsx(
277
+ "line",
278
+ {
279
+ className: "ds-dial__tick" + (tick.faint ? " ds-dial__tick--faint" : "") + (tick.station ? " ds-dial__tick--station" : ""),
280
+ x1,
281
+ y1,
282
+ x2,
283
+ y2
284
+ },
285
+ `b${i}`
286
+ );
287
+ }),
288
+ /* @__PURE__ */ jsx(
289
+ "circle",
290
+ {
291
+ className: "ds-dial__log-plate",
292
+ cx,
293
+ cy,
294
+ r: spec.rLogPlate
295
+ }
296
+ ),
297
+ logTicks.map((tick, i) => {
298
+ const outer = spec.rLogInner + (tick.major ? 14 : 6);
299
+ const [x1, y1] = polar(cx, cy, spec.rLogInner, tick.angle);
300
+ const [x2, y2] = polar(cx, cy, outer, tick.angle);
301
+ const [tx, ty] = polar(cx, cy, spec.rLogNumeral, tick.angle);
302
+ return /* @__PURE__ */ jsxs("g", { children: [
303
+ /* @__PURE__ */ jsx(
304
+ "line",
305
+ {
306
+ className: "ds-dial__tick" + (tick.major ? "" : " ds-dial__tick--faint"),
307
+ x1,
308
+ y1,
309
+ x2,
310
+ y2
311
+ }
312
+ ),
313
+ tick.major && /* @__PURE__ */ jsx(
314
+ "text",
315
+ {
316
+ className: "ds-dial__numeral",
317
+ x: tx,
318
+ y: ty + 3,
319
+ textAnchor: "middle",
320
+ transform: `rotate(${tick.angle + 90 + (uprightFlip(tick.angle + 90) ? 180 : 0)} ${tx} ${ty})`,
321
+ children: Math.round(tick.v)
322
+ }
323
+ )
324
+ ] }, `l${i}`);
325
+ })
326
+ ] })
327
+ ] }),
328
+ /* @__PURE__ */ jsx("circle", { className: "ds-dial__hub", cx, cy, r: spec.rHub }),
329
+ /* @__PURE__ */ jsx("circle", { className: "ds-dial__rivet", cx, cy, r: 9 }),
330
+ /* @__PURE__ */ jsx("circle", { className: "ds-dial__rivet-hi", cx, cy, r: 4 }),
331
+ /* @__PURE__ */ jsxs("g", { className: "ds-dial__arm", children: [
332
+ /* @__PURE__ */ jsx(
333
+ "rect",
334
+ {
335
+ className: "ds-dial__glass",
336
+ x: cx + arm.x,
337
+ y: cy - arm.h / 2,
338
+ width: arm.w,
339
+ height: arm.h,
340
+ rx: 2
341
+ }
342
+ ),
343
+ /* @__PURE__ */ jsx(
344
+ "rect",
345
+ {
346
+ className: "ds-dial__frame-line",
347
+ x: cx + arm.x,
348
+ y: cy - arm.h / 2,
349
+ width: arm.w,
350
+ height: arm.h,
351
+ rx: 3
352
+ }
353
+ ),
354
+ /* @__PURE__ */ jsx(
355
+ "line",
356
+ {
357
+ className: "ds-dial__hairline",
358
+ x1: cx + arm.x,
359
+ y1: cy,
360
+ x2: cx + arm.hairline,
361
+ y2: cy
362
+ }
363
+ ),
364
+ /* @__PURE__ */ jsx(
365
+ "path",
366
+ {
367
+ className: "ds-dial__index",
368
+ d: `M${cx + arm.index},${cy - 11} L${cx + arm.index},${cy + 11} L${cx + arm.index - 18},${cy} Z`
369
+ }
370
+ )
371
+ ] })
372
+ ]
373
+ }
374
+ );
375
+ });
376
+
377
+ // src/components/DialNav/variants/Concentric.jsx
378
+ import { forwardRef as forwardRef2, useMemo as useMemo2, useRef as useRef2 } from "react";
379
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
380
+ var Concentric = forwardRef2(function Concentric2({
381
+ spec,
382
+ count,
383
+ travel,
384
+ theta,
385
+ pitch,
386
+ onDragStateChange,
387
+ pages,
388
+ innerTravel,
389
+ renderStation,
390
+ renderPage
391
+ }, ref) {
392
+ const [vbW, vbH] = spec.viewBox;
393
+ const { cx, cy, arm, index } = spec;
394
+ const ringRef = useRef2(null);
395
+ const armRef = useRef2(null);
396
+ const grabRing = useRef2({ angle: 0, pos: 0 });
397
+ const grabArm = useRef2({ angle: 0, pos: 0 });
398
+ const angleAt = (event) => {
399
+ const box = ref.current?.getBoundingClientRect();
400
+ if (!box?.width) return null;
401
+ const scale = vbW / box.width;
402
+ const x = (event.clientX - box.left) * scale - cx;
403
+ const y = (event.clientY - box.top) * scale - cy;
404
+ return Math.atan2(y, x) * 180 / Math.PI;
405
+ };
406
+ usePointerDrag(ringRef, {
407
+ onBegin: (event) => {
408
+ const a = angleAt(event);
409
+ if (a == null) return;
410
+ grabRing.current = { angle: a, pos: travel.pos };
411
+ travel.begin();
412
+ onDragStateChange?.(true);
413
+ },
414
+ onMove: (event) => {
415
+ const a = angleAt(event);
416
+ if (a == null) return;
417
+ travel.track(
418
+ grabRing.current.pos - angleDelta(a, grabRing.current.angle) / pitch
419
+ );
420
+ },
421
+ onEnd: (_e, wasTap) => {
422
+ if (!wasTap) travel.release();
423
+ onDragStateChange?.(false);
424
+ }
425
+ });
426
+ const innerPitch = innerPitchFor(spec, pages.length);
427
+ usePointerDrag(armRef, {
428
+ onBegin: (event) => {
429
+ const a = angleAt(event);
430
+ if (a == null) return;
431
+ grabArm.current = { angle: a, pos: innerTravel.pos };
432
+ innerTravel.begin();
433
+ onDragStateChange?.(true);
434
+ },
435
+ onMove: (event) => {
436
+ const a = angleAt(event);
437
+ if (a == null) return;
438
+ innerTravel.track(
439
+ grabArm.current.pos + angleDelta(a, grabArm.current.angle) / innerPitch
440
+ );
441
+ },
442
+ onEnd: (_e, wasTap) => {
443
+ if (!wasTap) innerTravel.release();
444
+ onDragStateChange?.(false);
445
+ }
446
+ });
447
+ const ringTicks = useMemo2(() => {
448
+ const out = [];
449
+ for (let q = 0; q < 96; q++) {
450
+ const a = q * 3.75;
451
+ out.push({
452
+ a,
453
+ len: q % 16 === 0 ? 18 : q % 4 === 0 ? 10 : 5,
454
+ faint: q % 16 !== 0
455
+ });
456
+ }
457
+ return out;
458
+ }, []);
459
+ const innerTicks = useMemo2(() => {
460
+ const out = [];
461
+ for (let q = 0; q <= (pages.length - 1) * 4; q++) {
462
+ const major = q % 4 === 0;
463
+ out.push({
464
+ a: innerAngle(spec, pages.length, q / 4),
465
+ len: major ? 16 : q % 2 === 0 ? 9 : 5,
466
+ major
467
+ });
468
+ }
469
+ return out;
470
+ }, [pages.length, spec]);
471
+ const armAngle = innerAngle(spec, pages.length, innerTravel.pos);
472
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
473
+ /* @__PURE__ */ jsxs2(
474
+ "svg",
475
+ {
476
+ ref,
477
+ className: "ds-dial__chrome",
478
+ viewBox: `0 0 ${vbW} ${vbH}`,
479
+ "aria-hidden": "true",
480
+ focusable: "false",
481
+ children: [
482
+ /* @__PURE__ */ jsx2("circle", { className: "ds-dial__face", cx, cy, r: spec.r }),
483
+ /* @__PURE__ */ jsxs2("g", { ref: ringRef, className: "ds-dial__disc", children: [
484
+ /* @__PURE__ */ jsx2(
485
+ "circle",
486
+ {
487
+ className: "ds-dial__grab",
488
+ cx,
489
+ cy,
490
+ r: spec.rOuter
491
+ }
492
+ ),
493
+ /* @__PURE__ */ jsx2(
494
+ "circle",
495
+ {
496
+ className: "ds-dial__grab-hole",
497
+ cx,
498
+ cy,
499
+ r: spec.rOuterInner
500
+ }
501
+ ),
502
+ /* @__PURE__ */ jsxs2("g", { transform: `rotate(${theta} ${cx} ${cy})`, children: [
503
+ /* @__PURE__ */ jsx2(
504
+ "circle",
505
+ {
506
+ className: "ds-dial__hairline-ring",
507
+ cx,
508
+ cy,
509
+ r: spec.rOuter
510
+ }
511
+ ),
512
+ /* @__PURE__ */ jsx2(
513
+ "circle",
514
+ {
515
+ className: "ds-dial__hairline-ring",
516
+ cx,
517
+ cy,
518
+ r: spec.rOuterInner
519
+ }
520
+ ),
521
+ ringTicks.map((tick, i) => {
522
+ const [x1, y1] = polar(cx, cy, spec.rOuter, tick.a);
523
+ const [x2, y2] = polar(cx, cy, spec.rOuter - tick.len, tick.a);
524
+ return /* @__PURE__ */ jsx2(
525
+ "line",
526
+ {
527
+ className: "ds-dial__tick" + (tick.faint ? " ds-dial__tick--faint" : ""),
528
+ x1,
529
+ y1,
530
+ x2,
531
+ y2
532
+ },
533
+ i
534
+ );
535
+ })
536
+ ] })
537
+ ] }),
538
+ /* @__PURE__ */ jsx2(
539
+ "circle",
540
+ {
541
+ className: "ds-dial__log-plate",
542
+ cx,
543
+ cy,
544
+ r: spec.rInnerPlate
545
+ }
546
+ ),
547
+ innerTicks.map((tick, i) => {
548
+ const [x1, y1] = polar(cx, cy, spec.rInnerPlate, tick.a);
549
+ const [x2, y2] = polar(cx, cy, spec.rInnerPlate - tick.len, tick.a);
550
+ return /* @__PURE__ */ jsx2(
551
+ "line",
552
+ {
553
+ className: "ds-dial__tick" + (tick.major ? " ds-dial__tick--station" : " ds-dial__tick--faint"),
554
+ x1,
555
+ y1,
556
+ x2,
557
+ y2
558
+ },
559
+ `i${i}`
560
+ );
561
+ }),
562
+ /* @__PURE__ */ jsx2(
563
+ "circle",
564
+ {
565
+ className: "ds-dial__hub",
566
+ cx,
567
+ cy,
568
+ r: spec.rHubPlate
569
+ }
570
+ ),
571
+ /* @__PURE__ */ jsxs2(
572
+ "g",
573
+ {
574
+ ref: armRef,
575
+ className: "ds-dial__arm-sweep",
576
+ transform: `rotate(${armAngle} ${cx} ${cy})`,
577
+ children: [
578
+ /* @__PURE__ */ jsx2(
579
+ "rect",
580
+ {
581
+ className: "ds-dial__glass",
582
+ x: cx + arm.x,
583
+ y: cy - arm.h / 2,
584
+ width: arm.w,
585
+ height: arm.h,
586
+ rx: 2
587
+ }
588
+ ),
589
+ /* @__PURE__ */ jsx2(
590
+ "line",
591
+ {
592
+ className: "ds-dial__brass-rail",
593
+ x1: cx + arm.x,
594
+ y1: cy - arm.h / 2,
595
+ x2: cx + arm.tip,
596
+ y2: cy - arm.h / 2
597
+ }
598
+ ),
599
+ /* @__PURE__ */ jsx2(
600
+ "line",
601
+ {
602
+ className: "ds-dial__brass-rail",
603
+ x1: cx + arm.x,
604
+ y1: cy + arm.h / 2,
605
+ x2: cx + arm.tip,
606
+ y2: cy + arm.h / 2
607
+ }
608
+ ),
609
+ /* @__PURE__ */ jsx2(
610
+ "line",
611
+ {
612
+ className: "ds-dial__hairline",
613
+ x1: cx + arm.x,
614
+ y1: cy,
615
+ x2: cx + arm.tip,
616
+ y2: cy
617
+ }
618
+ ),
619
+ /* @__PURE__ */ jsx2(
620
+ "path",
621
+ {
622
+ className: "ds-dial__brass-point",
623
+ d: `M${cx + arm.tip},${cy - 9} L${cx + arm.point},${cy} L${cx + arm.tip},${cy + 9} Z`
624
+ }
625
+ )
626
+ ]
627
+ }
628
+ ),
629
+ /* @__PURE__ */ jsx2("circle", { className: "ds-dial__rivet", cx, cy, r: 11 }),
630
+ /* @__PURE__ */ jsx2("circle", { className: "ds-dial__rivet-hi", cx, cy, r: 5 }),
631
+ /* @__PURE__ */ jsxs2("g", { className: "ds-dial__arm", children: [
632
+ /* @__PURE__ */ jsx2(
633
+ "line",
634
+ {
635
+ className: "ds-dial__hairline",
636
+ x1: cx + index.from,
637
+ y1: cy,
638
+ x2: cx + index.to,
639
+ y2: cy
640
+ }
641
+ ),
642
+ /* @__PURE__ */ jsx2(
643
+ "path",
644
+ {
645
+ className: "ds-dial__index",
646
+ d: `M${cx + index.chevron},${cy - 12} L${cx + index.chevron},${cy + 12} L${cx + index.back},${cy} Z`
647
+ }
648
+ )
649
+ ] })
650
+ ]
651
+ }
652
+ ),
653
+ /* @__PURE__ */ jsx2("ul", { className: "ds-dial__stations", style: { transform: `rotate(${theta}deg)` }, children: Array.from({ length: count }, (_, i) => {
654
+ const a = i * pitch;
655
+ return /* @__PURE__ */ jsx2(
656
+ "li",
657
+ {
658
+ className: "ds-dial__station ds-dial__station--outer",
659
+ style: {
660
+ "--a": `${a}deg`,
661
+ "--r": `${spec.labelR}px`,
662
+ "--flip": uprightFlip(a + theta) ? "180deg" : "0deg"
663
+ },
664
+ children: renderStation(i)
665
+ },
666
+ i
667
+ );
668
+ }) }),
669
+ /* @__PURE__ */ jsx2("ul", { className: "ds-dial__stations ds-dial__stations--inner", children: pages.map((page, k) => {
670
+ const a = innerAngle(spec, pages.length, k);
671
+ return /* @__PURE__ */ jsx2(
672
+ "li",
673
+ {
674
+ className: "ds-dial__station",
675
+ style: {
676
+ "--a": `${a}deg`,
677
+ "--r": `${spec.innerLabelR}px`,
678
+ "--flip": "0deg"
679
+ },
680
+ children: renderPage(page, k)
681
+ },
682
+ page.id
683
+ );
684
+ }) })
685
+ ] });
686
+ });
687
+
688
+ // src/components/DialNav/variants/RatchetFan.jsx
689
+ import { forwardRef as forwardRef3, useMemo as useMemo3, useRef as useRef3 } from "react";
690
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
691
+ var RatchetFan = forwardRef3(function RatchetFan2({ spec, count, travel, pitch, onDragStateChange }, ref) {
692
+ const [vbW, vbH] = spec.viewBox;
693
+ const { cx, cy, r, hub } = spec;
694
+ const a0 = startAngle(spec);
695
+ const detent = pitch / 4;
696
+ const lead = toothLead(spec, detent);
697
+ const pawlRef = useRef3(null);
698
+ const grab = useRef3({ angle: 0, pos: 0 });
699
+ const angleAt = (event) => {
700
+ const box = ref.current?.getBoundingClientRect();
701
+ if (!box?.width) return null;
702
+ const scale = vbW / box.width;
703
+ const x = (event.clientX - box.left) * scale - cx;
704
+ const y = (event.clientY - box.top) * scale - cy;
705
+ return Math.atan2(y, x) * 180 / Math.PI;
706
+ };
707
+ usePointerDrag(pawlRef, {
708
+ onBegin: (event) => {
709
+ const a = angleAt(event);
710
+ if (a == null) return;
711
+ grab.current = { angle: a, pos: travel.pos };
712
+ travel.begin();
713
+ onDragStateChange?.(true);
714
+ },
715
+ onMove: (event) => {
716
+ const a = angleAt(event);
717
+ if (a == null) return;
718
+ travel.track(grab.current.pos + angleDelta(a, grab.current.angle) / pitch);
719
+ },
720
+ onEnd: (_event, wasTap) => {
721
+ if (!wasTap) travel.release();
722
+ onDragStateChange?.(false);
723
+ }
724
+ });
725
+ const ratchet = useMemo3(() => {
726
+ let d = "";
727
+ for (let q = 0; q <= (count - 1) * 4; q++) {
728
+ const a = a0 + q / 4 * pitch;
729
+ const [x1, y1] = polar(cx, cy, r + spec.tooth.from, a);
730
+ const [x2, y2] = polar(cx, cy, r + spec.tooth.to, a + lead);
731
+ d += `${q ? "L" : "M"}${x1},${y1} L${x2},${y2} `;
732
+ }
733
+ return d;
734
+ }, [a0, count, cx, cy, lead, pitch, r, spec.tooth.from, spec.tooth.to]);
735
+ const detents = useMemo3(() => {
736
+ const out = [];
737
+ for (let q = 0; q <= (count - 1) * 4; q++) {
738
+ const major = q % 4 === 0;
739
+ const half = q % 2 === 0;
740
+ out.push({
741
+ a: a0 + q / 4 * pitch,
742
+ len: major ? 22 : half ? 11 : 6,
743
+ major
744
+ });
745
+ }
746
+ return out;
747
+ }, [a0, count, pitch]);
748
+ const [arcX0, arcY0] = polar(cx, cy, r, a0);
749
+ const [arcX1, arcY1] = polar(cx, cy, r, stationAngle(spec, count, count - 1));
750
+ const pawlAngle = stationAngle(spec, count, travel.pos);
751
+ const frac = Math.abs(travel.pos * 4 - Math.round(travel.pos * 4));
752
+ return /* @__PURE__ */ jsxs3(
753
+ "svg",
754
+ {
755
+ ref,
756
+ className: "ds-dial__chrome",
757
+ viewBox: `0 0 ${vbW} ${vbH}`,
758
+ "aria-hidden": "true",
759
+ focusable: "false",
760
+ children: [
761
+ /* @__PURE__ */ jsx3("circle", { className: "ds-dial__log-plate", cx, cy, r: hub.outer }),
762
+ /* @__PURE__ */ jsx3("circle", { className: "ds-dial__hub", cx, cy, r: hub.inner }),
763
+ /* @__PURE__ */ jsxs3("g", { className: "ds-dial__fanchrome", children: [
764
+ /* @__PURE__ */ jsx3("path", { className: "ds-dial__wedge", d: wedgePath(spec) }),
765
+ /* @__PURE__ */ jsx3("path", { className: "ds-dial__ratchet", d: ratchet }),
766
+ /* @__PURE__ */ jsx3(
767
+ "path",
768
+ {
769
+ className: "ds-dial__hairline-ring",
770
+ d: `M${arcX0},${arcY0} A${r},${r} 0 0 1 ${arcX1},${arcY1}`
771
+ }
772
+ ),
773
+ detents.map((tick, i) => {
774
+ const [x1, y1] = polar(cx, cy, r - tick.len, tick.a);
775
+ const [x2, y2] = polar(cx, cy, r, tick.a);
776
+ return /* @__PURE__ */ jsx3(
777
+ "line",
778
+ {
779
+ className: "ds-dial__tick" + (tick.major ? " ds-dial__tick--station" : " ds-dial__tick--faint"),
780
+ x1,
781
+ y1,
782
+ x2,
783
+ y2
784
+ },
785
+ i
786
+ );
787
+ })
788
+ ] }),
789
+ Array.from({ length: count }, (_, i) => {
790
+ const a = stationAngle(spec, count, i);
791
+ return /* @__PURE__ */ jsx3("g", { transform: `rotate(${a} ${cx} ${cy})`, children: /* @__PURE__ */ jsx3(
792
+ "g",
793
+ {
794
+ className: "ds-dial__spoke",
795
+ style: {
796
+ transformOrigin: `${cx}px ${cy}px`,
797
+ transitionDelay: `${i * spec.bloomStaggerMs}ms`
798
+ },
799
+ children: /* @__PURE__ */ jsx3(
800
+ "line",
801
+ {
802
+ className: "ds-dial__tick ds-dial__tick--faint",
803
+ x1: cx + hub.outer - 6,
804
+ y1: cy,
805
+ x2: cx + r - 8,
806
+ y2: cy
807
+ }
808
+ )
809
+ }
810
+ ) }, i);
811
+ }),
812
+ /* @__PURE__ */ jsxs3(
813
+ "g",
814
+ {
815
+ ref: pawlRef,
816
+ className: "ds-dial__pawl",
817
+ transform: `rotate(${pawlAngle} ${cx} ${cy})`,
818
+ children: [
819
+ /* @__PURE__ */ jsx3(
820
+ "rect",
821
+ {
822
+ className: "ds-dial__glass",
823
+ x: cx + 50,
824
+ y: cy - 16,
825
+ width: r - 38,
826
+ height: 32,
827
+ rx: 2
828
+ }
829
+ ),
830
+ /* @__PURE__ */ jsx3(
831
+ "rect",
832
+ {
833
+ className: "ds-dial__frame-line",
834
+ x: cx + 50,
835
+ y: cy - 16,
836
+ width: r - 38,
837
+ height: 32,
838
+ rx: 3
839
+ }
840
+ ),
841
+ /* @__PURE__ */ jsx3(
842
+ "line",
843
+ {
844
+ className: "ds-dial__hairline",
845
+ x1: cx + 50,
846
+ y1: cy,
847
+ x2: cx + r + 2,
848
+ y2: cy
849
+ }
850
+ ),
851
+ /* @__PURE__ */ jsx3(
852
+ "path",
853
+ {
854
+ className: "ds-dial__index",
855
+ d: `M${cx + r + 2},${cy - 8} L${cx + r + 16},${cy} L${cx + r + 2},${cy + 8} Z`,
856
+ transform: `translate(${-frac * 5} 0)`
857
+ }
858
+ )
859
+ ]
860
+ }
861
+ ),
862
+ /* @__PURE__ */ jsx3("circle", { className: "ds-dial__rivet", cx, cy, r: 9 }),
863
+ /* @__PURE__ */ jsx3(
864
+ "text",
865
+ {
866
+ className: "ds-dial__caption",
867
+ x: cx,
868
+ y: cy + 30,
869
+ textAnchor: "middle",
870
+ children: "PIVOT"
871
+ }
872
+ )
873
+ ]
874
+ }
875
+ );
876
+ });
877
+
878
+ // src/components/DialNav/DialNav.jsx
879
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
880
+ var VARIANTS = {
881
+ volvelle: {
882
+ spec: DIAL.volvelle,
883
+ render: Volvelle,
884
+ layout: (spec, count, pos) => {
885
+ const theta = rotationFor(spec, count, pos);
886
+ return {
887
+ railRotation: theta,
888
+ angleOf: (i) => stationAngle(spec, count, i),
889
+ flipOf: (i) => uprightFlip(stationAngle(spec, count, i) + theta) ? 180 : 0
890
+ };
891
+ }
892
+ },
893
+ concentric: {
894
+ spec: DIAL.concentric,
895
+ render: Concentric,
896
+ ownsStations: true,
897
+ twoLevel: true,
898
+ layout: (spec, count, pos) => ({
899
+ railRotation: -(pos * (360 / Math.max(1, count))),
900
+ angleOf: () => 0,
901
+ flipOf: () => 0
902
+ })
903
+ },
904
+ fan: {
905
+ spec: DIAL.fan,
906
+ render: RatchetFan,
907
+ layout: (spec, count, pos, station) => ({
908
+ railRotation: 0,
909
+ angleOf: (i) => stationAngle(spec, count, i),
910
+ // The indexed spoke's label rights itself; the rest stay radial.
911
+ flipOf: (i) => i === station ? -stationAngle(spec, count, i) : 0,
912
+ bloom: true
913
+ })
914
+ }
915
+ };
916
+ var isDev = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
917
+ function ActiveProbe({ isActive, index, onRouteActive, children }) {
918
+ useEffect(() => {
919
+ if (isActive) onRouteActive(index);
920
+ }, [isActive, index, onRouteActive]);
921
+ return children;
922
+ }
923
+ function DialNav({
924
+ variant = "volvelle",
925
+ items = [],
926
+ value,
927
+ defaultValue,
928
+ onChange,
929
+ expanded,
930
+ defaultExpanded = false,
931
+ onExpandedChange,
932
+ label = "Sections",
933
+ className = ""
934
+ }) {
935
+ const resolved = VARIANTS[variant] ? variant : (isDev && console.warn(
936
+ `[DialNav] variant "${variant}" is not implemented yet; falling back to "volvelle".`
937
+ ), "volvelle");
938
+ const { spec, render: Chrome, layout, ownsStations, twoLevel } = VARIANTS[resolved];
939
+ const count = validateStationCount(items.length, "DialNav");
940
+ const stations = useMemo4(() => items.slice(0, count), [items, count]);
941
+ const [vbW, vbH] = spec.viewBox;
942
+ const indexOfId = useCallback(
943
+ (id) => {
944
+ const i = stations.findIndex((item) => item.id === id);
945
+ return i < 0 ? null : i;
946
+ },
947
+ [stations]
948
+ );
949
+ const routerMode = stations.some((item) => item.to);
950
+ const [routeIndex, setRouteIndex] = useState(null);
951
+ const syncedOnce = useRef4(false);
952
+ const controlledIndex = value != null ? indexOfId(value) : routerMode ? routeIndex : void 0;
953
+ const stageRef = useRef4(null);
954
+ const chromeRef = useRef4(null);
955
+ const linkRefs = useRef4([]);
956
+ const pageRefs = useRef4([]);
957
+ const draggingRef = useRef4(false);
958
+ const leftDuringDragRef = useRef4(false);
959
+ const handleCommit = useCallback(
960
+ (index) => {
961
+ const item = stations[index];
962
+ if (!item) return;
963
+ if (item.to && linkRefs.current[index]) linkRefs.current[index].click();
964
+ item.onSelect?.();
965
+ onChange?.(item.id);
966
+ },
967
+ [onChange, stations]
968
+ );
969
+ const travel = useDetentTravel({
970
+ count,
971
+ value: controlledIndex ?? void 0,
972
+ defaultValue: indexOfId(defaultValue) ?? 0,
973
+ onChange: handleCommit,
974
+ settleMs: spec.settleMs
975
+ });
976
+ const pages = twoLevel ? stations[travel.station]?.pages ?? [] : [];
977
+ const pageCount = Math.max(1, pages.length);
978
+ const [pageIndex, setPageIndex] = useState(0);
979
+ const handlePageCommit = useCallback(
980
+ (k) => {
981
+ const page = pages[k];
982
+ if (!page) return;
983
+ setPageIndex(k);
984
+ if (page.to && pageRefs.current[k]) pageRefs.current[k].click();
985
+ page.onSelect?.();
986
+ onChange?.(page.id);
987
+ },
988
+ [onChange, pages]
989
+ );
990
+ const innerTravel = useDetentTravel({
991
+ count: pageCount,
992
+ defaultValue: 0,
993
+ onChange: handlePageCommit,
994
+ settleMs: spec.innerSettleMs ?? spec.settleMs
995
+ });
996
+ const { goTo: innerGoTo } = innerTravel;
997
+ const sectionKey = twoLevel ? stations[travel.station]?.id : null;
998
+ useEffect(() => {
999
+ if (twoLevel) innerGoTo(0, 0);
1000
+ }, [sectionKey, twoLevel, innerGoTo]);
1001
+ const { goTo } = travel;
1002
+ const onRouteActive = useCallback(
1003
+ (index) => {
1004
+ setRouteIndex(index);
1005
+ if (!syncedOnce.current) {
1006
+ syncedOnce.current = true;
1007
+ goTo(index, 0);
1008
+ }
1009
+ },
1010
+ [goTo]
1011
+ );
1012
+ const isControlledOpen = expanded != null;
1013
+ const [internalOpen, setInternalOpen] = useState(defaultExpanded);
1014
+ const open = isControlledOpen ? expanded : internalOpen;
1015
+ const setOpen = useCallback(
1016
+ (next) => {
1017
+ if (!isControlledOpen) setInternalOpen(next);
1018
+ onExpandedChange?.(next);
1019
+ },
1020
+ [isControlledOpen, onExpandedChange]
1021
+ );
1022
+ const closeIfIdle = useCallback(() => {
1023
+ const stage = stageRef.current;
1024
+ if (stage && stage.contains(document.activeElement)) return;
1025
+ if (draggingRef.current) {
1026
+ leftDuringDragRef.current = true;
1027
+ return;
1028
+ }
1029
+ setOpen(false);
1030
+ }, [setOpen]);
1031
+ const onDragStateChange = useCallback(
1032
+ (isDragging) => {
1033
+ draggingRef.current = isDragging;
1034
+ if (isDragging) {
1035
+ leftDuringDragRef.current = false;
1036
+ return;
1037
+ }
1038
+ if (leftDuringDragRef.current) {
1039
+ leftDuringDragRef.current = false;
1040
+ closeIfIdle();
1041
+ }
1042
+ },
1043
+ [closeIfIdle]
1044
+ );
1045
+ const reducedMotion = useReducedMotion();
1046
+ const pitch = twoLevel ? 360 / Math.max(1, count) : pitchFor(spec, count);
1047
+ const active = twoLevel ? (travel.station % count + count) % count : travel.station;
1048
+ const place = layout(spec, count, travel.pos, active);
1049
+ const theta = place.railRotation;
1050
+ function onKeyDown(event) {
1051
+ const { key } = event;
1052
+ if (key === "ArrowDown" || key === "ArrowRight") travel.step(1);
1053
+ else if (key === "ArrowUp" || key === "ArrowLeft") travel.step(-1);
1054
+ else if (key === "Home") travel.goTo(0);
1055
+ else if (key === "End") travel.goTo(count - 1);
1056
+ else if (key === "Escape") {
1057
+ setOpen(false);
1058
+ return;
1059
+ } else return;
1060
+ event.preventDefault();
1061
+ }
1062
+ const linkClass = (isActive) => "ds-dial__link" + (isActive ? " ds-dial__link--active" : "");
1063
+ const station = (item, i, { refs, isActive, onPick, extraClass = "" }) => item.to ? /* @__PURE__ */ jsx4(
1064
+ NavLink,
1065
+ {
1066
+ to: item.to,
1067
+ end: item.end,
1068
+ ref: (el) => refs.current[i] = el,
1069
+ className: ({ isActive: routerActive }) => linkClass(routerActive) + extraClass,
1070
+ children: ({ isActive: routerActive }) => /* @__PURE__ */ jsx4(
1071
+ ActiveProbe,
1072
+ {
1073
+ isActive: routerActive,
1074
+ index: i,
1075
+ onRouteActive,
1076
+ children: item.label
1077
+ }
1078
+ )
1079
+ }
1080
+ ) : /* @__PURE__ */ jsx4(
1081
+ "button",
1082
+ {
1083
+ type: "button",
1084
+ className: linkClass(isActive) + extraClass,
1085
+ "aria-current": isActive ? "true" : void 0,
1086
+ onClick: onPick,
1087
+ children: item.label
1088
+ }
1089
+ );
1090
+ const renderStation = (i) => station(stations[i], i, {
1091
+ refs: linkRefs,
1092
+ isActive: i === active,
1093
+ onPick: () => travel.goTo(i)
1094
+ });
1095
+ const renderPage = (page, k) => station(page, k, {
1096
+ refs: pageRefs,
1097
+ isActive: k === pageIndex,
1098
+ onPick: () => innerTravel.goTo(k),
1099
+ extraClass: " ds-dial__link--page"
1100
+ });
1101
+ return /* @__PURE__ */ jsx4(
1102
+ "nav",
1103
+ {
1104
+ className: [
1105
+ "ds-dial",
1106
+ `ds-dial--${resolved}`,
1107
+ open ? "is-out" : "",
1108
+ className
1109
+ ].filter(Boolean).join(" "),
1110
+ "aria-label": label,
1111
+ onKeyDown,
1112
+ children: /* @__PURE__ */ jsxs4(
1113
+ "div",
1114
+ {
1115
+ className: "ds-dial__stage",
1116
+ ref: stageRef,
1117
+ onPointerLeave: closeIfIdle,
1118
+ onFocus: () => setOpen(true),
1119
+ style: {
1120
+ "--dial-w": `${vbW}px`,
1121
+ "--dial-h": `${vbH}px`,
1122
+ "--dial-cx": `${spec.cx}px`,
1123
+ "--dial-cy": `${spec.cy}px`
1124
+ },
1125
+ children: [
1126
+ /* @__PURE__ */ jsx4("span", { className: "ds-dial__peek", "aria-hidden": "true", children: stations[active]?.label }),
1127
+ /* @__PURE__ */ jsx4("div", { className: "ds-dial__frame", children: /* @__PURE__ */ jsxs4(
1128
+ "div",
1129
+ {
1130
+ className: "ds-dial__dock",
1131
+ style: spec.restClip != null ? {
1132
+ // C-III pivots at the edge, so it is unveiled by opening a
1133
+ // clip rather than by sliding sideways.
1134
+ clipPath: open ? "inset(0 0 0 0)" : `inset(0 ${spec.restClip}px 0 0)`
1135
+ } : {
1136
+ transform: `translateX(${open ? spec.out : spec.rest}px)`
1137
+ },
1138
+ children: [
1139
+ /* @__PURE__ */ jsx4(
1140
+ Chrome,
1141
+ {
1142
+ ref: chromeRef,
1143
+ spec,
1144
+ count,
1145
+ travel,
1146
+ theta,
1147
+ pitch,
1148
+ onDragStateChange,
1149
+ reducedMotion,
1150
+ pages,
1151
+ innerTravel,
1152
+ renderStation,
1153
+ renderPage
1154
+ }
1155
+ ),
1156
+ !ownsStations && /* @__PURE__ */ jsx4(
1157
+ "ul",
1158
+ {
1159
+ className: "ds-dial__stations",
1160
+ style: { transform: `rotate(${theta}deg)` },
1161
+ children: stations.map((item, index) => /* @__PURE__ */ jsx4(
1162
+ "li",
1163
+ {
1164
+ className: "ds-dial__station",
1165
+ style: {
1166
+ "--a": `${place.angleOf(index)}deg`,
1167
+ "--r": `${spec.labelR}px`,
1168
+ "--flip": `${place.flipOf(index)}deg`,
1169
+ "--bloom-delay": place.bloom ? `${index * spec.bloomStaggerMs}ms` : "0ms"
1170
+ },
1171
+ children: renderStation(index)
1172
+ },
1173
+ item.id
1174
+ ))
1175
+ }
1176
+ )
1177
+ ]
1178
+ }
1179
+ ) }),
1180
+ /* @__PURE__ */ jsx4(
1181
+ "button",
1182
+ {
1183
+ type: "button",
1184
+ className: "ds-dial__hotzone",
1185
+ "aria-expanded": open,
1186
+ "aria-label": open ? `Close ${label}` : `Open ${label}`,
1187
+ onPointerEnter: (e) => {
1188
+ if (e.pointerType === "mouse") setOpen(true);
1189
+ },
1190
+ onPointerDown: (e) => {
1191
+ if (e.pointerType !== "mouse") setOpen(!open);
1192
+ },
1193
+ onClick: (e) => {
1194
+ if (!e.detail) setOpen(!open);
1195
+ }
1196
+ }
1197
+ )
1198
+ ]
1199
+ }
1200
+ )
1201
+ }
1202
+ );
1203
+ }
1204
+
1205
+ export {
1206
+ DialNav
1207
+ };
1208
+ //# sourceMappingURL=chunk-NJ2SBQYA.js.map