@buoy-gg/shared-ui 7.0.27 → 7.0.29

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