@buoy-gg/shared-ui 7.0.26 → 7.0.28

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 (221) hide show
  1. package/lib/commonjs/JsModal.js +156 -20
  2. package/lib/commonjs/JsModal.web.js +85 -39
  3. package/lib/commonjs/dataViewer/VirtualizedDataExplorer.js +14 -4
  4. package/lib/commonjs/dataViewer/diffThemes.js +20 -18
  5. package/lib/commonjs/dataViewer/tree/TreeDiffViewer.js +8 -8
  6. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  7. package/lib/commonjs/icons/lucide-icons.js +74 -15
  8. package/lib/commonjs/index.js +86 -0
  9. package/lib/commonjs/ui/backgrounds/BackgroundSwitcher.js +248 -74
  10. package/lib/commonjs/ui/backgrounds/README.md +102 -20
  11. package/lib/commonjs/ui/backgrounds/backgroundStore.js +20 -6
  12. package/lib/commonjs/ui/backgrounds/index.js +91 -1
  13. package/lib/commonjs/ui/backgrounds/primitives.js +50 -14
  14. package/lib/commonjs/ui/backgrounds/registry.js +41 -64
  15. package/lib/commonjs/ui/backgrounds/rng.js +21 -0
  16. package/lib/commonjs/ui/backgrounds/skyClock.js +45 -0
  17. package/lib/commonjs/ui/backgrounds/variants/daylight.js +453 -0
  18. package/lib/commonjs/ui/backgrounds/variants/deepField.js +60 -30
  19. package/lib/commonjs/ui/backgrounds/variants/hyperspace.js +218 -0
  20. package/lib/commonjs/ui/backgrounds/variants/liveSky.js +898 -0
  21. package/lib/commonjs/ui/backgrounds/variants/meteors.js +257 -0
  22. package/lib/commonjs/ui/backgrounds/variants/moonlight.js +472 -0
  23. package/lib/commonjs/ui/backgrounds/variants/underwater.js +757 -0
  24. package/lib/commonjs/ui/components/Badge.js +11 -10
  25. package/lib/commonjs/ui/components/CompactRow.js +35 -18
  26. package/lib/commonjs/ui/components/CopyButton.js +6 -4
  27. package/lib/commonjs/ui/components/DevToolsCard.js +16 -9
  28. package/lib/commonjs/ui/components/DynamicFilterView.js +43 -41
  29. package/lib/commonjs/ui/components/EventStepperFooter.js +38 -10
  30. package/lib/commonjs/ui/components/FilterComponents.js +11 -11
  31. package/lib/commonjs/ui/components/ModalHeader.js +28 -15
  32. package/lib/commonjs/ui/components/SectionHeader.js +3 -1
  33. package/lib/commonjs/ui/components/StorageTypeBadge.js +3 -2
  34. package/lib/commonjs/ui/components/TabSelector.js +56 -38
  35. package/lib/commonjs/ui/components/ValueTypeBadge.js +7 -6
  36. package/lib/commonjs/ui/components/WindowControls.js +37 -10
  37. package/lib/commonjs/ui/gameUI/constants/gameUIColors.js +8 -0
  38. package/lib/commonjs/ui/night/nightTheme.js +120 -0
  39. package/lib/commonjs/ui/night/primitives.js +498 -0
  40. package/lib/module/JsModal.js +156 -20
  41. package/lib/module/JsModal.web.js +83 -39
  42. package/lib/module/dataViewer/VirtualizedDataExplorer.js +14 -4
  43. package/lib/module/dataViewer/diffThemes.js +20 -18
  44. package/lib/module/dataViewer/tree/TreeDiffViewer.js +9 -9
  45. package/lib/module/hooks/safe-area-impl.js +1 -1
  46. package/lib/module/icons/lucide-icons.js +74 -15
  47. package/lib/module/index.js +4 -0
  48. package/lib/module/ui/backgrounds/BackgroundSwitcher.js +250 -76
  49. package/lib/module/ui/backgrounds/README.md +102 -20
  50. package/lib/module/ui/backgrounds/backgroundStore.js +19 -5
  51. package/lib/module/ui/backgrounds/index.js +11 -2
  52. package/lib/module/ui/backgrounds/primitives.js +53 -16
  53. package/lib/module/ui/backgrounds/registry.js +41 -64
  54. package/lib/module/ui/backgrounds/rng.js +20 -0
  55. package/lib/module/ui/backgrounds/skyClock.js +39 -0
  56. package/lib/module/ui/backgrounds/variants/daylight.js +449 -0
  57. package/lib/module/ui/backgrounds/variants/deepField.js +62 -31
  58. package/lib/module/ui/backgrounds/variants/hyperspace.js +214 -0
  59. package/lib/module/ui/backgrounds/variants/liveSky.js +893 -0
  60. package/lib/module/ui/backgrounds/variants/meteors.js +253 -0
  61. package/lib/module/ui/backgrounds/variants/moonlight.js +468 -0
  62. package/lib/module/ui/backgrounds/variants/underwater.js +753 -0
  63. package/lib/module/ui/components/Badge.js +11 -10
  64. package/lib/module/ui/components/CompactRow.js +35 -18
  65. package/lib/module/ui/components/CopyButton.js +6 -4
  66. package/lib/module/ui/components/DevToolsCard.js +16 -9
  67. package/lib/module/ui/components/DynamicFilterView.js +43 -41
  68. package/lib/module/ui/components/EventStepperFooter.js +38 -10
  69. package/lib/module/ui/components/FilterComponents.js +11 -11
  70. package/lib/module/ui/components/ModalHeader.js +28 -15
  71. package/lib/module/ui/components/SectionHeader.js +3 -1
  72. package/lib/module/ui/components/StorageTypeBadge.js +3 -2
  73. package/lib/module/ui/components/TabSelector.js +57 -39
  74. package/lib/module/ui/components/ValueTypeBadge.js +7 -6
  75. package/lib/module/ui/components/WindowControls.js +37 -10
  76. package/lib/module/ui/gameUI/constants/gameUIColors.js +8 -0
  77. package/lib/module/ui/night/nightTheme.js +116 -0
  78. package/lib/module/ui/night/primitives.js +485 -0
  79. package/lib/typescript/commonjs/JsModal.d.ts +18 -0
  80. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/JsModal.web.d.ts +13 -0
  82. package/lib/typescript/commonjs/JsModal.web.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/dataViewer/diffThemes.d.ts +3 -1
  85. package/lib/typescript/commonjs/dataViewer/diffThemes.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  88. package/lib/typescript/commonjs/icons/lucide-icons.d.ts +12 -1
  89. package/lib/typescript/commonjs/icons/lucide-icons.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/index.d.ts +3 -0
  91. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/ui/backgrounds/BackgroundSwitcher.d.ts +26 -6
  93. package/lib/typescript/commonjs/ui/backgrounds/BackgroundSwitcher.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/ui/backgrounds/backgroundStore.d.ts +3 -0
  95. package/lib/typescript/commonjs/ui/backgrounds/backgroundStore.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/ui/backgrounds/index.d.ts +7 -1
  97. package/lib/typescript/commonjs/ui/backgrounds/index.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/ui/backgrounds/primitives.d.ts +18 -3
  99. package/lib/typescript/commonjs/ui/backgrounds/primitives.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts +8 -0
  101. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/backgrounds/rng.d.ts +6 -0
  103. package/lib/typescript/commonjs/ui/backgrounds/rng.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/backgrounds/skyClock.d.ts +17 -0
  105. package/lib/typescript/commonjs/ui/backgrounds/skyClock.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/ui/backgrounds/types.d.ts +1 -1
  107. package/lib/typescript/commonjs/ui/backgrounds/types.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/backgrounds/variants/daylight.d.ts +30 -0
  109. package/lib/typescript/commonjs/ui/backgrounds/variants/daylight.d.ts.map +1 -0
  110. package/lib/typescript/commonjs/ui/backgrounds/variants/deepField.d.ts +19 -4
  111. package/lib/typescript/commonjs/ui/backgrounds/variants/deepField.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/ui/backgrounds/variants/hyperspace.d.ts +39 -0
  113. package/lib/typescript/commonjs/ui/backgrounds/variants/hyperspace.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/ui/backgrounds/variants/liveSky.d.ts +41 -0
  115. package/lib/typescript/commonjs/ui/backgrounds/variants/liveSky.d.ts.map +1 -0
  116. package/lib/typescript/commonjs/ui/backgrounds/variants/meteors.d.ts +26 -0
  117. package/lib/typescript/commonjs/ui/backgrounds/variants/meteors.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/ui/backgrounds/variants/moonlight.d.ts +35 -0
  119. package/lib/typescript/commonjs/ui/backgrounds/variants/moonlight.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/ui/backgrounds/variants/underwater.d.ts +51 -0
  121. package/lib/typescript/commonjs/ui/backgrounds/variants/underwater.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/ui/components/Badge.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts +3 -1
  124. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/components/CopyButton.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/ui/components/DevToolsCard.d.ts +3 -1
  127. package/lib/typescript/commonjs/ui/components/DevToolsCard.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/ui/components/DynamicFilterView.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/ui/components/EventStepperFooter.d.ts +2 -0
  130. package/lib/typescript/commonjs/ui/components/EventStepperFooter.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/ui/components/StorageTypeBadge.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/ui/components/TabSelector.d.ts +9 -0
  134. package/lib/typescript/commonjs/ui/components/TabSelector.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/ui/components/ValueTypeBadge.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/ui/components/WindowControls.d.ts +4 -2
  137. package/lib/typescript/commonjs/ui/components/WindowControls.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts +8 -0
  139. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/ui/night/nightTheme.d.ts +113 -0
  141. package/lib/typescript/commonjs/ui/night/nightTheme.d.ts.map +1 -0
  142. package/lib/typescript/commonjs/ui/night/primitives.d.ts +111 -0
  143. package/lib/typescript/commonjs/ui/night/primitives.d.ts.map +1 -0
  144. package/lib/typescript/module/JsModal.d.ts +18 -0
  145. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  146. package/lib/typescript/module/JsModal.web.d.ts +13 -0
  147. package/lib/typescript/module/JsModal.web.d.ts.map +1 -1
  148. package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  149. package/lib/typescript/module/dataViewer/diffThemes.d.ts +3 -1
  150. package/lib/typescript/module/dataViewer/diffThemes.d.ts.map +1 -1
  151. package/lib/typescript/module/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
  152. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  153. package/lib/typescript/module/icons/lucide-icons.d.ts +12 -1
  154. package/lib/typescript/module/icons/lucide-icons.d.ts.map +1 -1
  155. package/lib/typescript/module/index.d.ts +3 -0
  156. package/lib/typescript/module/index.d.ts.map +1 -1
  157. package/lib/typescript/module/ui/backgrounds/BackgroundSwitcher.d.ts +26 -6
  158. package/lib/typescript/module/ui/backgrounds/BackgroundSwitcher.d.ts.map +1 -1
  159. package/lib/typescript/module/ui/backgrounds/backgroundStore.d.ts +3 -0
  160. package/lib/typescript/module/ui/backgrounds/backgroundStore.d.ts.map +1 -1
  161. package/lib/typescript/module/ui/backgrounds/index.d.ts +7 -1
  162. package/lib/typescript/module/ui/backgrounds/index.d.ts.map +1 -1
  163. package/lib/typescript/module/ui/backgrounds/primitives.d.ts +18 -3
  164. package/lib/typescript/module/ui/backgrounds/primitives.d.ts.map +1 -1
  165. package/lib/typescript/module/ui/backgrounds/registry.d.ts +8 -0
  166. package/lib/typescript/module/ui/backgrounds/registry.d.ts.map +1 -1
  167. package/lib/typescript/module/ui/backgrounds/rng.d.ts +6 -0
  168. package/lib/typescript/module/ui/backgrounds/rng.d.ts.map +1 -1
  169. package/lib/typescript/module/ui/backgrounds/skyClock.d.ts +17 -0
  170. package/lib/typescript/module/ui/backgrounds/skyClock.d.ts.map +1 -0
  171. package/lib/typescript/module/ui/backgrounds/types.d.ts +1 -1
  172. package/lib/typescript/module/ui/backgrounds/types.d.ts.map +1 -1
  173. package/lib/typescript/module/ui/backgrounds/variants/daylight.d.ts +30 -0
  174. package/lib/typescript/module/ui/backgrounds/variants/daylight.d.ts.map +1 -0
  175. package/lib/typescript/module/ui/backgrounds/variants/deepField.d.ts +19 -4
  176. package/lib/typescript/module/ui/backgrounds/variants/deepField.d.ts.map +1 -1
  177. package/lib/typescript/module/ui/backgrounds/variants/hyperspace.d.ts +39 -0
  178. package/lib/typescript/module/ui/backgrounds/variants/hyperspace.d.ts.map +1 -0
  179. package/lib/typescript/module/ui/backgrounds/variants/liveSky.d.ts +41 -0
  180. package/lib/typescript/module/ui/backgrounds/variants/liveSky.d.ts.map +1 -0
  181. package/lib/typescript/module/ui/backgrounds/variants/meteors.d.ts +26 -0
  182. package/lib/typescript/module/ui/backgrounds/variants/meteors.d.ts.map +1 -0
  183. package/lib/typescript/module/ui/backgrounds/variants/moonlight.d.ts +35 -0
  184. package/lib/typescript/module/ui/backgrounds/variants/moonlight.d.ts.map +1 -0
  185. package/lib/typescript/module/ui/backgrounds/variants/underwater.d.ts +51 -0
  186. package/lib/typescript/module/ui/backgrounds/variants/underwater.d.ts.map +1 -0
  187. package/lib/typescript/module/ui/components/Badge.d.ts.map +1 -1
  188. package/lib/typescript/module/ui/components/CompactRow.d.ts +3 -1
  189. package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
  190. package/lib/typescript/module/ui/components/CopyButton.d.ts.map +1 -1
  191. package/lib/typescript/module/ui/components/DevToolsCard.d.ts +3 -1
  192. package/lib/typescript/module/ui/components/DevToolsCard.d.ts.map +1 -1
  193. package/lib/typescript/module/ui/components/DynamicFilterView.d.ts.map +1 -1
  194. package/lib/typescript/module/ui/components/EventStepperFooter.d.ts +2 -0
  195. package/lib/typescript/module/ui/components/EventStepperFooter.d.ts.map +1 -1
  196. package/lib/typescript/module/ui/components/ModalHeader.d.ts.map +1 -1
  197. package/lib/typescript/module/ui/components/StorageTypeBadge.d.ts.map +1 -1
  198. package/lib/typescript/module/ui/components/TabSelector.d.ts +9 -0
  199. package/lib/typescript/module/ui/components/TabSelector.d.ts.map +1 -1
  200. package/lib/typescript/module/ui/components/ValueTypeBadge.d.ts.map +1 -1
  201. package/lib/typescript/module/ui/components/WindowControls.d.ts +4 -2
  202. package/lib/typescript/module/ui/components/WindowControls.d.ts.map +1 -1
  203. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts +8 -0
  204. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  205. package/lib/typescript/module/ui/night/nightTheme.d.ts +113 -0
  206. package/lib/typescript/module/ui/night/nightTheme.d.ts.map +1 -0
  207. package/lib/typescript/module/ui/night/primitives.d.ts +111 -0
  208. package/lib/typescript/module/ui/night/primitives.d.ts.map +1 -0
  209. package/package.json +4 -4
  210. package/lib/commonjs/ui/backgrounds/variants/lattice.js +0 -579
  211. package/lib/commonjs/ui/backgrounds/variants/starfield.js +0 -210
  212. package/lib/module/ui/backgrounds/variants/lattice.js +0 -575
  213. package/lib/module/ui/backgrounds/variants/starfield.js +0 -206
  214. package/lib/typescript/commonjs/ui/backgrounds/variants/lattice.d.ts +0 -33
  215. package/lib/typescript/commonjs/ui/backgrounds/variants/lattice.d.ts.map +0 -1
  216. package/lib/typescript/commonjs/ui/backgrounds/variants/starfield.d.ts +0 -12
  217. package/lib/typescript/commonjs/ui/backgrounds/variants/starfield.d.ts.map +0 -1
  218. package/lib/typescript/module/ui/backgrounds/variants/lattice.d.ts +0 -33
  219. package/lib/typescript/module/ui/backgrounds/variants/lattice.d.ts.map +0 -1
  220. package/lib/typescript/module/ui/backgrounds/variants/starfield.d.ts +0 -12
  221. package/lib/typescript/module/ui/backgrounds/variants/starfield.d.ts.map +0 -1
