@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,158 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Per-tier numeric limits — the ONLY thing that separates anonymous from free.
5
+ *
6
+ * Every binary Pro gate (copy-to-clipboard, state editing, storage mutations,
7
+ * navigation) is identical for both: locked. That keeps the tier split to one
8
+ * table instead of scattered conditionals.
9
+ *
10
+ * The governing rule: **the free column never moves.** Anonymous is carved out
11
+ * BELOW today's free tier — nothing is ever added to free in exchange for an
12
+ * account, because that would come straight out of the Pro upgrade.
13
+ */
14
+
15
+ import { getUseIsPro, getUseTier, isAnonymousTierEnabled } from "./licenseModule.js";
16
+ const LIMITS = {
17
+ // ── network ───────────────────────────────────────────────────────────────
18
+ "network.requests": {
19
+ anonymous: 5,
20
+ free: 25,
21
+ pro: Infinity
22
+ },
23
+ "network.saved": {
24
+ anonymous: 1,
25
+ free: 5,
26
+ pro: 50
27
+ },
28
+ // Already 1 for free, so 25% would be 0 — which deletes the feature rather
29
+ // than trialling it. Raising the FREE value to make room is not an option.
30
+ "network.overrideRules": {
31
+ anonymous: 1,
32
+ free: 1,
33
+ pro: 50
34
+ },
35
+ // ── storage ───────────────────────────────────────────────────────────────
36
+ "storage.events": {
37
+ anonymous: 5,
38
+ free: 25,
39
+ pro: Infinity
40
+ },
41
+ "storage.keys": {
42
+ anonymous: 5,
43
+ free: 25,
44
+ pro: Infinity
45
+ },
46
+ // Same floor-at-1 reasoning as network.overrideRules.
47
+ "storage.mmkvInstances": {
48
+ anonymous: 1,
49
+ free: 1,
50
+ pro: Infinity
51
+ },
52
+ // ── state tools ───────────────────────────────────────────────────────────
53
+ "redux.actions": {
54
+ anonymous: 5,
55
+ free: 25,
56
+ pro: Infinity
57
+ },
58
+ "zustand.changes": {
59
+ anonymous: 5,
60
+ free: 25,
61
+ pro: Infinity
62
+ },
63
+ "jotai.changes": {
64
+ anonymous: 5,
65
+ free: 25,
66
+ pro: Infinity
67
+ },
68
+ "events.entries": {
69
+ anonymous: 5,
70
+ free: 25,
71
+ pro: Infinity
72
+ },
73
+ // ── render / navigation ───────────────────────────────────────────────────
74
+ "highlightUpdates.components": {
75
+ anonymous: 5,
76
+ free: 25,
77
+ pro: Infinity
78
+ },
79
+ "highlightUpdates.historyEvents": {
80
+ anonymous: 1,
81
+ free: 3,
82
+ pro: Infinity
83
+ },
84
+ "highlightUpdates.detailEvents": {
85
+ anonymous: 2,
86
+ free: 5,
87
+ pro: Infinity
88
+ },
89
+ "routeEvents.entries": {
90
+ anonymous: 1,
91
+ free: 3,
92
+ pro: Infinity
93
+ },
94
+ "timeMachine.snapshots": {
95
+ anonymous: 1,
96
+ free: 2,
97
+ pro: Infinity
98
+ },
99
+ "perfMonitor.automationCases": {
100
+ anonymous: 1,
101
+ free: 3,
102
+ pro: Infinity
103
+ }
104
+ };
105
+ export const TIER_LIMITS = LIMITS;
106
+
107
+ /**
108
+ * The tier whose limits actually apply.
109
+ *
110
+ * `hasProAccess` is `useIsPro()`, which is true for a real Pro license OR the
111
+ * free Weekend Pass. Routing the Weekend Pass through here is what keeps
112
+ * today's behavior intact: weekend users get Pro limits, exactly as they do
113
+ * now. When the Weekend Pass is later gated to key-holders, that change lands
114
+ * here for free — no limit call site has to know about it.
115
+ */
116
+ export function resolveEffectiveTier(tier, hasProAccess) {
117
+ if (hasProAccess) return "pro";
118
+ // The switch lives in @buoy-gg/license so that this and the Weekend Pass
119
+ // change flip together — see isAnonymousTierEnabled().
120
+ if (!isAnonymousTierEnabled() && tier === "anonymous") return "free";
121
+ return tier;
122
+ }
123
+
124
+ /**
125
+ * The limit for the current tier.
126
+ *
127
+ * Replaces the per-package `const FREE_TIER_X = 25` + `isPro ? Infinity : X`
128
+ * pattern. `Infinity` is returned for uncapped tiers and is safe to use
129
+ * directly in `slice(0, limit)` and `length >= limit` comparisons.
130
+ */
131
+ export function useLimit(key) {
132
+ const hasProAccess = getUseIsPro()();
133
+ const tier = getUseTier()();
134
+ return TIER_LIMITS[key][resolveEffectiveTier(tier, hasProAccess)];
135
+ }
136
+
137
+ /**
138
+ * Whether claiming a FREE key would actually raise this limit right now.
139
+ *
140
+ * The anonymous upsell must never promise something a free key won't deliver.
141
+ * Two ways it could lie, and this closes both:
142
+ *
143
+ * - **Pro-exclusive features.** Copy-to-clipboard, state editing, storage
144
+ * mutations and navigation are locked for anonymous AND free alike. Telling
145
+ * an anonymous user a free key opens them would be false.
146
+ * - **Limits that are already equal.** Override rules and MMKV instances sit
147
+ * at 1 for both tiers, so a key changes nothing there either.
148
+ *
149
+ * It also respects `ANONYMOUS_TIER_ENABLED`: while the anonymous tier is off,
150
+ * an anonymous user is *already* getting the free limits, so there is nothing
151
+ * to claim and this correctly returns false everywhere. That falls out of
152
+ * asking `resolveEffectiveTier` rather than reading the table directly, so the
153
+ * switch and the copy can never disagree.
154
+ */
155
+ export function freeKeyRaises(key) {
156
+ const limits = TIER_LIMITS[key];
157
+ return limits[resolveEffectiveTier("anonymous", false)] < limits[resolveEffectiveTier("free", false)];
158
+ }
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The ‹ prev / next › strip for auditioning backgrounds.
5
+ *
6
+ * This is a REVIEW control, not shipped chrome: it exists so the whole
7
+ * catalogue can be walked without a rebuild, with the cost of each preset shown
8
+ * next to its name. Mount it at the top of a tool's body while evaluating, and
9
+ * pull it once a winner is picked (or keep it behind a setting).
10
+ *
11
+ * It writes to the shared `backgroundStore`, so stepping here changes the
12
+ * background in every tool at once.
13
+ */
14
+
15
+ import { memo } from "react";
16
+ import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
17
+ import { ChevronLeft, ChevronRight, Palette } from "../../icons/lucide-icons.js";
18
+ import { macOSColors } from "../gameUI/constants/macOSDesignSystemColors.js";
19
+ import { useToolBackground } from "./backgroundStore.js";
20
+ import { getPreset } from "./registry.js";
21
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22
+ export const BackgroundSwitcher = /*#__PURE__*/memo(function BackgroundSwitcher() {
23
+ const {
24
+ id,
25
+ index,
26
+ count,
27
+ motion,
28
+ next,
29
+ previous,
30
+ setMotion
31
+ } = useToolBackground();
32
+ const preset = getPreset(id);
33
+ return /*#__PURE__*/_jsxs(View, {
34
+ style: styles.root,
35
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
36
+ onPress: previous,
37
+ style: styles.step,
38
+ hitSlop: HIT_SLOP,
39
+ accessibilityLabel: "Previous background",
40
+ "sentry-label": "ignore background previous",
41
+ children: /*#__PURE__*/_jsx(ChevronLeft, {
42
+ size: 16,
43
+ color: macOSColors.text.secondary
44
+ })
45
+ }), /*#__PURE__*/_jsxs(View, {
46
+ style: styles.center,
47
+ children: [/*#__PURE__*/_jsxs(View, {
48
+ style: styles.titleRow,
49
+ children: [/*#__PURE__*/_jsx(Palette, {
50
+ size: 11,
51
+ color: macOSColors.semantic.info
52
+ }), /*#__PURE__*/_jsx(Text, {
53
+ style: styles.title,
54
+ numberOfLines: 1,
55
+ children: preset.label
56
+ }), /*#__PURE__*/_jsxs(Text, {
57
+ style: styles.counter,
58
+ children: [index + 1, "/", count]
59
+ })]
60
+ }), /*#__PURE__*/_jsx(Text, {
61
+ style: styles.blurb,
62
+ numberOfLines: 1,
63
+ children: preset.blurb
64
+ }), /*#__PURE__*/_jsxs(Text, {
65
+ style: styles.cost,
66
+ numberOfLines: 1,
67
+ children: [preset.nodes === 0 ? "0 animated nodes" : `${preset.nodes} native node${preset.nodes === 1 ? "" : "s"}`, " · ", "~", preset.views, " static views"]
68
+ })]
69
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
70
+ onPress: () => setMotion(!motion),
71
+ style: [styles.motion, motion ? styles.motionOn : null],
72
+ hitSlop: HIT_SLOP,
73
+ accessibilityLabel: motion ? "Turn motion off" : "Turn motion on",
74
+ "sentry-label": "ignore background motion",
75
+ children: /*#__PURE__*/_jsx(Text, {
76
+ style: [styles.motionText, motion ? styles.motionTextOn : null],
77
+ children: motion ? "MOTION" : "STATIC"
78
+ })
79
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
80
+ onPress: next,
81
+ style: styles.step,
82
+ hitSlop: HIT_SLOP,
83
+ accessibilityLabel: "Next background",
84
+ "sentry-label": "ignore background next",
85
+ children: /*#__PURE__*/_jsx(ChevronRight, {
86
+ size: 16,
87
+ color: macOSColors.text.secondary
88
+ })
89
+ })]
90
+ });
91
+ });
92
+ const HIT_SLOP = {
93
+ top: 8,
94
+ bottom: 8,
95
+ left: 8,
96
+ right: 8
97
+ };
98
+ const styles = StyleSheet.create({
99
+ root: {
100
+ flexDirection: "row",
101
+ alignItems: "center",
102
+ gap: 8,
103
+ paddingHorizontal: 8,
104
+ paddingVertical: 6,
105
+ marginHorizontal: 8,
106
+ marginTop: 6,
107
+ marginBottom: 2,
108
+ borderRadius: 10,
109
+ borderWidth: 1,
110
+ borderColor: macOSColors.border.default,
111
+ // Deliberately translucent: the strip sits ON the background it is
112
+ // previewing, so you can see the field through it.
113
+ backgroundColor: "rgba(26,26,28,0.72)"
114
+ },
115
+ step: {
116
+ width: 28,
117
+ height: 28,
118
+ borderRadius: 8,
119
+ alignItems: "center",
120
+ justifyContent: "center",
121
+ backgroundColor: "rgba(38,38,42,0.8)",
122
+ borderWidth: 1,
123
+ borderColor: macOSColors.border.default
124
+ },
125
+ center: {
126
+ flex: 1,
127
+ minWidth: 0
128
+ },
129
+ titleRow: {
130
+ flexDirection: "row",
131
+ alignItems: "center",
132
+ gap: 5
133
+ },
134
+ title: {
135
+ color: macOSColors.text.primary,
136
+ fontSize: 12,
137
+ fontWeight: "600"
138
+ },
139
+ counter: {
140
+ color: macOSColors.text.muted,
141
+ fontSize: 10,
142
+ fontVariant: ["tabular-nums"]
143
+ },
144
+ blurb: {
145
+ color: macOSColors.text.secondary,
146
+ fontSize: 10,
147
+ marginTop: 1
148
+ },
149
+ cost: {
150
+ color: macOSColors.text.icon,
151
+ fontSize: 9,
152
+ marginTop: 1,
153
+ fontVariant: ["tabular-nums"]
154
+ },
155
+ motion: {
156
+ paddingHorizontal: 7,
157
+ paddingVertical: 4,
158
+ borderRadius: 6,
159
+ borderWidth: 1,
160
+ borderColor: macOSColors.border.default,
161
+ backgroundColor: "rgba(38,38,42,0.8)"
162
+ },
163
+ motionOn: {
164
+ borderColor: macOSColors.semantic.info + "55",
165
+ backgroundColor: macOSColors.semantic.infoBackground
166
+ },
167
+ motionText: {
168
+ fontSize: 9,
169
+ fontWeight: "700",
170
+ letterSpacing: 0.6,
171
+ color: macOSColors.text.muted
172
+ },
173
+ motionTextOn: {
174
+ color: macOSColors.semantic.info
175
+ }
176
+ });
@@ -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,70 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Drop-in animated backdrop for a Buoy tool surface.
5
+ *
6
+ * Mount it as the FIRST child of a tool's body container and everything after
7
+ * it draws on top:
8
+ *
9
+ * <View style={styles.container}>
10
+ * <ToolBackground />
11
+ * ...screens...
12
+ * </View>
13
+ *
14
+ * It is `position: absolute` + `pointerEvents: "none"`, so it never affects
15
+ * layout and never eats a touch. The preset comes from the shared persisted
16
+ * selection (`backgroundStore`), so switching it in one tool switches it in all
17
+ * of them.
18
+ */
19
+
20
+ import { memo, useCallback, useState } from "react";
21
+ import { View } from "react-native";
22
+ import { absoluteFill } from "../../utils/absoluteFill.js";
23
+ import { useToolBackground } from "./backgroundStore.js";
24
+ import { getPreset } from "./registry.js";
25
+ import { jsx as _jsx } from "react/jsx-runtime";
26
+ /**
27
+ * Round the measured size into 8px buckets. Presets lay out from width/height,
28
+ * so an unbucketed measurement would rebuild the whole field on every frame of
29
+ * a modal resize drag; at this granularity nobody can see the difference and a
30
+ * drag costs a handful of rebuilds instead of hundreds.
31
+ */
32
+ const bucket = n => Math.max(0, Math.ceil(n / 8) * 8);
33
+ export const ToolBackground = /*#__PURE__*/memo(function ToolBackground({
34
+ id,
35
+ motion,
36
+ opacity = 1
37
+ }) {
38
+ const shared = useToolBackground();
39
+ const [size, setSize] = useState({
40
+ width: 0,
41
+ height: 0
42
+ });
43
+ const onLayout = useCallback(event => {
44
+ const {
45
+ width,
46
+ height
47
+ } = event.nativeEvent.layout;
48
+ const next = {
49
+ width: bucket(width),
50
+ height: bucket(height)
51
+ };
52
+ setSize(prev => prev.width === next.width && prev.height === next.height ? prev : next);
53
+ }, []);
54
+ const preset = getPreset(id ?? shared.id);
55
+ const animated = motion ?? shared.motion;
56
+ const Variant = preset.Component;
57
+ const ready = size.width > 0 && size.height > 0;
58
+ return /*#__PURE__*/_jsx(View, {
59
+ style: [absoluteFill, opacity === 1 ? null : {
60
+ opacity
61
+ }],
62
+ pointerEvents: "none",
63
+ onLayout: onLayout,
64
+ children: Variant && ready ? /*#__PURE__*/_jsx(Variant, {
65
+ width: size.width,
66
+ height: size.height,
67
+ animated: animated
68
+ }) : null
69
+ });
70
+ });
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The persisted background selection, shared by every tool.
5
+ *
6
+ * A singleton rather than context so a tool anywhere in the tree — modal,
7
+ * inline desktop panel, bottom sheet — reads the same choice without a provider
8
+ * having to wrap it. Same shape as `ignoredPatternsStore`: subscribe/notify,
9
+ * best-effort persistence, a dirty flag so a slow initial read can't clobber a
10
+ * choice the user already made.
11
+ */
12
+
13
+ import { useEffect, useState } from "react";
14
+ import { devToolsStorageKeys } from "../../storage/devToolsStorageKeys.js";
15
+ import { persistentStorage } from "../../utils/persistentStorage.js";
16
+ import { BACKGROUND_PRESETS, DEFAULT_BACKGROUND, presetAt, presetIndex } from "./registry.js";
17
+ const KEY = `${devToolsStorageKeys.base}_tool_background`;
18
+ function isValidId(value) {
19
+ return typeof value === "string" && BACKGROUND_PRESETS.some(p => p.id === value);
20
+ }
21
+ class BackgroundStore {
22
+ id = DEFAULT_BACKGROUND;
23
+ motion = true;
24
+ listeners = new Set();
25
+ loadPromise = null;
26
+ dirty = false;
27
+ getId() {
28
+ return this.id;
29
+ }
30
+ getMotion() {
31
+ return this.motion;
32
+ }
33
+ subscribe(listener) {
34
+ this.listeners.add(listener);
35
+ void this.load();
36
+ return () => {
37
+ this.listeners.delete(listener);
38
+ };
39
+ }
40
+ notify() {
41
+ for (const listener of this.listeners) listener();
42
+ }
43
+ load() {
44
+ if (this.loadPromise) return this.loadPromise;
45
+ this.loadPromise = (async () => {
46
+ try {
47
+ const raw = await persistentStorage.getItem(KEY);
48
+ if (!raw || this.dirty) return;
49
+ const parsed = JSON.parse(raw);
50
+ if (isValidId(parsed.id)) this.id = parsed.id;
51
+ if (typeof parsed.motion === "boolean") this.motion = parsed.motion;
52
+ this.notify();
53
+ } catch {
54
+ // A missing or corrupt entry just means "use the default".
55
+ }
56
+ })();
57
+ return this.loadPromise;
58
+ }
59
+ persist() {
60
+ this.dirty = true;
61
+ const payload = {
62
+ id: this.id,
63
+ motion: this.motion
64
+ };
65
+ void persistentStorage.setItem(KEY, JSON.stringify(payload)).catch(() => {});
66
+ }
67
+ setId(id) {
68
+ if (this.id === id) return;
69
+ this.id = id;
70
+ this.persist();
71
+ this.notify();
72
+ }
73
+
74
+ /** Step through the catalogue; `delta` is +1 (next) or -1 (previous). */
75
+ step(delta) {
76
+ this.setId(presetAt(presetIndex(this.id) + delta).id);
77
+ }
78
+ setMotion(motion) {
79
+ if (this.motion === motion) return;
80
+ this.motion = motion;
81
+ this.persist();
82
+ this.notify();
83
+ }
84
+ }
85
+ export const backgroundStore = new BackgroundStore();
86
+ /** Subscribe a component to the current background selection. */
87
+ export function useToolBackground() {
88
+ const [id, setLocalId] = useState(() => backgroundStore.getId());
89
+ const [motion, setLocalMotion] = useState(() => backgroundStore.getMotion());
90
+ useEffect(() => backgroundStore.subscribe(() => {
91
+ setLocalId(backgroundStore.getId());
92
+ setLocalMotion(backgroundStore.getMotion());
93
+ }), []);
94
+ return {
95
+ id,
96
+ motion,
97
+ index: presetIndex(id),
98
+ count: BACKGROUND_PRESETS.length,
99
+ setId: setId,
100
+ next: next,
101
+ previous: previous,
102
+ setMotion: setMotion
103
+ };
104
+ }
105
+
106
+ // Module-scope identities so the returned object's callbacks never change and
107
+ // consumers can hand them straight to a memoized button.
108
+ const setId = id => backgroundStore.setId(id);
109
+ const next = () => backgroundStore.step(1);
110
+ const previous = () => backgroundStore.step(-1);
111
+ const setMotion = motion => backgroundStore.setMotion(motion);
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ export { ToolBackground } from "./ToolBackground.js";
4
+ export { BackgroundSwitcher } from "./BackgroundSwitcher.js";
5
+ export { backgroundStore, useToolBackground } from "./backgroundStore.js";
6
+ export { BACKGROUND_PRESETS, DEFAULT_BACKGROUND, getPreset, presetAt, presetIndex } from "./registry.js";
7
+ // Exported for the bg-bench harness in example-dev-build, so every measured
8
+ // variant is the shipping component with knobs turned rather than a copy.
9
+ export { DeepFieldTunable } from "./variants/deepField.js";