@5voltfx/design-system 0.1.1 → 0.2.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 (205) hide show
  1. package/dist/chunk-2KHDM6UP.js +58 -0
  2. package/dist/chunk-2KHDM6UP.js.map +1 -0
  3. package/dist/chunk-45346SXP.js +65 -0
  4. package/dist/chunk-45346SXP.js.map +1 -0
  5. package/dist/chunk-BAISLPWP.js +14 -0
  6. package/dist/chunk-BAISLPWP.js.map +1 -0
  7. package/dist/chunk-CT2TMR44.js +1 -0
  8. package/dist/chunk-CT2TMR44.js.map +1 -0
  9. package/dist/chunk-DD5VKG3L.js +22 -0
  10. package/dist/chunk-DD5VKG3L.js.map +1 -0
  11. package/dist/chunk-HLP2MPQ6.js +110 -0
  12. package/dist/chunk-HLP2MPQ6.js.map +1 -0
  13. package/dist/chunk-HPOL5NKF.js +22 -0
  14. package/dist/chunk-HPOL5NKF.js.map +1 -0
  15. package/dist/chunk-I3NS5MAC.js +23 -0
  16. package/dist/chunk-I3NS5MAC.js.map +1 -0
  17. package/dist/chunk-LPEUQ2DE.js +48 -0
  18. package/dist/chunk-LPEUQ2DE.js.map +1 -0
  19. package/dist/chunk-LY5MPGW2.js +129 -0
  20. package/dist/chunk-LY5MPGW2.js.map +1 -0
  21. package/dist/chunk-R42A3ZCY.js +16 -0
  22. package/dist/chunk-R42A3ZCY.js.map +1 -0
  23. package/dist/chunk-SUYKOWXJ.js +1219 -0
  24. package/dist/chunk-SUYKOWXJ.js.map +1 -0
  25. package/dist/chunk-VRGTUKSJ.js +20 -0
  26. package/dist/chunk-VRGTUKSJ.js.map +1 -0
  27. package/dist/chunk-ZVUDPTFZ.js +76 -0
  28. package/dist/chunk-ZVUDPTFZ.js.map +1 -0
  29. package/dist/components/Accordion/Accordion.d.ts +29 -0
  30. package/dist/components/Accordion/Accordion.d.ts.map +1 -0
  31. package/dist/components/Accordion/index.css +58 -0
  32. package/dist/components/Accordion/index.css.map +1 -0
  33. package/dist/components/Accordion/index.d.ts +2 -0
  34. package/dist/components/Accordion/index.d.ts.map +1 -0
  35. package/dist/components/Accordion/index.js +7 -0
  36. package/dist/components/Accordion/index.js.map +1 -0
  37. package/dist/components/Button/Button.d.ts +8 -0
  38. package/dist/components/Button/Button.d.ts.map +1 -0
  39. package/dist/components/Button/index.d.ts +2 -0
  40. package/dist/components/Button/index.d.ts.map +1 -0
  41. package/dist/components/Card/Card.d.ts +6 -0
  42. package/dist/components/Card/Card.d.ts.map +1 -0
  43. package/dist/components/Card/index.d.ts +2 -0
  44. package/dist/components/Card/index.d.ts.map +1 -0
  45. package/dist/components/Checkbox/Checkbox.d.ts +15 -0
  46. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -0
  47. package/dist/components/Checkbox/index.css +57 -0
  48. package/dist/components/Checkbox/index.css.map +1 -0
  49. package/dist/components/Checkbox/index.d.ts +2 -0
  50. package/dist/components/Checkbox/index.d.ts.map +1 -0
  51. package/dist/components/Checkbox/index.js +7 -0
  52. package/dist/components/Checkbox/index.js.map +1 -0
  53. package/dist/components/Footer/Footer.d.ts +15 -0
  54. package/dist/components/Footer/Footer.d.ts.map +1 -0
  55. package/dist/components/Footer/index.d.ts +2 -0
  56. package/dist/components/Footer/index.d.ts.map +1 -0
  57. package/dist/components/FormField/FormField.d.ts +27 -0
  58. package/dist/components/FormField/FormField.d.ts.map +1 -0
  59. package/dist/components/FormField/index.css +31 -0
  60. package/dist/components/FormField/index.css.map +1 -0
  61. package/dist/components/FormField/index.d.ts +2 -0
  62. package/dist/components/FormField/index.d.ts.map +1 -0
  63. package/dist/components/FormField/index.js +8 -0
  64. package/dist/components/FormField/index.js.map +1 -0
  65. package/dist/components/Header/Header.d.ts +15 -0
  66. package/dist/components/Header/Header.d.ts.map +1 -0
  67. package/dist/components/Header/index.d.ts +2 -0
  68. package/dist/components/Header/index.d.ts.map +1 -0
  69. package/dist/components/Input/Input.d.ts +16 -0
  70. package/dist/components/Input/Input.d.ts.map +1 -0
  71. package/dist/components/Input/index.css +45 -0
  72. package/dist/components/Input/index.css.map +1 -0
  73. package/dist/components/Input/index.d.ts +2 -0
  74. package/dist/components/Input/index.d.ts.map +1 -0
  75. package/dist/components/Input/index.js +7 -0
  76. package/dist/components/Input/index.js.map +1 -0
  77. package/dist/components/Label/Label.d.ts +15 -0
  78. package/dist/components/Label/Label.d.ts.map +1 -0
  79. package/dist/components/Label/index.css +14 -0
  80. package/dist/components/Label/index.css.map +1 -0
  81. package/dist/components/Label/index.d.ts +2 -0
  82. package/dist/components/Label/index.d.ts.map +1 -0
  83. package/dist/components/Label/index.js +8 -0
  84. package/dist/components/Label/index.js.map +1 -0
  85. package/dist/components/Layout/Container.d.ts +6 -0
  86. package/dist/components/Layout/Container.d.ts.map +1 -0
  87. package/dist/components/Layout/Grid.d.ts +9 -0
  88. package/dist/components/Layout/Grid.d.ts.map +1 -0
  89. package/dist/components/Layout/Stack.d.ts +10 -0
  90. package/dist/components/Layout/Stack.d.ts.map +1 -0
  91. package/dist/components/Layout/index.d.ts +4 -0
  92. package/dist/components/Layout/index.d.ts.map +1 -0
  93. package/dist/components/Menu/Menu.d.ts +30 -0
  94. package/dist/components/Menu/Menu.d.ts.map +1 -0
  95. package/dist/components/Menu/index.css +77 -0
  96. package/dist/components/Menu/index.css.map +1 -0
  97. package/dist/components/Menu/index.d.ts +2 -0
  98. package/dist/components/Menu/index.d.ts.map +1 -0
  99. package/dist/components/Menu/index.js +7 -0
  100. package/dist/components/Menu/index.js.map +1 -0
  101. package/dist/components/Modal/Modal.d.ts +24 -0
  102. package/dist/components/Modal/Modal.d.ts.map +1 -0
  103. package/dist/components/Modal/index.css +101 -0
  104. package/dist/components/Modal/index.css.map +1 -0
  105. package/dist/components/Modal/index.d.ts +2 -0
  106. package/dist/components/Modal/index.d.ts.map +1 -0
  107. package/dist/components/Modal/index.js +7 -0
  108. package/dist/components/Modal/index.js.map +1 -0
  109. package/dist/components/Nav/Nav.d.ts +10 -0
  110. package/dist/components/Nav/Nav.d.ts.map +1 -0
  111. package/dist/components/Nav/index.d.ts +2 -0
  112. package/dist/components/Nav/index.d.ts.map +1 -0
  113. package/dist/components/Radio/Radio.d.ts +17 -0
  114. package/dist/components/Radio/Radio.d.ts.map +1 -0
  115. package/dist/components/Radio/RadioGroup.d.ts +25 -0
  116. package/dist/components/Radio/RadioGroup.d.ts.map +1 -0
  117. package/dist/components/Radio/RadioGroupContext.d.ts +7 -0
  118. package/dist/components/Radio/RadioGroupContext.d.ts.map +1 -0
  119. package/dist/components/Radio/index.css +57 -0
  120. package/dist/components/Radio/index.css.map +1 -0
  121. package/dist/components/Radio/index.d.ts +3 -0
  122. package/dist/components/Radio/index.d.ts.map +1 -0
  123. package/dist/components/Radio/index.js +9 -0
  124. package/dist/components/Radio/index.js.map +1 -0
  125. package/dist/components/Select/Select.d.ts +18 -0
  126. package/dist/components/Select/Select.d.ts.map +1 -0
  127. package/dist/components/Select/index.css +52 -0
  128. package/dist/components/Select/index.css.map +1 -0
  129. package/dist/components/Select/index.d.ts +2 -0
  130. package/dist/components/Select/index.d.ts.map +1 -0
  131. package/dist/components/Select/index.js +7 -0
  132. package/dist/components/Select/index.js.map +1 -0
  133. package/dist/components/SlideRuleNav/SlideRuleNav.d.ts +54 -0
  134. package/dist/components/SlideRuleNav/SlideRuleNav.d.ts.map +1 -0
  135. package/dist/components/SlideRuleNav/geometry.d.ts +168 -0
  136. package/dist/components/SlideRuleNav/geometry.d.ts.map +1 -0
  137. package/dist/components/SlideRuleNav/index.css +196 -0
  138. package/dist/components/SlideRuleNav/index.css.map +1 -0
  139. package/dist/components/SlideRuleNav/index.d.ts +2 -0
  140. package/dist/components/SlideRuleNav/index.d.ts.map +1 -0
  141. package/dist/components/SlideRuleNav/index.js +7 -0
  142. package/dist/components/SlideRuleNav/index.js.map +1 -0
  143. package/dist/components/SlideRuleNav/variants/CursorRule.d.ts +11 -0
  144. package/dist/components/SlideRuleNav/variants/CursorRule.d.ts.map +1 -0
  145. package/dist/components/SlideRuleNav/variants/SlideStator.d.ts +13 -0
  146. package/dist/components/SlideRuleNav/variants/SlideStator.d.ts.map +1 -0
  147. package/dist/components/SlideRuleNav/variants/VernierCarriage.d.ts +13 -0
  148. package/dist/components/SlideRuleNav/variants/VernierCarriage.d.ts.map +1 -0
  149. package/dist/components/Switch/Switch.d.ts +15 -0
  150. package/dist/components/Switch/Switch.d.ts.map +1 -0
  151. package/dist/components/Switch/index.css +49 -0
  152. package/dist/components/Switch/index.css.map +1 -0
  153. package/dist/components/Switch/index.d.ts +2 -0
  154. package/dist/components/Switch/index.d.ts.map +1 -0
  155. package/dist/components/Switch/index.js +7 -0
  156. package/dist/components/Switch/index.js.map +1 -0
  157. package/dist/components/Tabs/Tabs.d.ts +34 -0
  158. package/dist/components/Tabs/Tabs.d.ts.map +1 -0
  159. package/dist/components/Tabs/index.css +54 -0
  160. package/dist/components/Tabs/index.css.map +1 -0
  161. package/dist/components/Tabs/index.d.ts +2 -0
  162. package/dist/components/Tabs/index.d.ts.map +1 -0
  163. package/dist/components/Tabs/index.js +7 -0
  164. package/dist/components/Tabs/index.js.map +1 -0
  165. package/dist/components/Textarea/Textarea.d.ts +16 -0
  166. package/dist/components/Textarea/Textarea.d.ts.map +1 -0
  167. package/dist/components/Textarea/index.css +36 -0
  168. package/dist/components/Textarea/index.css.map +1 -0
  169. package/dist/components/Textarea/index.d.ts +2 -0
  170. package/dist/components/Textarea/index.d.ts.map +1 -0
  171. package/dist/components/Textarea/index.js +7 -0
  172. package/dist/components/Textarea/index.js.map +1 -0
  173. package/dist/components/ThemeSwitcher/ThemeSwitcher.d.ts +4 -0
  174. package/dist/components/ThemeSwitcher/ThemeSwitcher.d.ts.map +1 -0
  175. package/dist/components/ThemeSwitcher/index.d.ts +2 -0
  176. package/dist/components/ThemeSwitcher/index.d.ts.map +1 -0
  177. package/dist/components/Typography/Heading.d.ts +9 -0
  178. package/dist/components/Typography/Heading.d.ts.map +1 -0
  179. package/dist/components/Typography/Text.d.ts +9 -0
  180. package/dist/components/Typography/Text.d.ts.map +1 -0
  181. package/dist/components/Typography/index.d.ts +3 -0
  182. package/dist/components/Typography/index.d.ts.map +1 -0
  183. package/dist/components/_shared/useDetentTravel.d.ts +87 -0
  184. package/dist/components/_shared/useDetentTravel.d.ts.map +1 -0
  185. package/dist/components/_shared/usePointerDrag.d.ts +24 -0
  186. package/dist/components/_shared/usePointerDrag.d.ts.map +1 -0
  187. package/dist/components/_shared/useReducedMotion.d.ts +7 -0
  188. package/dist/components/_shared/useReducedMotion.d.ts.map +1 -0
  189. package/dist/components/_shared/validateStationCount.d.ts +13 -0
  190. package/dist/components/_shared/validateStationCount.d.ts.map +1 -0
  191. package/dist/index.css +854 -0
  192. package/dist/index.css.map +1 -1
  193. package/dist/index.d.ts +25 -0
  194. package/dist/index.d.ts.map +1 -0
  195. package/dist/index.js +67 -12
  196. package/dist/index.js.map +1 -1
  197. package/dist/theme/ThemeContext.d.ts +2 -0
  198. package/dist/theme/ThemeContext.d.ts.map +1 -0
  199. package/dist/theme/ThemeProvider.d.ts +12 -0
  200. package/dist/theme/ThemeProvider.d.ts.map +1 -0
  201. package/dist/theme/themeConstants.d.ts +8 -0
  202. package/dist/theme/themeConstants.d.ts.map +1 -0
  203. package/dist/theme/useTheme.d.ts +2 -0
  204. package/dist/theme/useTheme.d.ts.map +1 -0
  205. package/package.json +21 -5