@@ -11,27 +11,65 @@ top.
11
11
  </View>
12
12
  ```
13
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.
14
+ The preset is a single shared, persisted choice (`backgroundStore`, default:
15
+ **Hyperspace**), so switching it in one tool switches it everywhere. Shipping
16
+ surfaces get it automatically: `JsModal` and `BottomSheet` mount
17
+ `<ToolBackground />` inside their content box by default (opt out per tool
18
+ with `background={false}`; the network tool does, because it mounts its own so
19
+ the desktop inline panel matches).
17
20
 
18
- ## The two families
21
+ `<BackgroundSwitcher />` is the ‹ prev / next › review strip used to audition
22
+ the catalogue (plus Live Sky's time slider and ▶ movie). It is currently
23
+ mounted NOWHERE — the catalogue picked its winner — but stays in the package:
24
+ drop it back at the top of a tool body to audition changes.
25
+
26
+ ## The catalogue
27
+
28
+ Round 3 cut the set to the keepers plus three underwater variations (round 4
29
+ folded the Meteors preset into Deep Field):
19
30
 
20
- - **Starfield** (`variants/starfield.tsx`) — the cheap scenic preset: 99 stars
21
- quantised into five depth bands.
22
31
  - **Deep Field** (`variants/deepField.tsx`) — the Everlights
23
32
  `ParallaxDriftBackground`, ported 1:1: same seed and generator call order,
24
33
  same 160 stars, same far-plane bias, sizes, palette and drift. Speed is
25
34
  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
+ everything else stays per-star. The priciest preset — the file documents
36
+ exactly what could and couldn't come across from Skia. The shipping preset
37
+ also composes the **meteor layer** (`variants/meteors.tsx`, once its own
38
+ "Meteors" preset): shooting stars that streak in, hit a WALL of the surface
39
+ and burst — flash, expanding shockwave ring (half-clipped by the edge, which
40
+ sells the impact), sparks ricocheting back into the room; each meteor's
41
+ whole life cycle is keyframed into interpolate stops on one plain loop.
42
+ `DeepFieldTunable` stays meteor-free for the bench.
43
+ - **Hyperspace** (`variants/hyperspace.tsx`) — the Everlights Skia
44
+ `HyperspaceBackground` (lightspeed starfield) re-derived for this contract:
45
+ the per-frame `screen = plane/z` projection becomes scale keyframes on
46
+ phase-offset shells (a layer scaled by 1/z moves every star on exactly the
47
+ original's hyperbolic curve), with the wrap snap hidden inside the
48
+ original's own 6% fade edges. One animated node total; raster OFF on the
49
+ shells because a cached bitmap scaled 8× goes to mush.
50
+ - **Live Sky** (`variants/liveSky.tsx`) — the cove on the real local clock:
51
+ sun up 06:30–19:00 on a sine arc, moon on the night shift, dawn/golden/
52
+ sunset/dusk as lerped palette keyframes (night = Moonlight's palette, noon =
53
+ Daylight's). ZERO extra animated nodes — the sun moves ~0.4px/minute, so the
54
+ scene is laid out statically per minute and a once-a-minute AppState-gated
55
+ tick re-renders it; this also sidesteps the native driver's
56
+ transform/opacity-only limit for the colour transitions. Discs draw UNDER
57
+ the opaque sea so the horizon clips a rising/setting body for free. While
58
+ it's showing, the switcher grows a time slider (writes `skyClock.ts`,
59
+ 5-minute quantized, not persisted) to scrub the whole cycle by hand; LIVE
60
+ snaps back to the clock. See `LIVE_SKY_PLAN.md`.
61
+ - **The underwater family** (`variants/underwater.tsx`) — Abyss, Bubbles,
62
+ Jellyfish. One register — lit from the surface, near-black at the bottom,
63
+ motion in tens of seconds — with different life in the water: marine snow,
64
+ rising bubbles (a ring plus an off-centre highlight is what makes a bubble),
65
+ and pulsing jellies that traverse bottom-to-top entirely off-screen at the
66
+ loop seam.
67
+
68
+ Catalogue trims (2026-08-20): Off, Night Drift, Moonlight, Daylight, Kelp and
69
+ Kelp Forest were cut from the registry. `variants/moonlight.tsx` and
70
+ `variants/daylight.tsx` stay in the tree — Live Sky's palettes are documented
71
+ against them and the bg-bench lanes (`MoonlightTunable`/`DaylightTunable`)
72
+ still drive them — they just aren't offered in the switcher.
35
73
 
36
74
  ## The constraint
37
75
 
@@ -58,6 +96,15 @@ translucent layers. For the same reason nothing here uses `transformOrigin`
58
96
 
59
97
  Loops stop when the app backgrounds, and unmount with the tool.
60
98
 
99
+ 4. **Geometry never scales with the modal.** Resizing the surface must crop
100
+ the scene, not stretch it — rays, plants, blooms and particles size
101
+ themselves from `Dimensions.get("window")` and only their POSITIONS
102
+ distribute across the measured width/height (the preset root clips with
103
+ `overflow: "hidden"`). Smooth gradients (base washes, vignettes) may track
104
+ the live size — their scaling is imperceptible. The one sanctioned
105
+ exception is geometry that is ABOUT the surface itself, like the meteors'
106
+ wall-impact points.
107
+
61
108
  **Known trap:** `Animated.sequence([timing, Animated.loop(...)])` silently fails
62
109
  to drive the value on the native driver — no warning, the field renders and
63
110
  never moves. It's the obvious way to phase-offset siblings; don't. Stagger via
@@ -66,8 +113,9 @@ different periods, or bake the offset into the `interpolate`.
66
113
  ### Measured
67
114
 
68
115
  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.
116
+ `measure_renders`. (Starfield and the lattice presets were measured before the
117
+ round-3 cull; the rows stay as calibration points — Deep Field is still the
118
+ ceiling, and the newer presets all run 3–7 nodes.)
71
119
 
72
120
  **Steady state** — 12–13s idle recordings:
73
121
 
@@ -87,6 +135,40 @@ The commits in every column are the network tool's own once-a-minute
87
135
  reported one component render in every configuration — the animation contributes
88
136
  zero React work while it runs, which is rule 1 holding regardless of node count.
89
137
 
138
+ **Moonlight v3** (buoy-optimize loop, iPhone 17 Pro sim, bg-bench, 3-run
139
+ medians, reproduced across two batches):
140
+
141
+ | | UI FPS | UI jank | JS FPS |
142
+ |---|---:|---:|---:|
143
+ | control (no background) | 60.0 | 0 | 58.3 |
144
+ | Moonlight, shipping (~330 views, 5 nodes) | 59.9 | 0 | 57.5–58.1 |
145
+ | glade ×12 (~1.2k blocks) | 59.5 | 0 | 57.0 |
146
+ | glade ×30 (~3.1k blocks) | 56.4–56.8 | 2 | 53.8 |
147
+
148
+ Shipping Moonlight is indistinguishable from no background; the UI cliff sits
149
+ between 12× and 30× the shipping glade. Structural variants (fused glade,
150
+ raster off, chrome off) all tied with shipping at 1× AND failed to separate at
151
+ ×30 (±1 FPS, and raster-off ballooned memory 287→351MB), so the shipping
152
+ structure stays. Bench lane: `bg-bench?v=ml-*` drives `MoonlightTunable`.
153
+
154
+ **Confirmed on a real iPhone SE (375×667, 60Hz)** — both in-batch controls
155
+ held 60.0/0 (zero drift): ship 59.6 UI / 0 UI jank; ×12 58.8 / 1; ×30 breaks
156
+ (54.4 UI, CPU 57). Same ranking as the sim; the sim → device carry-over held.
157
+
158
+ **Animated vs static (real iPhone, same batch):** identical — 59.6 UI / 0
159
+ jank / 0 JS jank both; static saved ~2.7 CPU points, less than the batch's
160
+ own control-to-control CPU spread (4.4), though the direction repeated on the
161
+ sim (~2.4). The motion toggle is a preference, not a perf lever, at shipping
162
+ scale — which is the native-driver contract holding. (A JS-jank scare in an
163
+ earlier batch attributed ~3 events to the ml cases; a later batch put 4 on a
164
+ CONTROL, so those are between-case reload noise.)
165
+
166
+ **Benchmark-protocol note:** the batch's FINAL case is unreliable — in two
167
+ consecutive batches the trailing control recorded byte-identical bad numbers
168
+ (53.7 UI FPS, 4 JS jank, only 2/3 runs) while an identical control at slot 1
169
+ was clean, so the batch teardown bleeds into the last recording. End every
170
+ batch with a sacrificial case, the same way it starts with one.
171
+
90
172
  **Mount** — the cost that is real for all of them:
91
173
 
92
174
  ```
