@buoy-gg/shared-ui 7.0.28 → 7.0.30

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.
Files changed (117) hide show
  1. package/lib/commonjs/JsModal.js +12 -2
  2. package/lib/commonjs/dataViewer/DataTreeActionDock.js +1 -0
  3. package/lib/commonjs/dataViewer/DataViewer.js +6 -3
  4. package/lib/commonjs/dataViewer/VirtualizedDataExplorer.js +13 -4
  5. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  6. package/lib/commonjs/license/FeatureGate.js +9 -0
  7. package/lib/commonjs/ui/backgrounds/registry.js +3 -2
  8. package/lib/commonjs/ui/backgrounds/variants/meteors.js +222 -78
  9. package/lib/commonjs/ui/components/Badge.js +3 -1
  10. package/lib/commonjs/ui/components/CompactRow.js +10 -1
  11. package/lib/commonjs/ui/components/CopyButton.js +3 -1
  12. package/lib/commonjs/ui/components/DraggableHeader.js +5 -1
  13. package/lib/commonjs/ui/components/DynamicFilterView.js +23 -4
  14. package/lib/commonjs/ui/components/EventStepperFooter.js +8 -0
  15. package/lib/commonjs/ui/components/FilterComponents.js +18 -2
  16. package/lib/commonjs/ui/components/ModalHeader.js +12 -3
  17. package/lib/commonjs/ui/components/PowerToggleButton.js +2 -0
  18. package/lib/commonjs/ui/components/SearchBar.js +6 -1
  19. package/lib/commonjs/ui/components/SectionHeader.js +3 -1
  20. package/lib/commonjs/ui/components/TabSelector.js +15 -4
  21. package/lib/commonjs/ui/components/WindowControls.js +68 -96
  22. package/lib/commonjs/ui/gameUI/constants/gameUIColors.js +12 -8
  23. package/lib/module/JsModal.js +12 -2
  24. package/lib/module/dataViewer/DataTreeActionDock.js +1 -0
  25. package/lib/module/dataViewer/DataViewer.js +6 -3
  26. package/lib/module/dataViewer/VirtualizedDataExplorer.js +13 -4
  27. package/lib/module/hooks/safe-area-impl.js +1 -1
  28. package/lib/module/license/FeatureGate.js +9 -0
  29. package/lib/module/ui/backgrounds/registry.js +3 -2
  30. package/lib/module/ui/backgrounds/variants/meteors.js +224 -80
  31. package/lib/module/ui/components/Badge.js +3 -1
  32. package/lib/module/ui/components/CompactRow.js +10 -1
  33. package/lib/module/ui/components/CopyButton.js +3 -1
  34. package/lib/module/ui/components/DraggableHeader.js +5 -1
  35. package/lib/module/ui/components/DynamicFilterView.js +23 -4
  36. package/lib/module/ui/components/EventStepperFooter.js +8 -0
  37. package/lib/module/ui/components/FilterComponents.js +18 -2
  38. package/lib/module/ui/components/ModalHeader.js +12 -3
  39. package/lib/module/ui/components/PowerToggleButton.js +2 -0
  40. package/lib/module/ui/components/SearchBar.js +6 -1
  41. package/lib/module/ui/components/SectionHeader.js +3 -1
  42. package/lib/module/ui/components/TabSelector.js +15 -4
  43. package/lib/module/ui/components/WindowControls.js +69 -97
  44. package/lib/module/ui/gameUI/constants/gameUIColors.js +12 -8
  45. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/dataViewer/DataViewer.d.ts +2 -0
  48. package/lib/typescript/commonjs/dataViewer/DataViewer.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts +9 -0
  50. package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  52. package/lib/typescript/commonjs/license/FeatureGate.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/ui/backgrounds/variants/meteors.d.ts +22 -6
  55. package/lib/typescript/commonjs/ui/backgrounds/variants/meteors.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/ui/components/Badge.d.ts +3 -1
  57. package/lib/typescript/commonjs/ui/components/Badge.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts +5 -1
  59. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/ui/components/CopyButton.d.ts +5 -0
  61. package/lib/typescript/commonjs/ui/components/CopyButton.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/ui/components/DraggableHeader.d.ts +2 -0
  63. package/lib/typescript/commonjs/ui/components/DraggableHeader.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/ui/components/DynamicFilterView.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/components/EventStepperFooter.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/ui/components/FilterComponents.d.ts +10 -2
  67. package/lib/typescript/commonjs/ui/components/FilterComponents.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts +16 -2
  69. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ui/components/PowerToggleButton.d.ts +4 -1
  71. package/lib/typescript/commonjs/ui/components/PowerToggleButton.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/ui/components/SearchBar.d.ts +7 -1
  73. package/lib/typescript/commonjs/ui/components/SearchBar.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/components/SectionHeader.d.ts +7 -1
  75. package/lib/typescript/commonjs/ui/components/SectionHeader.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/ui/components/TabSelector.d.ts +20 -1
  77. package/lib/typescript/commonjs/ui/components/TabSelector.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/ui/components/WindowControls.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts +5 -0
  80. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  81. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  82. package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  83. package/lib/typescript/module/dataViewer/DataViewer.d.ts +2 -0
  84. package/lib/typescript/module/dataViewer/DataViewer.d.ts.map +1 -1
  85. package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts +9 -0
  86. package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  87. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  88. package/lib/typescript/module/license/FeatureGate.d.ts.map +1 -1
  89. package/lib/typescript/module/ui/backgrounds/registry.d.ts.map +1 -1
  90. package/lib/typescript/module/ui/backgrounds/variants/meteors.d.ts +22 -6
  91. package/lib/typescript/module/ui/backgrounds/variants/meteors.d.ts.map +1 -1
  92. package/lib/typescript/module/ui/components/Badge.d.ts +3 -1
  93. package/lib/typescript/module/ui/components/Badge.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/components/CompactRow.d.ts +5 -1
  95. package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
  96. package/lib/typescript/module/ui/components/CopyButton.d.ts +5 -0
  97. package/lib/typescript/module/ui/components/CopyButton.d.ts.map +1 -1
  98. package/lib/typescript/module/ui/components/DraggableHeader.d.ts +2 -0
  99. package/lib/typescript/module/ui/components/DraggableHeader.d.ts.map +1 -1
  100. package/lib/typescript/module/ui/components/DynamicFilterView.d.ts.map +1 -1
  101. package/lib/typescript/module/ui/components/EventStepperFooter.d.ts.map +1 -1
  102. package/lib/typescript/module/ui/components/FilterComponents.d.ts +10 -2
  103. package/lib/typescript/module/ui/components/FilterComponents.d.ts.map +1 -1
  104. package/lib/typescript/module/ui/components/ModalHeader.d.ts +16 -2
  105. package/lib/typescript/module/ui/components/ModalHeader.d.ts.map +1 -1
  106. package/lib/typescript/module/ui/components/PowerToggleButton.d.ts +4 -1
  107. package/lib/typescript/module/ui/components/PowerToggleButton.d.ts.map +1 -1
  108. package/lib/typescript/module/ui/components/SearchBar.d.ts +7 -1
  109. package/lib/typescript/module/ui/components/SearchBar.d.ts.map +1 -1
  110. package/lib/typescript/module/ui/components/SectionHeader.d.ts +7 -1
  111. package/lib/typescript/module/ui/components/SectionHeader.d.ts.map +1 -1
  112. package/lib/typescript/module/ui/components/TabSelector.d.ts +20 -1
  113. package/lib/typescript/module/ui/components/TabSelector.d.ts.map +1 -1
  114. package/lib/typescript/module/ui/components/WindowControls.d.ts.map +1 -1
  115. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts +5 -0
  116. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  117. package/package.json +4 -4
