@tastic/hud 0.22.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.
@@ -40,22 +40,28 @@ __export(chrome_exports, {
40
40
  ContentGutter: () => ContentGutter,
41
41
  CornerActionButtons: () => CornerActionButtons,
42
42
  DialogHeader: () => DialogHeader,
43
+ InlineColorPicker: () => InlineColorPicker,
43
44
  LOCKED_BADGE_COLOR: () => LOCKED_BADGE_COLOR,
44
45
  MONO_FONT: () => MONO_FONT,
45
46
  OverlayBackdrop: () => OverlayBackdrop,
46
47
  OverlayCard: () => OverlayCard,
48
+ PopoverBody: () => PopoverBody,
49
+ PressAwayOverlay: () => PressAwayOverlay,
47
50
  RotatedFrame: () => RotatedFrame,
48
51
  StaggeredWord: () => StaggeredWord,
49
52
  StatRow: () => StatRow,
50
53
  StatSection: () => StatSection,
51
54
  UpdateDialog: () => UpdateDialog,
52
55
  WELL_SHADOW: () => WELL_SHADOW,
56
+ getInlineColorPickerContentSize: () => getInlineColorPickerContentSize,
53
57
  getStaggeredWordDuration: () => getStaggeredWordDuration,
54
58
  isCompactDialogHeight: () => isCompactDialogHeight,
55
59
  overlayActionStyles: () => overlayActionStyles,
56
60
  overlayStyles: () => overlayStyles,
61
+ useAutoAlign: () => useAutoAlign,
57
62
  useCompactDialog: () => useCompactDialog,
58
63
  useOverlayCardColors: () => useOverlayCardColors,
64
+ usePopoverHost: () => usePopoverHost,
59
65
  useWellColors: () => useWellColors
60
66
  });
61
67
  module.exports = __toCommonJS(chrome_exports);
@@ -232,9 +238,10 @@ function ActivityStatSection({ stats, fg, fgMuted, sectionBg }) {
232
238
 
233
239
  // src/BaseSettingsDialog.tsx
234
240
  var import_auto_paper6 = require("@rific/auto-paper");
241
+ var import_core4 = require("@rific/core");
235
242
  var import_feedback_press2 = require("@rific/feedback-press");
236
243
  var import_updater = require("@rific/updater");
237
- var import_core4 = require("@tastic/core");
244
+ var import_core5 = require("@tastic/core");
238
245
  var import_react6 = require("react");
239
246
  var import_react_native12 = require("react-native");
240
247
  var import_react_native_paper8 = require("react-native-paper");
@@ -499,18 +506,20 @@ var import_jsx_runtime10 = require("react/jsx-runtime");
499
506
  function SettingIcon({ source, color, containerColor }) {
500
507
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native12.View, { style: [styles9.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Icon, { source, size: 18, color }) });
501
508
  }