@@ -110,6 +192,6 @@ Add the component to `variants/`, then one entry in `registry.tsx` with an
110
192
  honest `nodes` (animated nodes) and `views` (static views at ~380×620). The
111
193
  switcher shows both, which is the point — it's a review tool, not decoration.
112
194
 
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.
195
+ Alpha calibration: every preset runs a 0.4–0.55 vignette, and anything fainter
196
+ than ~0.2 alpha disappears under it — hairline strokes in particular need ~0.2
197
+ alpha or a second pixel of height to register at all.
@@ -14,7 +14,20 @@ import { useEffect, useState } from "react";
14
14
  import { devToolsStorageKeys } from "../../storage/devToolsStorageKeys.js";
15
15
  import { persistentStorage } from "../../utils/persistentStorage.js";
16
16
  import { BACKGROUND_PRESETS, DEFAULT_BACKGROUND, presetAt, presetIndex } from "./registry.js";
17
- const KEY = `${devToolsStorageKeys.base}_tool_background`;
17
+ // v3: bumped when the default became "off" (backgrounds are opt-in now), so
18
+ // devices that stepped the switcher during the (unreleased) audition rounds
19
+ // re-default to none instead of keeping a pre-release pick forever. Earlier
20
+ // keys are simply abandoned, same as v1 was.
21
+ const KEY = `${devToolsStorageKeys.base}_tool_background_v3`;
22
+
23
+ /** The persisted-selection key, exported so a host can ask "has the user ever
24
+ * picked a background?" (the desktop's first-run hint modal reads it). */
25
+ export const BACKGROUND_STORAGE_KEY = KEY;
26
+
27
+ // Motion is deliberately NOT persisted: the switcher no longer offers a
28
+ // motion toggle, so every session starts animated. `setMotion` survives as a
29
+ // session-only seam (bench harnesses, a tool forcing stillness).
30
+
18
31
  function isValidId(value) {
19
32
  return typeof value === "string" && BACKGROUND_PRESETS.some(p => p.id === value);
20
33
  }
