@tastic/hud 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,649 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/TriggerGauge.tsx
34
+ var TriggerGauge_exports = {};
35
+ __export(TriggerGauge_exports, {
36
+ TriggerGauge: () => TriggerGauge
37
+ });
38
+ function TriggerGauge({ segments, litIndices, size, accentColor, mutedColor, dashWidth, startDeg = DEFAULT_START_DEG, endDeg = startDeg }) {
39
+ const totalDurationMs = MOUNT_DURATION_MS + Math.max(0, segments - 1) * MOUNT_STAGGER_MS;
40
+ const mountProgress = (0, import_react_native_reanimated.useSharedValue)(0);
41
+ (0, import_react2.useLayoutEffect)(() => {
42
+ mountProgress.value = (0, import_react_native_reanimated.withTiming)(1, { duration: totalDurationMs, easing: import_react_native_reanimated.Easing.out(import_react_native_reanimated.Easing.cubic) });
43
+ }, []);
44
+ const rawSweepDeg = ((endDeg - startDeg) % 360 + 360) % 360;
45
+ const totalSweepDeg = rawSweepDeg === 0 ? 360 : rawSweepDeg;
46
+ const step = segments > 0 ? totalSweepDeg / segments : 0;
47
+ const centerDeg = (0, import_react2.useCallback)((i) => startDeg + (i + 0.5) * step, [startDeg, step]);
48
+ const singleSelect = litIndices.length === 1;
49
+ const angleDeg = (0, import_react_native_reanimated.useSharedValue)(centerDeg(litIndices[0] ?? 0));
50
+ const litIndicesKey = [...litIndices].sort((a, b) => a - b).join(",");
51
+ const [transitionFrom, setTransitionFrom] = (0, import_react2.useState)(litIndices);
52
+ const transitionFromKey = [...transitionFrom].sort((a, b) => a - b).join(",");
53
+ const lastKeyRef = (0, import_react2.useRef)(litIndicesKey);
54
+ const lastIndicesRef = (0, import_react2.useRef)(litIndices);
55
+ const selectionProgress = (0, import_react_native_reanimated.useSharedValue)(1);
56
+ (0, import_react2.useLayoutEffect)(() => {
57
+ if (litIndicesKey !== lastKeyRef.current) {
58
+ if (singleSelect && lastIndicesRef.current.length === 1) {
59
+ angleDeg.value = (0, import_react_native_reanimated.withTiming)(centerDeg(litIndices[0]), { duration: SELECTION_TRANSITION_DURATION_MS, easing: import_react_native_reanimated.Easing.out(import_react_native_reanimated.Easing.cubic) });
60
+ } else {
61
+ setTransitionFrom(lastIndicesRef.current);
62
+ selectionProgress.value = 0;
63
+ selectionProgress.value = (0, import_react_native_reanimated.withTiming)(1, { duration: SELECTION_TRANSITION_DURATION_MS, easing: import_react_native_reanimated.Easing.out(import_react_native_reanimated.Easing.cubic) });
64
+ }
65
+ lastKeyRef.current = litIndicesKey;
66
+ }
67
+ lastIndicesRef.current = litIndices;
68
+ }, [litIndicesKey, litIndices, selectionProgress, angleDeg, singleSelect, centerDeg]);
69
+ const radius = size / 2 + ARC_GAP;
70
+ const slotDeg = step;
71
+ const gapDeg = DASH_GAP_PX / radius * (180 / Math.PI);
72
+ const autoDashAngleDeg = Math.max(MIN_DASH_ANGLE_DEG, slotDeg - gapDeg);
73
+ const dashAngleDeg = dashWidth != null ? Math.max(MIN_DASH_ANGLE_DEG, dashWidth / radius * (180 / Math.PI)) : autoDashAngleDeg;
74
+ const diameter = radius * 2;
75
+ const canvasSize = diameter + LIT_THICKNESS;
76
+ const oval = import_react_native_skia.Skia.XYWHRect(LIT_THICKNESS / 2, LIT_THICKNESS / 2, diameter, diameter);
77
+ const unlitPath = (0, import_react_native_reanimated.useDerivedValue)(() => {
78
+ const path = import_react_native_skia.Skia.Path.Make();
79
+ for (let i = 0; i < segments; i++) {
80
+ if (litIndices.includes(i)) continue;
81
+ let local = 1;
82
+ if (mountProgress.value < 1) {
83
+ const startFrac = i * MOUNT_STAGGER_MS / totalDurationMs;
84
+ const endFrac = startFrac + MOUNT_DURATION_MS / totalDurationMs;
85
+ local = Math.min(1, Math.max(0, (mountProgress.value - startFrac) / (endFrac - startFrac)));
86
+ }
87
+ const sweep = dashAngleDeg * local;
88
+ if (sweep <= 0) continue;
89
+ const dashCenterDeg = startDeg + (i + 0.5) * step;
90
+ path.addArc(oval, dashCenterDeg - 90 - sweep / 2, sweep);
91
+ }
92
+ return path;
93
+ }, [segments, litIndicesKey, step, startDeg, dashAngleDeg, totalDurationMs, oval.x, oval.y, oval.width, oval.height]);
94
+ const litPath = (0, import_react_native_reanimated.useDerivedValue)(() => {
95
+ const path = import_react_native_skia.Skia.Path.Make();
96
+ if (mountProgress.value >= 1 && singleSelect) {
97
+ path.addArc(oval, angleDeg.value - 90 - dashAngleDeg / 2, dashAngleDeg);
98
+ return path;
99
+ }
100
+ for (let i = 0; i < segments; i++) {
101
+ const isLitNow = litIndices.includes(i);
102
+ const wasLit = transitionFrom.includes(i);
103
+ if (!isLitNow && !wasLit) continue;
104
+ let local;
105
+ if (mountProgress.value < 1) {
106
+ if (!isLitNow) continue;
107
+ const startFrac = i * MOUNT_STAGGER_MS / totalDurationMs;
108
+ const endFrac = startFrac + MOUNT_DURATION_MS / totalDurationMs;
109
+ local = Math.min(1, Math.max(0, (mountProgress.value - startFrac) / (endFrac - startFrac)));
110
+ } else if (isLitNow === wasLit) {
111
+ local = 1;
112
+ } else if (isLitNow) {
113
+ local = selectionProgress.value;
114
+ } else {
115
+ local = 1 - selectionProgress.value;
116
+ }
117
+ const sweep = dashAngleDeg * local;
118
+ if (sweep <= 0) continue;
119
+ const dashCenterDeg = startDeg + (i + 0.5) * step;
120
+ path.addArc(oval, dashCenterDeg - 90 - sweep / 2, sweep);
121
+ }
122
+ return path;
123
+ }, [segments, litIndicesKey, transitionFromKey, singleSelect, step, startDeg, dashAngleDeg, totalDurationMs, oval.x, oval.y, oval.width, oval.height]);
124
+ if (segments < 2) return null;
125
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native8.View, { style: [styles4.container, { height: size, width: size }], pointerEvents: "none", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native_skia.Canvas, { style: { height: canvasSize, width: canvasSize }, children: [
126
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_skia.Path, { path: unlitPath, style: "stroke", strokeWidth: UNLIT_THICKNESS, strokeCap: "round", color: mutedColor, opacity: 0.35 }),
127
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_skia.Path, { path: litPath, style: "stroke", strokeWidth: LIT_THICKNESS, strokeCap: "round", color: accentColor, opacity: 1 })
128
+ ] }) });
129
+ }
130
+ var import_react_native_skia, import_react2, import_react_native8, import_react_native_reanimated, import_jsx_runtime5, UNLIT_THICKNESS, LIT_THICKNESS, DASH_GAP_PX, ARC_GAP, DEFAULT_START_DEG, MIN_DASH_ANGLE_DEG, MOUNT_DURATION_MS, MOUNT_STAGGER_MS, SELECTION_TRANSITION_DURATION_MS, styles4;
131
+ var init_TriggerGauge = __esm({
132
+ "src/TriggerGauge.tsx"() {
133
+ "use strict";
134
+ import_react_native_skia = require("@shopify/react-native-skia");
135
+ import_react2 = require("react");
136
+ import_react_native8 = require("react-native");
137
+ import_react_native_reanimated = require("react-native-reanimated");
138
+ import_jsx_runtime5 = require("react/jsx-runtime");
139
+ UNLIT_THICKNESS = 3;
140
+ LIT_THICKNESS = 7;
141
+ DASH_GAP_PX = 9;
142
+ ARC_GAP = 2;
143
+ DEFAULT_START_DEG = 180;
144
+ MIN_DASH_ANGLE_DEG = 4;
145
+ MOUNT_DURATION_MS = 450;
146
+ MOUNT_STAGGER_MS = 40;
147
+ SELECTION_TRANSITION_DURATION_MS = 260;
148
+ styles4 = import_react_native8.StyleSheet.create({
149
+ container: {
150
+ alignItems: "center",
151
+ justifyContent: "center",
152
+ position: "absolute"
153
+ }
154
+ });
155
+ }
156
+ });
157
+
158
+ // src/index.ts
159
+ var index_exports = {};
160
+ __export(index_exports, {
161
+ InlineColorPicker: () => InlineColorPicker,
162
+ MONO_FONT: () => MONO_FONT,
163
+ PopoverBody: () => PopoverBody,
164
+ PressAwayOverlay: () => PressAwayOverlay,
165
+ ReadyButton: () => ReadyButton,
166
+ SectionedDropdown: () => SectionedDropdown,
167
+ TriggerGaugeHost: () => TriggerGaugeHost,
168
+ loadSkiaWeb: () => loadSkiaWeb,
169
+ useAutoAlign: () => useAutoAlign,
170
+ usePopoverHost: () => usePopoverHost
171
+ });
172
+ module.exports = __toCommonJS(index_exports);
173
+
174
+ // src/fonts.ts
175
+ var import_react_native = require("react-native");
176
+ var MONO_FONT = import_react_native.Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" });
177
+
178
+ // src/InlineColorPicker.tsx
179
+ var import_auto_paper = require("@rific/auto-paper");
180
+ var import_feedback_press = require("@rific/feedback-press");
181
+ var import_react_native4 = require("react-native");
182
+ var import_react_native_paper = require("react-native-paper");
183
+
184
+ // src/PopoverBody.tsx
185
+ var import_react_native2 = require("react-native");
186
+ var import_jsx_runtime = require("react/jsx-runtime");
187
+ var CARET_SIZE = 7;
188
+ var CARET_RING_OFFSET = 2;
189
+ var CARET_DIP = 1;
190
+ function PopoverBody({ visible, children, align = "center", verticalAlign = "below", caretColor, caretBorderColor, caretBorderWidth = 1, triggerSize = 0 }) {
191
+ if (!visible) return null;
192
+ const above = verticalAlign === "above";
193
+ const alignStyle = align === "left" ? styles.contentLeft : align === "right" ? styles.contentRight : styles.contentCenter;
194
+ const verticalStyle = above ? styles.contentAbove : styles.contentBelow;
195
+ const ringSize = CARET_SIZE + caretBorderWidth;
196
+ const caretOffset = (halfFootprint) => align === "right" ? { right: triggerSize / 2 - halfFootprint } : { left: triggerSize / 2 - halfFootprint };
197
+ const caretFill = above ? "borderTopColor" : "borderBottomColor";
198
+ const caretWidthProp = above ? "borderTopWidth" : "borderBottomWidth";
199
+ const caretEdge = (size) => above ? { bottom: -size + CARET_DIP } : { top: -size + CARET_DIP };
200
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: [styles.content, alignStyle, verticalStyle], children: [
201
+ children,
202
+ caretColor && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [styles.caret, caretOffset(ringSize), caretEdge(ringSize), { [caretFill]: caretBorderColor ?? caretColor, [caretWidthProp]: ringSize, borderLeftWidth: ringSize, borderRightWidth: ringSize }] }),
204
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: [styles.caret, caretOffset(CARET_SIZE), caretEdge(CARET_SIZE - CARET_RING_OFFSET), { [caretFill]: caretColor, [caretWidthProp]: CARET_SIZE, borderLeftWidth: CARET_SIZE, borderRightWidth: CARET_SIZE }] })
205
+ ] })
206
+ ] });
207
+ }
208
+ var styles = import_react_native2.StyleSheet.create({
209
+ caret: {
210
+ borderLeftColor: "transparent",
211
+ borderRightColor: "transparent",
212
+ height: 0,
213
+ position: "absolute",
214
+ width: 0,
215
+ // Explicit, not left to DOM order — the box it dips into is itself a position:'relative' view,
216
+ // which React Native Web always gives an implicit zIndex:0 (see PopoverBody's stacking-context
217
+ // notes elsewhere in this codebase), so painting on top needs a real zIndex to beat that
218
+ // reliably rather than relying on being the later sibling.
219
+ zIndex: 1
220
+ },
221
+ content: {
222
+ position: "absolute",
223
+ zIndex: 50
224
+ },
225
+ contentAbove: {
226
+ bottom: "100%",
227
+ marginBottom: 8
228
+ },
229
+ contentBelow: {
230
+ marginTop: 8,
231
+ top: "100%"
232
+ },
233
+ // Stretched to the anchor's own (trigger) width, with alignItems centering the actual (usually
234
+ // much wider) popover box inside via plain flexbox — not `left:'50%'` plus a percentage
235
+ // `transform`, which depends on native resolving the box's own auto width before applying the
236
+ // transform and has proven unreliable there, visibly shifting the box off-center.
237
+ contentCenter: {
238
+ alignItems: "center",
239
+ left: 0,
240
+ right: 0
241
+ },
242
+ contentLeft: {
243
+ left: 0
244
+ },
245
+ contentRight: {
246
+ right: 0
247
+ }
248
+ });
249
+
250
+ // src/useAutoAlign.ts
251
+ var import_react = require("react");
252
+ var import_react_native3 = require("react-native");
253
+ var EDGE_MARGIN = 12;
254
+ function useAutoAlign(open, contentWidth, contentHeight) {
255
+ const triggerRef = (0, import_react.useRef)(null);
256
+ const { width: windowWidth, height: windowHeight } = (0, import_react_native3.useWindowDimensions)();
257
+ const [align, setAlign] = (0, import_react.useState)("center");
258
+ const [verticalAlign, setVerticalAlign] = (0, import_react.useState)("below");
259
+ const [maxHeight, setMaxHeight] = (0, import_react.useState)(contentHeight);
260
+ const [measured, setMeasured] = (0, import_react.useState)(false);
261
+ const [prevOpen, setPrevOpen] = (0, import_react.useState)(open);
262
+ if (open !== prevOpen) {
263
+ setPrevOpen(open);
264
+ if (open) setMeasured(false);
265
+ }
266
+ const measure = (0, import_react.useCallback)(() => {
267
+ triggerRef.current?.measureInWindow((x, y, triggerWidth, triggerHeight) => {
268
+ const centerX = x + triggerWidth / 2;
269
+ const overflowsRight = centerX + contentWidth / 2 > windowWidth - EDGE_MARGIN;
270
+ const overflowsLeft = centerX - contentWidth / 2 < EDGE_MARGIN;
271
+ if (overflowsRight && !overflowsLeft) setAlign("right");
272
+ else if (overflowsLeft && !overflowsRight) setAlign("left");
273
+ else setAlign("center");
274
+ const roomBelow = windowHeight - (y + triggerHeight) - EDGE_MARGIN;
275
+ const roomAbove = y - EDGE_MARGIN;
276
+ const above = contentHeight > roomBelow && roomAbove > roomBelow;
277
+ setVerticalAlign(above ? "above" : "below");
278
+ setMaxHeight(Math.max(above ? roomAbove : roomBelow, 0));
279
+ setMeasured(true);
280
+ });
281
+ }, [contentWidth, contentHeight, windowWidth, windowHeight]);
282
+ (0, import_react.useEffect)(() => {
283
+ if (open) measure();
284
+ }, [open, measure]);
285
+ return { align, maxHeight, measured, triggerRef, verticalAlign };
286
+ }
287
+
288
+ // src/InlineColorPicker.tsx
289
+ var import_jsx_runtime2 = require("react/jsx-runtime");
290
+ var SIZE = 48;
291
+ var SWATCH_SIZE = 28;
292
+ var SWATCHES_PADDING = 8;
293
+ var SWATCHES_GAP = 6;
294
+ var SWATCHES_BORDER_WIDTH = 2;
295
+ var MIN_COLUMNS = 3;
296
+ var MAX_COLUMNS = 6;
297
+ var SCREEN_MARGIN = 40;
298
+ function widthForColumns(columns) {
299
+ return SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * columns + SWATCHES_GAP * (columns - 1);
300
+ }
301
+ function InlineColorPicker({ id, host, value, onChange, swatches = import_auto_paper.defaultColors, takenValue, allowSwapTaken, dark, align: alignOverride, icon = "palette", autoDismiss = true, columns }) {
302
+ const menuBg = dark ? "#000000" : "#FFFFFF";
303
+ const { width: windowWidth } = (0, import_react_native4.useWindowDimensions)();
304
+ const autoColumns = Math.min(MAX_COLUMNS, Math.max(MIN_COLUMNS, Math.floor((windowWidth - 2 * SCREEN_MARGIN + SWATCHES_GAP) / (SWATCH_SIZE + SWATCHES_GAP))));
305
+ const resolvedColumns = columns ?? autoColumns;
306
+ const swatchesWidth = widthForColumns(resolvedColumns);
307
+ const swatchRows = Math.ceil(swatches.length / resolvedColumns);
308
+ const swatchesHeight = SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * swatchRows + SWATCHES_GAP * (swatchRows - 1);
309
+ const open = host.openId === id;
310
+ const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, swatchesWidth, swatchesHeight);
311
+ const align = alignOverride ?? autoAlign;
312
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native4.View, { style: [styles2.anchor, open && styles2.anchorOpen], children: [
313
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_feedback_press.TouchableRipple, { onPress: () => host.toggle(id), borderless: true, style: [styles2.trigger, { backgroundColor: value }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.View, { ref: triggerRef, collapsable: false, style: styles2.triggerMeasure, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper.Icon, { source: icon, size: 20, color: (0, import_auto_paper.getContrastColor)(value) }) }) }),
314
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: value, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: SIZE, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.ScrollView, { style: [styles2.swatches, { backgroundColor: menuBg, borderColor: value, width: swatchesWidth, maxHeight }], contentContainerStyle: styles2.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
315
+ const selected = swatch.value.toLowerCase() === value.toLowerCase();
316
+ const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
317
+ const swappable = taken && allowSwapTaken;
318
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
319
+ import_feedback_press.TouchableRipple,
320
+ {
321
+ disabled: taken && !swappable,
322
+ onPress: () => {
323
+ onChange(swatch.value);
324
+ if (autoDismiss) host.close();
325
+ },
326
+ borderless: true,
327
+ style: [styles2.swatch, { backgroundColor: swatch.value }, selected && styles2.swatchSelected, taken && !swappable && styles2.swatchTaken],
328
+ children: selected ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper.Icon, { source: "check", size: 16, color: (0, import_auto_paper.getContrastColor)(swatch.value) }) : swappable ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper.Icon, { source: "swap-horizontal", size: 16, color: (0, import_auto_paper.getContrastColor)(swatch.value) }) : taken ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper.Icon, { source: "close", size: 16, color: (0, import_auto_paper.getContrastColor)(swatch.value) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.View, {})
329
+ },
330
+ swatch.value
331
+ );
332
+ }) }) })
333
+ ] });
334
+ }
335
+ var styles2 = import_react_native4.StyleSheet.create({
336
+ anchor: {
337
+ position: "relative"
338
+ },
339
+ // See SectionedDropdown's identical comment — React Native Web gives every position:'relative' view
340
+ // its own stacking context, so the elevation has to live on the anchor itself, not just the
341
+ // popover content nested inside it, to correctly paint above this anchor's own later siblings.
342
+ anchorOpen: {
343
+ zIndex: 100
344
+ },
345
+ swatch: {
346
+ alignItems: "center",
347
+ borderRadius: SWATCH_SIZE / 2,
348
+ height: SWATCH_SIZE,
349
+ justifyContent: "center",
350
+ width: SWATCH_SIZE
351
+ },
352
+ swatchSelected: {
353
+ borderColor: "#ffffff",
354
+ borderWidth: 2
355
+ },
356
+ swatchTaken: {
357
+ opacity: 0.35
358
+ },
359
+ swatches: {
360
+ borderRadius: 12,
361
+ borderWidth: SWATCHES_BORDER_WIDTH,
362
+ elevation: 8,
363
+ shadowColor: "#000000",
364
+ shadowOffset: { height: 2, width: 0 },
365
+ shadowOpacity: 0.3,
366
+ shadowRadius: 8
367
+ // width is set inline above — border-box sizing means it has to include the border too
368
+ // (2*SWATCHES_BORDER_WIDTH), not just padding+content, or exactly enough room goes missing that
369
+ // the last column silently wraps to a new row. Anything wider than the exact sum just shows as
370
+ // dead space on the right edge of each row instead.
371
+ },
372
+ // Separate from `swatches` itself — see SectionedDropdown's identical menu/menuContent split.
373
+ // The scroll frame's own bounds (including maxHeight) live on the ScrollView; the actual grid
374
+ // wrapping belongs to its contentContainerStyle, which sizes to the unclamped content instead.
375
+ swatchesContent: {
376
+ flexDirection: "row",
377
+ flexWrap: "wrap",
378
+ gap: SWATCHES_GAP,
379
+ padding: SWATCHES_PADDING
380
+ },
381
+ trigger: {
382
+ alignItems: "center",
383
+ alignSelf: "flex-start",
384
+ borderRadius: SIZE / 2,
385
+ height: SIZE,
386
+ justifyContent: "center",
387
+ width: SIZE
388
+ },
389
+ // Wraps just the icon, inside the TouchableRipple, purely so useAutoAlign has a plain View to
390
+ // attach its measurement ref to — TouchableRipple itself doesn't forward a ref to a measurable
391
+ // native node.
392
+ triggerMeasure: {
393
+ alignItems: "center",
394
+ height: "100%",
395
+ justifyContent: "center",
396
+ width: "100%"
397
+ }
398
+ });
399
+
400
+ // src/loadSkiaWeb.ts
401
+ var import_react_native5 = require("react-native");
402
+ function loadSkiaWeb() {
403
+ if (import_react_native5.Platform.OS !== "web") return Promise.resolve();
404
+ return import("@shopify/react-native-skia/src/web").then((m) => m.LoadSkiaWeb());
405
+ }
406
+
407
+ // src/PressAwayOverlay.tsx
408
+ var import_react_native6 = require("react-native");
409
+ var import_jsx_runtime3 = require("react/jsx-runtime");
410
+ function PressAwayOverlay({ active, onPress, style }) {
411
+ if (!active) return null;
412
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native6.Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [import_react_native6.StyleSheet.absoluteFill, style] });
413
+ }
414
+
415
+ // src/ReadyButton.tsx
416
+ var import_feedback_press2 = require("@rific/feedback-press");
417
+ var import_react_native7 = require("react-native");
418
+ var import_react_native_paper2 = require("react-native-paper");
419
+ var import_jsx_runtime4 = require("react/jsx-runtime");
420
+ function ReadyButton({ color, ready, onToggleReady, style, labelFontFamily = MONO_FONT }) {
421
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press2.TouchableRipple, { onPress: onToggleReady, borderless: true, style: [styles3.readyButton, { borderColor: color }, ready && { backgroundColor: color }, style], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { variant: "labelLarge", style: [{ fontFamily: labelFontFamily, fontWeight: "bold" }, { color: ready ? "#000000" : color }], children: ready ? "READY \u2713" : "READY?" }) });
422
+ }
423
+ var styles3 = import_react_native7.StyleSheet.create({
424
+ readyButton: {
425
+ alignItems: "center",
426
+ borderRadius: 12,
427
+ borderWidth: 1,
428
+ justifyContent: "center",
429
+ minWidth: 120,
430
+ paddingHorizontal: 16,
431
+ paddingVertical: 10
432
+ }
433
+ });
434
+
435
+ // src/SectionedDropdown.tsx
436
+ var import_auto_paper2 = require("@rific/auto-paper");
437
+ var import_feedback_press3 = require("@rific/feedback-press");
438
+ var import_react_native9 = require("react-native");
439
+ var import_react_native_paper3 = require("react-native-paper");
440
+
441
+ // src/TriggerGaugeHost.tsx
442
+ var import_react3 = require("react");
443
+ var import_jsx_runtime6 = require("react/jsx-runtime");
444
+ var LazyTriggerGauge = (0, import_react3.lazy)(() => loadSkiaWeb().then(() => Promise.resolve().then(() => (init_TriggerGauge(), TriggerGauge_exports)).then((m) => ({ default: m.TriggerGauge }))));
445
+ function TriggerGaugeHost(props) {
446
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react3.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LazyTriggerGauge, { ...props }) });
447
+ }
448
+
449
+ // src/SectionedDropdown.tsx
450
+ var import_jsx_runtime7 = require("react/jsx-runtime");
451
+ var MENU_BORDER_WIDTH = 1;
452
+ var TRIGGER_SIZE = 44;
453
+ var MENU_MIN_WIDTH = 200;
454
+ var MENU_MAX_WIDTH = 220;
455
+ var MENU_ITEM_ICON_SIZE = 18;
456
+ var MENU_ITEM_LABEL_MAX_WIDTH = 150;
457
+ var MENU_ITEM_HEIGHT = 40;
458
+ var MENU_ITEM_HEIGHT_WITH_DESCRIPTION = 58;
459
+ var MENU_DIVIDER_HEIGHT = 9;
460
+ var MENU_VERTICAL_PADDING = 12;
461
+ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accentColor, mutedColor, onAccentColor, dark, align: alignOverride, autoDismiss = true, labelFontFamily = MONO_FONT, allClearLabels = { all: "All", clear: "Clear" } }) {
462
+ const menuBg = dark ? "#000000" : "#FFFFFF";
463
+ const menuBorderColor = (0, import_auto_paper2.getBlendedColor)(mutedColor, menuBg, 0.5);
464
+ const resolvedOnAccentColor = onAccentColor ?? (0, import_auto_paper2.getColorRoles)(accentColor, menuBg).onColor;
465
+ const fg = dark ? "#FFFFFF" : "#000000";
466
+ const open = host.openId === id;
467
+ const estimatedHeight = MENU_VERTICAL_PADDING + sections.reduce((sum, section, sectionIndex) => {
468
+ const rows = section.options.reduce((rowSum, option) => rowSum + (option.description ? MENU_ITEM_HEIGHT_WITH_DESCRIPTION : MENU_ITEM_HEIGHT), 0);
469
+ const footer = section.kind === "multi" && section.allClear ? MENU_ITEM_HEIGHT : 0;
470
+ const divider = sectionIndex > 0 ? MENU_DIVIDER_HEIGHT : 0;
471
+ return sum + rows + footer + divider;
472
+ }, 0);
473
+ const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, MENU_MAX_WIDTH, estimatedHeight);
474
+ const align = alignOverride ?? autoAlign;
475
+ const onlySection = sections.length === 1 ? sections[0] : null;
476
+ const selectedOption = onlySection?.kind === "single" ? onlySection.options.find((o) => o.value === onlySection.value) : void 0;
477
+ const triggerIcon = selectedOption?.icon ?? icon;
478
+ const triggerIconSize = selectedOption?.iconSize ?? 22;
479
+ const hasMultiSection = sections.some((s) => s.kind === "multi");
480
+ const anyMultiSelected = sections.some((s) => s.kind === "multi" && s.value.length > 0);
481
+ const triggerIconColor = hasMultiSection ? anyMultiSelected ? accentColor : fg : fg;
482
+ const gaugeSegments = sections.reduce((sum, section) => sum + section.options.length, 0);
483
+ const gaugeLitIndices = [];
484
+ let gaugeOffset = 0;
485
+ for (const section of sections) {
486
+ if (section.kind === "single") {
487
+ const index = section.options.findIndex((o) => o.value === section.value);
488
+ if (index >= 0) gaugeLitIndices.push(gaugeOffset + index);
489
+ } else {
490
+ section.options.forEach((o, i) => {
491
+ if (section.value.includes(o.value)) gaugeLitIndices.push(gaugeOffset + i);
492
+ });
493
+ }
494
+ gaugeOffset += section.options.length;
495
+ }
496
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native9.View, { style: [styles5.anchor, open && styles5.anchorOpen], children: [
497
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native9.View, { ref: triggerRef, collapsable: false, style: styles5.triggerBox, children: [
498
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TriggerGaugeHost, { segments: gaugeSegments, litIndices: gaugeLitIndices, size: TRIGGER_SIZE, accentColor, mutedColor }),
499
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_feedback_press3.IconButton, { icon: triggerIcon, iconColor: triggerIconColor, size: triggerIconSize, accessibilityLabel, onPress: () => host.toggle(id) })
500
+ ] }),
501
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: menuBorderColor, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: TRIGGER_SIZE, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native9.ScrollView, { style: [styles5.menu, { backgroundColor: menuBg, borderColor: menuBorderColor, maxHeight }], contentContainerStyle: styles5.menuContent, showsVerticalScrollIndicator: false, children: sections.map((section, sectionIndex) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native9.View, { children: [
502
+ sectionIndex > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native9.View, { style: [styles5.divider, { backgroundColor: menuBorderColor }] }),
503
+ section.kind === "single" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
504
+ SingleSection,
505
+ {
506
+ section,
507
+ accentColor,
508
+ onAccentColor: resolvedOnAccentColor,
509
+ mutedColor,
510
+ labelFontFamily,
511
+ onSelect: (value) => {
512
+ section.onChange(value);
513
+ if (autoDismiss) host.close();
514
+ }
515
+ }
516
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MultiSection, { section, accentColor, onAccentColor: resolvedOnAccentColor, mutedColor, labelFontFamily, allClearLabels })
517
+ ] }, section.id)) }) })
518
+ ] });
519
+ }
520
+ function SingleSection({
521
+ section,
522
+ accentColor,
523
+ onAccentColor,
524
+ mutedColor,
525
+ labelFontFamily,
526
+ onSelect
527
+ }) {
528
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: section.options.map((option) => {
529
+ const selected = option.value === section.value;
530
+ const rowColor = selected ? onAccentColor : mutedColor;
531
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_feedback_press3.TouchableRipple, { onPress: () => onSelect(option.value), style: [styles5.item, selected && { backgroundColor: accentColor }], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native9.View, { style: styles5.itemRow, children: [
532
+ option.icon && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native_paper3.Icon, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
533
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native9.View, { style: styles5.itemLabelColumn, children: [
534
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native_paper3.Text, { variant: "labelLarge", style: [{ fontFamily: labelFontFamily }, { color: rowColor, fontWeight: selected ? "bold" : "normal" }], children: option.label }),
535
+ option.description && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native_paper3.Text, { variant: "labelSmall", style: [{ fontFamily: labelFontFamily }, { color: rowColor }], children: option.description })
536
+ ] })
537
+ ] }) }, option.value);
538
+ }) });
539
+ }
540
+ function MultiSection({
541
+ section,
542
+ accentColor,
543
+ onAccentColor,
544
+ mutedColor,
545
+ labelFontFamily,
546
+ allClearLabels
547
+ }) {
548
+ const allSelected = section.value.length === section.options.length;
549
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
550
+ section.options.map((option, i) => {
551
+ const selected = section.value.includes(option.value);
552
+ const rowColor = selected ? onAccentColor : mutedColor;
553
+ const prevSelected = selected && i > 0 && section.value.includes(section.options[i - 1].value);
554
+ const nextSelected = selected && i < section.options.length - 1 && section.value.includes(section.options[i + 1].value);
555
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_feedback_press3.TouchableRipple, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: [styles5.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native9.View, { style: styles5.itemRow, children: [
556
+ option.icon && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native_paper3.Icon, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
557
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native_paper3.Text, { variant: "labelLarge", style: [styles5.itemLabelFlex, { fontFamily: labelFontFamily }, { color: rowColor, fontWeight: selected ? "bold" : "normal" }], children: option.label })
558
+ ] }) }, option.value);
559
+ }),
560
+ section.allClear && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_feedback_press3.TouchableRipple, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles5.item, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native_paper3.Text, { variant: "labelLarge", style: [styles5.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }], children: allSelected ? allClearLabels.clear : allClearLabels.all }) })
561
+ ] });
562
+ }
563
+ var styles5 = import_react_native9.StyleSheet.create({
564
+ allClearLabel: {
565
+ fontWeight: "bold",
566
+ textAlign: "center"
567
+ },
568
+ anchor: {
569
+ position: "relative"
570
+ },
571
+ // React Native Web gives every position:'relative' view an explicit zIndex (0, not 'auto'),
572
+ // which makes each one its own stacking context — so a zIndex set only on the popover content
573
+ // below only wins comparisons within this anchor's own box, not against this anchor's own later
574
+ // siblings (e.g. a Ready button), which are otherwise painted on top by DOM-order tiebreak.
575
+ // Elevating the anchor itself, only while its popover is open, fixes that at the right level.
576
+ anchorOpen: {
577
+ zIndex: 100
578
+ },
579
+ divider: {
580
+ height: import_react_native9.StyleSheet.hairlineWidth,
581
+ marginVertical: 4
582
+ },
583
+ item: {
584
+ borderRadius: 8,
585
+ paddingHorizontal: 12,
586
+ paddingVertical: 8
587
+ },
588
+ itemLabelColumn: {
589
+ maxWidth: MENU_ITEM_LABEL_MAX_WIDTH
590
+ },
591
+ itemLabelFlex: {
592
+ flex: 1
593
+ },
594
+ itemRow: {
595
+ alignItems: "center",
596
+ flexDirection: "row",
597
+ gap: 8
598
+ },
599
+ menu: {
600
+ borderRadius: 12,
601
+ borderWidth: MENU_BORDER_WIDTH,
602
+ elevation: 8,
603
+ maxWidth: MENU_MAX_WIDTH,
604
+ minWidth: MENU_MIN_WIDTH,
605
+ shadowColor: "#000000",
606
+ shadowOffset: { height: 2, width: 0 },
607
+ shadowOpacity: 0.3,
608
+ shadowRadius: 8
609
+ },
610
+ // Separate from `menu` itself — a ScrollView's own style sets the scrolling frame's bounds
611
+ // (including the maxHeight that actually caps it), while padding/gap belong to its
612
+ // contentContainerStyle, which sizes to the *unclamped* content instead of the frame.
613
+ menuContent: {
614
+ gap: 4,
615
+ padding: 6
616
+ },
617
+ // Fixed hit-area regardless of the current option's iconSize, so a small-vs-large trigger icon
618
+ // doesn't shift this picker's position within its row.
619
+ triggerBox: {
620
+ alignItems: "center",
621
+ height: TRIGGER_SIZE,
622
+ justifyContent: "center",
623
+ width: TRIGGER_SIZE
624
+ }
625
+ });
626
+
627
+ // src/usePopoverHost.ts
628
+ var import_react4 = require("react");
629
+ function usePopoverHost() {
630
+ const [openId, setOpenId] = (0, import_react4.useState)(null);
631
+ const toggle = (0, import_react4.useCallback)((id) => {
632
+ setOpenId((prev) => prev === id ? null : id);
633
+ }, []);
634
+ const close = (0, import_react4.useCallback)(() => setOpenId(null), []);
635
+ return { openId, toggle, close };
636
+ }
637
+ // Annotate the CommonJS export names for ESM import in node:
638
+ 0 && (module.exports = {
639
+ InlineColorPicker,
640
+ MONO_FONT,
641
+ PopoverBody,
642
+ PressAwayOverlay,
643
+ ReadyButton,
644
+ SectionedDropdown,
645
+ TriggerGaugeHost,
646
+ loadSkiaWeb,
647
+ useAutoAlign,
648
+ usePopoverHost
649
+ });