502
- function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
509
+ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }) {
503
510
  const { colors } = (0, import_auto_paper6.useAutoPaperTheme)();
504
- const ambientRotation = (0, import_core4.useRotation)();
511
+ const ambientRotation = (0, import_core5.useRotation)();
505
512
  const rotation = rotationOverride ?? ambientRotation;
506
513
  const window = (0, import_react_native12.useWindowDimensions)();
507
514
  const turned = rotation === 90 || rotation === -90;
508
515
  const compactHeader = isCompactDialogHeight(turned ? window.width : window.height);
509
- const insets = (0, import_core4.rotateInsets)((0, import_react_native_safe_area_context2.useSafeAreaInsets)(), rotation);
516
+ const insets = (0, import_core5.rotateInsets)((0, import_react_native_safe_area_context2.useSafeAreaInsets)(), rotation);
510
517
  const sheetWidth = Math.min(overlayStyles.card.maxWidth, (turned ? window.height : window.width) - 2 * Math.max(insets.left, insets.right, SHEET_GUTTER));
511
- const isTouchPrimary = (0, import_core4.useIsTouchPrimaryDevice)();
518
+ const isTouchPrimary = (0, import_core5.useIsTouchPrimaryDevice)();
512
519
  const { settings: hapticSettings, set: setHapticSettings } = (0, import_feedback_press2.useHapticSettings)();
513
520
  const { settings: soundSettings, set: setSoundSettings } = (0, import_feedback_press2.useSoundSettings)();
521
+ const { settings: keepAwakeSettings, set: setKeepAwakeSettings } = (0, import_core4.useKeepAwakeSettings)();
522
+ const expoKeepAwake = (0, import_core4.useExpoKeepAwake)();
514
523
  const sound = (0, import_react6.useContext)(import_feedback_press2.SoundContext);
515
524
  const { forceShort: forceHapticFeedback } = (0, import_feedback_press2.useVibration)();
516
525
  const { settings: themeSettings, set: setThemeSettings } = (0, import_auto_paper6.useThemeSettings)();
@@ -529,7 +538,7 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
529
538
  if (visible && !opened) setOpened(true);
530
539
  const showLockOrientation = isTouchPrimary && lockOrientation !== void 0 && onLockOrientationChange !== void 0;
531
540
  const showEdgeGuard = import_react_native12.Platform.OS === "ios" && deferBottomEdgeGestures !== void 0 && onDeferBottomEdgeGestures !== void 0;
532
- const showKeepAwake = isTouchPrimary && keepAwake !== void 0 && onKeepAwakeChange !== void 0;
541
+ const showKeepAwake = isTouchPrimary && expoKeepAwake !== void 0;
533
542
  const showHaptics = import_react_native12.Platform.OS !== "web" && !hideHaptics;
534
543
  const showSound = !hideSound;
535
544
  const showAppearance = !hideAppearance;
@@ -556,8 +565,8 @@ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, ve
556
565
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Require second swipe" })
557
566
  ] })
558
567
  ] }) }),
