@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
@@ -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-21T15:43:18.203Z
6
+ * Generated at: 2026-08-24T23:44:23.535Z
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
  });
@@ -23,6 +23,8 @@ export function CompactRow({
23
23
  disabled,
24
24
  expandedGlowColor,
25
25
  testID,
26
+ statusTestID,
27
+ statusSublabelTestID,
26
28
  accessibilityLabel
27
29
  }) {
28
30
  return /*#__PURE__*/_jsx(View, {
@@ -41,7 +43,12 @@ export function CompactRow({
41
43
  activeOpacity: 0.8,
42
44
  disabled: disabled || !onPress,
43
45
  testID: testID,
44
- accessibilityLabel: accessibilityLabel,
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,
45
52
  children: /*#__PURE__*/_jsxs(View, {
46
53
  style: styles.rowContent,
47
54
  children: [/*#__PURE__*/_jsxs(View, {
@@ -57,10 +64,12 @@ export function CompactRow({
57
64
  color: statusDotColor
58
65
  }],
59
66
  numberOfLines: 1,
67
+ testID: statusTestID,
60
68
  children: statusLabel
61
69
  }), statusSublabel ? /*#__PURE__*/_jsx(Text, {
62
70
  style: styles.observerText,
63
71
  numberOfLines: 1,
72
+ testID: statusSublabelTestID,
64
73
  children: statusSublabel
65
74
  }) : null]
66
75
  })]
@@ -261,7 +261,8 @@ export const ActionCopyButton = /*#__PURE__*/memo(function ActionCopyButton(prop
261
261
  export const ToolbarClearButton = /*#__PURE__*/memo(function ToolbarClearButton({
262
262
  onPress,
263
263
  disabled = false,
264
- buttonStyle
264
+ buttonStyle,
265
+ testID
265
266
  }) {
266
267
  return /*#__PURE__*/_jsx(TouchableOpacity, {
267
268
  onPress: onPress,
@@ -273,6 +274,7 @@ export const ToolbarClearButton = /*#__PURE__*/memo(function ToolbarClearButton(
273
274
  left: 8,
274
275
  right: 8
275
276
  },
277
+ testID: testID,
276
278
  accessibilityLabel: "Clear",
277
279
  accessibilityRole: "button",
278
280
  children: /*#__PURE__*/_jsx(Trash2, {
@@ -36,7 +36,9 @@ export const DraggableHeader = /*#__PURE__*/memo(function DraggableHeader({
36
36
  },
37
37
  style,
38
38
  enabled = true,
39
- maxOverflowX = 0
39
+ maxOverflowX = 0,
40
+ testID,
41
+ accessibilityLabel
40
42
  }) {
41
43
  const isDraggingRef = useRef(false);
42
44
  const dragDistanceRef = useRef(0);
@@ -186,6 +188,8 @@ export const DraggableHeader = /*#__PURE__*/memo(function DraggableHeader({
186
188
  }, [position]);
187
189
  return /*#__PURE__*/_jsx(View, {
188
190
  style: style,
191
+ testID: testID,
192
+ accessibilityLabel: accessibilityLabel,
189
193
  ...panResponder.panHandlers,
190
194
  children: children
191
195
  });
@@ -103,6 +103,9 @@ export function DynamicFilterView({
103
103
  const countIsZero = hasCount && option.count === 0;
104
104
  const showLabel = section.type !== "method" || option.icon;
105
105
  return /*#__PURE__*/_jsxs(TouchableOpacity, {
106
+ testID: `buoy-filter-option-${option.id}`,
107
+ accessibilityRole: "button",
108
+ accessibilityLabel: String(option.label ?? option.id),
106
109
  style: [styles.filterCard, option.isActive && styles.activeFilterCard],
107
110
  onPress: () => onFilterChange?.(option.id, option.value),
108
111
  children: [option.icon ? /*#__PURE__*/_jsx(option.icon, {
@@ -187,7 +190,8 @@ export function DynamicFilterView({
187
190
  children: !includeFilterManager.showAddInput ? /*#__PURE__*/_jsx(AddFilterButton, {
188
191
  onPress: () => includeFilterManager.setShowAddInput(true),
189
192
  color: buoyColors.success,
190
- label: "Add include pattern"
193
+ label: "Add include pattern",
194
+ testID: "buoy-filter-include-add-open"
191
195
  }) : /*#__PURE__*/_jsx(AddFilterInput, {
192
196
  value: includeFilterManager.newFilter,
193
197
  onChange: includeFilterManager.setNewFilter,
@@ -197,9 +201,11 @@ export function DynamicFilterView({
197
201
  includeFilterManager.setNewFilter("");
198
202
  },
199
203
  placeholder: includeOnlySection.placeholder || "Enter pattern to include...",
200
- color: buoyColors.success
204
+ color: buoyColors.success,
205
+ testID: "buoy-filter-include-input"
201
206
  })
202
207
  }), includeOnlySection.patterns.size > 0 && /*#__PURE__*/_jsx(ScrollView, {
208
+ testID: "buoy-filter-include-scroll",
203
209
  style: styles.includeOnlyFiltersContent,
204
210
  showsVerticalScrollIndicator: true,
205
211
  nestedScrollEnabled: true,
@@ -244,7 +250,11 @@ export function DynamicFilterView({
244
250
  color: buoyColors.primary
245
251
  })]
246
252
  })
247
- }), activePatterns.size > 0 && /*#__PURE__*/_jsx(ScrollView, {
253
+ }), activePatterns.size > 0 && /*#__PURE__*/_jsx(ScrollView
254
+ // The active patterns live in their own short scroller, so a
255
+ // pattern added a moment ago is usually below its fold.
256
+ , {
257
+ testID: "buoy-filter-active-scroll",
248
258
  style: styles.excludeFiltersContent,
249
259
  showsVerticalScrollIndicator: true,
250
260
  nestedScrollEnabled: true,
@@ -283,6 +293,9 @@ export function DynamicFilterView({
283
293
  },
284
294
  style: styles.availableKeyItem,
285
295
  "sentry-label": "ignore-touchable-opacity",
296
+ testID: `buoy-filter-available-${item}`,
297
+ accessibilityRole: "button",
298
+ accessibilityLabel: `Add filter ${item}`,
286
299
  children: [/*#__PURE__*/_jsx(Text, {
287
300
  style: styles.availableKeyText,
288
301
  numberOfLines: 1,
@@ -340,7 +353,13 @@ export function DynamicFilterView({
340
353
  };
341
354
  return /*#__PURE__*/_jsxs(View, {
342
355
  style: styles.container,
343
- children: [renderTabs(), /*#__PURE__*/_jsx(ScrollView, {
356
+ children: [renderTabs(), /*#__PURE__*/_jsx(ScrollView
357
+ // The filter screen's own scroller. Named because the sections it
358
+ // holds — including the active-pattern list — run past the bottom of
359
+ // a docked sheet, so reaching one means scrolling THIS, not the short
360
+ // nested scroller the pattern happens to sit in.
361
+ , {
362
+ testID: "buoy-filter-scroll",
344
363
  style: styles.content,
345
364
  contentContainerStyle: styles.scrollContent,
346
365
  showsVerticalScrollIndicator: false,