@tastic/hud 0.21.0 → 0.23.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.
@@ -7,16 +7,24 @@ import {
7
7
  ConfirmDialog,
8
8
  ContentGutter,
9
9
  CornerActionButtons,
10
+ InlineColorPicker,
10
11
  LOCKED_BADGE_COLOR,
11
12
  MONO_FONT,
13
+ PopoverBody,
14
+ PressAwayOverlay,
12
15
  RotatedFrame,
13
16
  StaggeredWord,
14
17
  StatRow,
15
18
  StatSection,
16
19
  UpdateDialog,
20
+ WELL_SHADOW,
21
+ getInlineColorPickerContentSize,
17
22
  getStaggeredWordDuration,
18
- useCompactDialog
19
- } from "../chunk-UOA35OWJ.mjs";
23
+ useAutoAlign,
24
+ useCompactDialog,
25
+ usePopoverHost,
26
+ useWellColors
27
+ } from "../chunk-BXG44LIA.mjs";
20
28
  import {
21
29
  COMPACT_DIALOG_HEIGHT,
22
30
  DialogHeader,
@@ -38,19 +46,27 @@ export {
38
46
  ContentGutter,
39
47
  CornerActionButtons,
40
48
  DialogHeader,
49
+ InlineColorPicker,
41
50
  LOCKED_BADGE_COLOR,
42
51
  MONO_FONT,
43
52
  OverlayBackdrop,
44
53
  OverlayCard,
54
+ PopoverBody,
55
+ PressAwayOverlay,
45
56
  RotatedFrame,
46
57
  StaggeredWord,
47
58
  StatRow,
48
59
  StatSection,
49
60
  UpdateDialog,
61
+ WELL_SHADOW,
62
+ getInlineColorPickerContentSize,
50
63
  getStaggeredWordDuration,
51
64
  isCompactDialogHeight,
52
65
  overlayActionStyles,
53
66
  overlayStyles,
67
+ useAutoAlign,
54
68
  useCompactDialog,
55
- useOverlayCardColors
69
+ useOverlayCardColors,
70
+ usePopoverHost,
71
+ useWellColors
56
72
  };
@@ -8,17 +8,24 @@ import {
8
8
  useOverlayCardColors
9
9
  } from "./chunk-PUF6TABL.mjs";
10
10
 
11
- // src/AchievementRow.tsx
11
+ // src/wellSurface.ts
12
12
  import { useAutoPaperTheme } from "@rific/auto-paper";
13
+ var WELL_SHADOW = "0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15)";
14
+ function useWellColors() {
15
+ const { colors } = useAutoPaperTheme();
16
+ return { bg: colors.surface, fg: colors.onSurface, fgMuted: colors.onSurfaceVariant };
17
+ }
18
+
19
+ // src/AchievementRow.tsx
13
20
  import { StyleSheet, View } from "react-native";
14
21
  import { Icon, ProgressBar, Text } from "react-native-paper";
15
22
  import { jsx, jsxs } from "react/jsx-runtime";
16
23
  var LOCKED_BADGE_COLOR = "rgba(128,128,128,0.3)";
17
24
  function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }) {
18
- const { dark } = useAutoPaperTheme();
19
- const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
20
- const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
21
- const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
25
+ const well = useWellColors();
26
+ const fg = fgOverride ?? well.fg;
27
+ const fgMuted = fgMutedOverride ?? well.fgMuted;
28
+ const sectionBg = sectionBgOverride ?? well.bg;
22
29
  const unlocked = unlockedLabel !== void 0;
23
30
  return /* @__PURE__ */ jsxs(View, { style: [styles.row, { backgroundColor: sectionBg }], children: [
24
31
  /* @__PURE__ */ jsx(View, { style: [styles.badge, { backgroundColor: badgeColor }], children: /* @__PURE__ */ jsx(Icon, { source: icon, size: 20, color: unlocked ? "#000000" : fgMuted }) }),
@@ -61,6 +68,7 @@ var styles = StyleSheet.create({
61
68
  row: {
62
69
  alignItems: "center",
63
70
  borderRadius: 12,
71
+ boxShadow: WELL_SHADOW,
64
72
  flexDirection: "row",
65
73
  gap: 12,
66
74
  padding: 12
@@ -86,13 +94,13 @@ import { Fragment } from "react";
86
94
  import { StyleSheet as StyleSheet2 } from "react-native";
87
95
  import { Text as Text2 } from "react-native-paper";
88
96
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
89
- function AchievementCatalogSection({ rows, label = "ALL ACHIEVEMENTS", fg, fgMuted: fgMutedOverride }) {
97
+ function AchievementCatalogSection({ rows, label = "ALL ACHIEVEMENTS", fg, fgMuted: fgMutedOverride, sectionBg }) {
90
98
  const { dark } = useAutoPaperTheme2();
91
99
  const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
92
100
  const tierAccent = (tierColor) => dark ? tierColor : getBlendedColor("#000000", tierColor, 0.5);
93
101
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
94
102
  /* @__PURE__ */ jsx2(Text2, { variant: "labelMedium", style: [styles2.label, { color: fgMuted, fontFamily: MONO_FONT }], children: label }),
95
- rows.map((row) => /* @__PURE__ */ jsx2(AchievementRow, { icon: row.icon, title: row.title, description: row.description, badgeColor: row.unlockedAt !== void 0 ? row.tierColor : LOCKED_BADGE_COLOR, checkColor: tierAccent(row.tierColor), unlockedLabel: row.unlockedLabel, progress: row.progress, deviceMarker: row.deviceMarker, fg, fgMuted: fgMutedOverride }, row.id))
103
+ rows.map((row) => /* @__PURE__ */ jsx2(AchievementRow, { icon: row.icon, title: row.title, description: row.description, badgeColor: row.unlockedAt !== void 0 ? row.tierColor : LOCKED_BADGE_COLOR, checkColor: tierAccent(row.tierColor), unlockedLabel: row.unlockedLabel, progress: row.progress, deviceMarker: row.deviceMarker, fg, fgMuted: fgMutedOverride, sectionBg }, row.id))
96
104
  ] });
97
105
  }
98
106
  var styles2 = StyleSheet2.create({
@@ -103,14 +111,13 @@ var styles2 = StyleSheet2.create({
103
111
  });
104
112
 
105
113
  // src/StatRow.tsx
106
- import { useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
107
114
  import { StyleSheet as StyleSheet3, View as View2 } from "react-native";
108
115
  import { Text as Text3 } from "react-native-paper";
109
116
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
110
117
  function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }) {
111
- const { dark } = useAutoPaperTheme3();
112
- const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
113
- const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
118
+ const well = useWellColors();
119
+ const fg = fgOverride ?? well.fg;
120
+ const fgMuted = fgMutedOverride ?? well.fgMuted;
114
121
  return /* @__PURE__ */ jsxs3(View2, { style: styles3.row, children: [
115
122
  /* @__PURE__ */ jsx3(Text3, { variant: "bodyMedium", style: { color: fgMuted }, children: label }),
116
123
  /* @__PURE__ */ jsx3(Text3, { variant: "bodyMedium", style: [styles3.value, { color: fg }], children: value })
@@ -128,14 +135,13 @@ var styles3 = StyleSheet3.create({
128
135
  });
129
136
 
130
137
  // src/StatSection.tsx
131
- import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
132
138
  import { StyleSheet as StyleSheet4, View as View3 } from "react-native";
133
139
  import { Text as Text4 } from "react-native-paper";
134
140
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
135
141
  function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }) {
136
- const { dark } = useAutoPaperTheme4();
137
- const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
138
- const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
142
+ const well = useWellColors();
143
+ const fg = fgOverride ?? well.fg;
144
+ const sectionBg = sectionBgOverride ?? well.bg;
139
145
  return /* @__PURE__ */ jsxs4(View3, { style: [styles4.section, { backgroundColor: sectionBg }], children: [
140
146
  /* @__PURE__ */ jsx4(Text4, { variant: "labelMedium", style: [styles4.label, { color: fg }], children: label }),
141
147
  children
@@ -152,6 +158,7 @@ var styles4 = StyleSheet4.create({
152
158
  },
153
159
  section: {
154
160
  borderRadius: 12,
161
+ boxShadow: WELL_SHADOW,
155
162
  gap: 8,
156
163
  padding: 16
157
164
  }
@@ -159,11 +166,11 @@ var styles4 = StyleSheet4.create({
159
166
 
160
167
  // src/ActivityStatSection.tsx
161
168
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
162
- function ActivityStatSection({ stats }) {
163
- return /* @__PURE__ */ jsxs5(StatSection, { label: "ACTIVITY", children: [
164
- /* @__PURE__ */ jsx5(StatRow, { label: "Days Played", value: String(stats.distinctDaysPlayed) }),
165
- /* @__PURE__ */ jsx5(StatRow, { label: "Day Streak", value: String(stats.currentDayStreak) }),
166
- /* @__PURE__ */ jsx5(StatRow, { label: "Best Day Streak", value: String(stats.bestDayStreak) })
169
+ function ActivityStatSection({ stats, fg, fgMuted, sectionBg }) {
170
+ return /* @__PURE__ */ jsxs5(StatSection, { label: "ACTIVITY", fg, sectionBg, children: [
171
+ /* @__PURE__ */ jsx5(StatRow, { label: "Days Played", value: String(stats.distinctDaysPlayed), fg, fgMuted }),
172
+ /* @__PURE__ */ jsx5(StatRow, { label: "Day Streak", value: String(stats.currentDayStreak), fg, fgMuted }),
173
+ /* @__PURE__ */ jsx5(StatRow, { label: "Best Day Streak", value: String(stats.bestDayStreak), fg, fgMuted })
167
174
  ] });
168
175
  }
169
176
 
@@ -185,7 +192,7 @@ function useCompactDialog(rotation = 0) {
185
192
  }
186
193
 
187
194
  // src/ConfirmDialog.tsx
188
- import { useAutoPaperTheme as useAutoPaperTheme5 } from "@rific/auto-paper";
195
+ import { useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
189
196
  import { Button } from "@rific/feedback-press";
190
197
  import { toRotationStyle } from "@tastic/core";
191
198
  import { useEffect, useRef } from "react";
@@ -193,7 +200,7 @@ import { BackHandler, StyleSheet as StyleSheet5, View as View4 } from "react-nat
193
200
  import { Portal, Text as Text5 } from "react-native-paper";
194
201
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
195
202
  function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = "help-circle-outline", destructive = false }) {
196
- const { colors } = useAutoPaperTheme5();
203
+ const { colors } = useAutoPaperTheme3();
197
204
  const { fg } = useOverlayCardColors();
198
205
  const { compact, onLayout } = useCompactDialog(rotation);
199
206
  const onCancelRef = useRef(onCancel);
@@ -273,7 +280,8 @@ var styles6 = StyleSheet6.create({
273
280
  });
274
281
 
275
282
  // src/BaseSettingsDialog.tsx
276
- import { AutoAppearancePicker, Dialog, FadingScrollView, useAutoPaperTheme as useAutoPaperTheme6, useBlurModule, useThemeSettings } from "@rific/auto-paper";
283
+ import { AutoAppearancePicker, Dialog, FadingScrollView, useAutoPaperTheme as useAutoPaperTheme4, useBlurModule, useThemeSettings } from "@rific/auto-paper";
284
+ import { useExpoKeepAwake, useKeepAwakeSettings } from "@rific/core";
277
285
  import { Button as Button2, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from "@rific/feedback-press";
278
286
  import { useUpdater } from "@rific/updater";
279
287
  import { rotateInsets as rotateInsets2, useIsTouchPrimaryDevice, useRotation as useRotation2 } from "@tastic/core";
@@ -307,8 +315,8 @@ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
307
315
  function SettingIcon({ source, color, containerColor }) {
308
316
  return /* @__PURE__ */ jsx8(View6, { style: [styles7.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx8(Icon2, { source, size: 18, color }) });
309
317
  }
310
- function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
311
- const { colors } = useAutoPaperTheme6();
318
+ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
319
+ const { colors } = useAutoPaperTheme4();
312
320
  const ambientRotation = useRotation2();
313
321
  const rotation = rotationOverride ?? ambientRotation;
314
322
  const window = useWindowDimensions3();
@@ -319,6 +327,8 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
319
327
  const isTouchPrimary = useIsTouchPrimaryDevice();
320
328
  const { settings: hapticSettings, set: setHapticSettings } = useHapticSettings();
321
329
  const { settings: soundSettings, set: setSoundSettings } = useSoundSettings();
330
+ const { settings: keepAwakeSettings, set: setKeepAwakeSettings } = useKeepAwakeSettings();
331
+ const expoKeepAwake = useExpoKeepAwake();
322
332
  const sound = useContext(SoundContext);
323
333
  const { forceShort: forceHapticFeedback } = useVibration();
324
334
  const { settings: themeSettings, set: setThemeSettings } = useThemeSettings();
@@ -337,7 +347,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
337
347
  if (visible && !opened) setOpened(true);
338
348
  const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
339
349
  const showEdgeGuard = Platform2.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
340
- const showKeepAwake = isTouchPrimary && keepAwake !== void 0 && onKeepAwakeChange !== void 0;
350
+ const showKeepAwake = isTouchPrimary && expoKeepAwake !== void 0;
341
351
  const showHaptics = Platform2.OS !== "web" && !hideHaptics;
342
352
  const showSound = !hideSound;
343
353
  const showAppearance = !hideAppearance;
@@ -364,8 +374,8 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
364
374
  /* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Require second swipe" })
365
375
  ] })
366
376
  ] }) }),
367
- showKeepAwake && /* @__PURE__ */ jsx8(TouchableRipple, { onPress: () => onKeepAwakeChange(!keepAwake), style: styles7.toggleButton, accessibilityLabel: `Keep awake ${keepAwake ? "on" : "off"}`, children: /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleContent, children: [
368
- /* @__PURE__ */ jsx8(SettingIcon, { source: keepAwake ? "eye" : "eye-closed", color: keepAwake ? colors.tertiary : colors.onSurfaceVariant, containerColor: keepAwake ? colors.tertiaryContainer : colors.surfaceVariant }),
377
+ showKeepAwake && /* @__PURE__ */ jsx8(TouchableRipple, { onPress: () => setKeepAwakeSettings({ enabled: !keepAwakeSettings.enabled }), style: styles7.toggleButton, accessibilityLabel: `Keep awake ${keepAwakeSettings.enabled ? "on" : "off"}`, children: /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleContent, children: [
378
+ /* @__PURE__ */ jsx8(SettingIcon, { source: keepAwakeSettings.enabled ? "eye" : "eye-closed", color: keepAwakeSettings.enabled ? colors.tertiary : colors.onSurfaceVariant, containerColor: keepAwakeSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant }),
369
379
  /* @__PURE__ */ jsxs7(View6, { style: styles7.flexShrink, children: [
370
380
  /* @__PURE__ */ jsx8(Text6, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Keep Awake" }),
371
381
  /* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Stop auto-lock mid-match" })
@@ -539,7 +549,7 @@ var styles7 = StyleSheet7.create({
539
549
  });
540
550
 
541
551
  // src/BaseStatsScreen.tsx
542
- import { FadingScrollView as FadingScrollView2, useAutoPaperTheme as useAutoPaperTheme7 } from "@rific/auto-paper";
552
+ import { FadingScrollView as FadingScrollView2, useAutoPaperTheme as useAutoPaperTheme5 } from "@rific/auto-paper";
543
553
  import { Button as Button3, IconButton } from "@rific/feedback-press";
544
554
  import { useRotation as useRotation3 } from "@tastic/core";
545
555
  import { useState as useState4 } from "react";
@@ -547,7 +557,7 @@ import { StyleSheet as StyleSheet8, View as View7 } from "react-native";
547
557
  import { Text as Text7 } from "react-native-paper";
548
558
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
549
559
  function BaseStatsScreen({ rotation: rotationOverride, title = "Achievements", titleVariant = "headlineSmall", onBack, backLabel = "Back", hitSlop, insets, onReset, resetLabel = "Reset All Stats", resetConfirmTitle = "Reset Everything?", resetConfirmBody = "This permanently erases all stats and achievements. This cannot be undone.", fg: fgOverride, bg: bgOverride, children }) {
550
- const { dark, colors } = useAutoPaperTheme7();
560
+ const { dark, colors } = useAutoPaperTheme5();
551
561
  const ambientRotation = useRotation3();
552
562
  const rotation = rotationOverride ?? ambientRotation;
553
563
  const [confirmResetVisible, setConfirmResetVisible] = useState4(false);
@@ -637,17 +647,131 @@ var styles9 = StyleSheet9.create({
637
647
  }
638
648
  });
639
649
 
650
+ // src/PopoverBody.tsx
651
+ import { StyleSheet as StyleSheet10, View as View9 } from "react-native";
652
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
653
+ var CARET_SIZE = 7;
654
+ var CARET_RING_OFFSET = 2;
655
+ var CARET_DIP = 1;
656
+ function PopoverBody({ visible, children, align = "center", verticalAlign = "below", caretColor, caretBorderColor, caretBorderWidth = 1, triggerSize = 0 }) {
657
+ if (!visible) return null;
658
+ const above = verticalAlign === "above";
659
+ const alignStyle = align === "left" ? styles10.contentLeft : align === "right" ? styles10.contentRight : styles10.contentCenter;
660
+ const verticalStyle = above ? styles10.contentAbove : styles10.contentBelow;
661
+ const ringSize = CARET_SIZE + caretBorderWidth;
662
+ const caretOffset = (halfFootprint) => align === "right" ? { right: triggerSize / 2 - halfFootprint } : { left: triggerSize / 2 - halfFootprint };
663
+ const caretFill = above ? "borderTopColor" : "borderBottomColor";
664
+ const caretWidthProp = above ? "borderTopWidth" : "borderBottomWidth";
665
+ const caretEdge = (size) => above ? { bottom: -size + CARET_DIP } : { top: -size + CARET_DIP };
666
+ return /* @__PURE__ */ jsxs10(View9, { style: [styles10.content, alignStyle, verticalStyle], children: [
667
+ children,
668
+ caretColor && /* @__PURE__ */ jsxs10(Fragment2, { children: [
669
+ /* @__PURE__ */ jsx11(View9, { style: [styles10.caret, caretOffset(ringSize), caretEdge(ringSize), { [caretFill]: caretBorderColor ?? caretColor, [caretWidthProp]: ringSize, borderLeftWidth: ringSize, borderRightWidth: ringSize }] }),
670
+ /* @__PURE__ */ jsx11(View9, { style: [styles10.caret, caretOffset(CARET_SIZE), caretEdge(CARET_SIZE - CARET_RING_OFFSET), { [caretFill]: caretColor, [caretWidthProp]: CARET_SIZE, borderLeftWidth: CARET_SIZE, borderRightWidth: CARET_SIZE }] })
671
+ ] })
672
+ ] });
673
+ }
674
+ var styles10 = StyleSheet10.create({
675
+ caret: {
676
+ borderLeftColor: "transparent",
677
+ borderRightColor: "transparent",
678
+ height: 0,
679
+ position: "absolute",
680
+ width: 0,
681
+ // Explicit, not left to DOM order - the box it dips into is itself a position:'relative' view,
682
+ // which React Native Web always gives an implicit zIndex:0 (see PopoverBody's stacking-context
683
+ // notes elsewhere in this codebase), so painting on top needs a real zIndex to beat that
684
+ // reliably rather than relying on being the later sibling.
685
+ zIndex: 1
686
+ },
687
+ content: {
688
+ position: "absolute",
689
+ zIndex: 50
690
+ },
691
+ contentAbove: {
692
+ bottom: "100%",
693
+ marginBottom: 8
694
+ },
695
+ contentBelow: {
696
+ marginTop: 8,
697
+ top: "100%"
698
+ },
699
+ // Stretched to the anchor's own (trigger) width, with alignItems centering the actual (usually
700
+ // much wider) popover box inside via plain flexbox - not `left:'50%'` plus a percentage
701
+ // `transform`, which depends on native resolving the box's own auto width before applying the
702
+ // transform and has proven unreliable there, visibly shifting the box off-center.
703
+ contentCenter: {
704
+ alignItems: "center",
705
+ left: 0,
706
+ right: 0
707
+ },
708
+ contentLeft: {
709
+ left: 0
710
+ },
711
+ contentRight: {
712
+ right: 0
713
+ }
714
+ });
715
+
716
+ // src/useAutoAlign.ts
717
+ import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef3, useState as useState5 } from "react";
718
+ import { useWindowDimensions as useWindowDimensions4 } from "react-native";
719
+ var EDGE_MARGIN = 12;
720
+ function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
721
+ if (rotation === 0) return { x, y, width, height };
722
+ const cx = windowWidth / 2;
723
+ const cy = windowHeight / 2;
724
+ const dx = x + width / 2 - cx;
725
+ const dy = y + height / 2 - cy;
726
+ const [newDx, newDy, newWidth, newHeight] = rotation === 180 ? [-dx, -dy, width, height] : rotation === 90 ? [-dy, dx, height, width] : [dy, -dx, height, width];
727
+ return { x: cx + newDx - newWidth / 2, y: cy + newDy - newHeight / 2, width: newWidth, height: newHeight };
728
+ }
729
+ function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
730
+ const triggerRef = useRef3(null);
731
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions4();
732
+ const [align, setAlign] = useState5("center");
733
+ const [verticalAlign, setVerticalAlign] = useState5("below");
734
+ const [maxHeight, setMaxHeight] = useState5(contentHeight);
735
+ const [measured, setMeasured] = useState5(false);
736
+ const [prevOpen, setPrevOpen] = useState5(open);
737
+ if (open !== prevOpen) {
738
+ setPrevOpen(open);
739
+ if (open) setMeasured(false);
740
+ }
741
+ const measure = useCallback3(() => {
742
+ triggerRef.current?.measureInWindow((rawX, rawY, rawWidth, rawHeight) => {
743
+ const { x, y, width: triggerWidth, height: triggerHeight } = rotateRect(rawX, rawY, rawWidth, rawHeight, rotation, windowWidth, windowHeight);
744
+ const centerX = x + triggerWidth / 2;
745
+ const overflowsRight = centerX + contentWidth / 2 > windowWidth - EDGE_MARGIN;
746
+ const overflowsLeft = centerX - contentWidth / 2 < EDGE_MARGIN;
747
+ if (overflowsRight && !overflowsLeft) setAlign("right");
748
+ else if (overflowsLeft && !overflowsRight) setAlign("left");
749
+ else setAlign("center");
750
+ const roomBelow = windowHeight - (y + triggerHeight) - EDGE_MARGIN;
751
+ const roomAbove = y - EDGE_MARGIN;
752
+ const above = contentHeight > roomBelow && roomAbove > roomBelow;
753
+ setVerticalAlign(above ? "above" : "below");
754
+ setMaxHeight(Math.max(above ? roomAbove : roomBelow, 0));
755
+ setMeasured(true);
756
+ });
757
+ }, [contentWidth, contentHeight, windowWidth, windowHeight, rotation]);
758
+ useEffect2(() => {
759
+ if (open) measure();
760
+ }, [open, measure]);
761
+ return { align, maxHeight, measured, triggerRef, verticalAlign };
762
+ }
763
+
640
764
  // src/CornerActionButtons.tsx
641
765
  import { IconButton as IconButton2 } from "@rific/feedback-press";
642
- import { StyleSheet as StyleSheet10 } from "react-native";
643
- import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
766
+ import { StyleSheet as StyleSheet11 } from "react-native";
767
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
644
768
  function CornerActionButtons({ onBack, onSettings, fg, insets, backIcon = "arrow-left", backLabel = "Back", hitSlop }) {
645
- return /* @__PURE__ */ jsxs10(Fragment2, { children: [
646
- /* @__PURE__ */ jsx11(IconButton2, { icon: backIcon, iconColor: fg, size: 24, style: [styles10.back, { top: 8 + insets.top, left: 8 + insets.left }], hitSlop, onPress: onBack, accessibilityLabel: backLabel }),
647
- /* @__PURE__ */ jsx11(IconButton2, { icon: "cog", iconColor: fg, size: 24, style: [styles10.topRight, { top: 8 + insets.top, right: 8 + insets.right }], hitSlop, onPress: onSettings, accessibilityLabel: "Settings" })
769
+ return /* @__PURE__ */ jsxs11(Fragment3, { children: [
770
+ /* @__PURE__ */ jsx12(IconButton2, { icon: backIcon, iconColor: fg, size: 24, style: [styles11.back, { top: 8 + insets.top, left: 8 + insets.left }], hitSlop, onPress: onBack, accessibilityLabel: backLabel }),
771
+ /* @__PURE__ */ jsx12(IconButton2, { icon: "cog", iconColor: fg, size: 24, style: [styles11.topRight, { top: 8 + insets.top, right: 8 + insets.right }], hitSlop, onPress: onSettings, accessibilityLabel: "Settings" })
648
772
  ] });
649
773
  }
650
- var styles10 = StyleSheet10.create({
774
+ var styles11 = StyleSheet11.create({
651
775
  back: {
652
776
  position: "absolute"
653
777
  },
@@ -656,11 +780,167 @@ var styles10 = StyleSheet10.create({
656
780
  }
657
781
  });
658
782
 
783
+ // src/InlineColorPicker.tsx
784
+ import { defaultColors, getContrastColor } from "@rific/auto-paper";
785
+ import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
786
+ import { clamp, useRotation as useRotation4 } from "@tastic/core";
787
+ import { ScrollView, StyleSheet as StyleSheet12, useWindowDimensions as useWindowDimensions5, View as View11 } from "react-native";
788
+ import { Icon as Icon3, Text as Text8 } from "react-native-paper";
789
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
790
+ var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
791
+ var DEFAULT_SIZE = 48;
792
+ var SWATCH_SIZE = 28;
793
+ var SWATCHES_PADDING = 8;
794
+ var SWATCHES_GAP = 6;
795
+ var SWATCHES_BORDER_WIDTH = 2;
796
+ var MIN_COLUMNS = 4;
797
+ var MAX_COLUMNS = 5;
798
+ var SCREEN_MARGIN = 40;
799
+ function widthForColumns(columns) {
800
+ return SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * columns + SWATCHES_GAP * (columns - 1);
801
+ }
802
+ function getInlineColorPickerContentSize(swatchCount, windowWidth, columns) {
803
+ const autoColumns = clamp(Math.floor((windowWidth - 2 * SCREEN_MARGIN + SWATCHES_GAP) / (SWATCH_SIZE + SWATCHES_GAP)), MIN_COLUMNS, MAX_COLUMNS);
804
+ const resolvedColumns = columns ?? autoColumns;
805
+ const width = widthForColumns(resolvedColumns);
806
+ const rows = Math.ceil(swatchCount / resolvedColumns);
807
+ const height = SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * rows + SWATCHES_GAP * (rows - 1);
808
+ return { width, height };
809
+ }
810
+ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches = defaultColors, takenValue, allowSwapTaken, dark, align: forcedAlign, alignOverride, rotation: rotationOverride, icon = "palette", tag, labelFontFamily = MONO_FONT, autoDismiss = true, columns, size = DEFAULT_SIZE }) {
811
+ const ambientRotation = useRotation4();
812
+ const rotation = rotationOverride ?? ambientRotation;
813
+ const menuBg = dark ? "#000000" : "#FFFFFF";
814
+ const { width: windowWidth } = useWindowDimensions5();
815
+ const { width: swatchesWidth, height: swatchesHeight } = getInlineColorPickerContentSize(swatches.length, windowWidth, columns);
816
+ const open = host.openId === id;
817
+ const auto = useAutoAlign(open, swatchesWidth, swatchesHeight, rotation);
818
+ const placement = alignOverride ?? auto;
819
+ const { maxHeight, measured, triggerRef, verticalAlign } = placement;
820
+ const align = forcedAlign ?? placement.align;
821
+ const displayFor = previewValue ?? ((hex) => hex);
822
+ const displayValue = displayFor(value);
823
+ const contrastColor = getContrastColor(displayValue);
824
+ const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4);
825
+ return /* @__PURE__ */ jsxs12(View11, { style: [styles12.anchor, open && styles12.anchorOpen], children: [
826
+ /* @__PURE__ */ jsx13(TouchableRipple2, { onPress: () => host.toggle(id), borderless: true, style: [styles12.trigger, { backgroundColor: displayValue, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ jsx13(View11, { ref: triggerRef, collapsable: false, style: styles12.triggerMeasure, children: tag ? /* @__PURE__ */ jsx13(Text8, { style: [styles12.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx13(Icon3, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
827
+ /* @__PURE__ */ jsx13(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: displayValue, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ jsx13(ScrollView, { style: [styles12.swatches, { backgroundColor: menuBg, borderColor: displayValue, width: swatchesWidth, maxHeight }], contentContainerStyle: styles12.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
828
+ const selected = swatch.value.toLowerCase() === value.toLowerCase();
829
+ const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
830
+ const swappable = taken && allowSwapTaken;
831
+ const swatchDisplay = displayFor(swatch.value);
832
+ return /* @__PURE__ */ jsx13(
833
+ TouchableRipple2,
834
+ {
835
+ disabled: taken && !swappable,
836
+ onPress: () => {
837
+ onChange(swatch.value);
838
+ if (autoDismiss) host.close();
839
+ },
840
+ borderless: true,
841
+ style: [styles12.swatch, { backgroundColor: swatchDisplay }, selected && styles12.swatchSelected, taken && !swappable && styles12.swatchTaken],
842
+ children: selected ? /* @__PURE__ */ jsx13(Icon3, { source: "check", size: 16, color: getContrastColor(swatchDisplay) }) : swappable ? /* @__PURE__ */ jsx13(Icon3, { source: "swap-horizontal", size: 16, color: getContrastColor(swatchDisplay) }) : taken ? /* @__PURE__ */ jsx13(Icon3, { source: "close", size: 16, color: getContrastColor(swatchDisplay) }) : /* @__PURE__ */ jsx13(View11, {})
843
+ },
844
+ swatch.value
845
+ );
846
+ }) }) })
847
+ ] });
848
+ }
849
+ var styles12 = StyleSheet12.create({
850
+ anchor: {
851
+ position: "relative"
852
+ },
853
+ // See SectionedDropdown's identical comment - React Native Web gives every position:'relative' view
854
+ // its own stacking context, so the elevation has to live on the anchor itself, not just the
855
+ // popover content nested inside it, to correctly paint above this anchor's own later siblings.
856
+ anchorOpen: {
857
+ zIndex: 100
858
+ },
859
+ swatch: {
860
+ alignItems: "center",
861
+ borderRadius: SWATCH_SIZE / 2,
862
+ height: SWATCH_SIZE,
863
+ justifyContent: "center",
864
+ width: SWATCH_SIZE
865
+ },
866
+ swatchSelected: {
867
+ borderColor: "#ffffff",
868
+ borderWidth: 2
869
+ },
870
+ swatchTaken: {
871
+ opacity: 0.35
872
+ },
873
+ swatches: {
874
+ borderRadius: 12,
875
+ borderWidth: SWATCHES_BORDER_WIDTH,
876
+ elevation: 8,
877
+ shadowColor: "#000000",
878
+ shadowOffset: { height: 2, width: 0 },
879
+ shadowOpacity: 0.3,
880
+ shadowRadius: 8
881
+ // width is set inline above - border-box sizing means it has to include the border too
882
+ // (2*SWATCHES_BORDER_WIDTH), not just padding+content, or exactly enough room goes missing that
883
+ // the last column silently wraps to a new row. Anything wider than the exact sum just shows as
884
+ // dead space on the right edge of each row instead.
885
+ },
886
+ // Separate from `swatches` itself - see SectionedDropdown's identical menu/menuContent split.
887
+ // The scroll frame's own bounds (including maxHeight) live on the ScrollView; the actual grid
888
+ // wrapping belongs to its contentContainerStyle, which sizes to the unclamped content instead.
889
+ swatchesContent: {
890
+ flexDirection: "row",
891
+ flexWrap: "wrap",
892
+ gap: SWATCHES_GAP,
893
+ padding: SWATCHES_PADDING
894
+ },
895
+ tagLabel: {
896
+ fontWeight: "700",
897
+ letterSpacing: 0.5
898
+ },
899
+ // borderRadius/height/width come from the `size` prop instead (see render-site override) - no
900
+ // fixed default here, since this style object is shared by every caller regardless of size.
901
+ trigger: {
902
+ alignItems: "center",
903
+ alignSelf: "flex-start",
904
+ justifyContent: "center"
905
+ },
906
+ // Wraps just the icon/tag, inside the TouchableRipple, purely so useAutoAlign has a plain View
907
+ // to attach its measurement ref to - TouchableRipple itself doesn't forward a ref to a
908
+ // measurable native node. Padded horizontally so a multi-character tag doesn't butt right up
909
+ // against the trigger's own circular edge (an icon alone doesn't need it, but this style is
910
+ // shared by both).
911
+ triggerMeasure: {
912
+ alignItems: "center",
913
+ height: "100%",
914
+ justifyContent: "center",
915
+ paddingHorizontal: 4,
916
+ width: "100%"
917
+ }
918
+ });
919
+
920
+ // src/usePopoverHost.ts
921
+ import { useCallback as useCallback4, useState as useState6 } from "react";
922
+ function usePopoverHost() {
923
+ const [openId, setOpenId] = useState6(null);
924
+ const toggle = useCallback4((id) => {
925
+ setOpenId((prev) => prev === id ? null : id);
926
+ }, []);
927
+ const close = useCallback4(() => setOpenId(null), []);
928
+ return { openId, toggle, close };
929
+ }
930
+
931
+ // src/PressAwayOverlay.tsx
932
+ import { Pressable, StyleSheet as StyleSheet13 } from "react-native";
933
+ import { jsx as jsx14 } from "react/jsx-runtime";
934
+ function PressAwayOverlay({ active, onPress, style }) {
935
+ if (!active) return null;
936
+ return /* @__PURE__ */ jsx14(Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [StyleSheet13.absoluteFill, style] });
937
+ }
938
+
659
939
  // src/StaggeredWord.tsx
660
- import { useEffect as useEffect2 } from "react";
661
- import { StyleSheet as StyleSheet11, View as View9 } from "react-native";
940
+ import { useEffect as useEffect3 } from "react";
941
+ import { StyleSheet as StyleSheet14, View as View12 } from "react-native";
662
942
  import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming } from "react-native-reanimated";
663
- import { jsx as jsx12 } from "react/jsx-runtime";
943
+ import { jsx as jsx15 } from "react/jsx-runtime";
664
944
  var STAGGER_MS = 45;
665
945
  var LETTER_DURATION_MS = 320;
666
946
  function getStaggeredWordDuration(letterCount) {
@@ -668,7 +948,7 @@ function getStaggeredWordDuration(letterCount) {
668
948
  }
669
949
  function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
670
950
  const progress = useSharedValue(reducedMotion ? 1 : 0);
671
- useEffect2(() => {
951
+ useEffect3(() => {
672
952
  if (reducedMotion) return;
673
953
  progress.value = withDelay(index * STAGGER_MS, withTiming(1, { duration: LETTER_DURATION_MS, easing: Easing.out(Easing.back(1.5)) }));
674
954
  }, [reducedMotion]);
@@ -676,12 +956,12 @@ function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSiz
676
956
  opacity: progress.value,
677
957
  transform: [{ translateY: (1 - progress.value) * 12 }, { scale: 0.85 + progress.value * 0.15 }]
678
958
  }));
679
- return /* @__PURE__ */ jsx12(Animated.Text, { allowFontScaling: false, style: [{ color, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, style], children: char });
959
+ return /* @__PURE__ */ jsx15(Animated.Text, { allowFontScaling: false, style: [{ color, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, style], children: char });
680
960
  }
681
961
  function StaggeredWord({ word, color, fontFamily, fontSize, lineHeight, reducedMotion, startIndex = 0, fontWeight = "bold", letterSpacing, style, onLayout }) {
682
- return /* @__PURE__ */ jsx12(View9, { style: [styles11.row, style], onLayout, children: word.split("").map((char, i) => /* @__PURE__ */ jsx12(AnimatedLetter, { char, index: startIndex + i, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, i)) });
962
+ return /* @__PURE__ */ jsx15(View12, { style: [styles13.row, style], onLayout, children: word.split("").map((char, i) => /* @__PURE__ */ jsx15(AnimatedLetter, { char, index: startIndex + i, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, i)) });
683
963
  }
684
- var styles11 = StyleSheet11.create({
964
+ var styles13 = StyleSheet14.create({
685
965
  row: {
686
966
  flexDirection: "row"
687
967
  }
@@ -689,15 +969,17 @@ var styles11 = StyleSheet11.create({
689
969
 
690
970
  // src/UpdateDialog.tsx
691
971
  import { useUpdater as useUpdater2 } from "@rific/updater";
692
- import { jsx as jsx13 } from "react/jsx-runtime";
972
+ import { jsx as jsx16 } from "react/jsx-runtime";
693
973
  function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }) {
694
974
  const { manifest, onConfirm, respond } = useUpdateConfirmBridge();
695
975
  useUpdater2({ autoCheck, autoPrompt, onConfirm, onError });
696
976
  const date = manifest ? new Date(manifest.createdAt) : null;
697
- return /* @__PURE__ */ jsx13(ConfirmDialog, { visible: !!manifest, title: "Update Available", message: date ? `Released ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation, onConfirm: () => respond(true), onCancel: () => respond(false) });
977
+ return /* @__PURE__ */ jsx16(ConfirmDialog, { visible: !!manifest, title: "Update Available", message: date ? `Released ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation, onConfirm: () => respond(true), onCancel: () => respond(false) });
698
978
  }
699
979
 
700
980
  export {
981
+ WELL_SHADOW,
982
+ useWellColors,
701
983
  LOCKED_BADGE_COLOR,
702
984
  AchievementRow,
703
985
  MONO_FONT,
@@ -711,7 +993,13 @@ export {
711
993
  BaseSettingsDialog,
712
994
  BaseStatsScreen,
713
995
  ContentGutter,
996
+ PopoverBody,
997
+ useAutoAlign,
714
998
  CornerActionButtons,
999
+ getInlineColorPickerContentSize,
1000
+ InlineColorPicker,
1001
+ usePopoverHost,
1002
+ PressAwayOverlay,
715
1003
  getStaggeredWordDuration,
716
1004
  StaggeredWord,
717
1005
  UpdateDialog