559
- showKeepAwake && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => onKeepAwakeChange(!keepAwake), style: styles9.toggleButton, accessibilityLabel: `Keep awake ${keepAwake ? "on" : "off"}`, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native12.View, { style: styles9.toggleContent, children: [
560
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SettingIcon, { source: keepAwake ? "eye" : "eye-closed", color: keepAwake ? colors.tertiary : colors.onSurfaceVariant, containerColor: keepAwake ? colors.tertiaryContainer : colors.surfaceVariant }),
568
+ showKeepAwake && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => setKeepAwakeSettings({ enabled: !keepAwakeSettings.enabled }), style: styles9.toggleButton, accessibilityLabel: `Keep awake ${keepAwakeSettings.enabled ? "on" : "off"}`, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native12.View, { style: styles9.toggleContent, children: [
569
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SettingIcon, { source: keepAwakeSettings.enabled ? "eye" : "eye-closed", color: keepAwakeSettings.enabled ? colors.tertiary : colors.onSurfaceVariant, containerColor: keepAwakeSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant }),
561
570
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native12.View, { style: styles9.flexShrink, children: [
562
571
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Text, { variant: "bodyLarge", style: { color: colors.onSurface }, children: "Keep Awake" }),
563
572
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native_paper8.Text, { variant: "bodySmall", numberOfLines: 1, style: { color: colors.onSurfaceVariant }, children: "Stop auto-lock mid-match" })
@@ -733,14 +742,14 @@ var styles9 = import_react_native12.StyleSheet.create({
733
742
  // src/BaseStatsScreen.tsx
734
743
  var import_auto_paper7 = require("@rific/auto-paper");
735
744
  var import_feedback_press3 = require("@rific/feedback-press");
736
- var import_core5 = require("@tastic/core");
745
+ var import_core6 = require("@tastic/core");
737
746
  var import_react7 = require("react");
738
747
  var import_react_native13 = require("react-native");
739
748
  var import_react_native_paper9 = require("react-native-paper");
740
749
  var import_jsx_runtime11 = require("react/jsx-runtime");
741
750
  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 }) {
742
751
  const { dark, colors } = (0, import_auto_paper7.useAutoPaperTheme)();
743
- const ambientRotation = (0, import_core5.useRotation)();
752
+ const ambientRotation = (0, import_core6.useRotation)();
744
753
  const rotation = rotationOverride ?? ambientRotation;
745
754
  const [confirmResetVisible, setConfirmResetVisible] = (0, import_react7.useState)(false);
746
755
  const showReset = !!onReset;
@@ -806,12 +815,12 @@ var styles10 = import_react_native13.StyleSheet.create({
806
815
  });
807
816
 
808
817
  // src/ContentGutter.tsx
809
- var import_core6 = require("@tastic/core");
818
+ var import_core7 = require("@tastic/core");
810
819
  var import_react_native14 = require("react-native");
811
820
  var import_jsx_runtime12 = require("react/jsx-runtime");
812
821
  function ContentGutter({ maxContentWidth, leftGutter, rightGutter, style, children }) {
813
- const { width: windowWidth } = (0, import_core6.useRotatedWindowDimensions)();
814
- const { contentWidth, gutterWidth } = (0, import_core6.computeContentBounds)(windowWidth, maxContentWidth ?? Infinity);
822
+ const { width: windowWidth } = (0, import_core7.useRotatedWindowDimensions)();
823
+ const { contentWidth, gutterWidth } = (0, import_core7.computeContentBounds)(windowWidth, maxContentWidth ?? Infinity);
815
824
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native14.View, { style: [styles11.row, style], children: [
816
825
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native14.View, { style: [styles11.gutter, { width: gutterWidth }], children: leftGutter }),
817
826
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native14.View, { style: { width: contentWidth }, children }),
@@ -848,11 +857,272 @@ var styles12 = import_react_native15.StyleSheet.create({
848
857
  }
849
858
  });
850
859
 
851
- // src/StaggeredWord.tsx
852
- var import_react8 = require("react");
860
+ // src/InlineColorPicker.tsx
861
+ var import_auto_paper8 = require("@rific/auto-paper");
862
+ var import_feedback_press5 = require("@rific/feedback-press");
863
+ var import_core8 = require("@tastic/core");
864
+ var import_react_native18 = require("react-native");
865
+ var import_react_native_paper10 = require("react-native-paper");
866
+
867
+ // src/PopoverBody.tsx
853
868
  var import_react_native16 = require("react-native");
854
- var import_react_native_reanimated = __toESM(require("react-native-reanimated"));
855
869
  var import_jsx_runtime14 = require("react/jsx-runtime");
870
+ var CARET_SIZE = 7;
871
+ var CARET_RING_OFFSET = 2;
872
+ var CARET_DIP = 1;
873
+ function PopoverBody({ visible, children, align = "center", verticalAlign = "below", caretColor, caretBorderColor, caretBorderWidth = 1, triggerSize = 0 }) {
874
+ if (!visible) return null;
875
+ const above = verticalAlign === "above";
876
+ const alignStyle = align === "left" ? styles13.contentLeft : align === "right" ? styles13.contentRight : styles13.contentCenter;
877
+ const verticalStyle = above ? styles13.contentAbove : styles13.contentBelow;
878
+ const ringSize = CARET_SIZE + caretBorderWidth;
879
+ const caretOffset = (halfFootprint) => align === "right" ? { right: triggerSize / 2 - halfFootprint } : { left: triggerSize / 2 - halfFootprint };
880
+ const caretFill = above ? "borderTopColor" : "borderBottomColor";
881
+ const caretWidthProp = above ? "borderTopWidth" : "borderBottomWidth";
882
+ const caretEdge = (size) => above ? { bottom: -size + CARET_DIP } : { top: -size + CARET_DIP };
883
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native16.View, { style: [styles13.content, alignStyle, verticalStyle], children: [
884
+ children,
885
+ caretColor && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
886
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native16.View, { style: [styles13.caret, caretOffset(ringSize), caretEdge(ringSize), { [caretFill]: caretBorderColor ?? caretColor, [caretWidthProp]: ringSize, borderLeftWidth: ringSize, borderRightWidth: ringSize }] }),
887
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native16.View, { style: [styles13.caret, caretOffset(CARET_SIZE), caretEdge(CARET_SIZE - CARET_RING_OFFSET), { [caretFill]: caretColor, [caretWidthProp]: CARET_SIZE, borderLeftWidth: CARET_SIZE, borderRightWidth: CARET_SIZE }] })
888
+ ] })
889
+ ] });
890
+ }
891
+ var styles13 = import_react_native16.StyleSheet.create({
892
+ caret: {
893
+ borderLeftColor: "transparent",
894
+ borderRightColor: "transparent",
895
+ height: 0,
896
+ position: "absolute",
897
+ width: 0,
898
+ // Explicit, not left to DOM order - the box it dips into is itself a position:'relative' view,
899
+ // which React Native Web always gives an implicit zIndex:0 (see PopoverBody's stacking-context
900
+ // notes elsewhere in this codebase), so painting on top needs a real zIndex to beat that
901
+ // reliably rather than relying on being the later sibling.
902
+ zIndex: 1
903
+ },
904
+ content: {
905
+ position: "absolute",
906
+ zIndex: 50
907
+ },
908
+ contentAbove: {
909
+ bottom: "100%",
910
+ marginBottom: 8
911
+ },
912
+ contentBelow: {
913
+ marginTop: 8,
914
+ top: "100%"
915
+ },
916
+ // Stretched to the anchor's own (trigger) width, with alignItems centering the actual (usually
917
+ // much wider) popover box inside via plain flexbox - not `left:'50%'` plus a percentage
918
+ // `transform`, which depends on native resolving the box's own auto width before applying the
919
+ // transform and has proven unreliable there, visibly shifting the box off-center.
920
+ contentCenter: {
921
+ alignItems: "center",
922
+ left: 0,
923
+ right: 0
924
+ },
925
+ contentLeft: {
926
+ left: 0
927
+ },
928
+ contentRight: {
929
+ right: 0
930
+ }
931
+ });
932
+
933
+ // src/useAutoAlign.ts
934
+ var import_react8 = require("react");
935
+ var import_react_native17 = require("react-native");
936
+ var EDGE_MARGIN = 12;
937
+ function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
938
+ if (rotation === 0) return { x, y, width, height };
939
+ const cx = windowWidth / 2;
940
+ const cy = windowHeight / 2;
941
+ const dx = x + width / 2 - cx;
942
+ const dy = y + height / 2 - cy;
943
+ const [newDx, newDy, newWidth, newHeight] = rotation === 180 ? [-dx, -dy, width, height] : rotation === 90 ? [-dy, dx, height, width] : [dy, -dx, height, width];
944
+ return { x: cx + newDx - newWidth / 2, y: cy + newDy - newHeight / 2, width: newWidth, height: newHeight };
945
+ }
946
+ function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
947
+ const triggerRef = (0, import_react8.useRef)(null);
948
+ const { width: windowWidth, height: windowHeight } = (0, import_react_native17.useWindowDimensions)();
949
+ const [align, setAlign] = (0, import_react8.useState)("center");
950
+ const [verticalAlign, setVerticalAlign] = (0, import_react8.useState)("below");
951
+ const [maxHeight, setMaxHeight] = (0, import_react8.useState)(contentHeight);
952
+ const [measured, setMeasured] = (0, import_react8.useState)(false);
953
+ const [prevOpen, setPrevOpen] = (0, import_react8.useState)(open);
954
+ if (open !== prevOpen) {
955
+ setPrevOpen(open);
956
+ if (open) setMeasured(false);
957
+ }
958
+ const measure = (0, import_react8.useCallback)(() => {
959
+ triggerRef.current?.measureInWindow((rawX, rawY, rawWidth, rawHeight) => {
960
+ const { x, y, width: triggerWidth, height: triggerHeight } = rotateRect(rawX, rawY, rawWidth, rawHeight, rotation, windowWidth, windowHeight);
961
+ const centerX = x + triggerWidth / 2;
962
+ const overflowsRight = centerX + contentWidth / 2 > windowWidth - EDGE_MARGIN;
963
+ const overflowsLeft = centerX - contentWidth / 2 < EDGE_MARGIN;
964
+ if (overflowsRight && !overflowsLeft) setAlign("right");
965
+ else if (overflowsLeft && !overflowsRight) setAlign("left");
966
+ else setAlign("center");
967
+ const roomBelow = windowHeight - (y + triggerHeight) - EDGE_MARGIN;
968
+ const roomAbove = y - EDGE_MARGIN;
969
+ const above = contentHeight > roomBelow && roomAbove > roomBelow;
970
+ setVerticalAlign(above ? "above" : "below");
971
+ setMaxHeight(Math.max(above ? roomAbove : roomBelow, 0));
972
+ setMeasured(true);
973
+ });
974
+ }, [contentWidth, contentHeight, windowWidth, windowHeight, rotation]);
975
+ (0, import_react8.useEffect)(() => {
976
+ if (open) measure();
977
+ }, [open, measure]);
978
+ return { align, maxHeight, measured, triggerRef, verticalAlign };
979
+ }
980
+
981
+ // src/InlineColorPicker.tsx
982
+ var import_jsx_runtime15 = require("react/jsx-runtime");
983
+ var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
984
+ var DEFAULT_SIZE = 48;
985
+ var SWATCH_SIZE = 28;
986
+ var SWATCHES_PADDING = 8;
987
+ var SWATCHES_GAP = 6;
988
+ var SWATCHES_BORDER_WIDTH = 2;
989
+ var MIN_COLUMNS = 4;
990
+ var MAX_COLUMNS = 5;
991
+ var SCREEN_MARGIN = 40;
992
+ function widthForColumns(columns) {
993
+ return SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * columns + SWATCHES_GAP * (columns - 1);
994
+ }
995
+ function getInlineColorPickerContentSize(swatchCount, windowWidth, columns) {
996
+ const autoColumns = (0, import_core8.clamp)(Math.floor((windowWidth - 2 * SCREEN_MARGIN + SWATCHES_GAP) / (SWATCH_SIZE + SWATCHES_GAP)), MIN_COLUMNS, MAX_COLUMNS);
997
+ const resolvedColumns = columns ?? autoColumns;
998
+ const width = widthForColumns(resolvedColumns);
999
+ const rows = Math.ceil(swatchCount / resolvedColumns);
1000
+ const height = SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * rows + SWATCHES_GAP * (rows - 1);
1001
+ return { width, height };
1002
+ }
1003
+ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches = import_auto_paper8.defaultColors, takenValue, allowSwapTaken, dark, align: forcedAlign, alignOverride, rotation: rotationOverride, icon = "palette", tag, labelFontFamily = MONO_FONT, autoDismiss = true, columns, size = DEFAULT_SIZE }) {
1004
+ const ambientRotation = (0, import_core8.useRotation)();
1005
+ const rotation = rotationOverride ?? ambientRotation;
1006
+ const menuBg = dark ? "#000000" : "#FFFFFF";
1007
+ const { width: windowWidth } = (0, import_react_native18.useWindowDimensions)();
1008
+ const { width: swatchesWidth, height: swatchesHeight } = getInlineColorPickerContentSize(swatches.length, windowWidth, columns);
1009
+ const open = host.openId === id;
1010
+ const auto = useAutoAlign(open, swatchesWidth, swatchesHeight, rotation);
1011
+ const placement = alignOverride ?? auto;
1012
+ const { maxHeight, measured, triggerRef, verticalAlign } = placement;
1013
+ const align = forcedAlign ?? placement.align;
1014
+ const displayFor = previewValue ?? ((hex) => hex);
1015
+ const displayValue = displayFor(value);
1016
+ const contrastColor = (0, import_auto_paper8.getContrastColor)(displayValue);
1017
+ const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4);
1018
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native18.View, { style: [styles14.anchor, open && styles14.anchorOpen], children: [
1019
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_feedback_press5.TouchableRipple, { onPress: () => host.toggle(id), borderless: true, style: [styles14.trigger, { backgroundColor: displayValue, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native18.View, { ref: triggerRef, collapsable: false, style: styles14.triggerMeasure, children: tag ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_paper10.Text, { style: [styles14.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_paper10.Icon, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
1020
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: displayValue, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native18.ScrollView, { style: [styles14.swatches, { backgroundColor: menuBg, borderColor: displayValue, width: swatchesWidth, maxHeight }], contentContainerStyle: styles14.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
1021
+ const selected = swatch.value.toLowerCase() === value.toLowerCase();
1022
+ const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
1023
+ const swappable = taken && allowSwapTaken;
1024
+ const swatchDisplay = displayFor(swatch.value);
1025
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1026
+ import_feedback_press5.TouchableRipple,
1027
+ {
1028
+ disabled: taken && !swappable,
1029
+ onPress: () => {
1030
+ onChange(swatch.value);
1031
+ if (autoDismiss) host.close();
1032
+ },
1033
+ borderless: true,
1034
+ style: [styles14.swatch, { backgroundColor: swatchDisplay }, selected && styles14.swatchSelected, taken && !swappable && styles14.swatchTaken],
1035
+ children: selected ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_paper10.Icon, { source: "check", size: 16, color: (0, import_auto_paper8.getContrastColor)(swatchDisplay) }) : swappable ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_paper10.Icon, { source: "swap-horizontal", size: 16, color: (0, import_auto_paper8.getContrastColor)(swatchDisplay) }) : taken ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_paper10.Icon, { source: "close", size: 16, color: (0, import_auto_paper8.getContrastColor)(swatchDisplay) }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native18.View, {})
1036
+ },
1037
+ swatch.value
1038
+ );
1039
+ }) }) })
1040
+ ] });
1041
+ }
1042
+ var styles14 = import_react_native18.StyleSheet.create({
1043
+ anchor: {
1044
+ position: "relative"
1045
+ },
1046
+ // See SectionedDropdown's identical comment - React Native Web gives every position:'relative' view
1047
+ // its own stacking context, so the elevation has to live on the anchor itself, not just the
1048
+ // popover content nested inside it, to correctly paint above this anchor's own later siblings.
1049
+ anchorOpen: {
1050
+ zIndex: 100
1051
+ },
1052
+ swatch: {
1053
+ alignItems: "center",
1054
+ borderRadius: SWATCH_SIZE / 2,
1055
+ height: SWATCH_SIZE,
1056
+ justifyContent: "center",
1057
+ width: SWATCH_SIZE
1058
+ },
1059
+ swatchSelected: {
1060
+ borderColor: "#ffffff",
1061
+ borderWidth: 2
1062
+ },
1063
+ swatchTaken: {
1064
+ opacity: 0.35
1065
+ },
1066
+ swatches: {
1067
+ borderRadius: 12,
1068
+ borderWidth: SWATCHES_BORDER_WIDTH,
1069
+ elevation: 8,
1070
+ shadowColor: "#000000",
1071
+ shadowOffset: { height: 2, width: 0 },
1072
+ shadowOpacity: 0.3,
1073
+ shadowRadius: 8
1074
+ // width is set inline above - border-box sizing means it has to include the border too
1075
+ // (2*SWATCHES_BORDER_WIDTH), not just padding+content, or exactly enough room goes missing that
1076
+ // the last column silently wraps to a new row. Anything wider than the exact sum just shows as
1077
+ // dead space on the right edge of each row instead.
1078
+ },
1079
+ // Separate from `swatches` itself - see SectionedDropdown's identical menu/menuContent split.
1080
+ // The scroll frame's own bounds (including maxHeight) live on the ScrollView; the actual grid
1081
+ // wrapping belongs to its contentContainerStyle, which sizes to the unclamped content instead.
1082
+ swatchesContent: {
1083
+ flexDirection: "row",
1084
+ flexWrap: "wrap",
1085
+ gap: SWATCHES_GAP,
1086
+ padding: SWATCHES_PADDING
1087
+ },
1088
+ tagLabel: {
1089
+ fontWeight: "700",
1090
+ letterSpacing: 0.5
1091
+ },
1092
+ // borderRadius/height/width come from the `size` prop instead (see render-site override) - no
1093
+ // fixed default here, since this style object is shared by every caller regardless of size.
1094
+ trigger: {
1095
+ alignItems: "center",
1096
+ alignSelf: "flex-start",
1097
+ justifyContent: "center"
1098
+ },
1099
+ // Wraps just the icon/tag, inside the TouchableRipple, purely so useAutoAlign has a plain View
1100
+ // to attach its measurement ref to - TouchableRipple itself doesn't forward a ref to a
1101
+ // measurable native node. Padded horizontally so a multi-character tag doesn't butt right up
1102
+ // against the trigger's own circular edge (an icon alone doesn't need it, but this style is
1103
+ // shared by both).
1104
+ triggerMeasure: {
1105
+ alignItems: "center",
1106
+ height: "100%",
1107
+ justifyContent: "center",
1108
+ paddingHorizontal: 4,
1109
+ width: "100%"
1110
+ }
1111
+ });
1112
+
1113
+ // src/PressAwayOverlay.tsx
1114
+ var import_react_native19 = require("react-native");
1115
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1116
+ function PressAwayOverlay({ active, onPress, style }) {
1117
+ if (!active) return null;
1118
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native19.Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [import_react_native19.StyleSheet.absoluteFill, style] });
1119
+ }
1120
+
1121
+ // src/StaggeredWord.tsx
1122
+ var import_react9 = require("react");
1123
+ var import_react_native20 = require("react-native");
1124
+ var import_react_native_reanimated = __toESM(require("react-native-reanimated"));
1125
+ var import_jsx_runtime17 = require("react/jsx-runtime");
856
1126
  var STAGGER_MS = 45;