@@ -47,8 +60,10 @@ class BackgroundStore {
47
60
  const raw = await persistentStorage.getItem(KEY);
48
61
  if (!raw || this.dirty) return;
49
62
  const parsed = JSON.parse(raw);
50
- if (isValidId(parsed.id)) this.id = parsed.id;
51
- if (typeof parsed.motion === "boolean") this.motion = parsed.motion;
63
+ // The standalone Meteors preset was folded into Deep Field — re-home
64
+ // anyone still persisted on it rather than dropping them to the default.
65
+ const id = parsed.id === "meteors" ? "deepfield" : parsed.id;
66
+ if (isValidId(id)) this.id = id;
52
67
  this.notify();
53
68
  } catch {
54
69
  // A missing or corrupt entry just means "use the default".
@@ -59,8 +74,7 @@ class BackgroundStore {
59
74
  persist() {
60
75
  this.dirty = true;
61
76
  const payload = {
62
- id: this.id,
63
- motion: this.motion
77
+ id: this.id
64
78
  };
65
79
  void persistentStorage.setItem(KEY, JSON.stringify(payload)).catch(() => {});
66
80
  }
@@ -2,8 +2,17 @@
2
2
 
3
3
  export { ToolBackground } from "./ToolBackground.js";
4
4
  export { BackgroundSwitcher } from "./BackgroundSwitcher.js";
5
- export { backgroundStore, useToolBackground } from "./backgroundStore.js";
5
+ export { backgroundStore, BACKGROUND_STORAGE_KEY, useToolBackground } from "./backgroundStore.js";
6
6
  export { BACKGROUND_PRESETS, DEFAULT_BACKGROUND, getPreset, presetAt, presetIndex } from "./registry.js";
7
+ // Drawing/animation primitives, exported so other Buoy chrome (the dial menu's
8
+ // snow effect in @buoy-gg/core) can build matching effects under the same
9
+ // native-driven performance contract instead of copying the implementations.
10
+ export { LinearFade, RadialGlow, RasterLayer, useLoop, usePulse } from "./primitives.js";
11
+ export { scatter, withAlpha as withBgAlpha } from "./rng.js";
7
12
  // Exported for the bg-bench harness in example-dev-build, so every measured
8
13
  // variant is the shipping component with knobs turned rather than a copy.
9
- export { DeepFieldTunable } from "./variants/deepField.js";
14
+ export { DeepFieldTunable } from "./variants/deepField.js";
15
+ export { MoonlightTunable } from "./variants/moonlight.js";
16
+ export { DaylightTunable } from "./variants/daylight.js";
17
+ export { LiveSkyTunable } from "./variants/liveSky.js";
18
+ export { getSkyScrub, setSkyScrub, useSkyScrub } from "./skyClock.js";
@@ -15,7 +15,9 @@
15
15
  * looping `Animated.timing` means the whole loop is handed to the UI thread
16
16
  * once and never touches JS again — no per-frame callback, no re-render, no
17
17
  * bridge traffic while it runs. A preset animates a handful of *layers*
18
- * (transform on one parent view), never per-particle.
18
+ * (transform on one parent view), never per-particle. (On web there is no
19
+ * native driver and the JS driver must be requested explicitly — see
20
+ * NATIVE_DRIVER below for why the flag can't simply be left `true`.)
19
21
  *
20
22
  * 2. **Every static layer is rasterized.** `shouldRasterizeIOS` /
21
23
  * `renderToHardwareTextureAndroid` turn a layer of 40 dots into a single
@@ -29,21 +31,35 @@
29
31
  import { memo, useEffect, useRef } from "react";
30
32
  import { Animated, AppState, Easing, Platform, StyleSheet, View } from "react-native";
31
33
  import { absoluteFill } from "../../utils/absoluteFill.js";
32
- import { withAlpha } from "./rng.js";
34
+ import { flattenOver, withAlpha } from "./rng.js";
33
35
 
34
36
  // ============================================================================
35
37
  // Animation
36
38
  // ============================================================================
37
39
 
38
40
  /**
39
- * A native-driven 0→1 loop.
41
+ * Ask for the native driver ONLY where the native animated module exists.
42
+ *
43
+ * On react-native-web (the desktop dashboard) `useNativeDriver: true` is
44
+ * demoted to the JS driver — but `Animated.loop` trusts the config flag, not
45
+ * the demotion: for a bare `loop(timing(...))` it takes its native-loop
46
+ * shortcut and starts ONE plain timing whose `iterations: -1` only the
47
+ * (absent) native module would honor. The loop plays a single pass and
48
+ * freezes — on desktop every background stopped seconds after opening.
49
+ * Requesting the JS driver honestly keeps loop's JS restart path in charge,
50
+ * which is the only driver web has anyway.
51
+ */
52
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
53
+ const NATIVE_DRIVER = Platform.OS !== "web";
54
+
55
+ /**
56
+ * A native-driven 0→1 loop (JS-driven on web — see NATIVE_DRIVER).
40
57
  *
41
58
  * The returned value is only ever consumed by `interpolate` inside an
42
59
  * `Animated.View` style, so React never re-renders while it runs. The loop is
43
60
  * stopped when the app backgrounds (a background animation is pure battery
44
61
  * burn) and restarted on foreground.
45
62
  */
46
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
47
63
  export function useLoop(durationMs, enabled = true) {
48
64
  const value = useRef(new Animated.Value(0)).current;
49
65
  useEffect(() => {
@@ -56,7 +72,11 @@ export function useLoop(durationMs, enabled = true) {
56
72
  toValue: 1,
57
73
  duration: durationMs,
58
74
  easing: Easing.linear,
59
- useNativeDriver: true
75
+ useNativeDriver: NATIVE_DRIVER,
76
+ // A decorative endless loop must never register as an interaction —
77
+ // the default (true) would block InteractionManager callbacks in the
78
+ // host app for as long as the background is mounted.
79
+ isInteraction: false
60
80
  }));
61
81
  anim.start();
62
82
  };
@@ -93,12 +113,16 @@ export function usePulse(durationMs, enabled = true) {
93
113
  toValue: 1,
94
114
  duration: durationMs / 2,
95
115
  easing: Easing.inOut(Easing.quad),
96
- useNativeDriver: true
116
+ useNativeDriver: NATIVE_DRIVER,
117
+ // See useLoop: endless decorative loops must not hold
118
+ // InteractionManager (isInteraction defaults to true).
119
+ isInteraction: false
97
120
  }), Animated.timing(value, {
98
121
  toValue: 0,
99
122
  duration: durationMs / 2,
100
123
  easing: Easing.inOut(Easing.quad),
101
- useNativeDriver: true
124
+ useNativeDriver: NATIVE_DRIVER,
125
+ isInteraction: false
102
126
  })]));
