@tastic/hud 0.1.6 → 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.
package/dist/index.mjs CHANGED
@@ -1,39 +1,241 @@
1
+ // src/BaseSettingsDialog.tsx
2
+ import { AutoAppearancePicker, Dialog, useAutoPaperTheme } from "@rific/auto-paper";
3
+ import { Button, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from "@rific/feedback-press";
4
+ import { useUpdater } from "@rific/updater";
5
+ import { useIsTouchPrimaryDevice } from "@tastic/core";
6
+ import { useContext, useState } from "react";
7
+ import { Platform, ScrollView, StyleSheet, View } from "react-native";
8
+ import { Icon, Portal, Text } from "react-native-paper";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ function SettingIcon({ source, color, containerColor }) {
11
+ return /* @__PURE__ */ jsx(View, { style: [styles.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx(Icon, { source, size: 18, color }) });
12
+ }
13
+ function BaseSettingsDialog({ visible, onDismiss, rotation = 0, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, children }) {
14
+ const { dark, colors } = useAutoPaperTheme();
15
+ const isTouchPrimary = useIsTouchPrimaryDevice();
16
+ const { settings: hapticSettings, set: setHapticSettings } = useHapticSettings();
17
+ const { settings: soundSettings, set: setSoundSettings } = useSoundSettings();
18
+ const sound = useContext(SoundContext);
19
+ const { forceShort: forceHapticFeedback } = useVibration();
20
+ const [infoMessage, setInfoMessage] = useState(null);
21
+ const { check, checking, updateReady } = useUpdater({
22
+ autoCheck: false,
23
+ autoPrompt: false,
24
+ onError: onUpdateError,
25
+ onInfo: (title, message) => setInfoMessage({ title, message })
26
+ });
27
+ const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
28
+ const showEdgeGuard = Platform.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
29
+ const showHaptics = Platform.OS !== "web" && !hideHaptics;
30
+ const showSound = !hideSound;
31
+ const showAppearance = !hideAppearance;
32
+ const showUpdateCheck = Platform.OS !== "web" && !hideUpdateCheck;
33
+ const fg = dark ? "#FFFFFF" : "#000000";
34
+ const cardBg = dark ? "#111111" : "#F2F2F2";
35
+ const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
36
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
37
+ /* @__PURE__ */ jsxs(Dialog, { visible, onDismiss, style: [styles.dialog, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: [
38
+ /* @__PURE__ */ jsx(Dialog.Title, { children: "Settings" }),
39
+ /* @__PURE__ */ jsx(Dialog.ScrollArea, { style: styles.scrollArea, children: /* @__PURE__ */ jsxs(ScrollView, { contentContainerStyle: styles.content, showsVerticalScrollIndicator: false, children: [
40
+ /* @__PURE__ */ jsxs(View, { style: styles.toggleGroup, children: [
41
+ showLockOrientation && /* @__PURE__ */ jsx(TouchableRipple, { onPress: () => onLockOrientationChange(!lockOrientation), style: styles.toggleButton, accessibilityLabel: `Lock orientation ${lockOrientation ? "on" : "off"}`, children: /* @__PURE__ */ jsxs(View, { style: styles.toggleContent, children: [
42
+ /* @__PURE__ */ jsx(SettingIcon, { source: lockOrientation ? "lock" : "lock-open-variant-outline", color: lockOrientation ? colors.secondary : colors.onSurfaceVariant, containerColor: lockOrientation ? colors.secondaryContainer : colors.surfaceVariant }),
43
+ /* @__PURE__ */ jsxs(View, { style: styles.flexShrink, children: [
44
+ /* @__PURE__ */ jsx(Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Lock Orientation" }),
45
+ /* @__PURE__ */ jsx(Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Pins the current layout" })
46
+ ] })
47
+ ] }) }),
48
+ showEdgeGuard && /* @__PURE__ */ jsx(TouchableRipple, { onPress: () => onDeferBottomEdgeGestures(!deferBottomEdgeGestures), style: styles.toggleButton, accessibilityLabel: `Edge guard ${deferBottomEdgeGestures ? "on" : "off"}`, children: /* @__PURE__ */ jsxs(View, { style: styles.toggleContent, children: [
49
+ /* @__PURE__ */ jsx(SettingIcon, { source: deferBottomEdgeGestures ? "shield-check-outline" : "shield-off-outline", color: deferBottomEdgeGestures ? colors.secondary : colors.onSurfaceVariant, containerColor: deferBottomEdgeGestures ? colors.secondaryContainer : colors.surfaceVariant }),
50
+ /* @__PURE__ */ jsxs(View, { style: styles.flexShrink, children: [
51
+ /* @__PURE__ */ jsx(Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Edge Guard" }),
52
+ /* @__PURE__ */ jsx(Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Requires a second swipe" })
53
+ ] })
54
+ ] }) }),
55
+ (showSound || showHaptics) && /* @__PURE__ */ jsxs(View, { style: styles.toggleRow, children: [
56
+ showSound && /* @__PURE__ */ jsx(
57
+ TouchableRipple,
58
+ {
59
+ soundDisabled: true,
60
+ onPress: () => {
61
+ const enabled = !soundSettings.enabled;
62
+ setSoundSettings({ enabled });
63
+ if (enabled) sound.selection?.();
64
+ },
65
+ style: styles.toggleButtonInRow,
66
+ accessibilityLabel: `Sound ${soundSettings.enabled ? "on" : "off"}`,
67
+ children: /* @__PURE__ */ jsxs(View, { style: styles.toggleContent, children: [
68
+ /* @__PURE__ */ jsx(SettingIcon, { source: soundSettings.enabled ? "volume-high" : "volume-off", color: soundSettings.enabled ? colors.tertiary : colors.onSurfaceVariant, containerColor: soundSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant }),
69
+ /* @__PURE__ */ jsx(Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Sound" })
70
+ ] })
71
+ }
72
+ ),
73
+ showHaptics && /* @__PURE__ */ jsx(
74
+ TouchableRipple,
75
+ {
76
+ hapticDisabled: true,
77
+ onPress: () => {
78
+ const vibrate = !hapticSettings.vibrate;
79
+ setHapticSettings({ vibrate });
80
+ if (vibrate) forceHapticFeedback();
81
+ },
82
+ style: styles.toggleButtonInRow,
83
+ accessibilityLabel: `Haptics ${hapticSettings.vibrate ? "on" : "off"}`,
84
+ children: /* @__PURE__ */ jsxs(View, { style: styles.toggleContent, children: [
85
+ /* @__PURE__ */ jsx(SettingIcon, { source: hapticSettings.vibrate ? "vibrate" : "vibrate-off", color: hapticSettings.vibrate ? colors.tertiary : colors.onSurfaceVariant, containerColor: hapticSettings.vibrate ? colors.tertiaryContainer : colors.surfaceVariant }),
86
+ /* @__PURE__ */ jsx(Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Haptics" })
87
+ ] })
88
+ }
89
+ )
90
+ ] })
91
+ ] }),
92
+ showAppearance && /* @__PURE__ */ jsxs(View, { style: styles.section, children: [
93
+ /* @__PURE__ */ jsx(Text, { variant: "labelMedium", style: [styles.sectionLabel, { color: colors.onSurfaceVariant }], children: "APPEARANCE" }),
94
+ /* @__PURE__ */ jsx(AutoAppearancePicker, { showLabels: false })
95
+ ] }),
96
+ children,
97
+ showUpdateCheck && /* @__PURE__ */ jsxs(View, { style: styles.section, children: [
98
+ /* @__PURE__ */ jsxs(Text, { variant: "labelSmall", style: [styles.sectionLabel, { color: colors.onSurfaceVariant }], children: [
99
+ "VERSION ",
100
+ version,
101
+ updateReady ? " \xB7 UPDATE READY" : ""
102
+ ] }),
103
+ /* @__PURE__ */ jsx(Button, { mode: "outlined", onPress: check, loading: checking, disabled: checking, children: "Check for Updates" })
104
+ ] })
105
+ ] }) })
106
+ ] }),
107
+ infoMessage && /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(View, { style: styles.overlay, children: /* @__PURE__ */ jsxs(View, { style: [styles.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: [
108
+ /* @__PURE__ */ jsx(Icon, { source: "information-outline", size: 64, color: colors.secondary }),
109
+ /* @__PURE__ */ jsx(Text, { variant: "headlineLarge", style: [styles.overlayTitle, { color: colors.secondary }], children: infoMessage.title }),
110
+ /* @__PURE__ */ jsx(Text, { variant: "bodyLarge", style: [styles.overlayBody, { color: fg }], children: infoMessage.message }),
111
+ /* @__PURE__ */ jsx(Button, { mode: "contained", onPress: () => setInfoMessage(null), style: styles.overlayButton, buttonColor: colors.primary, textColor: colors.onPrimary, children: "OK" })
112
+ ] }) }) })
113
+ ] });
114
+ }
115
+ var styles = StyleSheet.create({
116
+ content: {
117
+ gap: 24,
118
+ paddingVertical: 20
119
+ },
120
+ // Caps the card so it never grows past the screen — without this the dialog just keeps
121
+ // growing to fit its content and the overflow gets clipped by the screen edge, which is
122
+ // what happened in landscape where there's less height to work with. Dialog.ScrollArea +
123
+ // ScrollView below then take over and let the content scroll within that bound.
124
+ dialog: {
125
+ maxHeight: "90%"
126
+ },
127
+ flexShrink: {
128
+ flexShrink: 1
129
+ },
130
+ iconBadge: {
131
+ alignItems: "center",
132
+ borderRadius: 10,
133
+ height: 36,
134
+ justifyContent: "center",
135
+ width: 36
136
+ },
137
+ overlay: {
138
+ alignItems: "center",
139
+ backgroundColor: "rgba(0,0,0,0.72)",
140
+ bottom: 0,
141
+ justifyContent: "center",
142
+ left: 0,
143
+ position: "absolute",
144
+ right: 0,
145
+ top: 0
146
+ },
147
+ overlayBody: { textAlign: "center" },
148
+ overlayButton: { width: 160 },
149
+ overlayCard: {
150
+ alignItems: "center",
151
+ borderRadius: 20,
152
+ borderWidth: 1,
153
+ gap: 16,
154
+ maxWidth: 360,
155
+ padding: 32
156
+ },
157
+ overlayTitle: { fontWeight: "bold" },
158
+ scrollArea: {
159
+ marginBottom: 0
160
+ },
161
+ section: {
162
+ gap: 12
163
+ },
164
+ sectionLabel: {
165
+ letterSpacing: 2
166
+ },
167
+ // Negative margin cancels the padding so the icon still lines up with APPEARANCE/SOUND &
168
+ // HAPTICS below, while the ripple/hover highlight itself gets room to breathe on both sides
169
+ // instead of a flush edge-to-edge slab. overflow: 'hidden' makes sure that highlight actually
170
+ // clips to borderRadius instead of drawing as a plain rectangle.
171
+ toggleButton: {
172
+ borderRadius: 12,
173
+ marginHorizontal: -12,
174
+ overflow: "hidden",
175
+ paddingHorizontal: 12,
176
+ paddingVertical: 10
177
+ },
178
+ // Same shape as toggleButton but `flex: 1` instead of its own `marginHorizontal: -12` — two of
179
+ // these side by side in toggleRow would otherwise both pull inward and collide in the middle.
180
+ // toggleRow carries that edge-alignment margin once, for the row as a whole, instead.
181
+ toggleButtonInRow: {
182
+ borderRadius: 12,
183
+ flex: 1,
184
+ overflow: "hidden",
185
+ paddingHorizontal: 12,
186
+ paddingVertical: 10
187
+ },
188
+ toggleContent: {
189
+ alignItems: "center",
190
+ flexDirection: "row",
191
+ gap: 12
192
+ },
193
+ toggleGroup: {
194
+ gap: 4
195
+ },
196
+ toggleRow: {
197
+ flexDirection: "row",
198
+ gap: 4,
199
+ marginHorizontal: -12
200
+ }
201
+ });
202
+
1
203
  // src/fonts.ts
2
- import { Platform } from "react-native";
3
- var MONO_FONT = Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" });
204
+ import { Platform as Platform2 } from "react-native";
205
+ var MONO_FONT = Platform2.select({ ios: "Menlo", android: "monospace", default: "monospace" });
4
206
 
5
207
  // src/InlineColorPicker.tsx
6
208
  import { defaultColors, getContrastColor } from "@rific/auto-paper";
7
- import { TouchableRipple } from "@rific/feedback-press";
209
+ import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
8
210
  import { clamp } from "@tastic/core";
9
- import { ScrollView, StyleSheet as StyleSheet2, useWindowDimensions as useWindowDimensions2, View as View3 } from "react-native";
10
- import { Icon, Text } from "react-native-paper";
211
+ import { ScrollView as ScrollView2, StyleSheet as StyleSheet3, useWindowDimensions as useWindowDimensions2, View as View4 } from "react-native";
212
+ import { Icon as Icon2, Text as Text2 } from "react-native-paper";
11
213
 
12
214
  // src/PopoverBody.tsx
13
- import { StyleSheet, View } from "react-native";
14
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
215
+ import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
216
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
15
217
  var CARET_SIZE = 7;
16
218
  var CARET_RING_OFFSET = 2;
17
219
  var CARET_DIP = 1;
18
220
  function PopoverBody({ visible, children, align = "center", verticalAlign = "below", caretColor, caretBorderColor, caretBorderWidth = 1, triggerSize = 0 }) {
19
221
  if (!visible) return null;
20
222
  const above = verticalAlign === "above";
21
- const alignStyle = align === "left" ? styles.contentLeft : align === "right" ? styles.contentRight : styles.contentCenter;
22
- const verticalStyle = above ? styles.contentAbove : styles.contentBelow;
223
+ const alignStyle = align === "left" ? styles2.contentLeft : align === "right" ? styles2.contentRight : styles2.contentCenter;
224
+ const verticalStyle = above ? styles2.contentAbove : styles2.contentBelow;
23
225
  const ringSize = CARET_SIZE + caretBorderWidth;
24
226
  const caretOffset = (halfFootprint) => align === "right" ? { right: triggerSize / 2 - halfFootprint } : { left: triggerSize / 2 - halfFootprint };
25
227
  const caretFill = above ? "borderTopColor" : "borderBottomColor";
26
228
  const caretWidthProp = above ? "borderTopWidth" : "borderBottomWidth";
27
229
  const caretEdge = (size) => above ? { bottom: -size + CARET_DIP } : { top: -size + CARET_DIP };
28
- return /* @__PURE__ */ jsxs(View, { style: [styles.content, alignStyle, verticalStyle], children: [
230
+ return /* @__PURE__ */ jsxs2(View2, { style: [styles2.content, alignStyle, verticalStyle], children: [
29
231
  children,
30
- caretColor && /* @__PURE__ */ jsxs(Fragment, { children: [
31
- /* @__PURE__ */ jsx(View, { style: [styles.caret, caretOffset(ringSize), caretEdge(ringSize), { [caretFill]: caretBorderColor ?? caretColor, [caretWidthProp]: ringSize, borderLeftWidth: ringSize, borderRightWidth: ringSize }] }),
32
- /* @__PURE__ */ jsx(View, { style: [styles.caret, caretOffset(CARET_SIZE), caretEdge(CARET_SIZE - CARET_RING_OFFSET), { [caretFill]: caretColor, [caretWidthProp]: CARET_SIZE, borderLeftWidth: CARET_SIZE, borderRightWidth: CARET_SIZE }] })
232
+ caretColor && /* @__PURE__ */ jsxs2(Fragment2, { children: [
233
+ /* @__PURE__ */ jsx2(View2, { style: [styles2.caret, caretOffset(ringSize), caretEdge(ringSize), { [caretFill]: caretBorderColor ?? caretColor, [caretWidthProp]: ringSize, borderLeftWidth: ringSize, borderRightWidth: ringSize }] }),
234
+ /* @__PURE__ */ jsx2(View2, { style: [styles2.caret, caretOffset(CARET_SIZE), caretEdge(CARET_SIZE - CARET_RING_OFFSET), { [caretFill]: caretColor, [caretWidthProp]: CARET_SIZE, borderLeftWidth: CARET_SIZE, borderRightWidth: CARET_SIZE }] })
33
235
  ] })
34
236
  ] });
35
237
  }
36
- var styles = StyleSheet.create({
238
+ var styles2 = StyleSheet2.create({
37
239
  caret: {
38
240
  borderLeftColor: "transparent",
39
241
  borderRightColor: "transparent",
@@ -76,17 +278,17 @@ var styles = StyleSheet.create({
76
278
  });
77
279
 
78
280
  // src/useAutoAlign.ts
79
- import { useCallback, useEffect, useRef, useState } from "react";
281
+ import { useCallback, useEffect, useRef, useState as useState2 } from "react";
80
282
  import { useWindowDimensions } from "react-native";
81
283
  var EDGE_MARGIN = 12;
82
284
  function useAutoAlign(open, contentWidth, contentHeight) {
83
285
  const triggerRef = useRef(null);
84
286
  const { width: windowWidth, height: windowHeight } = useWindowDimensions();
85
- const [align, setAlign] = useState("center");
86
- const [verticalAlign, setVerticalAlign] = useState("below");
87
- const [maxHeight, setMaxHeight] = useState(contentHeight);
88
- const [measured, setMeasured] = useState(false);
89
- const [prevOpen, setPrevOpen] = useState(open);
287
+ const [align, setAlign] = useState2("center");
288
+ const [verticalAlign, setVerticalAlign] = useState2("below");
289
+ const [maxHeight, setMaxHeight] = useState2(contentHeight);
290
+ const [measured, setMeasured] = useState2(false);
291
+ const [prevOpen, setPrevOpen] = useState2(open);
90
292
  if (open !== prevOpen) {
91
293
  setPrevOpen(open);
92
294
  if (open) setMeasured(false);
@@ -114,7 +316,7 @@ function useAutoAlign(open, contentWidth, contentHeight) {
114
316
  }
115
317
 
116
318
  // src/InlineColorPicker.tsx
117
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
319
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
118
320
  var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
119
321
  var DEFAULT_SIZE = 48;
120
322
  var SWATCH_SIZE = 28;
@@ -140,14 +342,14 @@ function InlineColorPicker({ id, host, value, onChange, swatches = defaultColors
140
342
  const align = alignOverride ?? autoAlign;
141
343
  const contrastColor = getContrastColor(value);
142
344
  const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4);
143
- return /* @__PURE__ */ jsxs2(View3, { style: [styles2.anchor, open && styles2.anchorOpen], children: [
144
- /* @__PURE__ */ jsx2(TouchableRipple, { onPress: () => host.toggle(id), borderless: true, style: [styles2.trigger, { backgroundColor: value, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ jsx2(View3, { ref: triggerRef, collapsable: false, style: styles2.triggerMeasure, children: tag ? /* @__PURE__ */ jsx2(Text, { style: [styles2.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx2(Icon, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
145
- /* @__PURE__ */ jsx2(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: value, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ jsx2(ScrollView, { style: [styles2.swatches, { backgroundColor: menuBg, borderColor: value, width: swatchesWidth, maxHeight }], contentContainerStyle: styles2.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
345
+ return /* @__PURE__ */ jsxs3(View4, { style: [styles3.anchor, open && styles3.anchorOpen], children: [
346
+ /* @__PURE__ */ jsx3(TouchableRipple2, { onPress: () => host.toggle(id), borderless: true, style: [styles3.trigger, { backgroundColor: value, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ jsx3(View4, { ref: triggerRef, collapsable: false, style: styles3.triggerMeasure, children: tag ? /* @__PURE__ */ jsx3(Text2, { style: [styles3.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx3(Icon2, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
347
+ /* @__PURE__ */ jsx3(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: value, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ jsx3(ScrollView2, { style: [styles3.swatches, { backgroundColor: menuBg, borderColor: value, width: swatchesWidth, maxHeight }], contentContainerStyle: styles3.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
146
348
  const selected = swatch.value.toLowerCase() === value.toLowerCase();
147
349
  const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
148
350
  const swappable = taken && allowSwapTaken;
149
- return /* @__PURE__ */ jsx2(
150
- TouchableRipple,
351
+ return /* @__PURE__ */ jsx3(
352
+ TouchableRipple2,
151
353
  {
152
354
  disabled: taken && !swappable,
153
355
  onPress: () => {
@@ -155,15 +357,15 @@ function InlineColorPicker({ id, host, value, onChange, swatches = defaultColors
155
357
  if (autoDismiss) host.close();
156
358
  },
157
359
  borderless: true,
158
- style: [styles2.swatch, { backgroundColor: swatch.value }, selected && styles2.swatchSelected, taken && !swappable && styles2.swatchTaken],
159
- children: selected ? /* @__PURE__ */ jsx2(Icon, { source: "check", size: 16, color: getContrastColor(swatch.value) }) : swappable ? /* @__PURE__ */ jsx2(Icon, { source: "swap-horizontal", size: 16, color: getContrastColor(swatch.value) }) : taken ? /* @__PURE__ */ jsx2(Icon, { source: "close", size: 16, color: getContrastColor(swatch.value) }) : /* @__PURE__ */ jsx2(View3, {})
360
+ style: [styles3.swatch, { backgroundColor: swatch.value }, selected && styles3.swatchSelected, taken && !swappable && styles3.swatchTaken],
361
+ children: selected ? /* @__PURE__ */ jsx3(Icon2, { source: "check", size: 16, color: getContrastColor(swatch.value) }) : swappable ? /* @__PURE__ */ jsx3(Icon2, { source: "swap-horizontal", size: 16, color: getContrastColor(swatch.value) }) : taken ? /* @__PURE__ */ jsx3(Icon2, { source: "close", size: 16, color: getContrastColor(swatch.value) }) : /* @__PURE__ */ jsx3(View4, {})
160
362
  },
161
363
  swatch.value
162
364
  );
163
365
  }) }) })
164
366
  ] });
165
367
  }
166
- var styles2 = StyleSheet2.create({
368
+ var styles3 = StyleSheet3.create({
167
369
  anchor: {
168
370
  position: "relative"
169
371
  },
@@ -240,25 +442,25 @@ function loadSkiaWeb() {
240
442
  }
241
443
 
242
444
  // src/PressAwayOverlay.tsx
243
- import { Pressable, StyleSheet as StyleSheet3 } from "react-native";
244
- import { jsx as jsx3 } from "react/jsx-runtime";
445
+ import { Pressable, StyleSheet as StyleSheet4 } from "react-native";
446
+ import { jsx as jsx4 } from "react/jsx-runtime";
245
447
  function PressAwayOverlay({ active, onPress, style }) {
246
448
  if (!active) return null;
247
- return /* @__PURE__ */ jsx3(Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [StyleSheet3.absoluteFill, style] });
449
+ return /* @__PURE__ */ jsx4(Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [StyleSheet4.absoluteFill, style] });
248
450
  }
249
451
 
250
452
  // src/ReadyButton.tsx
251
- import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
252
- import { StyleSheet as StyleSheet4 } from "react-native";
253
- import { Text as Text2 } from "react-native-paper";
254
- import { jsx as jsx4 } from "react/jsx-runtime";
453
+ import { TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
454
+ import { StyleSheet as StyleSheet5 } from "react-native";
455
+ import { Text as Text3 } from "react-native-paper";
456
+ import { jsx as jsx5 } from "react/jsx-runtime";
255
457
  function ReadyButton({ color, ready, onToggleReady, style, labelFontFamily = MONO_FONT }) {
256
- const readyButtonStyle = [styles3.readyButton, { borderColor: color }, ready && { backgroundColor: color }, style];
458
+ const readyButtonStyle = [styles4.readyButton, { borderColor: color }, ready && { backgroundColor: color }, style];
257
459
  const labelTextStyle = { fontFamily: labelFontFamily, fontWeight: "bold" };
258
460
  const labelColorStyle = { color: ready ? "#000000" : color };
259
- return /* @__PURE__ */ jsx4(TouchableRipple2, { onPress: onToggleReady, borderless: true, style: readyButtonStyle, children: /* @__PURE__ */ jsx4(Text2, { variant: "labelLarge", style: [labelTextStyle, labelColorStyle], children: ready ? "READY \u2713" : "READY?" }) });
461
+ return /* @__PURE__ */ jsx5(TouchableRipple3, { onPress: onToggleReady, borderless: true, style: readyButtonStyle, children: /* @__PURE__ */ jsx5(Text3, { variant: "labelLarge", style: [labelTextStyle, labelColorStyle], children: ready ? "READY \u2713" : "READY?" }) });
260
462
  }
261
- var styles3 = StyleSheet4.create({
463
+ var styles4 = StyleSheet5.create({
262
464
  readyButton: {
263
465
  alignItems: "center",
264
466
  borderRadius: 12,
@@ -272,20 +474,20 @@ var styles3 = StyleSheet4.create({
272
474
 
273
475
  // src/SectionedDropdown.tsx
274
476
  import { getBlendedColor, getColorRoles } from "@rific/auto-paper";
275
- import { IconButton, TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
276
- import { ScrollView as ScrollView2, StyleSheet as StyleSheet5, View as View4 } from "react-native";
277
- import { Icon as Icon2, Text as Text3 } from "react-native-paper";
477
+ import { IconButton, TouchableRipple as TouchableRipple4 } from "@rific/feedback-press";
478
+ import { ScrollView as ScrollView3, StyleSheet as StyleSheet6, View as View5 } from "react-native";
479
+ import { Icon as Icon3, Text as Text4 } from "react-native-paper";
278
480
 
279
481
  // src/TriggerGaugeHost.tsx
280
482
  import { lazy, Suspense } from "react";
281
- import { jsx as jsx5 } from "react/jsx-runtime";
483
+ import { jsx as jsx6 } from "react/jsx-runtime";
282
484
  var LazyTriggerGauge = lazy(() => loadSkiaWeb().then(() => import("./TriggerGauge-2OJOOF6I.mjs").then((m) => ({ default: m.TriggerGauge }))));
283
485
  function TriggerGaugeHost(props) {
284
- return /* @__PURE__ */ jsx5(Suspense, { fallback: null, children: /* @__PURE__ */ jsx5(LazyTriggerGauge, { ...props }) });
486
+ return /* @__PURE__ */ jsx6(Suspense, { fallback: null, children: /* @__PURE__ */ jsx6(LazyTriggerGauge, { ...props }) });
285
487
  }
286
488
 
287
489
  // src/SectionedDropdown.tsx
288
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
490
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
289
491
  var MENU_BORDER_WIDTH = 1;
290
492
  var TRIGGER_SIZE = 44;
291
493
  var MENU_MIN_WIDTH = 200;
@@ -331,17 +533,17 @@ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accen
331
533
  }
332
534
  gaugeOffset += section.options.length;
333
535
  }
334
- const anchorStyle = [styles4.anchor, open && styles4.anchorOpen];
536
+ const anchorStyle = [styles5.anchor, open && styles5.anchorOpen];
335
537
  const menuStyle = { backgroundColor: menuBg, borderColor: menuBorderColor, maxHeight };
336
538
  const dividerStyle = { backgroundColor: menuBorderColor };
337
- return /* @__PURE__ */ jsxs3(View4, { style: anchorStyle, children: [
338
- /* @__PURE__ */ jsxs3(View4, { ref: triggerRef, collapsable: false, style: styles4.triggerBox, children: [
339
- /* @__PURE__ */ jsx6(TriggerGaugeHost, { segments: gaugeSegments, litIndices: gaugeLitIndices, size: TRIGGER_SIZE, accentColor, mutedColor }),
340
- /* @__PURE__ */ jsx6(IconButton, { icon: triggerIcon, iconColor: triggerIconColor, size: triggerIconSize, accessibilityLabel, onPress: () => host.toggle(id) })
539
+ return /* @__PURE__ */ jsxs4(View5, { style: anchorStyle, children: [
540
+ /* @__PURE__ */ jsxs4(View5, { ref: triggerRef, collapsable: false, style: styles5.triggerBox, children: [
541
+ /* @__PURE__ */ jsx7(TriggerGaugeHost, { segments: gaugeSegments, litIndices: gaugeLitIndices, size: TRIGGER_SIZE, accentColor, mutedColor }),
542
+ /* @__PURE__ */ jsx7(IconButton, { icon: triggerIcon, iconColor: triggerIconColor, size: triggerIconSize, accessibilityLabel, onPress: () => host.toggle(id) })
341
543
  ] }),
342
- /* @__PURE__ */ jsx6(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: menuBorderColor, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: TRIGGER_SIZE, children: /* @__PURE__ */ jsx6(ScrollView2, { style: [styles4.menu, menuStyle], contentContainerStyle: styles4.menuContent, showsVerticalScrollIndicator: false, children: sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs3(View4, { children: [
343
- sectionIndex > 0 && /* @__PURE__ */ jsx6(View4, { style: [styles4.divider, dividerStyle] }),
344
- section.kind === "single" ? /* @__PURE__ */ jsx6(
544
+ /* @__PURE__ */ jsx7(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: menuBorderColor, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: TRIGGER_SIZE, children: /* @__PURE__ */ jsx7(ScrollView3, { style: [styles5.menu, menuStyle], contentContainerStyle: styles5.menuContent, showsVerticalScrollIndicator: false, children: sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs4(View5, { children: [
545
+ sectionIndex > 0 && /* @__PURE__ */ jsx7(View5, { style: [styles5.divider, dividerStyle] }),
546
+ section.kind === "single" ? /* @__PURE__ */ jsx7(
345
547
  SingleSection,
346
548
  {
347
549
  section,
@@ -354,7 +556,7 @@ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accen
354
556
  if (autoDismiss) host.close();
355
557
  }
356
558
  }
357
- ) : /* @__PURE__ */ jsx6(MultiSection, { section, accentColor, onAccentColor: resolvedOnAccentColor, mutedColor, labelFontFamily, allClearLabels })
559
+ ) : /* @__PURE__ */ jsx7(MultiSection, { section, accentColor, onAccentColor: resolvedOnAccentColor, mutedColor, labelFontFamily, allClearLabels })
358
560
  ] }, section.id)) }) })
359
561
  ] });
360
562
  }
@@ -366,17 +568,17 @@ function SingleSection({
366
568
  labelFontFamily,
367
569
  onSelect
368
570
  }) {
369
- return /* @__PURE__ */ jsx6(Fragment2, { children: section.options.map((option) => {
571
+ return /* @__PURE__ */ jsx7(Fragment3, { children: section.options.map((option) => {
370
572
  const selected = option.value === section.value;
371
573
  const rowColor = selected ? onAccentColor : mutedColor;
372
- const itemStyle = [styles4.item, selected && { backgroundColor: accentColor }];
574
+ const itemStyle = [styles5.item, selected && { backgroundColor: accentColor }];
373
575
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
374
576
  const descriptionStyle = { fontFamily: labelFontFamily, color: rowColor };
375
- return /* @__PURE__ */ jsx6(TouchableRipple3, { onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ jsxs3(View4, { style: styles4.itemRow, children: [
376
- option.icon && /* @__PURE__ */ jsx6(Icon2, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
377
- /* @__PURE__ */ jsxs3(View4, { style: styles4.itemLabelColumn, children: [
378
- /* @__PURE__ */ jsx6(Text3, { variant: "labelLarge", style: labelStyle, children: option.label }),
379
- option.description && /* @__PURE__ */ jsx6(Text3, { variant: "labelSmall", style: descriptionStyle, children: option.description })
577
+ return /* @__PURE__ */ jsx7(TouchableRipple4, { onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ jsxs4(View5, { style: styles5.itemRow, children: [
578
+ option.icon && /* @__PURE__ */ jsx7(Icon3, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
579
+ /* @__PURE__ */ jsxs4(View5, { style: styles5.itemLabelColumn, children: [
580
+ /* @__PURE__ */ jsx7(Text4, { variant: "labelLarge", style: labelStyle, children: option.label }),
581
+ option.description && /* @__PURE__ */ jsx7(Text4, { variant: "labelSmall", style: descriptionStyle, children: option.description })
380
582
  ] })
381
583
  ] }) }, option.value);
382
584
  }) });
@@ -390,23 +592,23 @@ function MultiSection({
390
592
  allClearLabels
391
593
  }) {
392
594
  const allSelected = section.value.length === section.options.length;
393
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
595
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
394
596
  section.options.map((option, i) => {
395
597
  const selected = section.value.includes(option.value);
396
598
  const rowColor = selected ? onAccentColor : mutedColor;
397
599
  const prevSelected = selected && i > 0 && section.value.includes(section.options[i - 1].value);
398
600
  const nextSelected = selected && i < section.options.length - 1 && section.value.includes(section.options[i + 1].value);
399
- const itemStyle = [styles4.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }];
601
+ const itemStyle = [styles5.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }];
400
602
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
401
- return /* @__PURE__ */ jsx6(TouchableRipple3, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: itemStyle, children: /* @__PURE__ */ jsxs3(View4, { style: styles4.itemRow, children: [
402
- option.icon && /* @__PURE__ */ jsx6(Icon2, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
403
- /* @__PURE__ */ jsx6(Text3, { variant: "labelLarge", style: [styles4.itemLabelFlex, labelStyle], children: option.label })
603
+ return /* @__PURE__ */ jsx7(TouchableRipple4, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: itemStyle, children: /* @__PURE__ */ jsxs4(View5, { style: styles5.itemRow, children: [
604
+ option.icon && /* @__PURE__ */ jsx7(Icon3, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
605
+ /* @__PURE__ */ jsx7(Text4, { variant: "labelLarge", style: [styles5.itemLabelFlex, labelStyle], children: option.label })
404
606
  ] }) }, option.value);
405
607
  }),
406
- section.allClear && /* @__PURE__ */ jsx6(TouchableRipple3, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles4.item, children: /* @__PURE__ */ jsx6(Text3, { variant: "labelLarge", style: [styles4.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }], children: allSelected ? allClearLabels.clear : allClearLabels.all }) })
608
+ section.allClear && /* @__PURE__ */ jsx7(TouchableRipple4, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles5.item, children: /* @__PURE__ */ jsx7(Text4, { variant: "labelLarge", style: [styles5.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }], children: allSelected ? allClearLabels.clear : allClearLabels.all }) })
407
609
  ] });
408
610
  }
409
- var styles4 = StyleSheet5.create({
611
+ var styles5 = StyleSheet6.create({
410
612
  allClearLabel: {
411
613
  fontWeight: "bold",
412
614
  textAlign: "center"
@@ -423,7 +625,7 @@ var styles4 = StyleSheet5.create({
423
625
  zIndex: 100
424
626
  },
425
627
  divider: {
426
- height: StyleSheet5.hairlineWidth,
628
+ height: StyleSheet6.hairlineWidth,
427
629
  marginVertical: 4
428
630
  },
429
631
  item: {
@@ -471,9 +673,9 @@ var styles4 = StyleSheet5.create({
471
673
  });
472
674
 
473
675
  // src/usePopoverHost.ts
474
- import { useCallback as useCallback2, useState as useState2 } from "react";
676
+ import { useCallback as useCallback2, useState as useState3 } from "react";
475
677
  function usePopoverHost() {
476
- const [openId, setOpenId] = useState2(null);
678
+ const [openId, setOpenId] = useState3(null);
477
679
  const toggle = useCallback2((id) => {
478
680
  setOpenId((prev) => prev === id ? null : id);
479
681
  }, []);
@@ -481,6 +683,7 @@ function usePopoverHost() {
481
683
  return { openId, toggle, close };
482
684
  }
483
685
  export {
686
+ BaseSettingsDialog,
484
687
  InlineColorPicker,
485
688
  MONO_FONT,
486
689
  PopoverBody,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Visual component kit for local-multiplayer React Native games: inline (non-modal) popovers, dropdowns, color pickers, gauges, ready buttons, and dialogs, built to stay scoped to one player's own zone on a shared screen",
5
5
  "keywords": [
6
6
  "react-native",
@@ -68,6 +68,7 @@
68
68
  "@infinitetoken/tsconfig": "^0.3.0",
69
69
  "@rific/auto-paper": "^0.10.3",
70
70
  "@rific/feedback-press": "^0.10.8",
71
+ "@rific/updater": "^0.4.2",
71
72
  "@shopify/react-native-skia": "^2.6.2",
72
73
  "@tastic/core": "^0.1.5",
73
74
  "@testing-library/dom": "^10.4.1",
@@ -91,6 +92,7 @@
91
92
  "peerDependencies": {
92
93
  "@rific/auto-paper": ">=0.9.0",
93
94
  "@rific/feedback-press": ">=0.10.0",
95
+ "@rific/updater": ">=0.4.0",
94
96
  "@shopify/react-native-skia": ">=1.5.0",
95
97
  "@tastic/core": ">=0.1.0",
96
98
  "react": ">=19.0.0",