857
1127
  var LETTER_DURATION_MS = 320;
858
1128
  function getStaggeredWordDuration(letterCount) {
@@ -860,7 +1130,7 @@ function getStaggeredWordDuration(letterCount) {
860
1130
  }
861
1131
  function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }) {
862
1132
  const progress = (0, import_react_native_reanimated.useSharedValue)(reducedMotion ? 1 : 0);
863
- (0, import_react8.useEffect)(() => {
1133
+ (0, import_react9.useEffect)(() => {
864
1134
  if (reducedMotion) return;
865
1135
  progress.value = (0, import_react_native_reanimated.withDelay)(index * STAGGER_MS, (0, import_react_native_reanimated.withTiming)(1, { duration: LETTER_DURATION_MS, easing: import_react_native_reanimated.Easing.out(import_react_native_reanimated.Easing.back(1.5)) }));
866
1136
  }, [reducedMotion]);
@@ -868,12 +1138,12 @@ function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSiz
868
1138
  opacity: progress.value,
869
1139
  transform: [{ translateY: (1 - progress.value) * 12 }, { scale: 0.85 + progress.value * 0.15 }]
870
1140
  }));
871
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_reanimated.default.Text, { allowFontScaling: false, style: [{ color, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, style], children: char });
1141
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native_reanimated.default.Text, { allowFontScaling: false, style: [{ color, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, style], children: char });
872
1142
  }