103
127
  anim.start();
104
128
  };
@@ -193,7 +217,18 @@ export const RadialGlow = /*#__PURE__*/memo(function RadialGlow({
193
217
  /**
194
218
  * Fake linear gradient: `bands` stacked strips with linearly interpolated
195
219
  * alpha. Used for scrims (keep text readable over a busy field) and for
196
- * horizon glows. 12–16 bands is invisible as banding at these alphas.
220
+ * horizon glows.
221
+ *
222
+ * TRAP (found on the Abyss wash): translucent strips must NOT overlap. An
223
+ * overlap composites the same colour twice, doubling the alpha in a stripe at
224
+ * every band edge — on a dark surface that draws a visible line per band, and
225
+ * because the strips are %-sized the whole ruled pattern rescales as the
226
+ * modal resizes. Strips here butt exactly, with a 0.08% fudge (sub-pixel at
227
+ * any real size) so layout rounding can't open a hairline gap.
228
+ *
229
+ * `flattenBase`: when the fade sits DIRECTLY on a known opaque colour, pass
230
+ * that colour — each strip is then drawn opaque via `flattenOver`, which is
231
+ * pixel-identical, immune to seams entirely, and cheaper to composite.
197
232
  */
198
233
  export const LinearFade = /*#__PURE__*/memo(function LinearFade({
199
234
  color,
@@ -201,6 +236,7 @@ export const LinearFade = /*#__PURE__*/memo(function LinearFade({
201
236
  to = 1,
202
237
  bands = 14,
203
238
  direction = "down",
239
+ flattenBase,
204
240
  style
205
241
  }) {
206
242
  const vertical = direction === "down" || direction === "up";
@@ -210,21 +246,22 @@ export const LinearFade = /*#__PURE__*/memo(function LinearFade({
210
246
  for (let i = 0; i < bands; i++) {
211
247
  const t = (i + 0.5) / bands;
212
248
  const a = from + (to - from) * (reversed ? 1 - t : t);
249
+ const fill = flattenBase ? flattenOver(color, a, flattenBase) : withAlpha(color, a);
213
250
  strips.push(/*#__PURE__*/_jsx(View, {
214
251
  style: vertical ? {
215
252
  position: "absolute",
216
253
  left: 0,
217
254
  right: 0,
218
255
  top: `${i * pct}%`,
219
- height: `${pct + 1.2}%`,
220
- backgroundColor: withAlpha(color, a)
256
+ height: `${pct + 0.08}%`,
257
+ backgroundColor: fill
221
258
  } : {
222
259
  position: "absolute",
223
260
  top: 0,
224
261
  bottom: 0,
225
262
  left: `${i * pct}%`,
226
- width: `${pct + 1.2}%`,
227
- backgroundColor: withAlpha(color, a)
263
+ width: `${pct + 0.08}%`,
264
+ backgroundColor: fill
228
265
  }
229
266
  }, i));
230
267
  }
@@ -263,7 +300,7 @@ export const Vignette = /*#__PURE__*/memo(function Vignette({
263
300
  from: alpha,
264
301
  to: 0,
265
302
  direction: "down",
266
- bands: 10
303
+ bands: 14
267
304
  })
268
305
  }), /*#__PURE__*/_jsx(View, {
269
306
  style: {
@@ -278,7 +315,7 @@ export const Vignette = /*#__PURE__*/memo(function Vignette({
278
315
  from: 0,
279
316
  to: alpha,
280
317
  direction: "down",
281
- bands: 10
318
+ bands: 14
282
319
  })
283
320
  }), /*#__PURE__*/_jsx(View, {
284
321
  style: {
@@ -293,7 +330,7 @@ export const Vignette = /*#__PURE__*/memo(function Vignette({
293
330
  from: alpha,
294
331
  to: 0,
295
332
  direction: "right",
296
- bands: 8
333
+ bands: 10
297
334
  })
298
335
  }), /*#__PURE__*/_jsx(View, {
299
336
  style: {
@@ -308,7 +345,7 @@ export const Vignette = /*#__PURE__*/memo(function Vignette({
308
345
  from: 0,
309
346
  to: alpha,
310
347
  direction: "right",
311
- bands: 8
348
+ bands: 10
312
349
  })
313
350
  })]
314
351
  });
@@ -10,88 +10,65 @@
10
10
  * them honest.
11
11
  */
12
12
 
13
- import { Crosshatch, Halftone, HexMesh, Instrument, IsoGrid, PerfBoard, Radar, Waffle } from "./variants/lattice.js";
14
13
  import { DeepField } from "./variants/deepField.js";
15
- import { Starfield } from "./variants/starfield.js";
14
+ import { Hyperspace } from "./variants/hyperspace.js";
15
+ import { LiveSky } from "./variants/liveSky.js";
16
+ import { Abyss, Bubbles, Jellyfish } from "./variants/underwater.js";
16
17
  export const BACKGROUND_PRESETS = [{
17
18
  id: "off",
18
19
  label: "Off",
19
- blurb: "Plain black — the baseline to compare against",
20
+ blurb: "Plain black — no backdrop at all",
20
21
  nodes: 0,
21
22
  views: 1,
22
23
  Component: null
23
- }, {
24
- id: "starfield",
25
- label: "Starfield",
26
- blurb: "Five depth bands drifting at parallax speeds",
27
- nodes: 6,
28
- views: 222,
29
- Component: Starfield
30
24
  }, {
31
25
  id: "deepfield",
32
26
  label: "Deep Field",
33
- blurb: "The Everlights field, 1:1 — 160 stars, richer but pricier",
34
- nodes: 22,
35
- views: 462,
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.
29
+ nodes: 24,
30
+ views: 510,
36
31
  Component: DeepField
37
32
  }, {
38
- id: "hexmesh",
39
- label: "Hex Mesh",
40
- blurb: "Diamond lattice with a slow breath",
41
- nodes: 1,
42
- views: 160,
43
- Component: HexMesh
44
- }, {
45
- id: "isogrid",
46
- label: "Iso Grid",
47
- blurb: "Three-axis isometric ruling",
33
+ id: "hyperspace",
34
+ label: "Hyperspace",
35
+ blurb: "Lightspeed — crisp points streaming from a centre vanishing point",
48
36
  nodes: 1,
49
- views: 66,
50
- Component: IsoGrid
37
+ views: 570,
38
+ Component: Hyperspace
51
39
  }, {
52
- id: "crosshatch",
53
- label: "Crosshatch",
54
- blurb: "Fine 45° weave, drafting-paper quiet",
55
- nodes: 1,
56
- views: 64,
57
- Component: Crosshatch
40
+ id: "livesky",
41
+ label: "Live Sky",
42
+ blurb: "The cove on your clock — dawn to dusk, for real",
43
+ nodes: 5,
44
+ views: 290,
45
+ Component: LiveSky
58
46
  }, {
59
- id: "perfboard",
60
- label: "Perf Board",
61
- blurb: "Solder-pad matrix with datum pads every fourth",
62
- nodes: 1,
63
- views: 150,
64
- Component: PerfBoard
47
+ id: "abyss",
48
+ label: "Abyss",
49
+ blurb: "A surface light fanning into the deep",
50
+ nodes: 4,
51
+ views: 230,
52
+ Component: Abyss
65
53
  }, {
66
- id: "halftone",
67
- label: "Halftone",
68
- blurb: "Dot grid swelling toward the light",
69
- nodes: 1,
70
- views: 176,
71
- Component: Halftone
54
+ id: "bubbles",
55
+ label: "Bubbles",
56
+ blurb: "Bubbles wobbling up through the light",
57
+ nodes: 4,
58
+ views: 180,
59
+ Component: Bubbles
72
60
  }, {
73
- id: "waffle",
74
- label: "Waffle",
75
- blurb: "Machined panel of rounded cells",
76
- nodes: 1,
77
- views: 88,
78
- Component: Waffle
79
- }, {
80
- id: "instrument",
81
- label: "Instrument",
82
- blurb: "Ruled scales with major graduations",
83
- nodes: 1,
84
- views: 110,
85
- Component: Instrument
86
- }, {
87
- id: "radar",
88
- label: "Radar",
89
- blurb: "Rings and spokes under a rotating sweep",
90
- nodes: 2,
91
- views: 40,
92
- Component: Radar
61
+ id: "jellyfish",
62
+ label: "Jellyfish",
63
+ blurb: "A bloom of glowing jellies, big and small, pulsing past",
64
+ nodes: 15,
65
+ views: 250,
66
+ Component: Jellyfish
93
67
  }];
94
- export const DEFAULT_BACKGROUND = "hexmesh";
68
+
69
+ // Ships OFF: a backdrop is an opt-in, so first load is plain black and the
70
+ // choice only persists once the user actually picks one in the switcher.
71
+ export const DEFAULT_BACKGROUND = "off";
95
72
  export function presetIndex(id) {
96
73
  const i = BACKGROUND_PRESETS.findIndex(p => p.id === id);
97
74
  return i < 0 ? 0 : i;
@@ -42,6 +42,26 @@ export function withAlpha(hex, alpha) {
42
42
  return `rgba(${r},${g},${b},${a.toFixed(3)})`;
43
43
  }
44
44
 
45
+ /**
46
+ * RGB lerp between two `#RRGGBB` (or `#RGB`) colours, returned as `#RRGGBB`.
47
+ * Used by Live Sky to blend palette keyframes; at minute-sized steps a plain
48
+ * RGB lerp is indistinguishable from anything fancier.
49
+ */
50
+ export function lerpColor(a, b, t) {
51
+ const parse = h => {
52
+ let v = h.replace("#", "");
53
+ if (v.length === 3) v = v[0] + v[0] + v[1] + v[1] + v[2] + v[2];
54
+ const n = parseInt(v, 16);
55
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
56
+ };
57
+ const [ar, ag, ab] = parse(a);
58
+ const [br, bg, bb] = parse(b);
59
+ const c = t < 0 ? 0 : t > 1 ? 1 : t;
60
+ const mix = (x, y) => Math.round(x + (y - x) * c);
61
+ const hex = n => n.toString(16).padStart(2, "0");
62
+ return `#${hex(mix(ar, br))}${hex(mix(ag, bg))}${hex(mix(ab, bb))}`;
63
+ }
64
+
45
65
  /**
46
66
  * `hex` at `alpha` composited over an opaque `base`, returned as an OPAQUE
47
67
  * colour.
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The Live Sky scrub clock.
5
+ *
6
+ * `null` means "live" — the Live Sky preset follows the real local clock. A
7
+ * number (minutes since midnight, quantized by the slider) freezes the scene
8
+ * at that time of day so the whole cycle can be scrubbed through by hand.
9
+ *
10
+ * Deliberately NOT part of `backgroundStore`: the variant itself has to read
11
+ * this, and a variant importing the store would close an import cycle through
12
+ * the registry (store → registry → variant → store). It is also deliberately
13
+ * not persisted — a scrub is a preview gesture, not a setting.
14
+ */
15
+
16
+ import { useEffect, useState } from "react";
17
+ let scrub = null;
18
+ const listeners = new Set();
19
+ export function getSkyScrub() {
20
+ return scrub;
21
+ }
22
+ export function setSkyScrub(value) {
23
+ if (value === scrub) return;
24
+ scrub = value;
25
+ for (const listener of listeners) listener();
26
+ }
27
+
28
+ /** Subscribe a component to the scrub value. */
29
+ export function useSkyScrub() {
30
+ const [value, setValue] = useState(scrub);
31
+ useEffect(() => {
32
+ const listener = () => setValue(scrub);
33
+ listeners.add(listener);
34
+ return () => {
35
+ listeners.delete(listener);
36
+ };
37
+ }, []);
38
+ return value;
39
+ }