@tastic/hud 0.22.0 → 0.24.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.
@@ -281,6 +281,7 @@ var styles6 = StyleSheet6.create({
281
281
 
282
282
  // src/BaseSettingsDialog.tsx
283
283
  import { AutoAppearancePicker, Dialog, FadingScrollView, useAutoPaperTheme as useAutoPaperTheme4, useBlurModule, useThemeSettings } from "@rific/auto-paper";
284
+ import { useExpoKeepAwake, useKeepAwakeSettings } from "@rific/core";
284
285
  import { Button as Button2, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from "@rific/feedback-press";
285
286
  import { useUpdater } from "@rific/updater";
286
287
  import { rotateInsets as rotateInsets2, useIsTouchPrimaryDevice, useRotation as useRotation2 } from "@tastic/core";
@@ -314,7 +315,7 @@ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
314
315
  function SettingIcon({ source, color, containerColor }) {
315
316
  return /* @__PURE__ */ jsx8(View6, { style: [styles7.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx8(Icon2, { source, size: 18, color }) });
316
317
  }
317
- function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
318
+ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
318
319
  const { colors } = useAutoPaperTheme4();
319
320
  const ambientRotation = useRotation2();
320
321
  const rotation = rotationOverride ?? ambientRotation;
@@ -326,6 +327,8 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
326
327
  const isTouchPrimary = useIsTouchPrimaryDevice();
327
328
  const { settings: hapticSettings, set: setHapticSettings } = useHapticSettings();
328
329
  const { settings: soundSettings, set: setSoundSettings } = useSoundSettings();
330
+ const { settings: keepAwakeSettings, set: setKeepAwakeSettings } = useKeepAwakeSettings();
331
+ const expoKeepAwake = useExpoKeepAwake();
329
332
  const sound = useContext(SoundContext);
330
333
  const { forceShort: forceHapticFeedback } = useVibration();
331
334
  const { settings: themeSettings, set: setThemeSettings } = useThemeSettings();
@@ -344,7 +347,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
344
347
  if (visible && !opened) setOpened(true);
345
348
  const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
346
349
  const showEdgeGuard = Platform2.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
347
- const showKeepAwake = isTouchPrimary && keepAwake !== void 0 && onKeepAwakeChange !== void 0;
350
+ const showKeepAwake = isTouchPrimary && expoKeepAwake !== void 0;
348
351
  const showHaptics = Platform2.OS !== "web" && !hideHaptics;
349
352
  const showSound = !hideSound;
350
353
  const showAppearance = !hideAppearance;
@@ -371,8 +374,8 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
371
374
  /* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Require second swipe" })
372
375
  ] })
373
376
  ] }) }),
374
- showKeepAwake && /* @__PURE__ */ jsx8(TouchableRipple, { onPress: () => onKeepAwakeChange(!keepAwake), style: styles7.toggleButton, accessibilityLabel: `Keep awake ${keepAwake ? "on" : "off"}`, children: /* @__PURE__ */ jsxs7(View6, { style: styles7.toggleContent, children: [
375
- /* @__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 }),
376
379
  /* @__PURE__ */ jsxs7(View6, { style: styles7.flexShrink, children: [
377
380
  /* @__PURE__ */ jsx8(Text6, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Keep Awake" }),
378
381
  /* @__PURE__ */ jsx8(Text6, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Stop auto-lock mid-match" })
@@ -644,17 +647,131 @@ var styles9 = StyleSheet9.create({
644
647
  }
645
648
  });
646
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
+
647
764
  // src/CornerActionButtons.tsx
648
765
  import { IconButton as IconButton2 } from "@rific/feedback-press";
649
- import { StyleSheet as StyleSheet10 } from "react-native";
650
- 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";
651
768
  function CornerActionButtons({ onBack, onSettings, fg, insets, backIcon = "arrow-left", backLabel = "Back", hitSlop }) {
652
- return /* @__PURE__ */ jsxs10(Fragment2, { children: [
653
- /* @__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 }),
654
- /* @__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" })
655
772
  ] });
656
773
  }
657
- var styles10 = StyleSheet10.create({
774
+ var styles11 = StyleSheet11.create({
658
775
  back: {
659
776
  position: "absolute"
660
777
  },
@@ -663,11 +780,167 @@ var styles10 = StyleSheet10.create({
663
780
  }
664
781
  });
665
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
+
666
939
  // src/StaggeredWord.tsx
667
- import { useEffect as useEffect2 } from "react";
668
- 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";
669
942
  import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming } from "react-native-reanimated";
670
- import { jsx as jsx12 } from "react/jsx-runtime";
943
+ import { jsx as jsx15 } from "react/jsx-runtime";
671
944
  var STAGGER_MS = 45;
672
945
  var LETTER_DURATION_MS = 320;
673
946
  function getStaggeredWordDuration(letterCount) {
@@ -675,7 +948,7 @@ function getStaggeredWordDuration(letterCount) {
675
948
  }
676
949
  function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
677
950
  const progress = useSharedValue(reducedMotion ? 1 : 0);
678
- useEffect2(() => {
951
+ useEffect3(() => {
679
952
  if (reducedMotion) return;
680
953
  progress.value = withDelay(index * STAGGER_MS, withTiming(1, { duration: LETTER_DURATION_MS, easing: Easing.out(Easing.back(1.5)) }));
681
954
  }, [reducedMotion]);
@@ -683,12 +956,12 @@ function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSiz
683
956
  opacity: progress.value,
684
957
  transform: [{ translateY: (1 - progress.value) * 12 }, { scale: 0.85 + progress.value * 0.15 }]
685
958
  }));
686
- 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 });
687
960
  }
688
961
  function StaggeredWord({ word, color, fontFamily, fontSize, lineHeight, reducedMotion, startIndex = 0, fontWeight = "bold", letterSpacing, style, onLayout }) {
689
- 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)) });
690
963
  }
691
- var styles11 = StyleSheet11.create({
964
+ var styles13 = StyleSheet14.create({
692
965
  row: {
693
966
  flexDirection: "row"
694
967
  }
@@ -696,12 +969,12 @@ var styles11 = StyleSheet11.create({
696
969
 
697
970
  // src/UpdateDialog.tsx
698
971
  import { useUpdater as useUpdater2 } from "@rific/updater";
699
- import { jsx as jsx13 } from "react/jsx-runtime";
972
+ import { jsx as jsx16 } from "react/jsx-runtime";
700
973
  function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }) {
701
974
  const { manifest, onConfirm, respond } = useUpdateConfirmBridge();
702
975
  useUpdater2({ autoCheck, autoPrompt, onConfirm, onError });
703
976
  const date = manifest ? new Date(manifest.createdAt) : null;
704
- 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) });
705
978
  }
706
979
 
707
980
  export {
@@ -720,7 +993,13 @@ export {
720
993
  BaseSettingsDialog,
721
994
  BaseStatsScreen,
722
995
  ContentGutter,
996
+ PopoverBody,
997
+ useAutoAlign,
723
998
  CornerActionButtons,
999
+ getInlineColorPickerContentSize,
1000
+ InlineColorPicker,
1001
+ usePopoverHost,
1002
+ PressAwayOverlay,
724
1003
  getStaggeredWordDuration,
725
1004
  StaggeredWord,
726
1005
  UpdateDialog
package/dist/index.d.mts CHANGED
@@ -1,12 +1,13 @@
1
- export { AchievementCatalogSection, AchievementRow, ActivityStatSection, BaseSettingsDialog, BaseSettingsDialogProps, BaseStatsScreen, COMPACT_DIALOG_HEIGHT, ConfirmDialog, ConfirmDialogProps, ContentGutter, CornerActionButtons, DialogHeader, DialogHeaderProps, LOCKED_BADGE_COLOR, MONO_FONT, OverlayBackdrop, OverlayBackdropProps, OverlayCard, OverlayCardProps, OverlayCardThemeColors, RotatedFrame, RotatedFrameProps, StaggeredWord, StaggeredWordProps, StatRow, StatSection, UpdateDialog, UpdateDialogProps, WELL_SHADOW, WellColors, getStaggeredWordDuration, isCompactDialogHeight, overlayActionStyles, overlayStyles, useCompactDialog, useOverlayCardColors, useWellColors } from './chrome/index.mjs';
1
+ import { PopoverHost, AlignResult, PopoverRotation, useAutoAlign } from './chrome/index.mjs';
2
+ export { AchievementCatalogSection, AchievementRow, ActivityStatSection, BaseSettingsDialog, BaseSettingsDialogProps, BaseStatsScreen, COMPACT_DIALOG_HEIGHT, ConfirmDialog, ConfirmDialogProps, ContentGutter, CornerActionButtons, DialogHeader, DialogHeaderProps, InlineColorPicker, LOCKED_BADGE_COLOR, MONO_FONT, OverlayBackdrop, OverlayBackdropProps, OverlayCard, OverlayCardProps, OverlayCardThemeColors, PopoverAlign, PopoverBody, PopoverVerticalAlign, PressAwayOverlay, RotatedFrame, RotatedFrameProps, StaggeredWord, StaggeredWordProps, StatRow, StatSection, UpdateDialog, UpdateDialogProps, WELL_SHADOW, WellColors, getInlineColorPickerContentSize, getStaggeredWordDuration, isCompactDialogHeight, overlayActionStyles, overlayStyles, useCompactDialog, useOverlayCardColors, usePopoverHost, useWellColors } from './chrome/index.mjs';
2
3
  import * as react from 'react';
3
- import { RefObject, ReactNode } from 'react';
4
+ import { ReactNode } from 'react';
4
5
  import { AchievementTier } from '@tastic/achievements';
5
6
  import { Profile } from '@tastic/profile';
6
- import { ViewStyle, View, StyleProp } from 'react-native';
7
- import { ViewRotation } from '@tastic/core';
7
+ import { ViewStyle, StyleProp } from 'react-native';
8
+ export { loadSkiaWeb, locateSkiaWebFile } from './skia/index.mjs';
8
9
  import { SeedColor } from '@rific/auto-paper';
9
- export { SkiaGate, SkiaGateProps } from './SkiaGate.mjs';
10
+ import '@tastic/core';
10
11
 
11
12
  interface UnlockedAchievement {
12
13
  id: string;
@@ -18,7 +19,7 @@ interface AchievementUnlockOwner {
18
19
  profile?: Profile | null;
19
20
  color: string;
20
21
  }
21
- interface Props$7<Seat extends number> {
22
+ interface Props$4<Seat extends number> {
22
23
  unlocks: Partial<Record<Seat, UnlockedAchievement[]>>;
23
24
  seats: readonly Seat[];
24
25
  owners: Record<Seat, AchievementUnlockOwner>;
@@ -26,32 +27,7 @@ interface Props$7<Seat extends number> {
26
27
  fg?: string;
27
28
  style?: ViewStyle;
28
29
  }
29
- declare function AchievementUnlockList<Seat extends number>({ unlocks, seats, owners, tierColors, fg: fgOverride, style }: Props$7<Seat>): react.JSX.Element | null;
30
-
31
- type PopoverAlign = 'left' | 'right' | 'center';
32
- type PopoverVerticalAlign = 'below' | 'above';
33
- type PopoverRotation = ViewRotation;
34
- interface AlignResult {
35
- align: PopoverAlign;
36
- verticalAlign: PopoverVerticalAlign;
37
- maxHeight: number;
38
- measured: boolean;
39
- triggerRef: RefObject<View | null>;
40
- }
41
- declare function useAutoAlign(open: boolean, contentWidth: number, contentHeight: number, rotation?: PopoverRotation): {
42
- align: PopoverAlign;
43
- maxHeight: number;
44
- measured: boolean;
45
- triggerRef: RefObject<View | null>;
46
- verticalAlign: PopoverVerticalAlign;
47
- };
48
-
49
- interface PopoverHost {
50
- openId: string | null;
51
- toggle: (id: string) => void;
52
- close: () => void;
53
- }
54
- declare function usePopoverHost(): PopoverHost;
30
+ declare function AchievementUnlockList<Seat extends number>({ unlocks, seats, owners, tierColors, fg: fgOverride, style }: Props$4<Seat>): react.JSX.Element | null;
55
31
 
56
32
  interface MenuOption<T extends string | number> {
57
33
  value: T;
@@ -77,7 +53,7 @@ interface MultiSelectSection<T extends string | number> {
77
53
  allClear?: boolean;
78
54
  }
79
55
  type MenuSection = SingleSelectSection<any> | MultiSelectSection<any>;
80
- interface Props$6 {
56
+ interface Props$3 {
81
57
  id: string;
82
58
  host: PopoverHost;
83
59
  icon: string;
@@ -97,7 +73,7 @@ interface Props$6 {
97
73
  clear: string;
98
74
  };
99
75
  }
100
- declare function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accentColor, mutedColor, onAccentColor, dark, align: forcedAlign, alignOverride, rotation: rotationOverride, autoDismiss, labelFontFamily, allClearLabels }: Props$6): react.JSX.Element;
76
+ declare function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accentColor, mutedColor, onAccentColor, dark, align: forcedAlign, alignOverride, rotation: rotationOverride, autoDismiss, labelFontFamily, allClearLabels }: Props$3): react.JSX.Element;
101
77
 
102
78
  interface ControlSchemePickerProps<T extends string | number> {
103
79
  id: string;
@@ -129,32 +105,6 @@ interface CornerStatusBadgesProps {
129
105
  }
130
106
  declare function CornerStatusBadges({ orientationMode, p1OnRight, p1, p2 }: CornerStatusBadgesProps): react.JSX.Element;
131
107
 
132
- declare function getInlineColorPickerContentSize(swatchCount: number, windowWidth: number, columns?: number): {
133
- width: number;
134
- height: number;
135
- };
136
- interface Props$5 {
137
- id: string;
138
- host: PopoverHost;
139
- value: string;
140
- onChange: (hex: string) => void;
141
- previewValue?: (hex: string) => string;
142
- swatches?: SeedColor[];
143
- takenValue?: string;
144
- allowSwapTaken?: boolean;
145
- dark: boolean;
146
- align?: 'left' | 'right' | 'center';
147
- alignOverride?: AlignResult;
148
- rotation?: PopoverRotation;
149
- icon?: string;
150
- tag?: string;
151
- labelFontFamily?: string;
152
- autoDismiss?: boolean;
153
- columns?: number;
154
- size?: number;
155
- }
156
- declare function InlineColorPicker({ id, host, value, onChange, previewValue, swatches, takenValue, allowSwapTaken, dark, align: forcedAlign, alignOverride, rotation: rotationOverride, icon, tag, labelFontFamily, autoDismiss, columns, size }: Props$5): react.JSX.Element;
157
-
158
108
  declare const LABELED_DROPDOWN_POPOVER_WIDTH = 180;
159
109
  declare function getLabeledDropdownContentHeight(optionCount: number): number;
160
110
  interface LabeledDropdownOption<T extends string> {
@@ -162,7 +112,7 @@ interface LabeledDropdownOption<T extends string> {
162
112
  label: string;
163
113
  icon?: string;
164
114
  }
165
- interface Props$4<T extends string> {
115
+ interface Props$2<T extends string> {
166
116
  id: string;
167
117
  host: PopoverHost;
168
118
  options: LabeledDropdownOption<T>[];
@@ -173,9 +123,7 @@ interface Props$4<T extends string> {
173
123
  align?: 'left' | 'right' | 'center';
174
124
  alignOverride?: AlignResult;
175
125
  }
176
- declare function LabeledDropdown<T extends string>({ id, host, options, value, onChange, color, dark, align: forcedAlign, alignOverride }: Props$4<T>): react.JSX.Element;
177
-
178
- declare function loadSkiaWeb(): Promise<void>;
126
+ declare function LabeledDropdown<T extends string>({ id, host, options, value, onChange, color, dark, align: forcedAlign, alignOverride }: Props$2<T>): react.JSX.Element;
179
127
 
180
128
  declare function getColorPopoverId(idPrefix: string): string;
181
129
  declare function getProfilePopoverId(idPrefix: string): string;
@@ -215,25 +163,6 @@ interface PlayerSetupPanelProps<C extends string = string> {
215
163
  }
216
164
  declare function PlayerSetupPanel<C extends string = string>({ idPrefix, host, label, labelFontFamily, color, onColorChange, swatches, takenColor, allowSwapTaken, colorCaption, showColor, colorAlignOverride, colorRotation, isHuman, secondPicker, secondPickerId, ready, onToggleReady, dark, showReadyButton, profiles, selectedProfileId, takenProfileId, guestLabel, onProfileSelect, onManageProfiles, cpuDifficulty, cpuDifficultyOptions, onCpuDifficultyChange, humanIcon, cpuIcon }: PlayerSetupPanelProps<C>): react.JSX.Element;
217
165
 
218
- interface Props$3 {
219
- visible: boolean;
220
- children: ReactNode;
221
- align?: 'left' | 'right' | 'center';
222
- verticalAlign?: 'below' | 'above';
223
- caretColor?: string;
224
- caretBorderColor?: string;
225
- caretBorderWidth?: number;
226
- triggerSize?: number;
227
- }
228
- declare function PopoverBody({ visible, children, align, verticalAlign, caretColor, caretBorderColor, caretBorderWidth, triggerSize }: Props$3): react.JSX.Element | null;
229
-
230
- interface Props$2 {
231
- active: boolean;
232
- onPress: () => void;
233
- style?: StyleProp<ViewStyle>;
234
- }
235
- declare function PressAwayOverlay({ active, onPress, style }: Props$2): react.JSX.Element | null;
236
-
237
166
  interface Props$1 {
238
167
  color: string;
239
168
  ready: boolean;
@@ -279,4 +208,4 @@ declare function useQuitConfirmation(hasProgress: () => boolean, onConfirmedBack
279
208
 
280
209
  declare function useZoneClampedAlign(open: boolean, contentWidth: number, contentHeight: number, rotation?: PopoverRotation): ReturnType<typeof useAutoAlign>;
281
210
 
282
- export { AchievementUnlockList, type AchievementUnlockOwner, type AlignResult, ControlSchemePicker, type ControlSchemePickerProps, type CornerStatusBadgeSeat, CornerStatusBadges, type CornerStatusBadgesProps, InlineColorPicker, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption, type MenuOption, type MenuSection, type MultiSelectSection, type OrientationMode, PlayerSetupPanel, type PlayerSetupPanelProps, type PopoverAlign, PopoverBody, type PopoverHost, type PopoverRotation, type PopoverVerticalAlign, PressAwayOverlay, type QuitConfirmation, ReadyButton, SectionedDropdown, SharedActionBand, type SingleSelectSection, TriggerGaugeHost, type TriggerGaugeProps, type UnlockedAchievement, getColorPopoverId, getCpuDifficultyPopoverId, getInlineColorPickerContentSize, getLabeledDropdownContentHeight, getProfilePopoverId, loadSkiaWeb, useAutoAlign, usePopoverHost, useQuitConfirmation, useZoneClampedAlign };
211
+ export { AchievementUnlockList, type AchievementUnlockOwner, AlignResult, ControlSchemePicker, type ControlSchemePickerProps, type CornerStatusBadgeSeat, CornerStatusBadges, type CornerStatusBadgesProps, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption, type MenuOption, type MenuSection, type MultiSelectSection, type OrientationMode, PlayerSetupPanel, type PlayerSetupPanelProps, PopoverHost, PopoverRotation, type QuitConfirmation, ReadyButton, SectionedDropdown, SharedActionBand, type SingleSelectSection, TriggerGaugeHost, type TriggerGaugeProps, type UnlockedAchievement, getColorPopoverId, getCpuDifficultyPopoverId, getLabeledDropdownContentHeight, getProfilePopoverId, useAutoAlign, useQuitConfirmation, useZoneClampedAlign };