873
1143
  function StaggeredWord({ word, color, fontFamily, fontSize, lineHeight, reducedMotion, startIndex = 0, fontWeight = "bold", letterSpacing, style, onLayout }) {
874
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native16.View, { style: [styles13.row, style], onLayout, children: word.split("").map((char, i) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(AnimatedLetter, { char, index: startIndex + i, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, i)) });
1144
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native20.View, { style: [styles15.row, style], onLayout, children: word.split("").map((char, i) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(AnimatedLetter, { char, index: startIndex + i, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, i)) });
875
1145
  }
876
- var styles13 = import_react_native16.StyleSheet.create({
1146
+ var styles15 = import_react_native20.StyleSheet.create({
877
1147
  row: {
878
1148
  flexDirection: "row"
879
1149
  }
@@ -881,12 +1151,23 @@ var styles13 = import_react_native16.StyleSheet.create({
881
1151
 
882
1152
  // src/UpdateDialog.tsx
883
1153
  var import_updater2 = require("@rific/updater");
884
- var import_jsx_runtime15 = require("react/jsx-runtime");
1154
+ var import_jsx_runtime18 = require("react/jsx-runtime");
885
1155
  function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }) {
886
1156
  const { manifest, onConfirm, respond } = useUpdateConfirmBridge();
887
1157
  (0, import_updater2.useUpdater)({ autoCheck, autoPrompt, onConfirm, onError });
888
1158
  const date = manifest ? new Date(manifest.createdAt) : null;
889
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(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) });
1159
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(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) });
1160
+ }
1161
+
1162
+ // src/usePopoverHost.ts
1163
+ var import_react10 = require("react");
1164
+ function usePopoverHost() {
1165
+ const [openId, setOpenId] = (0, import_react10.useState)(null);
1166
+ const toggle = (0, import_react10.useCallback)((id) => {
1167
+ setOpenId((prev) => prev === id ? null : id);
1168
+ }, []);
1169
+ const close = (0, import_react10.useCallback)(() => setOpenId(null), []);
1170
+ return { openId, toggle, close };
890
1171
  }
