@buoy-gg/shared-ui 7.0.22 → 7.0.24

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 (154) hide show
  1. package/lib/commonjs/JsModal.js +39 -16
  2. package/lib/commonjs/dataViewer/JsonStringNotice.js +59 -0
  3. package/lib/commonjs/dataViewer/ThemedSplitView.js +20 -1
  4. package/lib/commonjs/dataViewer/TimestampDeltaBar.js +96 -0
  5. package/lib/commonjs/dataViewer/index.js +52 -0
  6. package/lib/commonjs/dataViewer/jsonStringValue.js +94 -0
  7. package/lib/commonjs/dataViewer/timestampDiff.js +146 -0
  8. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  9. package/lib/commonjs/index.js +50 -1
  10. package/lib/commonjs/license/FeatureGate.js +43 -10
  11. package/lib/commonjs/license/FreeAccountPrompt.js +181 -0
  12. package/lib/commonjs/license/index.js +57 -42
  13. package/lib/commonjs/license/licenseModule.js +99 -0
  14. package/lib/commonjs/license/tierLimits.js +165 -0
  15. package/lib/commonjs/ui/backgrounds/BackgroundSwitcher.js +180 -0
  16. package/lib/commonjs/ui/backgrounds/README.md +115 -0
  17. package/lib/commonjs/ui/backgrounds/ToolBackground.js +74 -0
  18. package/lib/commonjs/ui/backgrounds/backgroundStore.js +116 -0
  19. package/lib/commonjs/ui/backgrounds/index.js +70 -0
  20. package/lib/commonjs/ui/backgrounds/primitives.js +333 -0
  21. package/lib/commonjs/ui/backgrounds/registry.js +112 -0
  22. package/lib/commonjs/ui/backgrounds/rng.js +72 -0
  23. package/lib/commonjs/ui/backgrounds/types.js +1 -0
  24. package/lib/commonjs/ui/backgrounds/variants/deepField.js +351 -0
  25. package/lib/commonjs/ui/backgrounds/variants/lattice.js +579 -0
  26. package/lib/commonjs/ui/backgrounds/variants/starfield.js +210 -0
  27. package/lib/commonjs/ui/index.js +15 -3
  28. package/lib/module/JsModal.js +39 -16
  29. package/lib/module/dataViewer/JsonStringNotice.js +54 -0
  30. package/lib/module/dataViewer/ThemedSplitView.js +20 -1
  31. package/lib/module/dataViewer/TimestampDeltaBar.js +91 -0
  32. package/lib/module/dataViewer/index.js +4 -0
  33. package/lib/module/dataViewer/jsonStringValue.js +89 -0
  34. package/lib/module/dataViewer/timestampDiff.js +139 -0
  35. package/lib/module/hooks/safe-area-impl.js +1 -1
  36. package/lib/module/index.js +1 -1
  37. package/lib/module/license/FeatureGate.js +43 -10
  38. package/lib/module/license/FreeAccountPrompt.js +176 -0
  39. package/lib/module/license/index.js +5 -40
  40. package/lib/module/license/licenseModule.js +92 -0
  41. package/lib/module/license/tierLimits.js +158 -0
  42. package/lib/module/ui/backgrounds/BackgroundSwitcher.js +176 -0
  43. package/lib/module/ui/backgrounds/README.md +115 -0
  44. package/lib/module/ui/backgrounds/ToolBackground.js +70 -0
  45. package/lib/module/ui/backgrounds/backgroundStore.js +111 -0
  46. package/lib/module/ui/backgrounds/index.js +9 -0
  47. package/lib/module/ui/backgrounds/primitives.js +328 -0
  48. package/lib/module/ui/backgrounds/registry.js +105 -0
  49. package/lib/module/ui/backgrounds/rng.js +65 -0
  50. package/lib/module/ui/backgrounds/types.js +1 -0
  51. package/lib/module/ui/backgrounds/variants/deepField.js +347 -0
  52. package/lib/module/ui/backgrounds/variants/lattice.js +575 -0
  53. package/lib/module/ui/backgrounds/variants/starfield.js +206 -0
  54. package/lib/module/ui/index.js +3 -0
  55. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/dataViewer/JsonStringNotice.d.ts +6 -0
  57. package/lib/typescript/commonjs/dataViewer/JsonStringNotice.d.ts.map +1 -0
  58. package/lib/typescript/commonjs/dataViewer/ThemedSplitView.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/dataViewer/TimestampDeltaBar.d.ts +7 -0
  60. package/lib/typescript/commonjs/dataViewer/TimestampDeltaBar.d.ts.map +1 -0
  61. package/lib/typescript/commonjs/dataViewer/index.d.ts +6 -0
  62. package/lib/typescript/commonjs/dataViewer/index.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/dataViewer/jsonStringValue.d.ts +25 -0
  64. package/lib/typescript/commonjs/dataViewer/jsonStringValue.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/dataViewer/timestampDiff.d.ts +55 -0
  66. package/lib/typescript/commonjs/dataViewer/timestampDiff.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  68. package/lib/typescript/commonjs/index.d.ts +2 -2
  69. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/license/FeatureGate.d.ts +19 -3
  71. package/lib/typescript/commonjs/license/FeatureGate.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/license/FreeAccountPrompt.d.ts +23 -0
  73. package/lib/typescript/commonjs/license/FreeAccountPrompt.d.ts.map +1 -0
  74. package/lib/typescript/commonjs/license/index.d.ts +6 -15
  75. package/lib/typescript/commonjs/license/index.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/license/licenseModule.d.ts +55 -0
  77. package/lib/typescript/commonjs/license/licenseModule.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/license/tierLimits.d.ts +143 -0
  79. package/lib/typescript/commonjs/license/tierLimits.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/ui/backgrounds/BackgroundSwitcher.d.ts +13 -0
  81. package/lib/typescript/commonjs/ui/backgrounds/BackgroundSwitcher.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/ui/backgrounds/ToolBackground.d.ts +30 -0
  83. package/lib/typescript/commonjs/ui/backgrounds/ToolBackground.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/ui/backgrounds/backgroundStore.d.ts +43 -0
  85. package/lib/typescript/commonjs/ui/backgrounds/backgroundStore.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/ui/backgrounds/index.d.ts +7 -0
  87. package/lib/typescript/commonjs/ui/backgrounds/index.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/ui/backgrounds/primitives.d.ts +106 -0
  89. package/lib/typescript/commonjs/ui/backgrounds/primitives.d.ts.map +1 -0
  90. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts +28 -0
  91. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts.map +1 -0
  92. package/lib/typescript/commonjs/ui/backgrounds/rng.d.ts +27 -0
  93. package/lib/typescript/commonjs/ui/backgrounds/rng.d.ts.map +1 -0
  94. package/lib/typescript/commonjs/ui/backgrounds/types.d.ts +14 -0
  95. package/lib/typescript/commonjs/ui/backgrounds/types.d.ts.map +1 -0
  96. package/lib/typescript/commonjs/ui/backgrounds/variants/deepField.d.ts +65 -0
  97. package/lib/typescript/commonjs/ui/backgrounds/variants/deepField.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/ui/backgrounds/variants/lattice.d.ts +33 -0
  99. package/lib/typescript/commonjs/ui/backgrounds/variants/lattice.d.ts.map +1 -0
  100. package/lib/typescript/commonjs/ui/backgrounds/variants/starfield.d.ts +12 -0
  101. package/lib/typescript/commonjs/ui/backgrounds/variants/starfield.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  103. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  104. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  105. package/lib/typescript/module/dataViewer/JsonStringNotice.d.ts +6 -0
  106. package/lib/typescript/module/dataViewer/JsonStringNotice.d.ts.map +1 -0
  107. package/lib/typescript/module/dataViewer/ThemedSplitView.d.ts.map +1 -1
  108. package/lib/typescript/module/dataViewer/TimestampDeltaBar.d.ts +7 -0
  109. package/lib/typescript/module/dataViewer/TimestampDeltaBar.d.ts.map +1 -0
  110. package/lib/typescript/module/dataViewer/index.d.ts +6 -0
  111. package/lib/typescript/module/dataViewer/index.d.ts.map +1 -1
  112. package/lib/typescript/module/dataViewer/jsonStringValue.d.ts +25 -0
  113. package/lib/typescript/module/dataViewer/jsonStringValue.d.ts.map +1 -0
  114. package/lib/typescript/module/dataViewer/timestampDiff.d.ts +55 -0
  115. package/lib/typescript/module/dataViewer/timestampDiff.d.ts.map +1 -0
  116. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  117. package/lib/typescript/module/index.d.ts +2 -2
  118. package/lib/typescript/module/index.d.ts.map +1 -1
  119. package/lib/typescript/module/license/FeatureGate.d.ts +19 -3
  120. package/lib/typescript/module/license/FeatureGate.d.ts.map +1 -1
  121. package/lib/typescript/module/license/FreeAccountPrompt.d.ts +23 -0
  122. package/lib/typescript/module/license/FreeAccountPrompt.d.ts.map +1 -0
  123. package/lib/typescript/module/license/index.d.ts +6 -15
  124. package/lib/typescript/module/license/index.d.ts.map +1 -1
  125. package/lib/typescript/module/license/licenseModule.d.ts +55 -0
  126. package/lib/typescript/module/license/licenseModule.d.ts.map +1 -0
  127. package/lib/typescript/module/license/tierLimits.d.ts +143 -0
  128. package/lib/typescript/module/license/tierLimits.d.ts.map +1 -0
  129. package/lib/typescript/module/ui/backgrounds/BackgroundSwitcher.d.ts +13 -0
  130. package/lib/typescript/module/ui/backgrounds/BackgroundSwitcher.d.ts.map +1 -0
  131. package/lib/typescript/module/ui/backgrounds/ToolBackground.d.ts +30 -0
  132. package/lib/typescript/module/ui/backgrounds/ToolBackground.d.ts.map +1 -0
  133. package/lib/typescript/module/ui/backgrounds/backgroundStore.d.ts +43 -0
  134. package/lib/typescript/module/ui/backgrounds/backgroundStore.d.ts.map +1 -0
  135. package/lib/typescript/module/ui/backgrounds/index.d.ts +7 -0
  136. package/lib/typescript/module/ui/backgrounds/index.d.ts.map +1 -0
  137. package/lib/typescript/module/ui/backgrounds/primitives.d.ts +106 -0
  138. package/lib/typescript/module/ui/backgrounds/primitives.d.ts.map +1 -0
  139. package/lib/typescript/module/ui/backgrounds/registry.d.ts +28 -0
  140. package/lib/typescript/module/ui/backgrounds/registry.d.ts.map +1 -0
  141. package/lib/typescript/module/ui/backgrounds/rng.d.ts +27 -0
  142. package/lib/typescript/module/ui/backgrounds/rng.d.ts.map +1 -0
  143. package/lib/typescript/module/ui/backgrounds/types.d.ts +14 -0
  144. package/lib/typescript/module/ui/backgrounds/types.d.ts.map +1 -0
  145. package/lib/typescript/module/ui/backgrounds/variants/deepField.d.ts +65 -0
  146. package/lib/typescript/module/ui/backgrounds/variants/deepField.d.ts.map +1 -0
  147. package/lib/typescript/module/ui/backgrounds/variants/lattice.d.ts +33 -0
  148. package/lib/typescript/module/ui/backgrounds/variants/lattice.d.ts.map +1 -0
  149. package/lib/typescript/module/ui/backgrounds/variants/starfield.d.ts +12 -0
  150. package/lib/typescript/module/ui/backgrounds/variants/starfield.d.ts.map +1 -0
  151. package/lib/typescript/module/ui/index.d.ts +1 -0
  152. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  153. package/package.json +4 -4
  154. package/scripts/detect-safe-area.js +0 -20
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.TIER_LIMITS = void 0;
7
+ exports.freeKeyRaises = freeKeyRaises;
8
+ exports.resolveEffectiveTier = resolveEffectiveTier;
9
+ exports.useLimit = useLimit;
10
+ var _licenseModule = require("./licenseModule.js");
11
+ /**
12
+ * Per-tier numeric limits — the ONLY thing that separates anonymous from free.
13
+ *
14
+ * Every binary Pro gate (copy-to-clipboard, state editing, storage mutations,
15
+ * navigation) is identical for both: locked. That keeps the tier split to one
16
+ * table instead of scattered conditionals.
17
+ *
18
+ * The governing rule: **the free column never moves.** Anonymous is carved out
19
+ * BELOW today's free tier — nothing is ever added to free in exchange for an
20
+ * account, because that would come straight out of the Pro upgrade.
21
+ */
22
+
23
+ const LIMITS = {
24
+ // ── network ───────────────────────────────────────────────────────────────
25
+ "network.requests": {
26
+ anonymous: 5,
27
+ free: 25,
28
+ pro: Infinity
29
+ },
30
+ "network.saved": {
31
+ anonymous: 1,
32
+ free: 5,
33
+ pro: 50
34
+ },
35
+ // Already 1 for free, so 25% would be 0 — which deletes the feature rather
36
+ // than trialling it. Raising the FREE value to make room is not an option.
37
+ "network.overrideRules": {
38
+ anonymous: 1,
39
+ free: 1,
40
+ pro: 50
41
+ },
42
+ // ── storage ───────────────────────────────────────────────────────────────
43
+ "storage.events": {
44
+ anonymous: 5,
45
+ free: 25,
46
+ pro: Infinity
47
+ },
48
+ "storage.keys": {
49
+ anonymous: 5,
50
+ free: 25,
51
+ pro: Infinity
52
+ },
53
+ // Same floor-at-1 reasoning as network.overrideRules.
54
+ "storage.mmkvInstances": {
55
+ anonymous: 1,
56
+ free: 1,
57
+ pro: Infinity
58
+ },
59
+ // ── state tools ───────────────────────────────────────────────────────────
60
+ "redux.actions": {
61
+ anonymous: 5,
62
+ free: 25,
63
+ pro: Infinity
64
+ },
65
+ "zustand.changes": {
66
+ anonymous: 5,
67
+ free: 25,
68
+ pro: Infinity
69
+ },
70
+ "jotai.changes": {
71
+ anonymous: 5,
72
+ free: 25,
73
+ pro: Infinity
74
+ },
75
+ "events.entries": {
76
+ anonymous: 5,
77
+ free: 25,
78
+ pro: Infinity
79
+ },
80
+ // ── render / navigation ───────────────────────────────────────────────────
81
+ "highlightUpdates.components": {
82
+ anonymous: 5,
83
+ free: 25,
84
+ pro: Infinity
85
+ },
86
+ "highlightUpdates.historyEvents": {
87
+ anonymous: 1,
88
+ free: 3,
89
+ pro: Infinity
90
+ },
91
+ "highlightUpdates.detailEvents": {
92
+ anonymous: 2,
93
+ free: 5,
94
+ pro: Infinity
95
+ },
96
+ "routeEvents.entries": {
97
+ anonymous: 1,
98
+ free: 3,
99
+ pro: Infinity
100
+ },
101
+ "timeMachine.snapshots": {
102
+ anonymous: 1,
103
+ free: 2,
104
+ pro: Infinity
105
+ },
106
+ "perfMonitor.automationCases": {
107
+ anonymous: 1,
108
+ free: 3,
109
+ pro: Infinity
110
+ }
111
+ };
112
+ const TIER_LIMITS = exports.TIER_LIMITS = LIMITS;
113
+
114
+ /**
115
+ * The tier whose limits actually apply.
116
+ *
117
+ * `hasProAccess` is `useIsPro()`, which is true for a real Pro license OR the
118
+ * free Weekend Pass. Routing the Weekend Pass through here is what keeps
119
+ * today's behavior intact: weekend users get Pro limits, exactly as they do
120
+ * now. When the Weekend Pass is later gated to key-holders, that change lands
121
+ * here for free — no limit call site has to know about it.
122
+ */
123
+ function resolveEffectiveTier(tier, hasProAccess) {
124
+ if (hasProAccess) return "pro";
125
+ // The switch lives in @buoy-gg/license so that this and the Weekend Pass
126
+ // change flip together — see isAnonymousTierEnabled().
127
+ if (!(0, _licenseModule.isAnonymousTierEnabled)() && tier === "anonymous") return "free";
128
+ return tier;
129
+ }
130
+
131
+ /**
132
+ * The limit for the current tier.
133
+ *
134
+ * Replaces the per-package `const FREE_TIER_X = 25` + `isPro ? Infinity : X`
135
+ * pattern. `Infinity` is returned for uncapped tiers and is safe to use
136
+ * directly in `slice(0, limit)` and `length >= limit` comparisons.
137
+ */
138
+ function useLimit(key) {
139
+ const hasProAccess = (0, _licenseModule.getUseIsPro)()();
140
+ const tier = (0, _licenseModule.getUseTier)()();
141
+ return TIER_LIMITS[key][resolveEffectiveTier(tier, hasProAccess)];
142
+ }
143
+
144
+ /**
145
+ * Whether claiming a FREE key would actually raise this limit right now.
146
+ *
147
+ * The anonymous upsell must never promise something a free key won't deliver.
148
+ * Two ways it could lie, and this closes both:
149
+ *
150
+ * - **Pro-exclusive features.** Copy-to-clipboard, state editing, storage
151
+ * mutations and navigation are locked for anonymous AND free alike. Telling
152
+ * an anonymous user a free key opens them would be false.
153
+ * - **Limits that are already equal.** Override rules and MMKV instances sit
154
+ * at 1 for both tiers, so a key changes nothing there either.
155
+ *
156
+ * It also respects `ANONYMOUS_TIER_ENABLED`: while the anonymous tier is off,
157
+ * an anonymous user is *already* getting the free limits, so there is nothing
158
+ * to claim and this correctly returns false everywhere. That falls out of
159
+ * asking `resolveEffectiveTier` rather than reading the table directly, so the
160
+ * switch and the copy can never disagree.
161
+ */
162
+ function freeKeyRaises(key) {
163
+ const limits = TIER_LIMITS[key];
164
+ return limits[resolveEffectiveTier("anonymous", false)] < limits[resolveEffectiveTier("free", false)];
165
+ }
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.BackgroundSwitcher = void 0;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _lucideIcons = require("../../icons/lucide-icons.js");
10
+ var _macOSDesignSystemColors = require("../gameUI/constants/macOSDesignSystemColors.js");
11
+ var _backgroundStore = require("./backgroundStore.js");
12
+ var _registry = require("./registry.js");
13
+ var _jsxRuntime = require("react/jsx-runtime");
14
+ /**
15
+ * The ‹ prev / next › strip for auditioning backgrounds.
16
+ *
17
+ * This is a REVIEW control, not shipped chrome: it exists so the whole
18
+ * catalogue can be walked without a rebuild, with the cost of each preset shown
19
+ * next to its name. Mount it at the top of a tool's body while evaluating, and
20
+ * pull it once a winner is picked (or keep it behind a setting).
21
+ *
22
+ * It writes to the shared `backgroundStore`, so stepping here changes the
23
+ * background in every tool at once.
24
+ */
25
+
26
+ const BackgroundSwitcher = exports.BackgroundSwitcher = /*#__PURE__*/(0, _react.memo)(function BackgroundSwitcher() {
27
+ const {
28
+ id,
29
+ index,
30
+ count,
31
+ motion,
32
+ next,
33
+ previous,
34
+ setMotion
35
+ } = (0, _backgroundStore.useToolBackground)();
36
+ const preset = (0, _registry.getPreset)(id);
37
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
38
+ style: styles.root,
39
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
40
+ onPress: previous,
41
+ style: styles.step,
42
+ hitSlop: HIT_SLOP,
43
+ accessibilityLabel: "Previous background",
44
+ "sentry-label": "ignore background previous",
45
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_lucideIcons.ChevronLeft, {
46
+ size: 16,
47
+ color: _macOSDesignSystemColors.macOSColors.text.secondary
48
+ })
49
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
50
+ style: styles.center,
51
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
52
+ style: styles.titleRow,
53
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_lucideIcons.Palette, {
54
+ size: 11,
55
+ color: _macOSDesignSystemColors.macOSColors.semantic.info
56
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
57
+ style: styles.title,
58
+ numberOfLines: 1,
59
+ children: preset.label
60
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
61
+ style: styles.counter,
62
+ children: [index + 1, "/", count]
63
+ })]
64
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
65
+ style: styles.blurb,
66
+ numberOfLines: 1,
67
+ children: preset.blurb
68
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
69
+ style: styles.cost,
70
+ numberOfLines: 1,
71
+ children: [preset.nodes === 0 ? "0 animated nodes" : `${preset.nodes} native node${preset.nodes === 1 ? "" : "s"}`, " · ", "~", preset.views, " static views"]
72
+ })]
73
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
74
+ onPress: () => setMotion(!motion),
75
+ style: [styles.motion, motion ? styles.motionOn : null],
76
+ hitSlop: HIT_SLOP,
77
+ accessibilityLabel: motion ? "Turn motion off" : "Turn motion on",
78
+ "sentry-label": "ignore background motion",
79
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
80
+ style: [styles.motionText, motion ? styles.motionTextOn : null],
81
+ children: motion ? "MOTION" : "STATIC"
82
+ })
83
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
84
+ onPress: next,
85
+ style: styles.step,
86
+ hitSlop: HIT_SLOP,
87
+ accessibilityLabel: "Next background",
88
+ "sentry-label": "ignore background next",
89
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_lucideIcons.ChevronRight, {
90
+ size: 16,
91
+ color: _macOSDesignSystemColors.macOSColors.text.secondary
92
+ })
93
+ })]
94
+ });
95
+ });
96
+ const HIT_SLOP = {
97
+ top: 8,
98
+ bottom: 8,
99
+ left: 8,
100
+ right: 8
101
+ };
102
+ const styles = _reactNative.StyleSheet.create({
103
+ root: {
104
+ flexDirection: "row",
105
+ alignItems: "center",
106
+ gap: 8,
107
+ paddingHorizontal: 8,
108
+ paddingVertical: 6,
109
+ marginHorizontal: 8,
110
+ marginTop: 6,
111
+ marginBottom: 2,
112
+ borderRadius: 10,
113
+ borderWidth: 1,
114
+ borderColor: _macOSDesignSystemColors.macOSColors.border.default,
115
+ // Deliberately translucent: the strip sits ON the background it is
116
+ // previewing, so you can see the field through it.
117
+ backgroundColor: "rgba(26,26,28,0.72)"
118
+ },
119
+ step: {
120
+ width: 28,
121
+ height: 28,
122
+ borderRadius: 8,
123
+ alignItems: "center",
124
+ justifyContent: "center",
125
+ backgroundColor: "rgba(38,38,42,0.8)",
126
+ borderWidth: 1,
127
+ borderColor: _macOSDesignSystemColors.macOSColors.border.default
128
+ },
129
+ center: {
130
+ flex: 1,
131
+ minWidth: 0
132
+ },
133
+ titleRow: {
134
+ flexDirection: "row",
135
+ alignItems: "center",
136
+ gap: 5
137
+ },
138
+ title: {
139
+ color: _macOSDesignSystemColors.macOSColors.text.primary,
140
+ fontSize: 12,
141
+ fontWeight: "600"
142
+ },
143
+ counter: {
144
+ color: _macOSDesignSystemColors.macOSColors.text.muted,
145
+ fontSize: 10,
146
+ fontVariant: ["tabular-nums"]
147
+ },
148
+ blurb: {
149
+ color: _macOSDesignSystemColors.macOSColors.text.secondary,
150
+ fontSize: 10,
151
+ marginTop: 1
152
+ },
153
+ cost: {
154
+ color: _macOSDesignSystemColors.macOSColors.text.icon,
155
+ fontSize: 9,
156
+ marginTop: 1,
157
+ fontVariant: ["tabular-nums"]
158
+ },
159
+ motion: {
160
+ paddingHorizontal: 7,
161
+ paddingVertical: 4,
162
+ borderRadius: 6,
163
+ borderWidth: 1,
164
+ borderColor: _macOSDesignSystemColors.macOSColors.border.default,
165
+ backgroundColor: "rgba(38,38,42,0.8)"
166
+ },
167
+ motionOn: {
168
+ borderColor: _macOSDesignSystemColors.macOSColors.semantic.info + "55",
169
+ backgroundColor: _macOSDesignSystemColors.macOSColors.semantic.infoBackground
170
+ },
171
+ motionText: {
172
+ fontSize: 9,
173
+ fontWeight: "700",
174
+ letterSpacing: 0.6,
175
+ color: _macOSDesignSystemColors.macOSColors.text.muted
176
+ },
177
+ motionTextOn: {
178
+ color: _macOSDesignSystemColors.macOSColors.semantic.info
179
+ }
180
+ });
@@ -0,0 +1,115 @@
1
+ # Tool backgrounds
2
+
3
+ Animated and static backdrops for Buoy tool surfaces. Drop `<ToolBackground />`
4
+ in as the first child of a tool's body container; everything after it draws on
5
+ top.
6
+
7
+ ```tsx
8
+ <View style={styles.container}>
9
+ <ToolBackground />
10
+ {/* screens */}
11
+ </View>
12
+ ```
13
+
14
+ The preset is a single shared, persisted choice (`backgroundStore`), so
15
+ switching it in one tool switches it everywhere. `<BackgroundSwitcher />` is the
16
+ ‹ prev / next › review strip used to audition the catalogue.
17
+
18
+ ## The two families
19
+
20
+ - **Starfield** (`variants/starfield.tsx`) — the cheap scenic preset: 99 stars
21
+ quantised into five depth bands.
22
+ - **Deep Field** (`variants/deepField.tsx`) — the Everlights
23
+ `ParallaxDriftBackground`, ported 1:1: same seed and generator call order,
24
+ same 160 stars, same far-plane bias, sizes, palette and drift. Speed is
25
+ quantised into 18 bands (a distinct speed costs a distinct animated node);
26
+ everything else stays per-star. Roughly twice Starfield's cost — the file
27
+ documents exactly what could and couldn't come across from Skia.
28
+ - **The lattice family** (`variants/lattice.tsx`) — Hex Mesh, Iso Grid,
29
+ Crosshatch, Perf Board, Halftone, Waffle, Instrument, Radar. Quiet technical
30
+ geometry: near-black base, one soft off-centre glow, a rasterized hairline
31
+ pattern at low alpha, a slow breath, a vignette. They read as instrument
32
+ panels and drafting paper rather than scenery, which is what keeps them
33
+ legible under dense tool UI. `LatticeShell` owns everything but the geometry,
34
+ so the family stays a family.
35
+
36
+ ## The constraint
37
+
38
+ Buoy's packages peer-depend on nothing but `react` and `react-native`. No Skia,
39
+ no Reanimated, no SVG, no gradient library — a devtool cannot force a native
40
+ dependency into someone's app. So every preset is plain `View`s plus `Animated`,
41
+ and the gradients and glows in `primitives.tsx` are faked by stacking
42
+ translucent layers. For the same reason nothing here uses `transformOrigin`
43
+ (RN 0.74+); end-pivot rotation is built out of layout instead — see Radar.
44
+
45
+ ## The performance contract
46
+
47
+ 1. **Every animation is native-driven.** `useNativeDriver: true` on a looping
48
+ `Animated.timing` hands the whole loop to the UI thread once and never
49
+ touches JS again — no per-frame callback, no re-render, no bridge traffic. A
50
+ preset animates a handful of *layers*, never per-particle.
51
+ 2. **Every static layer is rasterized.** `shouldRasterizeIOS` /
52
+ `renderToHardwareTextureAndroid` (see `RasterLayer`) flatten a layer of 40
53
+ dots into one cached GPU texture, so sliding it is a single composite.
54
+ 3. **Nothing recomputes.** Scatter tables are generated once at module load from
55
+ a fixed seed (`rng.ts`); presets only read them. `ToolBackground` buckets its
56
+ measured size to 8px so a resize drag rebuilds a handful of times, not
57
+ hundreds.
58
+
59
+ Loops stop when the app backgrounds, and unmount with the tool.
60
+
61
+ **Known trap:** `Animated.sequence([timing, Animated.loop(...)])` silently fails
62
+ to drive the value on the native driver — no warning, the field renders and
63
+ never moves. It's the obvious way to phase-offset siblings; don't. Stagger via
64
+ different periods, or bake the offset into the `interpolate`.
65
+
66
+ ### Measured
67
+
68
+ Network tool open on an iPhone 16 simulator, via perf-monitor and
69
+ `measure_renders`. Starfield is the heaviest preset (6 animated nodes, ~222
70
+ static views); the lattice presets are 1–2 nodes and 40–176 views.
71
+
72
+ **Steady state** — 12–13s idle recordings:
73
+
74
+ | | nodes | views | JS FPS | UI FPS | CPU avg | jank | render commits | render ms |
75
+ |---|---:|---:|---:|---:|---:|---:|---:|---:|
76
+ | Off | 0 | 1 | 60 | 60 | 23.65% | 0 | 3 | 0 |
77
+ | Starfield | 6 | 222 | 60 | 60 | 23.79% | 0 | 4 | 0 |
78
+ | Deep Field | 22 | 462 | 60 | 60 | 27.19% | 0 | 4 | 0 |
79
+
80
+ Starfield's 0.14pt is sample noise. **Deep Field's +3.5pt is not** — 22 nodes
81
+ driving 462 transformed views per frame is real UI-thread work, even native. It
82
+ still holds 60/60 with zero dropped frames, but it is the one preset here with a
83
+ measurable running cost, and that is the trade for matching the original.
84
+
85
+ The commits in every column are the network tool's own once-a-minute
86
+ `TickProvider`, not the background. A separate `measure_renders` idle sample
87
+ reported one component render in every configuration — the animation contributes
88
+ zero React work while it runs, which is rule 1 holding regardless of node count.
89
+
90
+ **Mount** — the cost that is real for all of them:
91
+
92
+ ```
93
+ Off → Starfield 348 renders / 3 commits · 13.3ms
94
+ StarBand 4.0ms ×5 mount ← 222 star views ≈ 4.5ms for the preset
95
+
96
+ Starfield → Deep Field 668 renders / 3 commits · 25.2ms
97
+ DeepBand 8.5ms ×18 mount ← 462 star views ≈ 9.6ms for the preset
98
+ TwinkleGroup 0.5ms ×4
99
+ RasterLayer 0.4ms ×18
100
+ ```
101
+
102
+ The balance in each is the switcher and its icons re-rendering. So: ~4.5ms and
103
+ ~9.6ms of React work respectively, once, when the tool opens — scaling with view
104
+ count, as expected. `registry.tsx` lists each preset's count so the trade is
105
+ visible while comparing.
106
+
107
+ ## Adding a preset
108
+
109
+ Add the component to `variants/`, then one entry in `registry.tsx` with an
110
+ honest `nodes` (animated nodes) and `views` (static views at ~380×620). The
111
+ switcher shows both, which is the point — it's a review tool, not decoration.
112
+
113
+ Alpha calibration: Hex Mesh is the reference. Anything fainter than its lattice
114
+ disappears under the vignette — hairline strokes in particular need ~0.2 alpha
115
+ or a second pixel of height to register at all.
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ToolBackground = void 0;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _absoluteFill = require("../../utils/absoluteFill.js");
10
+ var _backgroundStore = require("./backgroundStore.js");
11
+ var _registry = require("./registry.js");
12
+ var _jsxRuntime = require("react/jsx-runtime");
13
+ /**
14
+ * Drop-in animated backdrop for a Buoy tool surface.
15
+ *
16
+ * Mount it as the FIRST child of a tool's body container and everything after
17
+ * it draws on top:
18
+ *
19
+ * <View style={styles.container}>
20
+ * <ToolBackground />
21
+ * ...screens...
22
+ * </View>
23
+ *
24
+ * It is `position: absolute` + `pointerEvents: "none"`, so it never affects
25
+ * layout and never eats a touch. The preset comes from the shared persisted
26
+ * selection (`backgroundStore`), so switching it in one tool switches it in all
27
+ * of them.
28
+ */
29
+
30
+ /**
31
+ * Round the measured size into 8px buckets. Presets lay out from width/height,
32
+ * so an unbucketed measurement would rebuild the whole field on every frame of
33
+ * a modal resize drag; at this granularity nobody can see the difference and a
34
+ * drag costs a handful of rebuilds instead of hundreds.
35
+ */
36
+ const bucket = n => Math.max(0, Math.ceil(n / 8) * 8);
37
+ const ToolBackground = exports.ToolBackground = /*#__PURE__*/(0, _react.memo)(function ToolBackground({
38
+ id,
39
+ motion,
40
+ opacity = 1
41
+ }) {
42
+ const shared = (0, _backgroundStore.useToolBackground)();
43
+ const [size, setSize] = (0, _react.useState)({
44
+ width: 0,
45
+ height: 0
46
+ });
47
+ const onLayout = (0, _react.useCallback)(event => {
48
+ const {
49
+ width,
50
+ height
51
+ } = event.nativeEvent.layout;
52
+ const next = {
53
+ width: bucket(width),
54
+ height: bucket(height)
55
+ };
56
+ setSize(prev => prev.width === next.width && prev.height === next.height ? prev : next);
57
+ }, []);
58
+ const preset = (0, _registry.getPreset)(id ?? shared.id);
59
+ const animated = motion ?? shared.motion;
60
+ const Variant = preset.Component;
61
+ const ready = size.width > 0 && size.height > 0;
62
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
63
+ style: [_absoluteFill.absoluteFill, opacity === 1 ? null : {
64
+ opacity
65
+ }],
66
+ pointerEvents: "none",
67
+ onLayout: onLayout,
68
+ children: Variant && ready ? /*#__PURE__*/(0, _jsxRuntime.jsx)(Variant, {
69
+ width: size.width,
70
+ height: size.height,
71
+ animated: animated
72
+ }) : null
73
+ });
74
+ });
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.backgroundStore = void 0;
7
+ exports.useToolBackground = useToolBackground;
8
+ var _react = require("react");
9
+ var _devToolsStorageKeys = require("../../storage/devToolsStorageKeys.js");
10
+ var _persistentStorage = require("../../utils/persistentStorage.js");
11
+ var _registry = require("./registry.js");
12
+ /**
13
+ * The persisted background selection, shared by every tool.
14
+ *
15
+ * A singleton rather than context so a tool anywhere in the tree — modal,
16
+ * inline desktop panel, bottom sheet — reads the same choice without a provider
17
+ * having to wrap it. Same shape as `ignoredPatternsStore`: subscribe/notify,
18
+ * best-effort persistence, a dirty flag so a slow initial read can't clobber a
19
+ * choice the user already made.
20
+ */
21
+
22
+ const KEY = `${_devToolsStorageKeys.devToolsStorageKeys.base}_tool_background`;
23
+ function isValidId(value) {
24
+ return typeof value === "string" && _registry.BACKGROUND_PRESETS.some(p => p.id === value);
25
+ }
26
+ class BackgroundStore {
27
+ id = _registry.DEFAULT_BACKGROUND;
28
+ motion = true;
29
+ listeners = new Set();
30
+ loadPromise = null;
31
+ dirty = false;
32
+ getId() {
33
+ return this.id;
34
+ }
35
+ getMotion() {
36
+ return this.motion;
37
+ }
38
+ subscribe(listener) {
39
+ this.listeners.add(listener);
40
+ void this.load();
41
+ return () => {
42
+ this.listeners.delete(listener);
43
+ };
44
+ }
45
+ notify() {
46
+ for (const listener of this.listeners) listener();
47
+ }
48
+ load() {
49
+ if (this.loadPromise) return this.loadPromise;
50
+ this.loadPromise = (async () => {
51
+ try {
52
+ const raw = await _persistentStorage.persistentStorage.getItem(KEY);
53
+ if (!raw || this.dirty) return;
54
+ const parsed = JSON.parse(raw);
55
+ if (isValidId(parsed.id)) this.id = parsed.id;
56
+ if (typeof parsed.motion === "boolean") this.motion = parsed.motion;
57
+ this.notify();
58
+ } catch {
59
+ // A missing or corrupt entry just means "use the default".
60
+ }
61
+ })();
62
+ return this.loadPromise;
63
+ }
64
+ persist() {
65
+ this.dirty = true;
66
+ const payload = {
67
+ id: this.id,
68
+ motion: this.motion
69
+ };
70
+ void _persistentStorage.persistentStorage.setItem(KEY, JSON.stringify(payload)).catch(() => {});
71
+ }
72
+ setId(id) {
73
+ if (this.id === id) return;
74
+ this.id = id;
75
+ this.persist();
76
+ this.notify();
77
+ }
78
+
79
+ /** Step through the catalogue; `delta` is +1 (next) or -1 (previous). */
80
+ step(delta) {
81
+ this.setId((0, _registry.presetAt)((0, _registry.presetIndex)(this.id) + delta).id);
82
+ }
83
+ setMotion(motion) {
84
+ if (this.motion === motion) return;
85
+ this.motion = motion;
86
+ this.persist();
87
+ this.notify();
88
+ }
89
+ }
90
+ const backgroundStore = exports.backgroundStore = new BackgroundStore();
91
+ /** Subscribe a component to the current background selection. */
92
+ function useToolBackground() {
93
+ const [id, setLocalId] = (0, _react.useState)(() => backgroundStore.getId());
94
+ const [motion, setLocalMotion] = (0, _react.useState)(() => backgroundStore.getMotion());
95
+ (0, _react.useEffect)(() => backgroundStore.subscribe(() => {
96
+ setLocalId(backgroundStore.getId());
97
+ setLocalMotion(backgroundStore.getMotion());
98
+ }), []);
99
+ return {
100
+ id,
101
+ motion,
102
+ index: (0, _registry.presetIndex)(id),
103
+ count: _registry.BACKGROUND_PRESETS.length,
104
+ setId: setId,
105
+ next: next,
106
+ previous: previous,
107
+ setMotion: setMotion
108
+ };
109
+ }
110
+
111
+ // Module-scope identities so the returned object's callbacks never change and
112
+ // consumers can hand them straight to a memoized button.
113
+ const setId = id => backgroundStore.setId(id);
114
+ const next = () => backgroundStore.step(1);
115
+ const previous = () => backgroundStore.step(-1);
116
+ const setMotion = motion => backgroundStore.setMotion(motion);