@@ -0,0 +1,1219 @@
1
+ // src/components/SlideRuleNav/SlideRuleNav.jsx
2
+ import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo4, useRef as useRef6, useState as useState2 } from "react";
3
+ import { NavLink } from "react-router-dom";
4
+
5
+ // src/components/_shared/useDetentTravel.js
6
+ import { useCallback, useEffect, useRef, useState } from "react";
7
+
8
+ // src/components/_shared/useReducedMotion.js
9
+ import { useSyncExternalStore } from "react";
10
+ var QUERY = "(prefers-reduced-motion: reduce)";
11
+ function subscribe(onChange) {
12
+ const mq = window.matchMedia(QUERY);
13
+ mq.addEventListener("change", onChange);
14
+ return () => mq.removeEventListener("change", onChange);
15
+ }
16
+ var getSnapshot = () => window.matchMedia(QUERY).matches;
17
+ var getServerSnapshot = () => false;
18
+ function useReducedMotion() {
19
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
20
+ }
21
+
22
+ // src/components/_shared/useDetentTravel.js
23
+ function easeBack(x) {
24
+ const c1 = 1.02;
25
+ const c3 = c1 + 1;
26
+ return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
27
+ }
28
+ var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
29
+ function useDetentTravel({
30
+ count,
31
+ value,
32
+ defaultValue = 0,
33
+ onChange,
34
+ subdivisions = 4,
35
+ settleMs = 420,
36
+ onDetentCross
37
+ } = {}) {
38
+ const last = Math.max(0, count - 1);
39
+ const isControlled = value != null;
40
+ const [internalStation, setInternalStation] = useState(
41
+ () => clamp(Math.round(defaultValue), 0, last)
42
+ );
43
+ const station = clamp(
44
+ Math.round(isControlled ? value : internalStation),
45
+ 0,
46
+ last
47
+ );
48
+ const [pos, setPos] = useState(station);
49
+ const [dragging, setDragging] = useState(false);
50
+ const [settling, setSettling] = useState(false);
51
+ const reduced = useReducedMotion();
52
+ const posRef = useRef(pos);
53
+ const rafRef = useRef(0);
54
+ const targetRef = useRef(station);
55
+ const stationRef = useRef(station);
56
+ const detentRef = useRef(Math.round(station * subdivisions));
57
+ const onChangeRef = useRef(onChange);
58
+ const onDetentCrossRef = useRef(onDetentCross);
59
+ posRef.current = pos;
60
+ stationRef.current = station;
61
+ onChangeRef.current = onChange;
62
+ onDetentCrossRef.current = onDetentCross;
63
+ const cancel = useCallback(() => {
64
+ if (rafRef.current) {
65
+ cancelAnimationFrame(rafRef.current);
66
+ rafRef.current = 0;
67
+ }
68
+ }, []);
69
+ useEffect(() => cancel, [cancel]);
70
+ const writePos = useCallback((v) => {
71
+ posRef.current = v;
72
+ setPos(v);
73
+ }, []);
74
+ const animateTo = useCallback(
75
+ (target, ms, done) => {
76
+ cancel();
77
+ const from = posRef.current;
78
+ if (reduced || ms <= 0 || from === target) {
79
+ writePos(target);
80
+ setSettling(false);
81
+ done?.();
82
+ return;
83
+ }
84
+ setSettling(true);
85
+ const t0 = performance.now();
86
+ const frame = (now) => {
87
+ const k = Math.min(1, (now - t0) / ms);
88
+ if (k < 1) {
89
+ writePos(from + (target - from) * easeBack(k));
90
+ rafRef.current = requestAnimationFrame(frame);
91
+ return;
92
+ }
93
+ rafRef.current = 0;
94
+ writePos(target);
95
+ setSettling(false);
96
+ done?.();
97
+ };
98
+ rafRef.current = requestAnimationFrame(frame);
99
+ },
100
+ [cancel, reduced, writePos]
101
+ );
102
+ const goTo = useCallback(
103
+ (i, ms = settleMs) => {
104
+ const next = clamp(Math.round(i), 0, last);
105
+ targetRef.current = next;
106
+ detentRef.current = Math.round(next * subdivisions);
107
+ animateTo(next, ms, () => {
108
+ if (next === stationRef.current) return;
109
+ if (!isControlled) setInternalStation(next);
110
+ onChangeRef.current?.(next);
111
+ });
112
+ },
113
+ [animateTo, isControlled, last, settleMs, subdivisions]
114
+ );
115
+ useEffect(() => {
116
+ if (targetRef.current === station) return;
117
+ targetRef.current = station;
118
+ detentRef.current = Math.round(station * subdivisions);
119
+ animateTo(station, settleMs);
120
+ }, [station, animateTo, settleMs, subdivisions]);
121
+ const begin = useCallback(() => {
122
+ cancel();
123
+ setSettling(false);
124
+ setDragging(true);
125
+ }, [cancel]);
126
+ const track = useCallback(
127
+ (raw) => {
128
+ cancel();
129
+ const detent = Math.round(clamp(raw, 0, last) * subdivisions);
130
+ if (detent !== detentRef.current) {
131
+ detentRef.current = detent;
132
+ onDetentCrossRef.current?.();
133
+ }
134
+ writePos(detent / subdivisions);
135
+ },
136
+ [cancel, last, subdivisions, writePos]
137
+ );
138
+ const release = useCallback(() => {
139
+ setDragging(false);
140
+ goTo(Math.round(posRef.current));
141
+ }, [goTo]);
142
+ const step = useCallback(
143
+ (delta) => goTo(stationRef.current + delta),
144
+ [goTo]
145
+ );
146
+ return { pos, station, dragging, settling, goTo, step, track, begin, release };
147
+ }
148
+
149
+ // src/components/_shared/validateStationCount.js
150
+ var MIN_STATIONS = 2;
151
+ var MAX_STATIONS = 8;
152
+ var isDev = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
153
+ function validateStationCount(n, component) {
154
+ if (n > MAX_STATIONS) {
155
+ if (isDev) {
156
+ console.warn(
157
+ `[${component}] ${n} stations exceeds the supported maximum of ${MAX_STATIONS}; extra items are not rendered. Slide-rule navigations stay legible up to ${MAX_STATIONS} stations.`
158
+ );
159
+ }
160
+ return MAX_STATIONS;
161
+ }
162
+ if (n < MIN_STATIONS && isDev) {
163
+ console.warn(
164
+ `[${component}] needs at least ${MIN_STATIONS} stations; got ${n}.`
165
+ );
166
+ }
167
+ return n;
168
+ }
169
+
170
+ // src/components/SlideRuleNav/geometry.js
171
+ var MINOR_TARGET = 38;
172
+ var LINEAR = Object.freeze({
173
+ /**
174
+ * L-I — fixed scale, cursor travels.
175
+ *
176
+ * The stock runs from `x0 - cursorBox.w / 2` to `x1 + cursorBox.w / 2` for
177
+ * the same reason as L-III: the cursor is centred on the station it reads,
178
+ * so the stock has to extend past the end graduations for it to sit on the
179
+ * instrument at station 0 and at the last station.
180
+ */
181
+ cursor: Object.freeze({
182
+ viewBox: Object.freeze([900, 136]),
183
+ x0: 62,
184
+ x1: 838,
185
+ railX0: 30,
186
+ railX1: 870,
187
+ top: 40,
188
+ bot: 112,
189
+ labelY: 24,
190
+ cursorBox: Object.freeze({ w: 60, h: 100, y: 28 }),
191
+ settleMs: 420
192
+ }),
193
+ /** L-II — fixed index, slide travels, true log10 station spacing. */
194
+ slide: Object.freeze({
195
+ viewBox: Object.freeze([900, 150]),
196
+ l0: 60,
197
+ lw: 1400,
198
+ index: 450,
199
+ clip: Object.freeze([30, 870]),
200
+ statorL0: 40,
201
+ statorLw: 800,
202
+ slideY: 46,
203
+ slideH: 58,
204
+ labelY: 88,
205
+ settleMs: 460
206
+ }),
207
+ /**
208
+ * L-III — notched rail, vernier carriage. Reference plate.
209
+ *
210
+ * The station span is 772 units, exactly as drawn in the artboards, but it
211
+ * is inset 124 units from the left rather than 64. The carriage is centred
212
+ * on the station it reads, and its widest form (5 stations) is 224 units
213
+ * across — so at station 0 a 64-unit inset put half the vernier off the end
214
+ * of the rule. The artboards never showed it because they opened on station
215
+ * 2. Keeping the span identical preserves every derived division.
216
+ *
217
+ * The rail then runs from `x0 - maxCarriageHalf` to `x1 + maxCarriageHalf`,
218
+ * because a real stock extends past its scale far enough for the cursor to
219
+ * reach the end graduations without hanging off the instrument.
220
+ */
221
+ vernier: Object.freeze({
222
+ viewBox: Object.freeze([1020, 160]),
223
+ x0: 124,
224
+ x1: 896,
225
+ railX0: 12,
226
+ railX1: 1008,
227
+ railY: 46,
228
+ railH: 42,
229
+ labelY: 26,
230
+ vernierDivisions: 5,
231
+ mainDivisionsSpanned: 4,
232
+ /** Widest half-carriage across all supported counts; the inset floor. */
233
+ maxCarriageHalf: 112,
234
+ settleMs: 380
235
+ })
236
+ });
237
+ var SLIDE_VALUES = Object.freeze({
238
+ 2: Object.freeze([1, 8]),
239
+ 3: Object.freeze([1, 2, 8]),
240
+ 4: Object.freeze([1, 2, 3, 8]),
241
+ 5: Object.freeze([1, 1.5, 2, 3, 8]),
242
+ 6: Object.freeze([1, 1.5, 2, 3, 5, 8]),
243
+ 7: Object.freeze([1, 1.5, 2, 3, 4, 6, 8]),
244
+ 8: Object.freeze([1, 1.5, 2, 2.5, 3, 4, 6, 8])
245
+ });
246
+ var pct = (v, extent) => `${v / extent * 100}%`;
247
+ var clamp2 = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
248
+ function pitchFor(spec, count) {
249
+ return (spec.x1 - spec.x0) / Math.max(1, count - 1);
250
+ }
251
+ function minorDivisions(pitch) {
252
+ const minorsPerPitch = 4 * Math.max(1, Math.round(pitch / (4 * MINOR_TARGET)));
253
+ const minor = pitch / minorsPerPitch;
254
+ return {
255
+ minorsPerPitch,
256
+ minor,
257
+ vdiv: minor * 4 / 5,
258
+ minorsPerDetent: minorsPerPitch / 4
259
+ };
260
+ }
261
+ function coincidence({ pos, count, spec = LINEAR.vernier }) {
262
+ const pitch = pitchFor(spec, count);
263
+ const { minor, vdiv } = minorDivisions(pitch);
264
+ const cx = spec.x0 + pos * pitch;
265
+ let k = 0;
266
+ let d = Infinity;
267
+ for (let i = 0; i < LINEAR.vernier.vernierDivisions; i++) {
268
+ const vx = cx + (i - 2.5) * vdiv;
269
+ const off = vx - spec.x0;
270
+ const dist = Math.abs(off - Math.round(off / minor) * minor);
271
+ if (dist < d - 1e-9) {
272
+ d = dist;
273
+ k = i;
274
+ }
275
+ }
276
+ return { k, d, lit: d < vdiv * 0.34 };
277
+ }
278
+ var logX = (spec, v) => spec.l0 + Math.log10(v) * spec.lw;
279
+ var slideValues = (count) => SLIDE_VALUES[count] ?? SLIDE_VALUES[6];
280
+ var logStops = (spec, count) => slideValues(count).map((v) => logX(spec, v));
281
+ var statorX = (spec, v) => spec.statorL0 + Math.log10(v) * spec.statorLw;
282
+ function localAt(stops, pos) {
283
+ const last = stops.length - 1;
284
+ const i = clamp2(Math.floor(pos), 0, last - 1);
285
+ const f = clamp2(pos - i, 0, 1);
286
+ return stops[i] + (stops[i + 1] - stops[i]) * f;
287
+ }
288
+ function posFromLocal(stops, want) {
289
+ const last = stops.length - 1;
290
+ if (want <= stops[0]) return 0;
291
+ if (want >= stops[last]) return last;
292
+ for (let i = 0; i < last; i++) {
293
+ if (want <= stops[i + 1]) {
294
+ return i + (want - stops[i]) / (stops[i + 1] - stops[i]);
295
+ }
296
+ }
297
+ return last;
298
+ }
299
+
300
+ // src/components/SlideRuleNav/variants/CursorRule.jsx
301
+ import { forwardRef, useMemo, useRef as useRef3 } from "react";
302
+
303
+ // src/components/_shared/usePointerDrag.js
304
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
305
+ function usePointerDrag(ref, { onBegin, onMove, onEnd, threshold = 3 } = {}) {
306
+ const latest = useRef2({ onBegin, onMove, onEnd, threshold });
307
+ latest.current = { onBegin, onMove, onEnd, threshold };
308
+ useEffect2(() => {
309
+ const node = ref.current;
310
+ if (!node) return void 0;
311
+ function onPointerDown(event) {
312
+ if (event.button) return;
313
+ const startX = event.clientX;
314
+ const startY = event.clientY;
315
+ let moved = false;
316
+ latest.current.onBegin?.(event);
317
+ function onPointerMove(moveEvent) {
318
+ if (!moved && Math.hypot(moveEvent.clientX - startX, moveEvent.clientY - startY) < latest.current.threshold) {
319
+ return;
320
+ }
321
+ moved = true;
322
+ latest.current.onMove?.(moveEvent);
323
+ }
324
+ function onPointerUp(upEvent) {
325
+ window.removeEventListener("pointermove", onPointerMove);
326
+ window.removeEventListener("pointerup", onPointerUp);
327
+ window.removeEventListener("pointercancel", onPointerUp);
328
+ latest.current.onEnd?.(upEvent, !moved);
329
+ }
330
+ window.addEventListener("pointermove", onPointerMove);
331
+ window.addEventListener("pointerup", onPointerUp);
332
+ window.addEventListener("pointercancel", onPointerUp);
333
+ }
334
+ node.addEventListener("pointerdown", onPointerDown);
335
+ return () => node.removeEventListener("pointerdown", onPointerDown);
336
+ }, [ref]);
337
+ }
338
+
339
+ // src/components/SlideRuleNav/variants/CursorRule.jsx
340
+ import { jsx, jsxs } from "react/jsx-runtime";
341
+ var CursorRule = forwardRef(function CursorRule2({ spec, count, stations, travel, toViewBoxX }, ref) {
342
+ const [vbW, vbH] = spec.viewBox;
343
+ const { x0, railX0, railX1, top, bot, cursorBox } = spec;
344
+ const pitch = pitchFor(spec, count);
345
+ const { minorsPerPitch, minorsPerDetent } = minorDivisions(pitch);
346
+ const cursorRef = useRef3(null);
347
+ usePointerDrag(cursorRef, {
348
+ onBegin: () => travel.begin(),
349
+ onMove: (event) => {
350
+ const x = toViewBoxX(event.clientX);
351
+ if (x != null) travel.track((x - x0) / pitch);
352
+ },
353
+ onEnd: (_event, wasTap) => {
354
+ if (!wasTap) travel.release();
355
+ }
356
+ });
357
+ const ticks = useMemo(() => {
358
+ const out = [];
359
+ for (let q = 0; q <= (count - 1) * minorsPerPitch; q++) {
360
+ const isStation = q % minorsPerPitch === 0;
361
+ const isDetent = q % minorsPerDetent === 0;
362
+ out.push({
363
+ x: x0 + q / minorsPerPitch * pitch,
364
+ kind: isStation ? "major" : isDetent ? "half" : "quarter",
365
+ down: isStation ? 22 : isDetent ? 13 : 8,
366
+ up: isStation ? 18 : isDetent ? 11 : 7,
367
+ station: isStation ? q / minorsPerPitch : null
368
+ });
369
+ }
370
+ return out;
371
+ }, [count, minorsPerDetent, minorsPerPitch, pitch, x0]);
372
+ const cursorX = x0 + travel.pos * pitch;
373
+ const half = cursorBox.w / 2;
374
+ const cursorTop = cursorBox.y;
375
+ const cursorBottom = cursorBox.y + cursorBox.h;
376
+ return /* @__PURE__ */ jsxs(
377
+ "svg",
378
+ {
379
+ ref,
380
+ className: "ds-rule__chrome",
381
+ viewBox: `0 0 ${vbW} ${vbH}`,
382
+ "aria-hidden": "true",
383
+ focusable: "false",
384
+ children: [
385
+ /* @__PURE__ */ jsx(
386
+ "rect",
387
+ {
388
+ className: "ds-rule__rail",
389
+ x: railX0,
390
+ y: top,
391
+ width: railX1 - railX0,
392
+ height: bot - top,
393
+ rx: 2
394
+ }
395
+ ),
396
+ /* @__PURE__ */ jsx(
397
+ "line",
398
+ {
399
+ className: "ds-rule__tick ds-rule__tick--quarter",
400
+ x1: railX0,
401
+ y1: (top + bot) / 2,
402
+ x2: railX1,
403
+ y2: (top + bot) / 2
404
+ }
405
+ ),
406
+ [
407
+ [railX0 + 10, top + 10],
408
+ [railX1 - 10, top + 10],
409
+ [railX0 + 10, bot - 10],
410
+ [railX1 - 10, bot - 10]
411
+ ].map(([cx, cy], i) => /* @__PURE__ */ jsx("circle", { className: "ds-rule__rivet", cx, cy, r: 3 }, i)),
412
+ ticks.map((tick, i) => /* @__PURE__ */ jsxs("g", { children: [
413
+ /* @__PURE__ */ jsx(
414
+ "line",
415
+ {
416
+ className: `ds-rule__tick ds-rule__tick--${tick.kind}`,
417
+ x1: tick.x,
418
+ y1: top,
419
+ x2: tick.x,
420
+ y2: top + tick.down
421
+ }
422
+ ),
423
+ /* @__PURE__ */ jsx(
424
+ "line",
425
+ {
426
+ className: `ds-rule__tick ds-rule__tick--${tick.kind}`,
427
+ x1: tick.x,
428
+ y1: bot,
429
+ x2: tick.x,
430
+ y2: bot - tick.up
431
+ }
432
+ )
433
+ ] }, i)),
434
+ ticks.filter((tick) => tick.station != null).map((tick) => /* @__PURE__ */ jsxs("g", { children: [
435
+ /* @__PURE__ */ jsx(
436
+ "text",
437
+ {
438
+ className: "ds-rule__numeral ds-rule__numeral--accent",
439
+ x: tick.x,
440
+ y: top + 34,
441
+ textAnchor: "middle",
442
+ children: stations[tick.station]?.value ?? tick.station.toFixed(2)
443
+ }
444
+ ),
445
+ /* @__PURE__ */ jsx(
446
+ "text",
447
+ {
448
+ className: "ds-rule__numeral",
449
+ x: tick.x,
450
+ y: bot - 22,
451
+ textAnchor: "middle",
452
+ children: tick.station.toFixed(2)
453
+ }
454
+ )
455
+ ] }, `n${tick.station}`)),
456
+ /* @__PURE__ */ jsxs(
457
+ "g",
458
+ {
459
+ ref: cursorRef,
460
+ className: "ds-rule__carriage",
461
+ transform: `translate(${cursorX},0)`,
462
+ children: [
463
+ /* @__PURE__ */ jsx(
464
+ "rect",
465
+ {
466
+ className: "ds-rule__glass",
467
+ x: -half,
468
+ y: cursorTop,
469
+ width: cursorBox.w,
470
+ height: cursorBox.h,
471
+ rx: 2
472
+ }
473
+ ),
474
+ /* @__PURE__ */ jsx(
475
+ "rect",
476
+ {
477
+ className: "ds-rule__frame",
478
+ x: -half,
479
+ y: cursorTop,
480
+ width: cursorBox.w,
481
+ height: cursorBox.h,
482
+ rx: 3
483
+ }
484
+ ),
485
+ /* @__PURE__ */ jsx(
486
+ "rect",
487
+ {
488
+ className: "ds-rule__shoe",
489
+ x: -half - 2,
490
+ y: cursorTop,
491
+ width: cursorBox.w + 4,
492
+ height: 7,
493
+ rx: 2
494
+ }
495
+ ),
496
+ /* @__PURE__ */ jsx(
497
+ "rect",
498
+ {
499
+ className: "ds-rule__shoe",
500
+ x: -half - 2,
501
+ y: cursorBottom - 7,
502
+ width: cursorBox.w + 4,
503
+ height: 7,
504
+ rx: 2
505
+ }
506
+ ),
507
+ [
508
+ [-24, cursorTop + 3.5],
509
+ [24, cursorTop + 3.5],
510
+ [-24, cursorBottom - 3.5],
511
+ [24, cursorBottom - 3.5]
512
+ ].map(([cx, cy], i) => /* @__PURE__ */ jsx("circle", { className: "ds-rule__rivet", cx, cy, r: 2.2 }, i)),
513
+ /* @__PURE__ */ jsx(
514
+ "line",
515
+ {
516
+ className: "ds-rule__hairline",
517
+ x1: 0,
518
+ y1: cursorTop,
519
+ x2: 0,
520
+ y2: cursorBottom
521
+ }
522
+ )
523
+ ]
524
+ }
525
+ )
526
+ ]
527
+ }
528
+ );
529
+ });
530
+
531
+ // src/components/SlideRuleNav/variants/SlideStator.jsx
532
+ import { forwardRef as forwardRef2, useId, useMemo as useMemo2, useRef as useRef4 } from "react";
533
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
534
+ function decadeTicks(mapX) {
535
+ const bands = [
536
+ [1, 2, 0.02, 0.1],
537
+ [2, 5, 0.05, 0.5],
538
+ [5, 10, 0.1, 1]
539
+ ];
540
+ const out = [];
541
+ for (const [a, b, step, major] of bands) {
542
+ for (let v = a; v < b + 1e-9; v += step) {
543
+ const isMajor = Math.abs(v / major - Math.round(v / major)) < 1e-6;
544
+ const half = Math.abs(v / (major / 2) - Math.round(v / (major / 2))) < 1e-6;
545
+ out.push({
546
+ x: mapX(v),
547
+ v,
548
+ len: isMajor ? 12 : half ? 8 : 5,
549
+ kind: isMajor ? "major" : half ? "half" : "quarter",
550
+ label: isMajor ? String(Math.round(v * 10) / 10) : null
551
+ });
552
+ }
553
+ }
554
+ return out;
555
+ }
556
+ var SlideStator = forwardRef2(function SlideStator2({ spec, count, stations, travel, toViewBoxX }, ref) {
557
+ const [vbW, vbH] = spec.viewBox;
558
+ const { index, slideY, slideH, clip } = spec;
559
+ const clipId = useId();
560
+ const stops = useMemo2(() => logStops(spec, count), [spec, count]);
561
+ const values = slideValues(count);
562
+ const offset = index - localAt(stops, travel.pos);
563
+ const grabbed = useRef4(0);
564
+ const slideRef = useRef4(null);
565
+ usePointerDrag(slideRef, {
566
+ onBegin: (event) => {
567
+ travel.begin();
568
+ const x = toViewBoxX(event.clientX);
569
+ grabbed.current = localAt(stops, travel.pos) + (x == null ? 0 : x - index);
570
+ },
571
+ onMove: (event) => {
572
+ const x = toViewBoxX(event.clientX);
573
+ if (x == null) return;
574
+ travel.track(posFromLocal(stops, grabbed.current - (x - index)));
575
+ },
576
+ onEnd: (_event, wasTap) => {
577
+ if (!wasTap) travel.release();
578
+ }
579
+ });
580
+ const statorTicks = useMemo2(
581
+ () => decadeTicks((v) => statorX(spec, v)),
582
+ [spec]
583
+ );
584
+ const detents = useMemo2(() => {
585
+ const out = [];
586
+ for (let q = 0; q <= (count - 1) * 4; q++) {
587
+ if (q % 4 === 0) continue;
588
+ out.push({ x: localAt(stops, q / 4), half: q % 2 === 0 });
589
+ }
590
+ return out;
591
+ }, [count, stops]);
592
+ const slideBottom = slideY + slideH;
593
+ return /* @__PURE__ */ jsxs2(
594
+ "svg",
595
+ {
596
+ ref,
597
+ className: "ds-rule__chrome",
598
+ viewBox: `0 0 ${vbW} ${vbH}`,
599
+ "aria-hidden": "true",
600
+ focusable: "false",
601
+ children: [
602
+ /* @__PURE__ */ jsx2("defs", { children: /* @__PURE__ */ jsx2("clipPath", { id: clipId, children: /* @__PURE__ */ jsx2("rect", { x: clip[0], y: 0, width: clip[1] - clip[0], height: vbH }) }) }),
603
+ /* @__PURE__ */ jsx2(
604
+ "rect",
605
+ {
606
+ className: "ds-rule__rail",
607
+ x: clip[0],
608
+ y: 6,
609
+ width: clip[1] - clip[0],
610
+ height: 40
611
+ }
612
+ ),
613
+ /* @__PURE__ */ jsx2(
614
+ "rect",
615
+ {
616
+ className: "ds-rule__rail",
617
+ x: clip[0],
618
+ y: slideBottom,
619
+ width: clip[1] - clip[0],
620
+ height: 40
621
+ }
622
+ ),
623
+ statorTicks.map((tick, i) => /* @__PURE__ */ jsxs2("g", { children: [
624
+ /* @__PURE__ */ jsx2(
625
+ "line",
626
+ {
627
+ className: `ds-rule__tick ds-rule__tick--${tick.kind}`,
628
+ x1: tick.x,
629
+ y1: 46,
630
+ x2: tick.x,
631
+ y2: 46 - tick.len
632
+ }
633
+ ),
634
+ /* @__PURE__ */ jsx2(
635
+ "line",
636
+ {
637
+ className: `ds-rule__tick ds-rule__tick--${tick.kind}`,
638
+ x1: tick.x,
639
+ y1: slideBottom,
640
+ x2: tick.x,
641
+ y2: slideBottom + tick.len
642
+ }
643
+ ),
644
+ tick.label && /* @__PURE__ */ jsxs2(Fragment, { children: [
645
+ /* @__PURE__ */ jsx2(
646
+ "text",
647
+ {
648
+ className: "ds-rule__numeral",
649
+ x: tick.x,
650
+ y: 46 - tick.len - 3,
651
+ textAnchor: "middle",
652
+ children: tick.label
653
+ }
654
+ ),
655
+ /* @__PURE__ */ jsx2(
656
+ "text",
657
+ {
658
+ className: "ds-rule__numeral",
659
+ x: tick.x,
660
+ y: slideBottom + tick.len + 9,
661
+ textAnchor: "middle",
662
+ children: tick.label
663
+ }
664
+ )
665
+ ] })
666
+ ] }, i)),
667
+ /* @__PURE__ */ jsx2("text", { className: "ds-rule__caption", x: clip[0] + 6, y: 44, children: "C" }),
668
+ /* @__PURE__ */ jsx2("text", { className: "ds-rule__caption", x: clip[0] + 6, y: slideBottom + 12, children: "D" }),
669
+ /* @__PURE__ */ jsx2("g", { clipPath: `url(#${clipId})`, children: /* @__PURE__ */ jsxs2(
670
+ "g",
671
+ {
672
+ ref: slideRef,
673
+ className: "ds-rule__carriage",
674
+ transform: `translate(${offset},0)`,
675
+ children: [
676
+ /* @__PURE__ */ jsx2(
677
+ "rect",
678
+ {
679
+ className: "ds-rule__slide",
680
+ x: stops[0] - 400,
681
+ y: slideY,
682
+ width: stops[stops.length - 1] - stops[0] + 800,
683
+ height: slideH
684
+ }
685
+ ),
686
+ detents.map((d, i) => /* @__PURE__ */ jsxs2("g", { children: [
687
+ /* @__PURE__ */ jsx2(
688
+ "line",
689
+ {
690
+ className: "ds-rule__tick ds-rule__tick--quarter",
691
+ x1: d.x,
692
+ y1: slideY,
693
+ x2: d.x,
694
+ y2: slideY + (d.half ? 10 : 6)
695
+ }
696
+ ),
697
+ /* @__PURE__ */ jsx2(
698
+ "line",
699
+ {
700
+ className: "ds-rule__tick ds-rule__tick--quarter",
701
+ x1: d.x,
702
+ y1: slideBottom,
703
+ x2: d.x,
704
+ y2: slideBottom - (d.half ? 10 : 6)
705
+ }
706
+ )
707
+ ] }, i)),
708
+ stops.map((x, i) => /* @__PURE__ */ jsxs2("g", { children: [
709
+ /* @__PURE__ */ jsx2(
710
+ "line",
711
+ {
712
+ className: "ds-rule__tick ds-rule__tick--major",
713
+ x1: x,
714
+ y1: slideY,
715
+ x2: x,
716
+ y2: slideY + 14
717
+ }
718
+ ),
719
+ /* @__PURE__ */ jsx2(
720
+ "line",
721
+ {
722
+ className: "ds-rule__tick ds-rule__tick--major",
723
+ x1: x,
724
+ y1: slideBottom,
725
+ x2: x,
726
+ y2: slideBottom - 14
727
+ }
728
+ ),
729
+ /* @__PURE__ */ jsx2(
730
+ "text",
731
+ {
732
+ className: "ds-rule__numeral ds-rule__numeral--accent",
733
+ x,
734
+ y: slideBottom - 4,
735
+ textAnchor: "middle",
736
+ children: stations[i]?.value ?? values[i].toFixed(2)
737
+ }
738
+ )
739
+ ] }, i))
740
+ ]
741
+ }
742
+ ) }),
743
+ /* @__PURE__ */ jsxs2("g", { className: "ds-rule__window", children: [
744
+ /* @__PURE__ */ jsx2(
745
+ "rect",
746
+ {
747
+ className: "ds-rule__glass",
748
+ x: index - 58,
749
+ y: 2,
750
+ width: 116,
751
+ height: vbH - 4,
752
+ rx: 2
753
+ }
754
+ ),
755
+ /* @__PURE__ */ jsx2(
756
+ "rect",
757
+ {
758
+ className: "ds-rule__frame",
759
+ x: index - 58,
760
+ y: 2,
761
+ width: 116,
762
+ height: vbH - 4,
763
+ rx: 3
764
+ }
765
+ ),
766
+ /* @__PURE__ */ jsx2(
767
+ "line",
768
+ {
769
+ className: "ds-rule__hairline",
770
+ x1: index,
771
+ y1: 2,
772
+ x2: index,
773
+ y2: vbH - 2
774
+ }
775
+ ),
776
+ /* @__PURE__ */ jsx2(
777
+ "path",
778
+ {
779
+ className: "ds-rule__index",
780
+ d: `M${index - 7},2 L${index + 7},2 L${index},13 Z`
781
+ }
782
+ ),
783
+ /* @__PURE__ */ jsx2(
784
+ "path",
785
+ {
786
+ className: "ds-rule__index",
787
+ d: `M${index - 7},${vbH - 2} L${index + 7},${vbH - 2} L${index},${vbH - 13} Z`
788
+ }
789
+ ),
790
+ /* @__PURE__ */ jsx2(
791
+ "text",
792
+ {
793
+ className: "ds-rule__caption ds-rule__caption--accent",
794
+ x: index + 64,
795
+ y: 12,
796
+ children: "INDEX"
797
+ }
798
+ )
799
+ ] })
800
+ ]
801
+ }
802
+ );
803
+ });
804
+
805
+ // src/components/SlideRuleNav/variants/VernierCarriage.jsx
806
+ import { forwardRef as forwardRef3, useMemo as useMemo3, useRef as useRef5 } from "react";
807
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
808
+ var VernierCarriage = forwardRef3(function VernierCarriage2({ spec, count, stations, travel, toViewBoxX }, ref) {
809
+ const [vbW, vbH] = spec.viewBox;
810
+ const { x0, railX0, railX1, railY, railH } = spec;
811
+ const bot = railY + railH;
812
+ const pitch = pitchFor(spec, count);
813
+ const { minor, vdiv, minorsPerPitch, minorsPerDetent } = minorDivisions(pitch);
814
+ const carriageRef = useRef5(null);
815
+ usePointerDrag(carriageRef, {
816
+ onBegin: () => travel.begin(),
817
+ onMove: (event) => {
818
+ const x = toViewBoxX(event.clientX);
819
+ if (x != null) travel.track((x - x0) / pitch);
820
+ },
821
+ onEnd: (_event, wasTap) => {
822
+ if (!wasTap) travel.release();
823
+ }
824
+ });
825
+ const totalMinors = (count - 1) * minorsPerPitch;
826
+ const rail = useMemo3(() => {
827
+ let d = `M${railX0},${bot} L${railX0},${railY} `;
828
+ for (let q = 0; q <= totalMinors; q++) {
829
+ const tx = x0 + q / minorsPerPitch * pitch;
830
+ const deep = q % minorsPerDetent === 0;
831
+ const w = deep ? 7 : 4;
832
+ const dep = deep ? 9 : 5;
833
+ d += `L${tx - w},${railY} L${tx},${railY + dep} L${tx + w},${railY} `;
834
+ }
835
+ return `${d}L${railX1},${railY} L${railX1},${bot} Z`;
836
+ }, [bot, minorsPerDetent, minorsPerPitch, pitch, railX0, railX1, railY, totalMinors, x0]);
837
+ const ticks = useMemo3(() => {
838
+ const out = [];
839
+ for (let q = 0; q <= totalMinors; q++) {
840
+ const tx = x0 + q / minorsPerPitch * pitch;
841
+ const isStation = q % minorsPerPitch === 0;
842
+ const isDetent = q % minorsPerDetent === 0;
843
+ out.push({
844
+ x: tx,
845
+ len: isStation ? 20 : isDetent ? 13 : 8,
846
+ kind: isStation ? "major" : isDetent ? "half" : "quarter",
847
+ station: isStation ? q / minorsPerPitch : null
848
+ });
849
+ }
850
+ return out;
851
+ }, [minorsPerDetent, minorsPerPitch, pitch, totalMinors, x0]);
852
+ const { pos } = travel;
853
+ const carriageX = x0 + pos * pitch;
854
+ const lit = coincidence({ pos, count, spec });
855
+ const frac = Math.abs(pos * minorsPerPitch - Math.round(pos * minorsPerPitch));
856
+ const pawlY = railY + 2 - frac * 13;
857
+ const glassW = vdiv * 5.8;
858
+ const captionLeft = carriageX + glassW / 2 + 60 > railX1;
859
+ return /* @__PURE__ */ jsxs3(
860
+ "svg",
861
+ {
862
+ ref,
863
+ className: "ds-rule__chrome",
864
+ viewBox: `0 0 ${vbW} ${vbH}`,
865
+ "aria-hidden": "true",
866
+ focusable: "false",
867
+ children: [
868
+ /* @__PURE__ */ jsx3("path", { className: "ds-rule__rail", d: rail }),
869
+ /* @__PURE__ */ jsx3("circle", { className: "ds-rule__rivet", cx: railX0 + 10, cy: bot - 8, r: 3 }),
870
+ /* @__PURE__ */ jsx3("circle", { className: "ds-rule__rivet", cx: railX1 - 10, cy: bot - 8, r: 3 }),
871
+ ticks.map((tick, i) => /* @__PURE__ */ jsx3(
872
+ "line",
873
+ {
874
+ className: `ds-rule__tick ds-rule__tick--${tick.kind}`,
875
+ x1: tick.x,
876
+ y1: bot,
877
+ x2: tick.x,
878
+ y2: bot - tick.len
879
+ },
880
+ i
881
+ )),
882
+ ticks.filter((tick) => tick.station != null).map((tick) => /* @__PURE__ */ jsx3(
883
+ "text",
884
+ {
885
+ className: "ds-rule__numeral ds-rule__numeral--accent",
886
+ x: tick.x,
887
+ y: bot - 24,
888
+ textAnchor: "middle",
889
+ children: stations[tick.station]?.value ?? tick.station.toFixed(2)
890
+ },
891
+ `n${tick.station}`
892
+ )),
893
+ /* @__PURE__ */ jsx3("text", { className: "ds-rule__caption", x: railX0 + 6, y: railY - 8, children: "MAIN" }),
894
+ ticks.filter((tick) => tick.station != null).map((tick) => /* @__PURE__ */ jsx3(
895
+ "line",
896
+ {
897
+ className: "ds-rule__tick ds-rule__tick--quarter",
898
+ x1: tick.x,
899
+ y1: railY - 16,
900
+ x2: tick.x,
901
+ y2: railY - 9
902
+ },
903
+ `l${tick.station}`
904
+ )),
905
+ /* @__PURE__ */ jsxs3(
906
+ "g",
907
+ {
908
+ ref: carriageRef,
909
+ className: "ds-rule__carriage",
910
+ transform: `translate(${carriageX},0)`,
911
+ children: [
912
+ /* @__PURE__ */ jsx3(
913
+ "rect",
914
+ {
915
+ className: "ds-rule__pawl-spring",
916
+ x: -2,
917
+ y: pawlY - 24,
918
+ width: 4,
919
+ height: 11
920
+ }
921
+ ),
922
+ /* @__PURE__ */ jsx3(
923
+ "path",
924
+ {
925
+ className: "ds-rule__pawl",
926
+ d: "M-6,-11 L6,-11 L0,2 Z",
927
+ transform: `translate(0,${pawlY})`
928
+ }
929
+ ),
930
+ /* @__PURE__ */ jsx3(
931
+ "rect",
932
+ {
933
+ className: "ds-rule__glass",
934
+ x: -glassW / 2,
935
+ y: railY - 2,
936
+ width: glassW,
937
+ height: railH + 50,
938
+ rx: 2
939
+ }
940
+ ),
941
+ /* @__PURE__ */ jsx3(
942
+ "rect",
943
+ {
944
+ className: "ds-rule__frame",
945
+ x: -glassW / 2,
946
+ y: railY - 2,
947
+ width: glassW,
948
+ height: railH + 50,
949
+ rx: 3
950
+ }
951
+ ),
952
+ /* @__PURE__ */ jsx3(
953
+ "line",
954
+ {
955
+ className: "ds-rule__hairline",
956
+ x1: 0,
957
+ y1: railY - 2,
958
+ x2: 0,
959
+ y2: bot + 48
960
+ }
961
+ ),
962
+ Array.from({ length: 6 }, (_, k) => {
963
+ const vx = (k - 2.5) * vdiv;
964
+ const on = k === lit.k && lit.lit;
965
+ return /* @__PURE__ */ jsxs3("g", { children: [
966
+ /* @__PURE__ */ jsx3(
967
+ "line",
968
+ {
969
+ className: "ds-rule__vernier-line" + (on ? " ds-rule__vernier-line--lit" : ""),
970
+ x1: vx,
971
+ y1: bot + 2,
972
+ x2: vx,
973
+ y2: bot + 18
974
+ }
975
+ ),
976
+ k < 5 && /* @__PURE__ */ jsx3(
977
+ "text",
978
+ {
979
+ className: "ds-rule__numeral",
980
+ x: vx,
981
+ y: bot + 28,
982
+ textAnchor: "middle",
983
+ children: k
984
+ }
985
+ )
986
+ ] }, k);
987
+ }),
988
+ /* @__PURE__ */ jsx3(
989
+ "text",
990
+ {
991
+ className: "ds-rule__caption",
992
+ x: captionLeft ? -glassW / 2 - 6 : glassW / 2 + 6,
993
+ y: bot + 18,
994
+ textAnchor: captionLeft ? "end" : "start",
995
+ children: "VERNIER"
996
+ }
997
+ )
998
+ ]
999
+ }
1000
+ )
1001
+ ]
1002
+ }
1003
+ );
1004
+ });
1005
+
1006
+ // src/components/SlideRuleNav/SlideRuleNav.jsx
1007
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1008
+ var fixedLayout = (spec, count) => ({
1009
+ stationX: (i) => spec.x0 + i * pitchFor(spec, count),
1010
+ offset: 0
1011
+ });
1012
+ var slideLayout = (spec, count, pos) => {
1013
+ const stops = logStops(spec, count);
1014
+ return {
1015
+ stationX: (i) => stops[i],
1016
+ offset: spec.index - localAt(stops, pos)
1017
+ };
1018
+ };
1019
+ var VARIANTS = {
1020
+ cursor: { spec: LINEAR.cursor, render: CursorRule, layout: fixedLayout },
1021
+ slide: { spec: LINEAR.slide, render: SlideStator, layout: slideLayout },
1022
+ vernier: {
1023
+ spec: LINEAR.vernier,
1024
+ render: VernierCarriage,
1025
+ layout: fixedLayout
1026
+ }
1027
+ };
1028
+ var isDev2 = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1029
+ function ActiveProbe({ isActive, index, onRouteActive, children }) {
1030
+ useEffect3(() => {
1031
+ if (isActive) onRouteActive(index);
1032
+ }, [isActive, index, onRouteActive]);
1033
+ return children;
1034
+ }
1035
+ function SlideRuleNav({
1036
+ variant = "vernier",
1037
+ items = [],
1038
+ value,
1039
+ defaultValue,
1040
+ onChange,
1041
+ readout = false,
1042
+ label = "Main",
1043
+ className = ""
1044
+ }) {
1045
+ const resolved = VARIANTS[variant] ? variant : (isDev2 && console.warn(
1046
+ `[SlideRuleNav] variant "${variant}" is not implemented yet; falling back to "vernier".`
1047
+ ), "vernier");
1048
+ const { spec, render: Chrome, layout } = VARIANTS[resolved];
1049
+ const count = validateStationCount(items.length, "SlideRuleNav");
1050
+ const stations = useMemo4(() => items.slice(0, count), [items, count]);
1051
+ const [vbW, vbH] = spec.viewBox;
1052
+ const indexOfId = useCallback2(
1053
+ (id) => {
1054
+ const i = stations.findIndex((item) => item.id === id);
1055
+ return i < 0 ? null : i;
1056
+ },
1057
+ [stations]
1058
+ );
1059
+ const routerMode = stations.some((item) => item.to);
1060
+ const [routeIndex, setRouteIndex] = useState2(null);
1061
+ const syncedOnce = useRef6(false);
1062
+ const controlledIndex = value != null ? indexOfId(value) : routerMode ? routeIndex : void 0;
1063
+ const chromeRef = useRef6(null);
1064
+ const navRef = useRef6(null);
1065
+ const linkRefs = useRef6([]);
1066
+ const stationRefs = useRef6([]);
1067
+ const reducedMotion = useReducedMotion();
1068
+ const handleCommit = useCallback2(
1069
+ (index) => {
1070
+ const item = stations[index];
1071
+ if (!item) return;
1072
+ if (item.to && linkRefs.current[index]) linkRefs.current[index].click();
1073
+ item.onSelect?.();
1074
+ onChange?.(item.id);
1075
+ },
1076
+ [onChange, stations]
1077
+ );
1078
+ const travel = useDetentTravel({
1079
+ count,
1080
+ value: controlledIndex ?? void 0,
1081
+ defaultValue: indexOfId(defaultValue) ?? 0,
1082
+ onChange: handleCommit,
1083
+ settleMs: spec.settleMs
1084
+ });
1085
+ const { goTo } = travel;
1086
+ const onRouteActive = useCallback2(
1087
+ (index) => {
1088
+ setRouteIndex(index);
1089
+ if (!syncedOnce.current) {
1090
+ syncedOnce.current = true;
1091
+ goTo(index, 0);
1092
+ }
1093
+ },
1094
+ [goTo]
1095
+ );
1096
+ const toViewBoxX = useCallback2(
1097
+ (clientX) => {
1098
+ const box = chromeRef.current?.getBoundingClientRect();
1099
+ if (!box?.width) return null;
1100
+ return (clientX - box.left) / box.width * vbW;
1101
+ },
1102
+ [vbW]
1103
+ );
1104
+ const active = travel.station;
1105
+ useEffect3(() => {
1106
+ const nav = navRef.current;
1107
+ if (!nav) return void 0;
1108
+ const centre = (behavior) => {
1109
+ const station = stationRefs.current[active];
1110
+ if (!station || nav.scrollWidth <= nav.clientWidth) return;
1111
+ nav.scrollTo({
1112
+ left: Math.max(0, station.offsetLeft - nav.clientWidth / 2),
1113
+ behavior
1114
+ });
1115
+ };
1116
+ centre(reducedMotion ? "auto" : "smooth");
1117
+ if (typeof ResizeObserver === "undefined") return void 0;
1118
+ const observer = new ResizeObserver(() => centre("auto"));
1119
+ observer.observe(nav);
1120
+ return () => observer.disconnect();
1121
+ }, [active, reducedMotion]);
1122
+ function onKeyDown(event) {
1123
+ if (event.key === "ArrowRight") travel.step(1);
1124
+ else if (event.key === "ArrowLeft") travel.step(-1);
1125
+ else if (event.key === "Home") travel.goTo(0);
1126
+ else if (event.key === "End") travel.goTo(count - 1);
1127
+ else return;
1128
+ event.preventDefault();
1129
+ }
1130
+ const place = layout(spec, count, travel.pos);
1131
+ const linkClass = (isActive) => "ds-rule__link" + (isActive ? " ds-rule__link--active" : "");
1132
+ return /* @__PURE__ */ jsxs4(
1133
+ "nav",
1134
+ {
1135
+ ref: navRef,
1136
+ className: ["ds-rule", `ds-rule--${resolved}`, className].filter(Boolean).join(" "),
1137
+ "aria-label": label,
1138
+ onKeyDown,
1139
+ children: [
1140
+ /* @__PURE__ */ jsxs4("div", { className: "ds-rule__stage", children: [
1141
+ /* @__PURE__ */ jsx4(
1142
+ Chrome,
1143
+ {
1144
+ ref: chromeRef,
1145
+ spec,
1146
+ count,
1147
+ stations,
1148
+ travel,
1149
+ toViewBoxX
1150
+ }
1151
+ ),
1152
+ /* @__PURE__ */ jsx4(
1153
+ "ul",
1154
+ {
1155
+ className: "ds-rule__stations",
1156
+ style: { transform: `translateX(${pct(place.offset, vbW)})` },
1157
+ children: stations.map((item, index) => /* @__PURE__ */ jsx4(
1158
+ "li",
1159
+ {
1160
+ ref: (el) => stationRefs.current[index] = el,
1161
+ className: "ds-rule__station",
1162
+ style: {
1163
+ "--station-x": pct(place.stationX(index), vbW),
1164
+ "--station-y": pct(spec.labelY, vbH)
1165
+ },
1166
+ children: item.to ? /* @__PURE__ */ jsx4(
1167
+ NavLink,
1168
+ {
1169
+ to: item.to,
1170
+ end: item.end,
1171
+ ref: (el) => linkRefs.current[index] = el,
1172
+ className: ({ isActive }) => linkClass(isActive),
1173
+ children: ({ isActive }) => /* @__PURE__ */ jsx4(
1174
+ ActiveProbe,
1175
+ {
1176
+ isActive,
1177
+ index,
1178
+ onRouteActive,
1179
+ children: item.label
1180
+ }
1181
+ )
1182
+ }
1183
+ ) : /* @__PURE__ */ jsx4(
1184
+ "button",
1185
+ {
1186
+ type: "button",
1187
+ className: linkClass(index === active),
1188
+ "aria-current": index === active ? "true" : void 0,
1189
+ onClick: () => travel.goTo(index),
1190
+ children: item.label
1191
+ }
1192
+ )
1193
+ },
1194
+ item.id
1195
+ ))
1196
+ }
1197
+ )
1198
+ ] }),
1199
+ readout && /* @__PURE__ */ jsxs4("div", { className: "ds-rule__readout", children: [
1200
+ /* @__PURE__ */ jsxs4("span", { "aria-hidden": "true", children: [
1201
+ /* @__PURE__ */ jsx4("span", { className: "ds-rule__readout-key", children: "Carriage" }),
1202
+ " ",
1203
+ /* @__PURE__ */ jsx4("span", { className: "ds-rule__readout-val", children: travel.dragging ? "\u2014\u2014" : stations[active]?.label }),
1204
+ " ",
1205
+ /* @__PURE__ */ jsx4("span", { className: "ds-rule__readout-key", children: "Vernier" }),
1206
+ " ",
1207
+ /* @__PURE__ */ jsx4("span", { className: "ds-rule__readout-num", children: travel.pos.toFixed(2) })
1208
+ ] }),
1209
+ /* @__PURE__ */ jsx4("span", { className: "ds-rule__sr-only", "aria-live": "polite", children: travel.dragging || travel.settling ? "" : `${stations[active]?.label ?? ""} selected` })
1210
+ ] })
1211
+ ]
1212
+ }
1213
+ );
1214
+ }
1215
+
1216
+ export {
1217
+ SlideRuleNav
1218
+ };
1219
+ //# sourceMappingURL=chunk-SUYKOWXJ.js.map