891
1172
  // Annotate the CommonJS export names for ESM import in node:
892
1173
  0 && (module.exports = {
@@ -900,21 +1181,27 @@ function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onErr
900
1181
  ContentGutter,
901
1182
  CornerActionButtons,
902
1183
  DialogHeader,
1184
+ InlineColorPicker,
903
1185
  LOCKED_BADGE_COLOR,
904
1186
  MONO_FONT,
905
1187
  OverlayBackdrop,
906
1188
  OverlayCard,
1189
+ PopoverBody,
1190
+ PressAwayOverlay,
907
1191
  RotatedFrame,
908
1192
  StaggeredWord,
909
1193
  StatRow,
910
1194
  StatSection,
911
1195
  UpdateDialog,
912
1196
  WELL_SHADOW,
1197
+ getInlineColorPickerContentSize,
913
1198
  getStaggeredWordDuration,
914
1199
  isCompactDialogHeight,
915
1200
  overlayActionStyles,
916
1201
  overlayStyles,
1202
+ useAutoAlign,
917
1203
  useCompactDialog,
918
1204
  useOverlayCardColors,
1205
+ usePopoverHost,
919
1206
  useWellColors
920
1207
  });
@@ -7,18 +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,
17
20
  WELL_SHADOW,
21
+ getInlineColorPickerContentSize,
18
22
  getStaggeredWordDuration,