@@ -498,6 +498,16 @@ const JsModalComponent = ({
498
498
  }
499
499
  }, []);
500
500
 
501
+ // Parents routinely pass an inline `onModeChange` (a new identity every
502
+ // render). The persisted-state load below is mount/key-scoped — if the
503
+ // callback identity re-triggered it, every parent re-render would reload
504
+ // the OLD persisted mode and stomp a live toggleMode() before the 500ms
505
+ // save debounce could write the new one (mode flipped to floating, then
506
+ // snapped back ~50ms later). So the callback rides in a ref instead of
507
+ // the dependency list.
508
+ const onModeChangeRef = (0, _react.useRef)(onModeChange);
509
+ onModeChangeRef.current = onModeChange;
510
+
501
511
  // Load persisted state on mount
502
512
  (0, _react.useEffect)(() => {
503
513
  if (!enablePersistence || !persistenceKey) {
@@ -512,7 +522,7 @@ const JsModalComponent = ({
512
522
  if (savedState.mode) {
513
523
  setMode(savedState.mode);
514
524
  // Notify parent of loaded mode
515
- onModeChange?.(savedState.mode);
525
+ onModeChangeRef.current?.(savedState.mode);
516
526
  }
517
527
 
518
528
  // Restore bottom sheet height
@@ -539,7 +549,7 @@ const JsModalComponent = ({
539
549
  return () => {
540
550
  mounted = false;
541
551
  };
542
- }, [persistenceKey, enablePersistence, onModeChange, animatedBottomPosition, animatedFloatingHeight, animatedWidth, floatingPosition]);
552
+ }, [persistenceKey, enablePersistence, animatedBottomPosition, animatedFloatingHeight, animatedWidth, floatingPosition]);
543
553
 
544
554
  // Cleanup on unmount
545
555
  (0, _react.useEffect)(() => {
@@ -86,6 +86,7 @@ function Tile({
86
86
  const color = danger && enabled ? T.dangerText : T.tileIcon;
87
87
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
88
88
  "sentry-label": "ignore devtools tree action",
89
+ testID: `buoy-tree-action-${id}`,
89
90
  accessibilityLabel: label,
90
91
  accessibilityState: {
91
92
  disabled: !enabled
@@ -31,7 +31,8 @@ const DataViewer = ({
31
31
  onRequestInlineEdit,
32
32
  inlineEditPath,
33
33
  renderInlineEditor,
34
- selectPath
34
+ selectPath,
35
+ testIDPrefix
35
36
  }) => {
36
37
  const [activeFilter, setActiveFilter] = (0, _react.useState)(null);
37
38
 
@@ -113,7 +114,8 @@ const DataViewer = ({
113
114
  maxDepth: maxDepth,
114
115
  rawMode: rawMode,
115
116
  initialExpanded: initialExpanded,
116
- disableCopy: disableCopy
117
+ disableCopy: disableCopy,
118
+ testIDPrefix: testIDPrefix
117
119
  });
118
120
  }
119
121
 
@@ -129,7 +131,8 @@ const DataViewer = ({
129
131
  onRequestInlineEdit: onRequestInlineEdit,
130
132
  inlineEditPath: inlineEditPath,
131
133
  renderInlineEditor: renderInlineEditor,
132
- selectPath: selectPath
134
+ selectPath: selectPath,
135
+ testIDPrefix: testIDPrefix
133
136
  });
134
137
  };
135
138
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -826,7 +826,8 @@ const VirtualizedItemComponent = ({
826
826
  selectable,
827
827
  onRequestInlineEdit,
828
828
  isInlineEditing,
829
- inlineEditor
829
+ inlineEditor,
830
+ testIDPrefix
830
831
  }) => {
831
832
  const [isPressed, setIsPressed] = (0, _react.useState)(false);
832
833
  const [showFullKey, setShowFullKey] = (0, _react.useState)(false);
@@ -895,7 +896,13 @@ const VirtualizedItemComponent = ({
895
896
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
896
897
  style: [STABLE_STYLES.itemContainer, indentStyle],
897
898
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
898
- "sentry-label": "ignore devtools data explorer item",
899
+ "sentry-label": "ignore devtools data explorer item"
900
+ // `item.id` is the dotted path from the root ("root.settings.theme"),
901
+ // so this handle addresses a NODE rather than a list position — stable
902
+ // across expand/collapse and across re-renders.
903
+ ,
904
+ testID: `${testIDPrefix}-${item.id}`,
905
+ accessibilityLabel: `${item.key}: ${item.valueType}`,
899
906
  style: [STABLE_STYLES.itemTouchable, isPressed && STABLE_STYLES.itemTouchablePressed,
900
907
  // The selected wash exists for the action-bar/edit flow. In a plain
901
908
  // read-only viewer every expand/collapse tap ALSO selects, which
@@ -970,7 +977,8 @@ const VirtualizedDataExplorer = ({
970
977
  onRequestInlineEdit,
971
978
  inlineEditPath,
972
979
  renderInlineEditor,
973
- selectPath
980
+ selectPath,
981
+ testIDPrefix = "buoy-tree-row"
974
982
  }) => {
975
983
  const [isExpanded, setIsExpanded] = (0, _react.useState)(rawMode); // Auto-expand in raw mode
976
984
  const {
@@ -1086,7 +1094,8 @@ const VirtualizedDataExplorer = ({
1086
1094
  selectable: onSelectionChange != null,
1087
1095
  onRequestInlineEdit: handleRequestInlineEdit,
1088
1096
  isInlineEditing: inlineEditPath != null && inlineEditPath.join(".") === item.path.slice(1).join("."),
1089
- inlineEditor: renderInlineEditor ? renderInlineEditor(selectionOf(item)) : null
1097
+ inlineEditor: renderInlineEditor ? renderInlineEditor(selectionOf(item)) : null,
1098
+ testIDPrefix: testIDPrefix
1090
1099
  });
1091
1100
 
1092
1101
  // Uniform row height for crisp guide geometry
@@ -7,7 +7,7 @@ exports.useNativeSafeAreaInsets = exports.safeAreaType = exports.hasSafeAreaPack
7
7
  /**
8
8
  * Auto-generated safe area implementation
9
9
  * Detected: none
10
- * Generated at: 2026-08-21T15:43:18.203Z
10
+ * Generated at: 2026-08-24T23:44:23.535Z
11
11
  *
12
12
  * DO NOT EDIT - This file is generated by scripts/detect-safe-area.js
13
13
  *
@@ -388,11 +388,20 @@ const UpgradeModal = ({
388
388
  style: styles.modalContent,
389
389
  activeOpacity: 1,
390
390
  onPress: () => {} // Prevent overlay dismiss when tapping content
391
+ // The handle a test needs to say a Pro gate did NOT fire. Without
392
+ // it "the action worked" and "the upsell appeared over it" are
393
+ // told apart only by what happened next, which is exactly the
394
+ // assertion that was supposed to be the proof.
391
395
  ,
396
+ testID: "buoy-upgrade-modal",
397
+ accessibilityLabel: `Unlock ${featureName}`,
392
398
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
393
399
  style: styles.modalCloseButton,
394
400
  onPress: onClose,
395
401
  activeOpacity: 0.7,
402
+ testID: "buoy-upgrade-modal-close",
403
+ accessibilityRole: "button",
404
+ accessibilityLabel: "Close upgrade prompt",
396
405
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_lucideIcons.X, {
397
406
  size: 18,
398
407
  color: _gameUIColors.buoyColors.textMuted
@@ -32,9 +32,10 @@ const BACKGROUND_PRESETS = exports.BACKGROUND_PRESETS = [{
32
32
  id: "deepfield",
33
33
  label: "Deep Field",
34
34
  blurb: "The Everlights field — 160 stars, with meteors slamming the walls",
35
- // The old standalone Meteors preset folded in: +2 meteor loops, ~50 views.
35
+ // The old standalone Meteors preset folded in: +2 meteor loops, ~55 views
36
+ // (streak + flash + the pop's 6 rays and ~3 embers per impact).
36
37
  nodes: 24,
37
- views: 510,
38
+ views: 515,
38
39
  Component: _deepField.DeepField
39
40
  }, {
40
41
  id: "hyperspace",
@@ -14,12 +14,28 @@ var _jsxRuntime = require("react/jsx-runtime");
14
14
  * MeteorShower — shooting stars that slam into the walls.
15
15
  *
16
16
  * Each meteor streaks in from off-screen, hits an EDGE of the surface, and
17
- * bursts: a flash, an expanding shockwave ring (half-clipped by the wall,
18
- * which is what sells the impact), and a handful of sparks ricocheting back
19
- * into the room. The whole life cycle — parked invisible, streak, impact,
20
- * decay — is keyframed into interpolate stops on ONE plain native loop per
21
- * meteor; `Animated.sequence([timing, loop])` silently fails to drive
22
- * natively, so nothing here sequences.
17
+ * blows out like a firework: a flash, then a fan of streak RAYS that shoot
18
+ * back into the room — each ray grows out of the impact point while its tail
19
+ * chases the head, so it reads as a burning trail rather than a ring — with
20
+ * ember dots at the tips that decelerate, droop under gravity and gutter out.
21
+ * The whole life cycle — parked invisible, streak, impact, decay — is
22
+ * keyframed into interpolate stops on ONE plain native loop per meteor;
23
+ * `Animated.sequence([timing, loop])` silently fails to drive natively, so
24
+ * nothing here sequences.
25
+ *
26
+ * Each meteor also rolls WHAT IT'S MADE OF once per mount — a weighted draw
27
+ * over the real emission-line palette (sodium orange-yellow, iron yellow,
28
+ * magnesium blue-green, calcium violet, shocked atmospheric N₂/O₂ red,
29
+ * ionized-air blue-white) — and that mineral tints streak, trail, flash,
30
+ * rays and embers as one. See `MINERALS`.
31
+ *
32
+ * The ray trick, since there is no `transformOrigin`: a bar of full length
33
+ * `dist` is CENTERED on the impact point, rotated to its heading, then driven
34
+ * with `[{rotate}, {translateX}, {scaleX}]`. Rotate is outermost, so the
35
+ * translate runs along the ray's own axis; scaleX squashes about the bar's
36
+ * center, and translating by the midpoint of (head, tail) while scaling by
37
+ * (head − tail) puts the visible segment exactly between the two — a line
38
+ * growing out of the burst with both endpoints scripted.
23
39
  *
24
40
  * This used to be a standalone "Meteors" preset with its own 40-star sky;
25
41
  * that scene was folded into Deep Field (which supplies the far better sky),
@@ -32,8 +48,8 @@ var _jsxRuntime = require("react/jsx-runtime");
32
48
 
33
49
  /**
34
50
  * `to` is ON a wall — that's the impact point. `sparkArc` is the hemisphere
35
- * (in degrees) sparks scatter into, pointed back away from the wall the
36
- * meteor just hit. Different periods keep the two impacts from syncing up.
51
+ * (in degrees) the firework rays fan into, pointed back away from the wall
52
+ * the meteor just hit. Different periods keep the two impacts from syncing up.
37
53
  */
38
54
  const METEORS = [{
39
55
  from: [0.08, -0.06],
@@ -48,8 +64,112 @@ const METEORS = [{
48
64
  window: [0.56, 0.66],
49
65
  sparkArc: [-70, 70] // left wall → sparks fly right
50
66
  }];
51
- const SPARKS_PER_IMPACT = 6;
52
- const SHOCKWAVE_DIA = 92;
67
+ const RAYS_PER_IMPACT = 6;
68
+
69
+ /**
70
+ * Real meteor color comes from what's burning. The dominant visible emission
71
+ * lines are sodium (orange-yellow), iron (yellow) and magnesium (blue-green);
72
+ * calcium-rich rock flares violet, and the shocked ATMOSPHERE itself — the
73
+ * excited nitrogen/oxygen sheath around the meteoroid — glows red. Faint fast
74
+ * meteors mostly show the blue-white of ionized air. The weights lean the
75
+ * same way the sky does: yellows and oranges common, teal frequent, red
76
+ * occasional, violet the rare treat.
77
+ */
78
+
79
+ const MINERALS = [{
80
+ name: "sodium",
81
+ weight: 26,
82
+ head: "#FFF3DC",
83
+ glow: "#FFDCA8",
84
+ trail: "#FFCB85",
85
+ rays: ["#FFE8C2", "#FFCB85", "#F0B45E"]
86
+ }, {
87
+ name: "iron",
88
+ weight: 24,
89
+ head: "#FFF9E3",
90
+ glow: "#FFEDAE",
91
+ trail: "#F2DC8A",
92
+ rays: ["#FBF0C0", "#F2DC8A", "#E3C868"]
93
+ }, {
94
+ name: "magnesium",
95
+ weight: 20,
96
+ head: "#EAFFF9",
97
+ glow: "#ACF0DE",
98
+ trail: "#82E3CA",
99
+ rays: ["#D3FAEF", "#82E3CA", "#5FD6BC"]
100
+ }, {
101
+ // Not a mineral at all — ionized air, the old blue-white default.
102
+ name: "ionized-air",
103
+ weight: 14,
104
+ head: "#F5F9FF",
105
+ glow: "#E5EEFC",
106
+ trail: "#C9D9F0",
107
+ rays: ["#F5F9FF", "#DCE9FF", "#BBE5FA"]
108
+ }, {
109
+ // Also atmospheric: nitrogen/oxygen shocked red-hot in the sheath.
110
+ name: "nitrogen-oxygen",
111
+ weight: 10,
112
+ head: "#FFEAE5",
113
+ glow: "#FFB4A6",
114
+ trail: "#FF917E",
115
+ rays: ["#FFCFC6", "#FF917E", "#F27461"]
116
+ }, {
117
+ name: "calcium",
118
+ weight: 6,
119
+ head: "#F6EEFF",
120
+ glow: "#DCC2FF",
121
+ trail: "#C29BF7",
122
+ rays: ["#EADCFF", "#C29BF7", "#AB7EEC"]
123
+ }];
124
+ const MINERAL_WEIGHT = MINERALS.reduce((sum, m) => sum + m.weight, 0);
125
+
126
+ /**
127
+ * Weighted draw — `Math.random` ON PURPOSE, unlike the seeded burst geometry:
128
+ * each meteor rolls its make-up once per mount, so every session's sky burns
129
+ * a different mix while the animation itself stays deterministic.
130
+ */
131
+ function pickMineral() {
132
+ let r = Math.random() * MINERAL_WEIGHT;
133
+ for (const m of MINERALS) {
134
+ r -= m.weight;
135
+ if (r <= 0) return m;
136
+ }
137
+ return MINERALS[0];
138
+ }
139
+ /**
140
+ * The firework: a deterministic fan of rays per impact, seeded so both
141
+ * platforms (and every reload) burst identically. Angles are stratified
142
+ * across the arc with jitter, so the fan covers it without reading as a
143
+ * perfect star; reach, thickness and stagger vary per ray so it reads as
144
+ * debris blowing out, not a rigid spoke wheel.
145
+ */
146
+ function buildBurst(index) {
147
+ const m = METEORS[index];
148
+ const rnd = (0, _rng.mulberry32)((0xb1a57 ^ index) >>> 0);
149
+ const [a0, a1] = m.sparkArc;
150
+ const rays = [];
151
+ for (let i = 0; i < RAYS_PER_IMPACT; i++) {
152
+ const frac = (i + 0.5) / RAYS_PER_IMPACT;
153
+ const jitter = (rnd() - 0.5) * 0.7 / RAYS_PER_IMPACT;
154
+ const deg = a0 + (a1 - a0) * (frac + jitter);
155
+ // Small on purpose — a game-sprite pop, not a fireworks display: rays
156
+ // reach 10–22px, a hair over 1px thick, and the whole thing is over in a
157
+ // few frames.
158
+ rays.push({
159
+ rad: deg * Math.PI / 180,
160
+ dist: 10 + rnd() * 12,
161
+ thick: 1 + rnd() * 0.5,
162
+ delay: rnd() * 0.008,
163
+ grow: 0.03 + rnd() * 0.015,
164
+ ember: rnd() > 0.5,
165
+ emberDrop: 2 + rnd() * 3,
166
+ emberDrift: 1 + rnd() * 3,
167
+ emberDia: 1.2 + rnd() * 0.6
168
+ });
169
+ }
170
+ return rays;
171
+ }
172
+ const BURSTS = METEORS.map((_, i) => buildBurst(i));
53
173
  const Meteor = /*#__PURE__*/(0, _react.memo)(function Meteor({
54
174
  index,
55
175
  width,
@@ -57,6 +177,8 @@ const Meteor = /*#__PURE__*/(0, _react.memo)(function Meteor({
57
177
  animated
58
178
  }) {
59
179
  const m = METEORS[index];
180
+ // What this meteor is made of — rolled once per mount, held for its life.
181
+ const [mineral] = (0, _react.useState)(pickMineral);
60
182
  const t = (0, _primitives.useLoop)(m.periodMs, animated);
61
183
  const [a, b] = m.window;
62
184
  const x0 = m.from[0] * width;
@@ -96,12 +218,12 @@ const Meteor = /*#__PURE__*/(0, _react.memo)(function Meteor({
96
218
  width: d,
97
219
  height: d,
98
220
  borderRadius: d / 2,
99
- backgroundColor: (0, _rng.withAlpha)("#C9D9F0", 0.68 - i * 0.08)
221
+ backgroundColor: (0, _rng.withAlpha)(mineral.trail, 0.68 - i * 0.08)
100
222
  }
101
223
  }, i));
102
224
  }
103
225
 
104
- // --- The impact ---------------------------------------------------------
226
+ // --- The impact: firework blow-out --------------------------------------
105
227
  // Everything below reads the SAME loop value; the burst simply lives in the
106
228
  // slice of the timeline right after the streak arrives at the wall.
107
229
  const flashOpacity = t.interpolate({
@@ -109,56 +231,94 @@ const Meteor = /*#__PURE__*/(0, _react.memo)(function Meteor({
109
231
  outputRange: [0, 0, 1, 0, 0]
110
232
  });
111
233
  const flashScale = t.interpolate({
112
- inputRange: [0, b, Math.min(b + 0.07, 0.999), 1],
113
- outputRange: [0.4, 0.4, 1.4, 1.4]
114
- });
115
- const waveOpacity = t.interpolate({
116
- inputRange: [0, b, Math.min(b + 0.02, 0.999), Math.min(b + 0.11, 0.9995), 1],
117
- outputRange: [0, 0, 0.85, 0, 0]
118
- });
119
- const waveScale = t.interpolate({
120
- inputRange: [0, b, Math.min(b + 0.11, 0.999), 1],
121
- outputRange: [0.12, 0.12, 1, 1]
234
+ inputRange: [0, b, Math.min(b + 0.05, 0.999), 1],
235
+ outputRange: [0.5, 0.5, 1.05, 1.05]
122
236
  });
123
- const sparks = [];
124
- for (let i = 0; i < SPARKS_PER_IMPACT; i++) {
125
- // Deterministic fan across the arc, alternating reach so the burst reads
126
- // as debris rather than a perfect star.
127
- const frac = i / (SPARKS_PER_IMPACT - 1);
128
- const deg = m.sparkArc[0] + (m.sparkArc[1] - m.sparkArc[0]) * frac;
129
- const rad = deg * Math.PI / 180;
130
- const dist = 30 + i % 3 * 14;
131
- const sx = t.interpolate({
132
- inputRange: [0, b, Math.min(b + 0.09, 0.999), 1],
133
- outputRange: [0, 0, Math.cos(rad) * dist, Math.cos(rad) * dist]
237
+ const rays = [];
238
+ const embers = [];
239
+ const burst = BURSTS[index];
240
+ for (let i = 0; i < burst.length; i++) {
241
+ const r = burst[i];
242
+ // The ray's timeline: head races out over `grow`, the tail leaves the
243
+ // impact point once the head is under way and catches it shortly after —
244
+ // the segment between them is the burning trail.
245
+ const t0 = b + r.delay;
246
+ const k1 = t0 + r.grow * 0.5; // head at 75%, tail just leaving
247
+ const k2 = t0 + r.grow; // head at full reach, tail halfway
248
+ const k3 = k2 + 0.035; // tail catches the head — ray gone
249
+ // Segment endpoints head/tail (fractions of dist):
250
+ // t0 → 0/0, k1 → 0.75/0.1, k2 → 1/0.5, k3 → 1/1
251
+ // scaleX = head − tail, translateX = dist · (head + tail)/2.
252
+ const len = t.interpolate({
253
+ inputRange: [0, t0, k1, k2, k3, 1],
254
+ outputRange: [0, 0, 0.65, 0.5, 0, 0]
134
255
  });
135
- const sy = t.interpolate({
136
- inputRange: [0, b, Math.min(b + 0.09, 0.999), 1],
137
- outputRange: [0, 0, Math.sin(rad) * dist, Math.sin(rad) * dist]
256
+ const mid = t.interpolate({
257
+ inputRange: [0, t0, k1, k2, k3, 1],
258
+ outputRange: [0, 0, r.dist * 0.425, r.dist * 0.75, r.dist, r.dist]
138
259
  });
139
- const so = t.interpolate({
140
- inputRange: [0, b, Math.min(b + 0.02, 0.999), Math.min(b + 0.09, 0.9995), 1],
141
- outputRange: [0, 0, 0.9, 0, 0]
260
+ const rayOpacity = t.interpolate({
261
+ inputRange: [0, t0, t0 + 0.006, k2, k3, 1],
262
+ outputRange: [0, 0, 0.85, 0.7, 0, 0]
142
263
  });
143
- const d = 2.2 - i % 3 * 0.4;
144
- sparks.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
264
+ rays.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
145
265
  style: {
146
266
  position: "absolute",
147
- left: x1 - d / 2,
148
- top: y1 - d / 2,
149
- width: d,
150
- height: d,
151
- borderRadius: d / 2,
152
- backgroundColor: (0, _rng.withAlpha)("#E5EEFC", 1),
153
- opacity: so,
267
+ left: x1 - r.dist / 2,
268
+ top: y1 - r.thick / 2,
269
+ width: r.dist,
270
+ height: r.thick,
271
+ borderRadius: r.thick / 2,
272
+ backgroundColor: (0, _rng.withAlpha)(mineral.rays[i % mineral.rays.length], 0.8),
273
+ opacity: rayOpacity,
274
+ // Rotate is OUTERMOST, so the translate runs along the ray's own
275
+ // axis — see the header note on faking a transform origin.
154
276
  transform: [{
155
- translateX: sx
277
+ rotate: `${r.rad * 180 / Math.PI}deg`
156
278
  }, {
157
- translateY: sy
279
+ translateX: mid
280
+ }, {
281
+ scaleX: len
158
282
  }]
159
283
  },
160
284
  pointerEvents: "none"
161
- }, i));
285
+ }, `ray-${i}`));
286
+ if (!r.ember) continue;
287
+ // The ember rides the ray's head, then keeps coasting a touch further
288
+ // while gravity drags it DOWN in world space, guttering out last.
289
+ const e3 = k3 + 0.04;
290
+ const cos = Math.cos(r.rad);
291
+ const sin = Math.sin(r.rad);
292
+ const ex = t.interpolate({
293
+ inputRange: [0, t0, k1, k2, e3, 1],
294
+ outputRange: [0, 0, cos * r.dist * 0.75, cos * r.dist, cos * (r.dist + r.emberDrift), cos * (r.dist + r.emberDrift)]
295
+ });
296
+ const ey = t.interpolate({
297
+ inputRange: [0, t0, k1, k2, e3, 1],
298
+ outputRange: [0, 0, sin * r.dist * 0.75, sin * r.dist, sin * (r.dist + r.emberDrift) + r.emberDrop, sin * (r.dist + r.emberDrift) + r.emberDrop]
299
+ });
300
+ const eo = t.interpolate({
301
+ inputRange: [0, t0, k1, k2, e3, 1],
302
+ outputRange: [0, 0, 0.8, 0.65, 0, 0]
303
+ });
304
+ embers.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
305
+ style: {
306
+ position: "absolute",
307
+ left: x1 - r.emberDia / 2,
308
+ top: y1 - r.emberDia / 2,
309
+ width: r.emberDia,
310
+ height: r.emberDia,
311
+ borderRadius: r.emberDia / 2,
312
+ backgroundColor: (0, _rng.withAlpha)(mineral.head, 1),
313
+ opacity: eo,
314
+ transform: [{
315
+ translateX: ex
316
+ }, {
317
+ translateY: ey
318
+ }]
319
+ },
320
+ pointerEvents: "none"
321
+ }, `ember-${i}`));
162
322
  }
163
323
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
164
324
  style: _absoluteFill.absoluteFill,
@@ -188,7 +348,7 @@ const Meteor = /*#__PURE__*/(0, _react.memo)(function Meteor({
188
348
  width: 20,
189
349
  height: 20,
190
350
  borderRadius: 10,
191
- backgroundColor: (0, _rng.withAlpha)("#E5EEFC", 0.16)
351
+ backgroundColor: (0, _rng.withAlpha)(mineral.glow, 0.16)
192
352
  }
193
353
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
194
354
  style: {
@@ -198,16 +358,16 @@ const Meteor = /*#__PURE__*/(0, _react.memo)(function Meteor({
198
358
  width: 3,
199
359
  height: 3,
200
360
  borderRadius: 1.5,
201
- backgroundColor: (0, _rng.withAlpha)("#F5F9FF", 1)
361
+ backgroundColor: (0, _rng.withAlpha)(mineral.head, 1)
202
362
  }
203
363
  }), trail]
204
364
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
205
365
  style: {
206
366
  position: "absolute",
207
- left: x1 - 32,
208
- top: y1 - 32,
209
- width: 64,
210
- height: 64,
367
+ left: x1 - 18,
368
+ top: y1 - 18,
369
+ width: 36,
370
+ height: 36,
211
371
  opacity: flashOpacity,
212
372
  transform: [{
213
373
  scale: flashScale
@@ -215,29 +375,13 @@ const Meteor = /*#__PURE__*/(0, _react.memo)(function Meteor({
215
375
  },
216
376
  pointerEvents: "none",
217
377
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_primitives.RadialGlow, {
218
- size: 64,
219
- color: "#EDF3FC",
220
- alpha: 0.6,
221
- rings: 6,
378
+ size: 36,
379
+ color: mineral.glow,
380
+ alpha: 0.45,
381
+ rings: 5,
222
382
  falloff: 1.6
223
383
  })
224
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
225
- style: {
226
- position: "absolute",
227
- left: x1 - SHOCKWAVE_DIA / 2,
228
- top: y1 - SHOCKWAVE_DIA / 2,
229
- width: SHOCKWAVE_DIA,
230
- height: SHOCKWAVE_DIA,
231
- borderRadius: SHOCKWAVE_DIA / 2,
232
- borderWidth: 1.5,
233
- borderColor: (0, _rng.withAlpha)("#BBE5FA", 0.9),
234
- opacity: waveOpacity,
235
- transform: [{
236
- scale: waveScale
237
- }]
238
- },
239
- pointerEvents: "none"
240
- }), sparks]
384
+ }), rays, embers]
241
385
  });
242
386
  });
243
387
 
@@ -106,7 +106,8 @@ function TypeBadge({
106
106
  type,
107
107
  color,
108
108
  size = "small",
109
- style
109
+ style,
110
+ testID
110
111
  }) {
111
112
  const typeColor = color || getTypeColor(type);
112
113
  const badgeStyles = getBadgeStyles("type", typeColor, size);
@@ -116,6 +117,7 @@ function TypeBadge({
116
117
  style: [badgeStyles.text, {
117
118
  color: typeColor
118
119
  }],
120
+ testID: testID,
119
121
  children: type
120
122
  })
121
123
  });
@@ -27,6 +27,8 @@ function CompactRow({
27
27
  disabled,
28
28
  expandedGlowColor,
29
29
  testID,
30
+ statusTestID,
31
+ statusSublabelTestID,
30
32
  accessibilityLabel
31
33
  }) {
32
34
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -45,7 +47,12 @@ function CompactRow({
45
47
  activeOpacity: 0.8,
46
48
  disabled: disabled || !onPress,
47
49
  testID: testID,
48
- accessibilityLabel: accessibilityLabel,
50
+ accessibilityLabel: accessibilityLabel
51
+ // Label alone is not enough on Fabric: a row that announces text but
52
+ // no role reads to VoiceOver as static content, so the fact that it
53
+ // opens something is only discoverable by tapping it.
54
+ ,
55
+ accessibilityRole: onPress ? "button" : undefined,
49
56
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
50
57
  style: styles.rowContent,
51
58
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -61,10 +68,12 @@ function CompactRow({
61
68
  color: statusDotColor
62
69
  }],
63
70
  numberOfLines: 1,
71
+ testID: statusTestID,
64
72
  children: statusLabel
65
73
  }), statusSublabel ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
66
74
  style: styles.observerText,
67
75
  numberOfLines: 1,
76
+ testID: statusSublabelTestID,
68
77
  children: statusSublabel
69
78
  }) : null]
70
79
  })]
@@ -265,7 +265,8 @@ const ActionCopyButton = exports.ActionCopyButton = /*#__PURE__*/(0, _react.memo
265
265
  const ToolbarClearButton = exports.ToolbarClearButton = /*#__PURE__*/(0, _react.memo)(function ToolbarClearButton({
266
266
  onPress,
267
267
  disabled = false,
268
- buttonStyle
268
+ buttonStyle,
269
+ testID
269
270
  }) {
270
271
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
271
272
  onPress: onPress,
@@ -277,6 +278,7 @@ const ToolbarClearButton = exports.ToolbarClearButton = /*#__PURE__*/(0, _react.
277
278
  left: 8,
278
279
  right: 8
279
280
  },
281
+ testID: testID,
280
282
  accessibilityLabel: "Clear",
281
283
  accessibilityRole: "button",
282
284
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_lucideIcons.Trash2, {
@@ -40,7 +40,9 @@ const DraggableHeader = exports.DraggableHeader = /*#__PURE__*/(0, _react.memo)(
40
40
  },
41
41
  style,
42
42
  enabled = true,
43
- maxOverflowX = 0
43
+ maxOverflowX = 0,
44
+ testID,
45
+ accessibilityLabel
44
46
  }) {
45
47
  const isDraggingRef = (0, _react.useRef)(false);
46
48
  const dragDistanceRef = (0, _react.useRef)(0);
@@ -190,6 +192,8 @@ const DraggableHeader = exports.DraggableHeader = /*#__PURE__*/(0, _react.memo)(
190
192
  }, [position]);
191
193
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
192
194
  style: style,
195
+ testID: testID,
196
+ accessibilityLabel: accessibilityLabel,
193
197
  ...panResponder.panHandlers,
194
198
  children: children
195
199
  });