23
+ useAutoAlign,
19
24
  useCompactDialog,
25
+ usePopoverHost,
20
26
  useWellColors
21
- } from "../chunk-N7A7W72S.mjs";
27
+ } from "../chunk-BXG44LIA.mjs";
22
28
  import {
23
29
  COMPACT_DIALOG_HEIGHT,
24
30
  DialogHeader,
@@ -40,21 +46,27 @@ export {
40
46
  ContentGutter,
41
47
  CornerActionButtons,
42
48
  DialogHeader,
49
+ InlineColorPicker,
43
50
  LOCKED_BADGE_COLOR,
44
51
  MONO_FONT,
45
52
  OverlayBackdrop,
46
53
  OverlayCard,
54
+ PopoverBody,
55
+ PressAwayOverlay,
47
56
  RotatedFrame,
48
57
  StaggeredWord,
49
58
  StatRow,
50
59
  StatSection,
51
60
  UpdateDialog,
52
61
  WELL_SHADOW,
62
+ getInlineColorPickerContentSize,
53
63
  getStaggeredWordDuration,
54
64
  isCompactDialogHeight,
55
65
  overlayActionStyles,
56
66
  overlayStyles,
67
+ useAutoAlign,
57
68
  useCompactDialog,
58
69
  useOverlayCardColors,
70
+ usePopoverHost,
59
71
  useWellColors
60
72
  };